Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Joda Time LocalTime of 24:00 end-of-day

We're creating a scheduling application and we need to represent someone's available schedule during the day, regardless of what time zone they are in. Taking a cue from Joda Time's Interval, which represents an interval in absolute time between two instances (start inclusive, end exclusive), we created a LocalInterval. The LocalInterval is made up of two LocalTimes (start inclusive, end exclusive), and we even made a handy class for persisting this in Hibernate.

For example, if someone is available from 1:00pm to 5:00pm, we would create:

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

So far so good---until someone wants to be available from 11:00pm until midnight on some day. Since the end of an interval is exclusive, this should be easily represented as such:

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

Ack! No go. This throws an exception, because LocalTime cannot hold any hour greater than 23.

This seems like a design flaw to me---Joda didn't consider that someone may want a LocalTime that represents a non-inclusive endpoint.

This is really frustrating, as it blows a hole in what was otherwise a very elegant model that we created.

What are my options---other than forking Joda and taking out the check for hour 24? (No, I don't like the option of using a dummy value---say 23:59:59---to represent 24:00.)

Update: To those who keep saying that there is no such thing as 24:00, here's a quote from ISO 8601-2004 4.2.3 Notes 2,3: "The end of one calendar day [24:00] coincides with [00:00] at the start of the next calendar day ..." and "Representations where [hh] has the value [24] are only preferred to represent the end of a time interval ...."

like image 946
Garret Wilson Avatar asked Mar 21 '11 23:03

Garret Wilson


5 Answers

It's not a design flaw. LocalDate doesn't handle (24,0) because there's no such thing as 24:00.

Also, what happens when you want to represent an interval between, say 9pm and 3am?

What's wrong with this:

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

You just have to handle the possibility that the end time might be "before" the start time, and add a day when necessary, and just hope that noone wants to represent an interval longer than 24 hours.

Alternatively, represent the interval as a combination of a LocalDate and a Duration or Period. That removes the "longer than 24 hours" problem.

like image 105
skaffman Avatar answered Nov 19 '22 06:11

skaffman


For the sake of completeness this test fails:

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

This means the MIDNIGHT constant is not very usefull for the problem, as someone suggested.

like image 40
Jaime Casero Avatar answered Sep 22 '22 01:09

Jaime Casero


Well after 23:59:59 comes 00:00:00 on the next day. So maybe use a LocalTime of 0, 0 on the next calendar day?

Although since your start and end times are inclusive, 23:59:59 is really what you want anyways. That includes the 59th second of the 59th minute of the 23rd hour, and ends the range exactly on 00:00:00.

There is no such thing as 24:00 (when using LocalTime).

like image 5
aroth Avatar answered Nov 19 '22 06:11

aroth


The solution we finally went with was to use 00:00 as a stand-in for 24:00, with logic throughout the class and the rest of the application to interpret this local value. This is a true kludge, but it's the least intrusive and most elegant thing I could come up with.

First, the LocalTimeInterval class keeps an internal flag of whether the interval endpoint is end-of-day midnight (24:00). This flag will only be true if the end time is 00:00 (equal to 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;
}

By default the constructor considers 00:00 to be beginning-of-day, but there is an alternate constructor for manually creating an interval that goes all day:

public LocalTimeInterval(final LocalTime start, final LocalTime end, final boolean considerMidnightEndOfDay)
{
    ...
    this.isEndOfDay = considerMidnightEndOfDay && LocalTime.MIDNIGHT.equals(end);
}

There is a reason why this constructor doesn't just have a start time and an "is end-of-day" flag: when used with a UI with a drop-down list of times, we don't know if the user will choose 00:00 (which is rendered as 24:00), but we know that as the drop-down list is for the end of the range, in our use case it means 24:00. (Although LocalTimeInterval allows empty intervals, we don't allow them in our application.)

Overlap checking requires special logic to take care of 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());
}

Similarly, converting to an absolute Interval requires adding another day to the result if isEndOfDay() returns true. It is important that application code never constructs an Interval manually from a LocalTimeInterval's start and end values, as the end time may indicate end-of-day:

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

When persisting LocalTimeInterval in the database, we were able to make the kludge totally transparent, as Hibernate and SQL have no 24:00 restriction (and indeed have no concept of LocalTime anyway). If isEndOfDay() returns true, our PersistentLocalTimeIntervalAsTime implementation stores and retrieves a true time value of 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));

and

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

It's sad that we had to write a workaround in the first place; this is the best I could do.

like image 4
Garret Wilson Avatar answered Nov 19 '22 04:11

Garret Wilson


Your problem can be framed as defining an interval on a domain that wraps around. Your min is 00:00, and your max is 24:00 (not inclusive).

Suppose your interval is defined as (lower, upper). If you require that lower < upper, you can represent (21:00, 24:00), but you are still unable to represent (21:00, 02:00), an interval that wraps across the min/max boundary.

I don't know whether your scheduling application would involve wrap-around intervals, but if you are going to go to (21:00, 24:00) without involving days, I don't see what will stop you from requiring (21:00, 02:00) without involving days (thus leading to a wrap-around dimension).

If your design is amenable to a wrap-around implementation, the interval operators are quite trivial.

For example (in pseudo-code):

is x in (lower, upper)? :=
if (lower <= upper) return (lower <= x && x <= upper)
else return (lower <= x || x <= upper)

In this case, I have found that writing a wrapper around Joda-Time implementing the operators is simple enough, and reduces impedance between thought/math and API. Even if it is just for the inclusion of 24:00 as 00:00.

I do agree that the exclusion of 24:00 annoyed me at the start, and it'll be nice if someone offered a solution. Luckily for me, given that my use of time intervals is dominated by wrap-around semantics, I always end up with a wrapper, which incidentally solves the 24:00 exclusion.

like image 2
Dingfeng Quek Avatar answered Nov 19 '22 06:11

Dingfeng Quek