4
\$\begingroup\$

I need to get every UTC day stamp in yyyy-MM-dd format (e.g. "2015-07-02") between any two arbitrary Date fields.

My first attempt is this:

public static Set<String> getUTCDayStringsBetween(Date startDt, Date endDt) {
    if (!startDt.before(endDt)) {
        throw new IllegalArgumentException("Start date (" + startDt + ") must be before end date (" + endDt + ")");
    }
    final TimeZone UTC = TimeZone.getTimeZone("UTC");
    final Calendar c = Calendar.getInstance();
    final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    sdf.setTimeZone(UTC);
    final Set<String> dayStrings = new LinkedHashSet<>();

    c.setTime(startDt);        
    while (true) {
        dayStrings.add(sdf.format(c.getTime()));
        c.add(Calendar.DATE, 1);//add 1 day                    
        if (c.getTime().after(endDt)) {
            //reached the end of our range. set time to the endDt and get the final day string
            c.setTime(endDt);
            dayStrings.add(sdf.format(c.getTime()));
            break;
        }
    }
    return dayStrings;
}

It seems to work, but I'm wondering if there is a more efficient way to do it (aside from using external time libraries like Joda.). Also, is there anything I'm glossing over in terms of handling time zones and day-light-savings changes appropriately.

\$\endgroup\$
  • 1
    \$\begingroup\$ Are you on Java 8? \$\endgroup\$ – h.j.k. Jul 2 '15 at 14:19
  • 1
    \$\begingroup\$ No, but I welcome answers that use Java8 features \$\endgroup\$ – bradvido Jul 2 '15 at 15:20
2
\$\begingroup\$

No, but I welcome answers that use Java8 features - bradvido

In Java 8, the new Instant class is analogous to the old Date class, in the sense that both represent a single point on a time-line. Therefore, if this was to be given a Java 8 makeover, I'll suggest modifying your method to become a wrapper-method over one that takes in two Instant objects:

public static Set<String> getUTCDayStringsBetween(Date startDt, Date endDt) {
    return getUTCDayStringsBetween(startDt.toInstant(), endDt.toInstant());
}

public static Set<String> getUTCDayStringsBetween(Instant startInstant,
        Instant endInstant) {
    // ...
}

One nice thing about the new Java 8 Time APIs, which are largely (almost completely? wholly?) based on Joda-Time, is that the new classes come with a wealth of methods, letting us easily manipulate dates, times and time zones. For starters, let's convert our Instant objects to the desired UTC time zone:

// declared as class field
public static final ZoneId UTC = ZoneId.of("Z");

// inside the method
ZonedDateTime start = startInstant.atZone(UTC);
ZonedDateTime end = endInstant.atZone(UTC).with(start.toLocalTime());

start and end now represents the UTC date-time, and crucially end is now at the same time defined in start as well (will be explained below).

Now, we just need to:

  1. count the number of whole days between start and end,
  2. add each of the days to start,
  3. format to desired String representation, and
  4. collect to the desired Set.

Putting it all together (note the comments indicating the above points):

public static Set<String> getUTCDayStringsBetween(Instant startInstant,
        Instant endInstant) {
    if (endInstant.isBefore(startInstant)) {
        throw new IllegalArgumentException("Start date (" + startInstant +
                ") must be before end date (" + endInstant + ")");
    }
    ZonedDateTime start = startInstant.atZone(UTC);
    ZonedDateTime end = endInstant.atZone(UTC).with(start.toLocalTime());
    return LongStream.rangeClosed(0, start.until(end, ChronoUnit.DAYS))    // 1
            .mapToObj(start::plusDays)                                     // 2
            .map(DateTimeFormatter.ISO_LOCAL_DATE::format)                 // 3
            .collect(Collectors.toCollection(LinkedHashSet::new));         // 4
}

start.until(end, ChronoUnit.DAYS) only counts the complete days between the two arguments, hence the necessity to set the time of end to be the same as start. Effectively, we are doing a form of 'rounding up' here.

Having generated a Stream of days to add, we do so using (the method reference) start::plusDays. Next we use the predefined DateTimeFormatter instance ISO_LOCAL_DATE to get our desired results. Finally, I use a LinkedHashSet as the backing Set implementation since I thought it may make more sense to have a defined ordering (by insertion) as the result.

Don't forget your unit tests too! If you have yet to do so, unit testing allows you to easily and quickly verify whether the implementation is correct. For example, I have tested with the following two cases, although I will suggest testing for different time zones or even the exceptional cases:

ZonedDateTime test = ZonedDateTime.of(2015, 7, 1, 19, 59, 59, 0, ZoneId.of("US/Eastern"));
getUTCDayStringsBetween(test.toInstant(), test.plusSeconds(1).toInstant())
    .forEach(System.out::println);
getUTCDayStringsBetween(test.toInstant(), test.plusDays(7).toInstant())
    .forEach(System.out::println);

The test output I get is:

2015-07-01
2015-07-02
2015-07-01
2015-07-02
2015-07-03
2015-07-04
2015-07-05
2015-07-06
2015-07-07
2015-07-08

In the first case, since we ended on UTC midnight, two dates are printed. In the second case, we include the eighth day itself (the seventh day after test), hence there are eight dates.


Also, is there anything I'm glossing over in terms of handling time zones and day-light-savings changes appropriately.

Since Date objects do not have the concept of time zones (besides the fact that they are 'zero-ed' to UTC), and therefore unaffected by daylight savings, my take is that you don't have to worry about both.

\$\endgroup\$
3
\$\begingroup\$

Instead of an infinite while loop + an if to break out, it would be more natural to convert this to a do-while loop.

The comment "//reached the end of our range. set time to the endDt and get the final day string" is pointless, it just says the same thing with words that the code already tells us perfectly clearly.

c is not a great name for a Calendar. How about cal, or even calendar ? Instead of startDt and endDt, I would just also them out.

It's a bit smelly about the interface that the method requires the date parameters in a specific order, but this is not obvious from the name. And if the caller uses the wrong order, the runtime exception is a heavy penalty, crashing the application. It's not great to have rules that cannot be enforced at compile time, but I don't have a good counter proposal for you.

\$\endgroup\$
  • \$\begingroup\$ Regarding using a list, it's possible that the last daystamp could be added twice (depending on if the last iteration of the loop falls on the same utc day stamp as the endDt), so the Set takes care of duplicates automatically. \$\endgroup\$ – bradvido Jul 2 '15 at 15:37
  • \$\begingroup\$ Good point. I edited it out \$\endgroup\$ – janos Jul 2 '15 at 15:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.