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:
- count the number of whole days between
start
and end
,
- add each of the days to
start
,
- format to desired
String
representation, and
- 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.