.NET NodaTime - Derive custom class from DateTimeZone error

136 Views Asked by At

Based on this question, I have this (so I can have a sense of the object later in calling functions).

But it throws an error, and I could do with some help to understand what is wrong in this code..? I have spent a day reading through multiple comprehensive NodaTime documentations, but loosing time now. Perhaps an oversight of something trivial? Cue: Under Pressure - Queen

Error is caused in GetZoneInterval() at return new ZoneInterval(): System.InvalidOperationException: Zone interval extends to the end of time.

Stack trace:

   at NodaTime.Utility.Preconditions.CheckState(Boolean expression, String message)
   at NodaTime.TimeZones.ZoneInterval.get_End()
   at WebApp.PocTasks.Models.HeardIslandDateTimeZone.GetZoneInterval(Instant instant)
   at NodaTime.DateTimeZone.GetUtcOffset(Instant instant)
   at NodaTime.ZonedDateTime..ctor(Instant instant, DateTimeZone zone, CalendarSystem calendar)
   at NodaTime.ZonedDateTime.WithZone(DateTimeZone targetZone)
   at WebApp.PocTasks.DateTimeExtension.ConvertToZonedDateTime(ZonedDateTime zonedDateTime, DateTimeZone dateTimeZone)
   at WebApp.PocTasks.Controllers.HomeController.GetUserTimezoneRecords(ZonedDateTime sourceStartTime, ZonedDateTime sourceEndTime)
    public class HeardIslandDateTimeZone : DateTimeZone
    {
        private readonly DateTimeZone _brisbane;

        /// <summary>
        /// The Territory of Heard Island and McDonald Islands is an Australian external territory.
        /// <para>French Southern and Antarctic Time (TFT) is 5 hours ahead of Coordinated Universal Time (UTC). This time zone is in use during standard time in: Indian Ocean.</para>
        /// </summary>
        /// <remarks>
        /// <para>Ref: www.timeanddate.com/worldclock/@1547315</para>
        /// </remarks>
        public HeardIslandDateTimeZone()
            : base(
                  "Australia/HeardIsland",
                  false,
                  Offset.FromHours(5),
                  Offset.FromHours(5))
        {
            this._brisbane = DateTimeZoneProviders.Tzdb["Australia/Brisbane"];
        }

        public override int GetHashCode() => RuntimeHelpers.GetHashCode(this);

        public override ZoneInterval GetZoneInterval(Instant instant)
        {
            // Return the same zone interval, but offset by 5 hours.
            var brisbaneInterval = _brisbane.GetZoneInterval(instant);
            return new ZoneInterval(
                brisbaneInterval.Name,
                brisbaneInterval.Start,
                brisbaneInterval.End,
                brisbaneInterval.WallOffset + Offset.FromHours(-5), // taking-off 5 from +10 that is Brisbane.
                brisbaneInterval.Savings);
        }
        
    }

My calling POC function is using HomeController's Index action and corresponding view.

HomeController.cs

        [HttpGet]
        public IActionResult Index()
        {
            var meeting = new ViewModelExample();
            meeting.StartTime = new DateTime(2023, 2, 18, 9, 30, 0);
            meeting.EndTime = new DateTime(2023, 2, 18, 10, 30, 0);

            var sydney = DateTimeZoneProviders.Tzdb["Australia/Sydney"];
            meeting.HostStartTime = meeting.StartTime.ConvertToZonedDateTime(sydney);
            meeting.HostEndTime = meeting.EndTime.ConvertToZonedDateTime(sydney);

            var heard = new HeardIslandDateTimeZone();
            meeting.AttendeeStartTime = meeting.HostStartTime.ConvertToZonedDateTime(heard);
            meeting.AttendeeEndTime = meeting.HostEndTime.ConvertToZonedDateTime(heard);
                       
            return View(meeting);
        }

And displaying the data on the View is:

<div>
    <p>Database Time: @Model.StartTime.ToCustomString() - @Model.EndTime.ToCustomString()</p>
    <p>Host Time (Sydney): @Model.HostStartTime.ToCustomString() - @Model.HostEndTime.ToCustomString()</p>
    <p>Attendee Time (Heard Island): @Model.AttendeeStartTime.ToCustomString() - @Model.AttendeeEndTime.ToCustomString()</p>
</div>

Note: ConvertToZonedDateTime, ToCustomString are simple extension classes to convert (b/w DateTime & ZoneDateTime) and render to string in formats like "dd/MM/yyyy HH:mm.ss" and "dd/MM/yyyy HH:mm.ss '('o<g>')'".

An identical class works well, such as this:

// Return the same zone interval, but offset by 1 hour.
            var sydneyInterval = _sydney.GetZoneInterval(instant);
            return new ZoneInterval(
                sydneyInterval.Name,
                sydneyInterval.Start,
                sydneyInterval.End,
                sydneyInterval.WallOffset + Offset.FromHours(1), // adding +1 hr to +10:00 that is Sydney.
                sydneyInterval.Savings);

I am hoping to understand what I am missing, why the later works and to resolve the error?

C#, .NET 7, NodaTime 3.1.9, ASP.NET Core Web App, VS 2022

1

There are 1 best solutions below

5
Jon Skeet On BEST ANSWER

Ah, it looks like it's because you're using the End property for an interval which extends to the end of time - so it's throwing the (documented) exception.

You can detect this and avoid calling End where it's not appropriate:

return new ZoneInterval(
    sydneyInterval.Name,
    sydneyInterval.HasStart ? sydneyInterval.Start : default(Instant?),
    sydneyInterval.HasEnd ? sydneyInterval.End : default(Instant?),
    brisbaneInterval.WallOffset + Offset.FromHours(-5),
    sydneyInterval.Savings);

However, it would be better just to use DateTimeZone.ForOffset(Offset.FromHours(5)) if you want a fixed offset. Currently your zone will vary between UTC+5 and UTC+6, because Australia/Brisbane varies between UTC+10 and UTC+11, or at least did until 1992.