UTC offset time that will evaluate to a specific local time

48 Views Asked by At

I need to produce a UTC time that is relevant to a specific location anywhere in the world such that when the client translates the time locally it will produce the expected time.

Let me be more concrete.

The user is in Paris, +1 UTC offset.

I need to ensure the time displayed by the UX is 0300.

But what I send in the JSON payload to be the UTC offset that will produce that.

So, in this case, it needs to be 0200 in the UTC time.

This is what I've got started, but I'm pretty green to date time operations like this, so I'm not quite sure I'm even building the foundation right.

public static DateTime ToUtcOffsetFromBaseTime(this NextRunTime nextRunTime, Base? staffBase)
{
    ArgumentNullException.ThrowIfNull(staffBase, nameof(staffBase));

    var instant = DateTime.UtcNow.ToInstant();
    var baseOffset = staffBase.OffsetAsOf(instant);

    if (baseOffset.HasValue)
    {
        // get the NOW local offset time
        var baseOffsetInstant = instant.WithOffset(baseOffset.Value);

        // set the local time to the next run time
        baseOffsetInstant.PlusHours(baseOffsetInstant.Hour - nextRunTime.Hour);
        baseOffsetInstant.PlusMinutes(baseOffsetInstant.Minute - nextRunTime.Minute);

        // offset it to the relative UTC time and return
        return baseOffsetInstant.ToDateTimeOffset().UtcDateTime;
    }

    throw new Exception($"The base offset for {staffBase.BaseCode} could not be found for the date/time {instant}.");
}

The NextRunTime has the hour and minute that it needs to represent locally. So you might have an Hour of 3 and a Minute of 0 such that it needs to represent 0300 local time when the client converts it to local time. Or, you might have something like 9 for the Hour and 45 for the Minute.

public class NextRunTime
{
    public required int Hour { get; set; }

    public required int Minute { get; set; }

    public required string Type { get; set; }
}

But, I'm not sure where to go from here honestly, and I'm not sure what I've even started with is right.

1

There are 1 best solutions below

1
Mike Perrenoud On

I finally got it worked out. In the end my original solution wasn't too far off.

public static DateTime ToUtcOffsetFromBaseTime(this NextRunTime nextRunTime, Base? staffBase, DateTime? asOf)
{
    ArgumentNullException.ThrowIfNull(staffBase, nameof(staffBase));

    var instant = (asOf ?? DateTime.UtcNow).ToInstant();
    var offset = staffBase.OffsetAsOf(instant);

    if (offset.HasValue)
    {
        // get the NOW local offset time
        var offsetInstant = instant.WithOffset(offset.Value);

        // set the local time to the next run time
        var nextRunTimeOffsetInstant = offsetInstant
            .PlusHours(nextRunTime.Hour - offsetInstant.Hour)
            .PlusMinutes(nextRunTime.Minute - offsetInstant.Minute)
            .PlusSeconds(0 - offsetInstant.Second)
            .PlusMilliseconds(0 - offsetInstant.Millisecond)
            .PlusNanoseconds(0 - offsetInstant.NanosecondOfSecond);

        // offset it to the relative UTC time and return
        var nextRunTimeUtcOffsetInstant = nextRunTimeOffsetInstant.WithOffset(Offset.FromHours(0));

        return nextRunTimeUtcOffsetInstant.LocalDateTime.InUtc().ToDateTimeUtc();
    }

    throw new Exception($"The base offset for {staffBase.BaseCode} could not be found for the date/time {instant}.");
}

The basic flow can be described like this:

  • Take a moment in time (in UTC) and offset that using the stored Base offset from the database
  • Set the time components based off of the configured next run time (in local base time)
  • Convert that local base time to its relative UTC time
  • Return that to the client

I added the asOf for unit testing.

The reason this flow is so important is because the relative UTC time for each local location is going to be different.

If anybody knows a cleaner way of doing the same thing, let me know.

Here is a unit test that validates it.

[Fact]
public void ToUtcOffsetFromBaseTime()
{
    // setup
    var nextRunTime = new NextRunTime
    {
        Hour = 3,
        Minute = 0,
        Type = "STT"
    };

    var staffBase = new Base
    {
        BaseCode = "BAS",
        UTCOffsets =
        [
            new DSTOffset
            {
                TimezoneStart = Instant.FromUtc(2024, 3, 10, 15, 0),
                TimezoneEnd = Instant.FromUtc(2024, 11, 3, 14, 59, 59),
                UTCOffset = Offset.FromHours(-6)
            },
            new DSTOffset
            {
                TimezoneStart = Instant.FromUtc(2024, 11, 3, 15, 0),
                TimezoneEnd = Instant.FromUtc(2025, 3, 9, 14, 59, 59),
                UTCOffset = Offset.FromHours(-7)
            }
        ]
    };

    var asOf = new DateTime(2024, 3, 12, 2, 21, 45, DateTimeKind.Utc);

    // test
    var result = nextRunTime.ToUtcOffsetFromBaseTime(staffBase, asOf);

    Assert.Equal(9, result.Hour);
    Assert.Equal(0, result.Minute);
    Assert.Equal(0, result.Second);
    Assert.Equal(0, result.Millisecond);
    Assert.Equal(0, result.Nanosecond);
}