问题 Joda Time LocalTime 24:00结束


我们正在创建一个调度应用程序,我们需要在白天代表某人的可用时间表,无论他们在哪个时区。从Joda Time的Interval中获取一个提示,它表示两个实例之间的绝对时间间隔(开始包含,结束独家),我们创建了一个LocalInterval。 LocalInterval由两个LocalTimes(开始包含,最终独占)组成,我们甚至为Hibernate中的持久化做了一个方便的类。

例如,如果有人从下午1:00到下午5:00可用,我们将创建:

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

到目前为止一直很好 - 直到有人想在某天从晚上11点到午夜才有空。由于间隔结束是独占的,这个 应该 很容易被表示为:

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

确认!不行。这会引发异常,因为LocalTime不能保持大于23的任何小时。

这对我来说似乎是一个设计缺陷--- Joda没有考虑到有人可能想要一个代表非包容性端点的LocalTime。

这真是令人沮丧,因为它在我们创造的非常优雅的模型中打了一个洞。

我有什么选择---除了分支Joda并在24小时内取出支票? (不,我不喜欢使用虚拟值的选项---比如23:59:59 ---代表24:00。)

更新:对于那些一直说不存在24:00的人,这里引用ISO 8601-2004 4.2.3注释2,3:“一个日历日[24:00]的结束与[00]重合:00]在下一个日历日的开始......“和”[hh]具有值[24]的表示仅优选表示时间间隔的结束....“


5009
2018-03-21 23:24


起源

从您的问题的上下文,我猜你的意思是说“LocalInterval由两个LocalTimes组成(开始包含,最终排他),......” - MusiGenesis
@MusiGenesis:是的,这是一个错字。我已经更新了问题以表明最终排他性。谢谢。 - Garret Wilson
非常有趣的问题。我前段时间激励我将24:00的功能引入我自己的日期/时间库Time4J。它的类似坠饰 LocalTime,即 PlainTime 支持稍宽范围00:00/24:00。我发现这个功能没有任何问题(时间顺序仍然很清楚),但可以帮助以更优雅的方式解决一些其他问题(例如,IANA-TZDB也使用24:00的值)。 - Meno Hochschild


答案:


我们最终使用的解决方案是使用00:00作为24:00的替身,整个班级的逻辑和应用程序的其余部分用于解释此本地值。这是一个真正的kludge,但它是我能想出的最少侵入性和最优雅的东西。

首先,LocalTimeInterval类保留一个内部标志,表明间隔端点是否是当天午夜(24:00)。仅当结束时间为00:00(等于LocalTime.MIDNIGHT)时,此标志才会为true。

/**
 * @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的开始和结束值手动构造Interval,因为结束时间可能表示结束时间:

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);

令人遗憾的是,我们必须首先编写一个解决方法;这是我能做的最好的事情。


5
2018-04-05 14:01



对于调度应用程序,我认为 是 最优雅的解决方案。有多少次我遇到了一些人,他试图通过匹配确切的时间来代表他们的“全天事件”:把它转移到我们的时区差异中,而我们整个日历都是一团糟。这正是为什么在展望中你有一个单独的切换来代表一整天。 - YoYo


23:59:59之后,第二天00:00:00到来。所以也许用一个 LocalTime 的 0, 0 在下一个日历日?

虽然因为你的开始和结束时间是包容性的,但是23:59:59真的是你想要的。这包括第23个小时的第59分钟的第59秒,并在00:00:00准确结束范围。

没有24:00(使用时) LocalTime)。


5
2018-03-21 23:30



对不起,我原来的问题有一个错字。我的意思是“结束独家”,所以我想要的是24:00。确实有24:00这样的事情。见ISO 8601-2004第4.2.3节“午夜”。因为我的结局是独家的,这正是我想要的。 - Garret Wilson
@ garrett-wilson - 在阅读了一些Joda文档后,我有几点建议。首先,你有没有尝试过使用 LocalTime.MIDNIGHT 不变?其次,规范所说的内容以及实现的内容不一定相同。你可以尝试一下吗? System.out.println(new LocalTime(12,0).getChronology().hourOfDay().getMaximumValue())?如果该值小于24,则实现中不存在24:00。这可能是你可以在Joda中提出的一个错误。 - aroth
“23:59:59之后,第二天00:00:00到来。”不必要。有时在23:59:59之后23:59:60 - hpiers.obspm.fr/iers/bul/bulc/bulletinc.dat - Jonas
“没有24:00这样的事情。”是不准确的。 LocalTime不支持天> 23:59,但这并不是说它们不存在。 GTFS在> 24小时后运行。 developers.google.com/transit/gtfs/reference/#stop_timestxt - Linus Norton


这不是设计缺陷。 LocalDate 不处理 (24,0) 因为没有24:00这样的事情。

此外,当您想要表示介于晚上9点到凌晨3点之间的间隔时会发生什么?

这有什么问题:

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

你只需要处理结束时间可能在开始时间“之前”的可能性,并在必要时添加一天,并且只希望没有人想要表示超过24小时的间隔。

或者,将间隔表示为a的组合 LocalDate 和a Duration 要么 Period。这消除了“超过24小时”的问题。


2
2018-03-21 23:30



“[T]这里没有24:00这样的事情”。的确有。见ISO 8601-2004第4.2.3节“午夜”。因为我的结局是独家的,这正是我想要的。 - Garret Wilson
你给出的例子有什么问题,它代表了一个负面的间隔,从早上11点到凌晨00点,发生在11小时之前。考虑从00:00到00:00的间隔---这是一个没有长度的间隔,还是整天的间隔(忽略DST变化)?它应该是前者---一整天的间隔,结束独家,将是00:00至24:00。如果它不是因为它不喜欢值24而在某处抛出异常的单行代码,那么这一切都会非常出色并且符合ISO 8601标准。 - Garret Wilson


您的问题可以被定义为在包围的域上定义间隔。您的最低时间是00:00,最高时间是24:00(不包括在内)。

假设您的间隔定义为(下限,上限)。如果您需要低于<upper,则可以表示(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的排除问题。


2
2018-04-04 16:35





24:00的时间是一个困难的时间。虽然我们人类可以理解是什么意思,但编写一个API代表那个而不会对其他一切产生负面影响,但在我看来几乎是不可能的。

无效的值24在Joda-Time中被深度编码 - 试图删除它会在很多地方产生负面影响。我不建议尝试这样做。

对于您的问题,本地间隔应由两者组成 (LocalTime, LocalTime, Days) 要么 (LocalTime, Period)。后者稍微灵活一点。这需要正确支持从23:00到03:00的间隔。


1
2018-03-22 07:42



因此,请考虑您的(LocalTime,Period)提案将如何在2011年3月13日在美国发挥作用。如果我在周日00:00-12:00标记我的空房情况,使用您的方法将使用(00:00,12小时)。因为我们在3月13日失去了一个小时,你的代表将在当天00:00-13:00生成,因为在午夜的DateTime中增加12小时会产生下午1:00。如果我将结束时间保留为LocalTime,则将12:00解析为午夜的DateTime会给我正确的时间---因为夏令时,2011-03-13的间隔会缩短。 - Garret Wilson


我发现 JodaStephen的建议 (LocalTime, LocalTime, Days) 可以接受的。

考虑到2011年3月13日,您可以在周日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结束。这意味着您将在下一个日历日使用00:00的LocalTime aroth 建议。

当然最好直接使用24:00 LocalTime和ISO 8601一致,但这似乎不可能不改变JodaTime内部的很多,所以这种方法似乎是较小的邪恶。

最后但并非最不重要的是,你甚至可以用类似的东西延长一天的障碍 (16:00, 05:00, 1)...


1
2018-03-27 21:23





这是我们对TimeInterval的实现,使用null作为结束日期的结束日期。它支持overlapps()和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();
    }
}

0
2018-02-13 13:00





为了完整起见,此测试失败:

@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常量对问题不是很有用,正如有人建议的那样。


0
2018-01-20 16:03





这个问题很古老,但这些答案中的很多都集中在Joda Time上,而且只能部分解决真正的潜在问题:

OP代码中的模型与其建模的实际情况不符。

不幸的是,由于你似乎关心天之间的边界条件,你的“优雅模型”并不适合你正在建模的问题。您已使用一对时间值来表示间隔。尝试将模型简化为一对简化 低于现实世界问题的复杂性。实际上,日间界限确实存在,并且一次失去了那种类型的信息。与往常一样,过度简化会导致后续复杂性,以恢复或补偿丢失的信息。真正的复杂性只能从代码的一部分推到另一部分。

现实的复杂性只能通过“不支持的用例”的魔力来消除。

您的模型只有在问题空间才有意义,在这个问题空间中,人们不关心开始和结束时间之间可能存在多少天。这个问题空间与大多数现实世界的问题都不相符。因此,Joda Time不能很好地支持它并不奇怪。小时位置(0-24)使用25个值是a 代码味道 并且通常指出设计中的弱点。一天只有24小时,所以不需要25个值!

请注意,因为您没有捕获任何一端的日期 LocalInterval,您的班级也没有捕获足够的信息来说明夏令时。 [00:30:00 TO 04:00:00) 通常为3.5小时,但也可能是2.5小时或4.5小时。

您应该使用开始日期/时间和持续时间,或者开始日期/时间和结束日期/时间(包括开始,独占结束) 是一个很好的默认选择)。如果您打算显示结束时间,则使用持续时间变得棘手,因为夏令时,闰年和闰秒等事情。另一方面,如果您希望显示持续时间,则使用结束日期变得非常棘手。存储两者当然是危险的,因为它违反了 干燥原则。如果我正在编写这样的类,我将存储结束日期/时间并封装逻辑以通过对象上的方法获得持续时间。这样,类类的客户端并不都会提供自己的代码来计算持续时间。

我编写了一个例子,但是有一个更好的选择。使用标准 间隔 Joda时间的等级,已经接受了一个开始时刻,持续时间或结束时刻。它也会愉快地为您计算持续时间或结束时间。可悲的是,JSR-310没有间隔或类似的类别。 (虽然可以使用 ThreeTenExtra 弥补那个)

Joda Time和Sun / Oracle相对光明的人(JSR-310)都非常仔细地考虑过这些问题。你可能比他们聪明。这是可能的。然而,即使你是一个更明亮的灯泡,你的1小时可能无法完成他们多年来所做的事情。除非你处于一个深奥的边缘情况中,否则花费时间和金钱来花费精力再次猜测它们。 (当然在OP JSR-310时尚未完成......)

希望上述内容能够帮助那些在设计或修复类似问题时找到这个问题的人。


0
2018-01-12 00:09