Время Joda LocalTime 24:00 в конце дня

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

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

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

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

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

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

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

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

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

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

Ответ 1

В конечном итоге мы решили использовать 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. Важно, чтобы код приложения никогда не создавал интервал вручную из начальных и конечных значений 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 в базе данных мы смогли сделать kludge полностью прозрачным, так как 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);

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

Ответ 2

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

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

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

Ответ 3

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

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

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

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

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

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

Ответ 4

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

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

Я не знаю, будет ли ваше приложение для планирования включать интервалы обхода, но если вы собираетесь идти (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.

Ответ 5

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

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

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

Ответ 6

Я нахожу JodaStephen предложение (LocalTime, LocalTime, Days) приемлемым.

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

Доступность, например, с 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)...

Ответ 7

это наша реализация TimeInterval с использованием null в качестве конечной даты для конца дня. Он поддерживает методы overlaps() и contains() и также основан на 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();
    }
}

Ответ 8

Для полноты этого теста не удается:

@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 не очень полезна для проблемы, как это было предложено кем-то.

Ответ 9

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

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

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

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

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

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

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

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

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