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]的表示仅优选表示时间间隔的结束....“     
已邀请:
我们最终使用的解决方案是使用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);
令人遗憾的是,我们必须首先编写一个解决方法;这是我能做的最好的事情。     
23:59:59之后,第二天00:00:00到来。所以也许在下一个日历日使用
LocalTime
0, 0
? 虽然因为你的开始和结束时间是包容性的,但是23:59:59真的是你想要的。这包括第23个小时的第59分钟的第59秒,并在00:00:00准确结束范围。 没有24:00(使用
LocalTime
时)。     
这不是设计缺陷。
LocalDate
不处理
(24,0)
因为没有24:00这样的事情。 此外,如果您想表示介于晚上9点到凌晨3点之间的间隔,会发生什么? 这有什么问题:
new LocalInterval(new LocalTime(23, 0), new LocalTime(0, 0));
你只需要处理结束时间可能在开始时间“之前”的可能性,并在必要时添加一天,并且只希望没有人想要表示超过24小时的间隔。 或者,将间隔表示为
LocalDate
Duration
Period
的组合。这消除了“超过24小时”的问题。     
您的问题可以被定义为在包围的域上定义间隔。您的最低时间是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的排除问题。     
24:00的时间是一个困难的时间。虽然我们人类可以理解是什么意思,但编写一个API来代表那个而不会对其他一切产生负面影响,但在我看来,这几乎是不可能的。 无效的值24在Joda-Time中被深度编码 - 试图删除它会在很多地方产生负面影响。我不建议尝试这样做。 对于您的问题,本地间隔应包括
(LocalTime, LocalTime, Days)
(LocalTime, Period)
。后者稍微灵活一点。这需要正确支持从23:00到03:00的间隔。     
我发现JodaStephen的建议“18”可以接受。 考虑到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,就像已提出的那样。 当然最好直接使用24:00 LocalTime和ISO 8601一致,但这似乎不可能在JodaTime内部改变很多,所以这种方法似乎是较小的邪恶。 最后但并非最不重要的是,你甚至可以延长一天的障碍,比如
(16:00, 05:00, 1)
......     
这是我们对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();
    }
}
    
为了完整起见,此测试失败:
@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常量对问题不是很有用,正如有人建议的那样。     
这个问题已经过时了,但是这些答案很多都集中在Joda Time上,而且只能部分解决真正的潜在问题: OP代码中的模型与其建模的实际情况不符。 不幸的是,由于你似乎关心天之间的边界条件,你的“优雅模型”并不适合你正在建模的问题。您已使用一对时间值来表示间隔。尝试将模型简化为一对,简化了现实世界问题的复杂性。实际上,日间界限确实存在,并且有一次失去了那种类型的信息。与往常一样,过度简化会导致后续复杂性,以恢复或补偿丢失的信息。真正的复杂性只能从代码的一部分推到另一部分。 现实的复杂性只能通过“不支持的用例”的魔力来消除。 您的模型只有在问题空间才有意义,在这个问题空间中,人们不关心开始和结束时间之间可能存在多少天。这个问题空间与大多数现实世界的问题都不匹配。因此,Joda Time不能很好地支持它并不奇怪。小时位置(0-24)使用25个值是代码气味,通常表示设计中的弱点。一天只有24小时,所以不需要25个值! 请注意,由于您没有在
LocalInterval
的任何一端捕获日期,因此您的班级也没有捕获足够的信息来说明夏令时。
[00:30:00 TO 04:00:00)
通常为3.5小时,但也可能是2.5或4.5小时。 您应该使用开始日期/时间和持续时间,或者开始日期/时间和结束日期/时间(包括开始,独占结束是一个很好的默认选择)。如果您打算显示结束时间,使用持续时间变得棘手,因为夏令时,闰年和闰秒等事情。另一方面,如果您希望显示持续时间,则使用结束日期变得非常棘手。存储两者当然是危险的,因为它违反了DRY原则。如果我正在编写这样的类,我将存储结束日期/时间并封装用于通过对象上的方法获得持续时间的逻辑。这样,类类的客户端并不都会提供自己的代码来计算持续时间。 我编写了一个例子,但是有一个更好的选择。使用Joda时间的标准Interval Class,它已接受一个开始时刻和持续时间或结束时刻。它也会愉快地为您计算持续时间或结束时间。可悲的是,JSR-310没有间隔或类似的类。 (虽然可以使用ThreeTenExtra来弥补这一点) Joda Time和Sun / Oracle(JSR-310)的相对聪明的人都非常仔细地考虑了这些问题。你可能比他们聪明。这是可能的。然而,即使你是一个更明亮的灯泡,你的1小时可能无法完成他们多年来所做的事情。除非你处于一个深奥的边缘情况中,否则花费时间和金钱来花费精力再次猜测它们。 (当然在OP JSR-310时尚未完成......) 希望上述内容能够帮助那些在设计或修复类似问题时找到这个问题的人。     

要回复问题请先登录注册