Search code examples
c#nodatime

What is the neatest way to find the last possible instant of a LocalDate, in a particular time-zone?


The pre-existing SQL Server database we work with stores a 'period' as inclusive start -> inclusive end UTC DateTime values. (Both start & end columns are datetime2(7), automatically-converted to System.DateTime instances of DateTimeKind.UTC before we start working with them).

So, if I need to store the "whole day/month/year, given the user's time-zone" I need to find "The last possible instant of a specified LocalDate, in a particular DateTimeZone".

The methods I have are as follows:

public static LocalDateTime AtEndOfDay(this LocalDate localDate)
{
    return localDate
        .PlusDays(1)
        .AtMidnight()
        .PlusTicks(-1);
}

public static ZonedDateTime AtEndOfDay(this DateTimeZone zone, LocalDate localDate)
{
    return zone
        .AtStartOfDay(localDate.PlusDays(1))
        .Plus(Duration.FromTicks(-1));
}

I think I also need to avoid (anywhere else) mapping a "end of date" LocalDateTime using .AtLeniently(..) since if 23:59:59.9999999 is "skipped" and does not exist in the target DateTimeZone, then it would be mapped to the next available instant on the 'outer' side of the gap 00:00:00.000000, which would give me an exclusive ZonedDateTime value.

Amended Methods:

public static LocalDateTime AtEndOfDay(this LocalDate localDate)
{
    // TODO: Replace with localDate.At(LocalTime.MaxValue) when NodaTime 2.0 is released.
    return localDate
        .PlusDays(1)
        .AtMidnight()
        .PlusTicks(-1);
}

public static ZonedDateTime AtEndOfDay(this DateTimeZone zone, LocalDate localDate)
{
    return zone
        .AtStartOfDay(localDate.PlusDays(1))
        .Plus(-Duration.Epsilon);
}

public static ZonedDateTime AtEndOfDayInZone(this LocalDate localDate, DateTimeZone zone)
{
    return zone.AtEndOfDay(localDate);
}

Solution

  • Firstly, as noted in comments, any time you can work with an exclusive upper bound, that would be a good idea :)

    Your AtEndOfDay method looks reasonable to me, except that I'd use Duration.Epsilon instead of Duration.FromTicks. In particular, in Noda Time 2.0 we're going to move to a precision of nanoseconds instead of ticks; Duration.Epsilon will do what you want in both cases.

    For your LocalDate solution, it seems to me that we're missing a value of LocalTime.MaxValue (or EndOfDay), the largest representable LocalTime. If that were available, you could just write:

    return date.At(LocalTime.MaxValue);
    

    which would remove the same "ticks" problem as before. I'll try to remember to add that for 2.0 - although it will be documented with a comment to the effect of "Only use this if you're forced to by legacy systems" :)

    One downside of adding a day and then subtracting a tick (or a nanosecond) is that it will fail on LocalDate.MaxValue. This probably isn't a practical issue, but for code within Noda Time itself, we try to avoid things like that. I wouldn't try to avoid it for the ZonedDateTime version though, as it's a rather more complicated scenario. (There are probably ways of doing it, but it wouldn't be worth it.)