Hours in Day Locally using DotNet

Viewed 115

Are there any built in api's within dotnet that will tell you how many hours there are within a specified day?

This assumes that the API will appreciate daylight savings for a given timezone. So for the UK moving from GMT into summertime, there would be 23 hours in the day, and 25 in the summertime back to GMT.

I haven't as yet spotted anything in nodatime to do this directly.

I can do something like the following in NodaTime but are there any better ways?

DateTimeZone london = DateTimeZoneProviders.Tzdb["Europe/London"];
LocalDate start = new LocalDate(2021, 3, 28);
LocalDate end = new LocalDate(2021, 3, 29);

ZonedDateTime startZ = london.AtStartOfDay(start);
ZonedDateTime endZ = london.AtStartOfDay(end);
ZonedDateTime dt = startZ;

int period = 1;

while (dt.ToInstant() < endZ.ToInstant())
{
    testOutputHelper.WriteLine("Period: " + period + ", " + dt.ToString() + ", Hour: " + dt.Hour);
    
    dt = dt.PlusHours(1);
    period++;
}
2 Answers

Yes, you can definitely do it more simply in Noda Time:

public static Duration GetDayDuration(LocalDate date, DateTimeZone zone)
{
    var start = zone.AtStartOfDay(date);
    var end = zone.AtStartOfDay(date.PlusDays(1));
    return end - start;
}

That will return a Duration, which is the Noda Time representation of "an elapsed amount of time". You can use the TotalHours properties from that. (Using Hours would be a mistake, as a 25-hour duration would return 1 for the Hours property.)

Complete sample:

using NodaTime;
using System;

class Test
{
    static void Main()
    {
        DateTimeZone london = DateTimeZoneProviders.Tzdb["Europe/London"];
        ShowDayDuration(new LocalDate(2021, 3, 28), london);
        ShowDayDuration(new LocalDate(2021, 6, 19), london);
        ShowDayDuration(new LocalDate(2021, 10, 31), london);
    }
    
    public static void ShowDayDuration(LocalDate date, DateTimeZone zone)
    {
        var duration = GetDayDuration(date, zone);
        // Note: this truncation will give the result in rare cases,
        // when the UTC offset changes by a fractional number of hours.
        int hours = (int) duration.TotalHours;
        Console.WriteLine($"Duration of {date} in zone {zone.Id}: {hours} hours");
    }

    public static Duration GetDayDuration(LocalDate date, DateTimeZone zone)
    {
        var start = zone.AtStartOfDay(date);
        var end = zone.AtStartOfDay(date.PlusDays(1));
        return end.ToInstant() - start.ToInstant();
    }
}

Jon's answer is great, and shows how Noda Time makes such operations simple.

However, since the question also asked about built-in APIs, I will show an alternate approach that doesn't require Noda Time, for completeness.

using System;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
                    
public class Program
{
    public static void Main()
    {
        // Note, the following line is just an example for clarity.  If you know which form of identifier your platform supports
        // or are using TimeZoneConverter, or are using .NET 6+ (which will have conversions built-in), then you can just use the appropriate simple string.
        string tzid = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "GMT Standard Time" : "Europe/London";
        
        // Get the time zone and show a few different days and their duration.
        TimeZoneInfo zone = TimeZoneInfo.FindSystemTimeZoneById(tzid);
        ShowDayDuration(new DateTime(2021, 3, 28), zone);
        ShowDayDuration(new DateTime(2021, 6, 19), zone);
        ShowDayDuration(new DateTime(2021, 10, 31), zone);
    }
    
    public static void ShowDayDuration(DateTime date, TimeZoneInfo zone)
    {
        TimeSpan duration = GetDayDuration(date, zone);
        Console.WriteLine($"Duration of {date:yyyy-MM-dd} in zone {zone.Id}: {duration.TotalHours} hours");
    }
    
    public static TimeSpan GetDayDuration(DateTime date, TimeZoneInfo zone)
    {
        // Just to make sure the input value is strictly a date with the time truncated.
        Debug.Assert(date.Kind == DateTimeKind.Unspecified);
        Debug.Assert(date.TimeOfDay == TimeSpan.Zero);
        
        // Subtract the start of the given date and the subsequent date.
        DateTimeOffset dto1 = GetStartOfDay(date, zone);
        DateTimeOffset dto2 = GetStartOfDay(date.AddDays(1), zone);
        return dto2 - dto1;
    }
    
    public static DateTimeOffset GetStartOfDay(DateTime date, TimeZoneInfo zone)
    {
        // Just to make sure the input value is strictly a date with the time truncated.
        Debug.Assert(date.Kind == DateTimeKind.Unspecified);
        Debug.Assert(date.TimeOfDay == TimeSpan.Zero);
        
        TimeSpan offset;        
        if (zone.IsAmbiguousTime(date))
        {
            // When the date/time is ambiguous, use the larger of the two offsets because the resulting value will come first sequentially.
            offset = zone.GetAmbiguousTimeOffsets(date).Max();
        }
        else
        {
            // When the date/time is invalid, find the next valid time.  Search by 15 minute increments to accomodate oddities of various time zones.
            while (zone.IsInvalidTime(date))
            {
                date = date.AddMinutes(15);
            }

            offset = zone.GetUtcOffset(date);
        }
        
        return new DateTimeOffset(date, offset);
    }
}

Working fiddle here.

Output:

Duration of 2021-03-28 in zone Europe/London: 23 hours
Duration of 2021-06-19 in zone Europe/London: 24 hours
Duration of 2021-10-31 in zone Europe/London: 25 hours

As you can see, Noda Time's type system and built-in AtStartOfDay method makes Noda Time a simpler option, but it is still possible to get a correct answer using only the built-in types.

Related