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