What's the proper and more concise way to get the ZonedDateTime(s) which represent the start and the end of the current day in the timezone set on the system on which the code runs?
Isn't the following code too much complicated?
ZonedDateTime nowInZone = SystemClock.Instance.Now.InZone(DateTimeZoneProviders.Bcl.GetSystemDefault());
ZonedDateTime start = new LocalDateTime(nowInZone.Year, nowInZone.Month, nowInZone.Day, 0, 0, 0).InZoneStrictly(DateTimeZoneProviders.Bcl.GetSystemDefault());
ZonedDateTime end = new LocalDateTime(nowInZone.Year, nowInZone.Month, nowInZone.Day, 23, 59, 59).InZoneStrictly(DateTimeZoneProviders.Bcl.GetSystemDefault());
Given those values, I need to test if another ZonedDateTime is between them.
The AtStartOfDay
value on the DateTimeZone
object has the magic you're looking for.
// Get the current time
IClock systemClock = SystemClock.Instance;
Instant now = systemClock.Now;
// Get the local time zone, and the current date
DateTimeZone tz = DateTimeZoneProviders.Tzdb.GetSystemDefault();
LocalDate today = now.InZone(tz).Date;
// Get the start of the day, and the start of the next day as the end date
ZonedDateTime dayStart = tz.AtStartOfDay(today);
ZonedDateTime dayEnd = tz.AtStartOfDay(today.PlusDays(1));
// Compare instants using inclusive start and exclusive end
ZonedDateTime other = new ZonedDateTime(); // some other value
bool between = dayStart.ToInstant() <= other.ToInstant() &&
dayEnd.ToInstant() > other.ToInstant();
A couple of points:
It's better to get in the habit of separating the clock instance from the call to Now. This makes it easier to replace the clock later when unit testing.
You only need to get the local time zone once. I prefer to use the Tzdb
provider, but either provider will work for this purpose.
For the end of day, it's better to use the start of the next day. This prevents you from having to deal with granularity issues, such as whether you should take 23:59, 23:59:59, 23:59.999, 23:59:59.9999999, etc. Also, it makes it easier to get whole-number results when doing math.
In general, date+time ranges (or time-only ranges) should be treated as half-open intervals [start,end)
- while date-only ranges should be treated as fully-closed intervals [start,end]
.
Because of this, the start is compared with <=
but the end is compared with >
.
If you know for certain that the other ZonedDateTime
value is in the same time zone and uses the same calendar, you can omit the calls to ToInstant
and just compare them directly.
Update
As Jon mentioned in comments, the Interval
type may be a useful convenience for this purpose. It is already set up to work with a half-open range of Instant
values. The following function will get the interval for a the current "day" in a particular time zone:
public Interval GetTodaysInterval(IClock clock, DateTimeZone timeZone)
{
LocalDate today = clock.Now.InZone(timeZone).Date;
ZonedDateTime dayStart = timeZone.AtStartOfDay(today);
ZonedDateTime dayEnd = timeZone.AtStartOfDay(today.PlusDays(1));
return new Interval(dayStart.ToInstant(), dayEnd.ToInstant());
}
Call it like this (using the same values from above):
Interval day = GetTodaysInterval(systemClock, tz);
And now comparison can be done with the Contains
function:
bool between = day.Contains(other.ToInstant());
Note that you still have to convert to an Instant
, as the Interval
type is not time zone aware.