12

我们正在创建一个日程安排应用程序,我们需要表示某人在白天的可用日程安排,无论他们在哪个时区。从 Joda Time 的间隔中得到提示,它表示两个实例之间的绝对时间间隔(包括开始,结束独占),我们创建了一个LocalInterval。LocalInterval 由两个 LocalTime 组成(开始包含,结束排除),我们甚至制作了一个方便的类来在 Hibernate 中持久化它。

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

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

到目前为止一切顺利——直到有人想在某天晚上 11:00 到午夜有空。由于间隔的结束是排他的,这应该很容易表示为:

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] 的表示只首选表示时间间隔的结束......”

4

9 回答 9

5

23:59:59 之后就是第二天的 00:00:00。所以也许在下一个日历日 使用 a LocalTimeof ?0, 0

尽管由于您的开始和结束时间包含在内,但无论如何 23:59:59 确实是您想要的。这包括第 23 小时第 59 分钟的第 59 秒,并在 00:00:00 结束该范围。

没有 24:00(使用 时LocalTime)这样的东西。

于 2011-03-21T23:30:39.930 回答
4

我们最终采用的解决方案是使用 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);
}

这个构造函数不仅有开始时间和“结束日”标志是有原因的:当与带有时间下拉列表的 UI 一起使用时,我们不知道用户是否会选择 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);

很遗憾,我们不得不首先编写一个解决方法。这是我能做的最好的。

于 2011-04-05T14:01:27.513 回答
3

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

另外,当你想代表晚上 9 点和凌晨 3 点之间的时间间隔时会发生什么?

这有什么问题:

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

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

或者,将区间表示为 aLocalDate和 aDuration或的组合Period。这消除了“超过 24 小时”的问题。

于 2011-03-21T23:30:31.277 回答
2

您的问题可以定义为在环绕的域上定义一个间隔。你的最小值是 00:00,你的最大值是 24:00(不包括在内)。

假设您的区间定义为(下限,上限)。如果您要求 lower < 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 排除。

于 2011-04-04T16:35:15.517 回答
1

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

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

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

于 2011-03-22T07:42:01.553 回答
1

我觉得JodaStephen的提议(LocalTime, LocalTime, Days)可以接受。

(00:00, 12:00, 0)考虑到 2011 年 3 月 13 日和周日 00:00-12:00 的可用性,由于 DST,您实际上会有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)最后但并非最不重要的一点是,您甚至可以使用类似...的东西来延长一天的障碍。

于 2011-03-27T21:23:07.240 回答
1

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

@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 常量对于该问题不是很有用。

于 2014-01-20T16:03:53.483 回答
0

这是我们的 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();
    }
}
于 2013-02-13T13:00:33.093 回答
0

这个问题很老,但是其中许多答案都集中在 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 原则。如果我正在编写这样一个类,我将存储结束日期/时间并封装通过对象上的方法获取持续时间的逻辑。这样类类的客户就不会都想出自己的代码来计算持续时间。

我会编写一个示例,但还有更好的选择。使用 Joda 时间的标准间隔类,它已经接受了开始瞬间和持续时间或结束瞬间。它还将愉快地为您计算持续时间或结束时间。遗憾的是,JSR-310 没有间隔或类似的类。(尽管可以使用ThreeTenExtra来弥补)

Joda Time 和 Sun/Oracle ( JSR-310 )相对聪明的人都非常仔细地考虑过这些问题。你可能比他们聪明。这是可能的。然而,即使你是一个更亮的灯泡,你的 1 小时也可能无法完成他们花费数年的时间。除非您处于深奥的边缘案例中,否则花费精力进行第二次猜测通常是浪费时间和金钱。(当然在 OP JSR-310 还没有完成的时候......)

希望以上内容可以帮助那些在设计或解决类似问题时发现这个问题的人。

于 2016-01-12T00:09:04.983 回答