Joda Time LocalTime 24:00 в конце дня

Мы создаем приложение для планирования, и нам нужно представить чье-то доступное расписание в течение дня, независимо от того, в каком часовом поясе они находятся. Беря пример из Joda Time Interval, который представляет собой интервал в абсолютном времени между двумя экземплярами (начало включительно, конец эксклюзивный), мы создали LocalInterval. LocalInterval состоит из двух LocalTimes (начальный включительно, конечный эксклюзивный), и мы даже создали удобный класс для его сохранения в Hibernate.

Например, если кто-то доступен с 13:00 до 17:00, мы создадим:

new LocalInterval(new LocalTime(13, 0), new LocalTime(17, 0));

Пока все хорошо --- пока кто-нибудь не захочет быть доступным с 23:00 до полуночи в какой-то день. Поскольку конец интервала является исключающим, это должно легко быть представлено как таковое:

new LocalInterval(new LocalTime(23, 0), new LocalTime(24, 0));

ак! Не идти. Это вызывает исключение, потому что LocalTime не может хранить часы больше 23.

Мне это кажется недостатком дизайна — Джода не учел, что кому-то может понадобиться LocalTime, представляющий неинклюзивную конечную точку.

Это действительно расстраивает, так как пробивает дыру в том, что в остальном было очень элегантной моделью, которую мы создали.

Какие у меня есть варианты, кроме разветвления Joda и получения чека за 24 часа? (Нет, мне не нравится вариант использования фиктивного значения --- скажем, 23:59:59 --- для представления 24:00.)

Обновление: Для тех, кто продолжает говорить, что 24:00 не существует, вот цитата из ISO 8601-2004 4.2.3 Примечания 2,3: «Конец одного календарного дня [24:00] совпадает с [00 :00] в начале следующего календарного дня..." и "Представления, в которых [чч] имеет значение [24], предпочтительны только для представления конца временного интервала...".


person Garret Wilson    schedule 21.03.2011    source источник
comment
Исходя из контекста вашего вопроса, я предполагаю, что вы хотели сказать, что LocalInterval состоит из двух LocalTimes (начало включительно, конец эксклюзивный),...   -  person MusiGenesis    schedule 22.03.2011
comment
@MusiGenesis: Да, это опечатка. Я обновил вопрос, чтобы указать конечный эксклюзив. Спасибо.   -  person Garret Wilson    schedule 22.03.2011
comment
Очень интересный вопрос. Некоторое время назад это вдохновило меня ввести функцию 24:00 в мою собственную библиотеку даты/времени Time4J. Подвеска, аналогичная LocalTime, а именно PlainTime, поддерживает немного более широкий диапазон 00:00/24:00. И я обнаружил, что эта функция не создает никаких проблем (временной порядок по-прежнему довольно ясен), но может помочь решить некоторые другие проблемы более элегантным способом (например, IANA-TZDB также использует значение 24:00).   -  person Meno Hochschild    schedule 15.06.2015


Ответы (9)


Ну после 23:59:59 наступает 00:00:00 следующего дня. Так что, может быть, использовать LocalTime из 0, 0 на следующий календарный день?

Хотя ваше время начала и окончания включено, 23:59:59 — это действительно то, что вам нужно. Это включает 59-ю секунду 59-й минуты 23-го часа и заканчивает диапазон ровно в 00:00:00.

Нет такой вещи, как 24:00 (при использовании LocalTime).

person aroth    schedule 21.03.2011
comment
Извините, в моем первоначальном вопросе была опечатка. Я имел в виду конец эксклюзива, поэтому я хочу 24:00. Действительно, есть такое понятие, как 24:00. См. ISO 8601-2004, раздел 4.2.3 Полночь. Поскольку мой конец эксклюзивный, это именно то, что я хочу. - person Garret Wilson; 22.03.2011
comment
@ garrett-wilson - После прочтения некоторых документов Joda у меня есть пара предложений. Во-первых, пробовали ли вы использовать константу LocalTime.MIDNIGHT? Во-вторых, то, что говорит спецификация, и то, что делает реализация, не обязательно совпадают. Можете ли вы попробовать что-то вроде System.out.println(new LocalTime(12,0).getChronology().hourOfDay().getMaximumValue())? Если значение меньше 24, то 24:00 в реализации не существует. Что может быть чем-то, что вы можете поднять как ошибку в Джоде. - person aroth; 22.03.2011
comment
Ну после 23:59:59 наступает 00:00:00 следующего дня. Не обязательно. Иногда после 23:59:59 наступает 23:59:60 — hpiers.obspm. fr/iers/bulc/bulletinc.dat - person Jonas; 20.06.2012
comment
Нет такого понятия, как 24:00. неточно. LocalTime не поддерживает дни › 23:59, но это не значит, что их не существует. GTFS работает › круглосуточно. developers.google.com/transit/gtfs/reference/#stop_timestxt - person Linus Norton; 21.12.2017

Решение, к которому мы, наконец, пришли, состояло в том, чтобы использовать 00:00 вместо 24:00, с логикой во всем классе и в остальной части приложения для интерпретации этого локального значения. Это настоящий кладж, но это наименее навязчивая и самая элегантная вещь, которую я мог придумать.

Во-первых, класс LocalTimeInterval хранит внутренний флаг того, является ли конечная точка интервала полночью конца дня (24:00). Этот флаг будет истинным, только если время окончания равно 00:00 (равно LocalTime.MIDNIGHT).

/**
 * @return Whether the end of the day is {@link LocalTime#MIDNIGHT} and this should be considered midnight of the
 *         following day.
 */
public boolean isEndOfDay()
{
    return isEndOfDay;
}

По умолчанию конструктор считает 00:00 началом дня, но есть альтернативный конструктор для ручного создания интервала на весь день:

public LocalTimeInterval(final LocalTime start, final LocalTime end, final boolean considerMidnightEndOfDay)
{
    ...
    this.isEndOfDay = considerMidnightEndOfDay && LocalTime.MIDNIGHT.equals(end);
}

Есть причина, по которой этот конструктор не имеет просто времени начала и флага «конец дня»: при использовании с пользовательским интерфейсом с раскрывающимся списком времен мы не знаем, будет ли пользователь выберите 00:00 (что отображается как 24:00), но мы знаем, что, поскольку раскрывающийся список находится в конце диапазона, в нашем случае это означает 24:00. (Хотя LocalTimeInterval допускает пустые интервалы, мы не допускаем их в нашем приложении.)

Проверка перекрытий требует специальной логики для работы с 24:00:

public boolean overlaps(final LocalTimeInterval localInterval)
{
    if (localInterval.isEndOfDay())
    {
        if (isEndOfDay())
        {
            return true;
        }
        return getEnd().isAfter(localInterval.getStart());
    }
    if (isEndOfDay())
    {
        return localInterval.getEnd().isAfter(getStart());
    }
    return localInterval.getEnd().isAfter(getStart()) && localInterval.getStart().isBefore(getEnd());
}

Точно так же преобразование в абсолютный интервал требует добавления еще одного дня к результату, если isEndOfDay() возвращает значение true. Важно, чтобы код приложения никогда не создавал Interval вручную из начального и конечного значений LocalTimeInterval, поскольку время окончания может указывать на конец дня:

public Interval toInterval(final ReadableInstant baseInstant)
{
    final DateTime start = getStart().toDateTime(baseInstant);
    DateTime end = getEnd().toDateTime(baseInstant);
    if (isEndOfDay())
    {
        end = end.plusDays(1);
    }
    return new Interval(start, end);
}

При сохранении LocalTimeInterval в базе данных мы смогли сделать кладж полностью прозрачным, поскольку Hibernate и SQL не имеют ограничения на 24:00 (и в любом случае не имеют понятия LocalTime). Если isEndOfDay() возвращает true, наша реализация PersistentLocalTimeIntervalAsTime сохраняет и извлекает истинное значение времени 24:00:

    ...
    final Time startTime = (Time) Hibernate.TIME.nullSafeGet(resultSet, names[0]);
    final Time endTime = (Time) Hibernate.TIME.nullSafeGet(resultSet, names[1]);
    ...
    final LocalTime start = new LocalTime(startTime, DateTimeZone.UTC);
    if (endTime.equals(TIME_2400))
    {
        return new LocalTimeInterval(start, LocalTime.MIDNIGHT, true);
    }
    return new LocalTimeInterval(start, new LocalTime(endTime, DateTimeZone.UTC));

а также

    final Time startTime = asTime(localTimeInterval.getStart());
    final Time endTime = localTimeInterval.isEndOfDay() ? TIME_2400 : asTime(localTimeInterval.getEnd());
    Hibernate.TIME.nullSafeSet(statement, startTime, index);
    Hibernate.TIME.nullSafeSet(statement, endTime, index + 1);

Печально, что нам пришлось писать обходной путь в первую очередь; это лучшее, что я мог сделать.

person Garret Wilson    schedule 05.04.2011
comment
Я думаю, что для приложения-планировщика это является наиболее элегантным решением. Сколько раз у меня возникали проблемы с тем, что кто-то в Outlook пытался представить свое «событие на весь день», сопоставляя точное время: сдвиньте это на разницу в наших часовых поясах, и весь наш календарь окажется в беспорядке. Именно по этой причине в Outlook у вас есть отдельный переключатель для представления целого дня. - person YoYo; 12.01.2016

Это не недостаток дизайна. LocalDate не справляется с (24,0), потому что 24:00 не существует.

Кроме того, что происходит, когда вы хотите представить интервал между, скажем, 9 вечера и 3 часа ночи?

Что с этим не так:

new LocalInterval(new LocalTime(23, 0), new LocalTime(0, 0));

Вам просто нужно обработать возможность того, что время окончания может быть «до» времени начала, и добавить день, когда это необходимо, и просто надеяться, что никто не хочет представлять интервал дольше 24 часов.

Альтернативно, представьте интервал как комбинацию LocalDate и Duration или Period. Это устраняет проблему «более 24 часов».

person skaffman    schedule 21.03.2011
comment
[T] нет такой вещи, как 24:00. Действительно есть. См. ISO 8601-2004, раздел 4.2.3 Полночь. Поскольку мой конец эксклюзивный, это именно то, что я хочу. - person Garret Wilson; 22.03.2011
comment
Что не так с приведенным вами примером, так это то, что он представляет отрицательный интервал с 23:00 до 00:00, который произошел 11 часов назад. Рассмотрим интервал с 00:00 до 00:00 --- это интервал без длины или интервал на весь день (игнорируя изменения летнего времени)? Это должно быть первое --- интервал всего дня, исключая конец, будет с 00:00 до 24:00. Все это работало бы блестяще и соответствовало бы стандарту ISO 8601, если бы не одна строка кода, где-то выбрасывающая исключение, потому что ей не нравится значение 24. - person Garret Wilson; 22.03.2011

Ваша проблема может быть сформулирована как определение интервала в домене, который обертывается. Ваш минимум – 00:00, а максимум – 24:00 (не включительно).

Предположим, ваш интервал определяется как (нижний, верхний). Если вам требуется, чтобы нижний ‹ верхний, вы можете представить (21:00, 24:00), но вы по-прежнему не можете представить (21:00, 02:00), интервал, который пересекает границу минимума/максимума.

Я не знаю, будет ли ваше приложение планирования включать интервалы переноса, но если вы собираетесь перейти к (21:00, 24:00) без включения дней, я не вижу, что помешает вам потребовать (21 :00, 02:00) без учета дней (что приводит к круговому измерению).

Если ваш проект допускает циклическую реализацию, операторы интервалов довольно тривиальны.

Например (в псевдокоде):

is x in (lower, upper)? :=
if (lower <= upper) return (lower <= x && x <= upper)
else return (lower <= x || x <= upper)

В этом случае я обнаружил, что написание оболочки вокруг Joda-Time, реализующей операторы, достаточно просто и уменьшает сопротивление между мышлением/математикой и API. Даже если это просто для включения 24:00 как 00:00.

Я согласен, что исключение 24:00 меня раздражало в начале, и было бы неплохо, если бы кто-то предложил решение. К счастью для меня, учитывая, что в моем использовании временных интервалов преобладает циклическая семантика, я всегда получаю оболочку, которая, кстати, решает проблему исключения 24:00.

person Dingfeng Quek    schedule 04.04.2011

Время 24:00 сложное. Хотя мы, люди, можем понять, что имеется в виду, кодирование API для представления этого без негативного влияния на все остальное кажется мне почти невозможным.

Недопустимое значение 24 глубоко закодировано в Joda-Time — попытка удалить его может иметь негативные последствия во многих местах. Я бы не рекомендовал пытаться это сделать.

Для вашей проблемы локальный интервал должен состоять из (LocalTime, LocalTime, Days) или (LocalTime, Period). Последний немного более гибкий. Это нужно для корректной поддержки интервала с 23:00 до 03:00.

person JodaStephen    schedule 22.03.2011
comment
Итак, подумайте, как ваше предложение (LocalTime, Period) сработало бы 13 марта 2011 года в США. Если я отмечу свою доступность в воскресенье с 00:00 до 12:00, при использовании вашего подхода будет использоваться (00:00, 12 часов). Поскольку мы потеряли час 13 марта, ваше представление даст 00:00-13:00 для этого дня, так как добавление 12 часов к DateTime полуночи даст 13:00. Если бы вместо этого я сохранил время окончания как LocalTime, преобразование 12:00 в полночь DateTime дало бы мне правильное время — интервал 13 марта 2011 года был бы короче из-за перехода на летнее время. - person Garret Wilson; 22.03.2011

Я считаю предложение JodaStephen (LocalTime, LocalTime, Days) приемлемым.

Учитывая 13 марта 2011 года и вашу доступность в воскресенье с 00:00 до 12:00, у вас будет (00:00, 12:00, 0), которые на самом деле длились 11 часов из-за летнего времени.

Доступность, скажем, с 15:00 до 24:00, вы можете затем закодировать как (15:00, 00:00, 1), что будет расширено до 2011-03-13T15:00 - 2011-03-14T00:00, где конец будет желательным 2011-03-13T24:00. Это означает, что вы должны использовать LocalTime 00:00 на следующий календарный день, как уже было предложено aroth.

Конечно, было бы неплохо использовать 24:00 LocalTime напрямую и соответствовать ISO 8601, но это кажется невозможным без значительных изменений внутри JodaTime, поэтому этот подход кажется меньшим злом.

И последнее, но не менее важное: вы можете даже расширить барьер одного дня чем-то вроде (16:00, 05:00, 1)...

person binuWADa    schedule 27.03.2011

Ради полноты этот тест терпит неудачу:

@Test()
public void testJoda() throws DGConstraintViolatedException {
    DateTimeFormatter simpleTimeFormatter = DateTimeFormat.forPattern("HHmm");
    LocalTime t1 = LocalTime.parse("0000", simpleTimeFormatter);
    LocalTime t2 = LocalTime.MIDNIGHT;
    Assert.assertTrue(t1.isBefore(t2));
}  

Это означает, что константа MIDNIGHT не очень полезна для решения проблемы, как кто-то предположил.

person Jaime Casero    schedule 20.01.2014

это наша реализация TimeInterval, использующая null в качестве даты окончания для конца дня. Он поддерживает методы перекрытия() и содержит(), а также основан на joda-time. Он поддерживает интервалы, охватывающие несколько дней.

/**
 * Description: Immutable time interval<br>
 * The start instant is inclusive but the end instant is exclusive.
 * The end is always greater than or equal to the start.
 * The interval is also restricted to just one chronology and time zone.
 * Start can be null (infinite).
 * End can be null and will stay null to let the interval last until end-of-day.
 * It supports intervals spanning multiple days.
 */
public class TimeInterval {

    public static final ReadableInstant INSTANT = null; // null means today
//    public static final ReadableInstant INSTANT = new Instant(0); // this means 1st jan 1970

    private final DateTime start;
    private final DateTime end;

    public TimeInterval() {
        this((LocalTime) null, null);
    }

    /**
     * @param from - null or a time  (null = left unbounded == LocalTime.MIDNIGHT)
     * @param to   - null or a time  (null = right unbounded)
     * @throws IllegalArgumentException if invalid (to is before from)
     */
    public TimeInterval(LocalTime from, LocalTime to) throws IllegalArgumentException {
        this(from == null ? null : from.toDateTime(INSTANT),
                to == null ? null : to.toDateTime(INSTANT));
    }

    /**
     * create interval spanning multiple days possibly.
     *
     * @param start - start distinct time
     * @param end   - end distinct time
     * @throws IllegalArgumentException - if start > end. start must be <= end
     */
    public TimeInterval(DateTime start, DateTime end) throws IllegalArgumentException {
        this.start = start;
        this.end = end;
        if (start != null && end != null && start.isAfter(end))
            throw new IllegalArgumentException("start must be less or equal to end");
    }

    public DateTime getStart() {
        return start;
    }

    public DateTime getEnd() {
        return end;
    }

    public boolean isEndUndefined() {
        return end == null;
    }

    public boolean isStartUndefined() {
        return start == null;
    }

    public boolean isUndefined() {
        return isEndUndefined() && isStartUndefined();
    }

    public boolean overlaps(TimeInterval other) {
        return (start == null || (other.end == null || start.isBefore(other.end))) &&
                (end == null || (other.start == null || other.start.isBefore(end)));
    }

    public boolean contains(TimeInterval other) {
        return ((start != null && other.start != null && !start.isAfter(other.start)) || (start == null)) &&
                ((end != null && other.end != null && !other.end.isAfter(end)) || (end == null));
    }

    public boolean contains(LocalTime other) {
        return contains(other == null ? null : other.toDateTime(INSTANT));
    }

    public boolean containsEnd(DateTime other) {
        if (other == null) {
            return end == null;
        } else {
            return (start == null || !other.isBefore(start)) &&
                    (end == null || !other.isAfter(end));
        }
    }

    public boolean contains(DateTime other) {
        if (other == null) {
            return start == null;
        } else {
            return (start == null || !other.isBefore(start)) &&
                    (end == null || other.isBefore(end));
        }
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append("TimeInterval");
        sb.append("{start=").append(start);
        sb.append(", end=").append(end);
        sb.append('}');
        return sb.toString();
    }
}
person Roman    schedule 13.02.2013

Этот вопрос старый, но многие из этих ответов сосредоточены на Joda Time и лишь частично решают истинную основную проблему:

Модель в коде OP не соответствует моделируемой реальности.

К сожалению, поскольку вы, кажется, заботитесь о граничных условиях между днями, ваша «в остальном элегантная модель» не подходит для проблемы, которую вы моделируете. Вы использовали пару значений времени для представления интервалов. Попытка упростить модель в пару раз означает упрощение ниже сложности реальной проблемы. Дневные границы на самом деле существуют, и пару раз теряется такая информация. Как всегда, чрезмерное упрощение приводит к последующей сложности для восстановления или компенсации недостающей информации. Настоящую сложность можно перенести только из одной части кода в другую.

Сложность реальности можно устранить только с помощью магии «неподдерживаемых вариантов использования».

Ваша модель будет иметь смысл только в проблемной области, где не важно, сколько дней может пройти между временем начала и окончания. Это проблемное пространство не соответствует большинству реальных проблем. Поэтому неудивительно, что Joda Time плохо его поддерживает. Использование 25 значений для часов (0-24) является запахом кода и обычно указывает на слабость конструкции. В сутках всего 24 часа, поэтому 25 значений не нужны!

Обратите внимание, что, поскольку вы не фиксируете дату ни на одном из концов LocalInterval, ваш класс также не собирает достаточно информации для учета перехода на летнее время. [00:30:00 TO 04:00:00) обычно длится 3,5 часа, но может быть и 2,5 или 4,5 часа.

Вы должны либо использовать дату/время и продолжительность начала, либо дату/время начала и дату/время окончания (включая начало, эксклюзивный конец является хорошим выбором по умолчанию). Использование длительности становится сложным, если вы собираетесь отображать время окончания из-за таких вещей, как переход на летнее время, високосные годы и високосные секунды. С другой стороны, использование конечной даты становится таким же сложным, если вы хотите отобразить продолжительность. Хранение обоих, конечно, опасно, потому что это нарушает принцип DRY. Если бы я писал такой класс, я бы сохранил дату/время окончания и инкапсулировал логику для получения длительности с помощью метода объекта. Таким образом, клиенты класса class не все придумывают собственный код для расчета продолжительности.

Я бы написал пример, но есть еще лучший вариант. Используйте стандартный класс Interval из времени Joda, который уже принимает начальный момент и либо продолжительность, либо конечный момент. Он также с радостью рассчитает продолжительность или время окончания для вас. К сожалению, JSR-310 не имеет интервала или подобного класса. (хотя можно использовать ThreeTenExtra, чтобы компенсировать это)

Относительно умные люди из Joda Time и Sun/Oracle (JSR-310) очень тщательно обдумывали эти проблемы. Возможно, вы умнее их. Это возможно. Однако, даже если вы более яркая лампочка, ваш 1 час, вероятно, не достигнет того, на что они потратили годы. Если вы не находитесь где-то в эзотерическом пограничном случае, обычно это пустая трата времени и денег, чтобы тратить усилия на то, чтобы угадать их. (конечно, во время OP JSR-310 не был завершен...)

Надеюсь, вышеизложенное поможет людям, которые находят этот вопрос при разработке или устранении подобных проблем.

person Gus    schedule 12.01.2016