scalatime4j

Add duration to a moment using time4j


As I am converting code using the java.time implementation to time4j, I want to add a Duration to a Moment but I get a compilation error. Under java.time, I would do the following:

val startTime: ZonedDateTime = ...
val duration: TemporalAmount = ...
val endTime: ZonedDateTime = startTime.plus(duration)

Using time4j though, the same does not work:

val startTime: Moment = ...
val duration: Duration[ClockUnit] = ...
val endTime: Moment = startTime.plus(duration)

The compiler complains about the generics interaction between the Duration and the Moment. No matter which way I create a Duration (at least that I found), it would need to have an associated generic of java.util.concurrent.TimeUnit, because Moment implements TimePoint[java.util.concurrent.TimeUnit] and the Moment#plus method hence needs a Duration[java.util.concurrent.TimeUnit] with the same associated generic as Moment for the time unit.

This surprises me that java.util.concurrent.TimeUnit is used as this is not a time4j type. Am I missing the finer details around this choice? I have the impression that this was decided by design.

One way that works is if I use a PlainTimestamp and adds a Duration[ClockUnit | CalendarUnit | IsoUnit], as the former type implements TimePoint[IsoUnit, PlainTime] and overloads extra supported units. Then I can convert the PlainTimestamp to whatever timezone I need afterward. Is this the intended design?

(And still:) What's the proper Duration type to use with Moment.plus method?

I am using time4j version 4.27.2


Solution

  • Short answer how to migrate zonedDateTime.plus(Duration.ofHours(24)):

    Moment.from(zonedDateTime.toInstant()).plus(MachineTime.of(24, TimeUnit.HOURS));
    

    See also the API of the global duration type MachineTime.

    Long answer in detail:

    In contrast to the java.time-API which only knows only one single class ChronoUnit for representing the most used temporal units applicable on arbitrary temporal entities (and the general interface TemporalUnit), the unit and duration design of the library Time4J is much more fine-grained.

    Why different unit types for Moment and PlainTimestamp?

    Partially this is already answered by the last item about suitable sets of units. Example, months and years do not make much sense for machine-like types as Moment. Therefore the chosen enum java.util.concurrent.TimeUnit covers what is needed as units for Moment.

    In addition, the different unit types of Time4J help to differentiate. A net.time4j.Duration<ClockUnit> is calculated in a local context while a MachineTime<TimeUnit> is calculated as global duration. This is not only true for clock-related units like hours but also for calendrical units. A year is not simply a year. We have ISO-calendar years (corresponding to gregorian years). We have ISO-week-based-years (length of 364 or 371 days). We have islamic years (354 or 355 days) and so on. Therefore Time4J knows a lot of different calendar units (watch out the API of the calendar-module). So Time4J finally adopted a design to prevent comparisons of durations with different unit types (which would be like comparing apples and oranges).

    Following example for the rare case of changing the international dateline in Samoa (2011-12-30 was left out) demonstrates how important the distinction of unit types might be:

    In Time4J, we just use different unit types to express if the arithmetic happens on the local or on the global timeline. Conclusion: In Java-8, we have to carefully study the context, in Time4J the unit types give valuable extra information.

    ZonedDateTime zdt = ZonedDateTime.of(2011, 12, 29, 0, 0, 0, 0, "Pacific/Apia");
    Moment m1 = Moment.from(zdt.toInstant());
    Moment m2 = m1.plus(MachineTime.of(24, TimeUnit.HOURS));
    assertThat(m2.isSimultaneous(m1.plus(MachineTime.of(1, TimeUnit.DAYS))), is(true));
    System.out.println(m2); // 2011-12-30T10:00:00Z
    System.out.println(m2.toZonalTimestamp(PACIFIC.APIA)); // 2011-12-31T00
    System.out.println(m1.toZonalTimestamp(PACIFIC.APIA).plus(2, CalendarUnit.DAYS)); // 2011-12-31T00
    

    The objects TimeUnit.DAYS and CalendarUnit.DAYS are obviously not the same. They require even different amounts (1 versus 2) to create the same results.

    Side note:

    I have now shortened the first version of my answer - mainly leaving out the Java-8-related stuff because I think it is easily possible to write too much about the topic of unit/duration-design in the narrow context of your question (and I have not even given any kind of complete answer). A tutorial page or extra documentation page like here on SO would indeed be a better place.

    But at least two other points not mentioned in my answer might be interesting for you, too (concerns net.time4j.Duration): Sign handling and specialized timezone-metric. Last one can even be an alternative for MachineTime in some cases.