3

Is it possible to (reasonably easy) add a day to a date in another timezone than that of DateTime.Local, while respecting the different adjustment rules (DST etc) for that particular timezone?

var rst = TimeZoneInfo.FindSystemTimeZoneById("Romance Standard Time");
var dayInSpecificTimezone = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, rst); // Got the datetime in specified timezone
// now I would like to add a "day" to that, still observing the rules of that timezone.. something like:
var sameTimeTheDayAfterThat = dayInSpecificTimezone.AddDays(1, rst); // no such method exists
  • 1
    Could this one answer your question? https://stackoverflow.com/questions/47710262/c-sharp-add-1-day-in-specific-timezone-to-datetimeoffset – C4d Jan 28 '20 at 16:06
  • `AddDays` does exist, but it expects a `double` parameter, and you're passing two parameters in this case. Unless I misunderstood your question. – Charmander Jan 28 '20 at 16:08
  • @C4d Thanks. It almost does it, but I need the new time in the specified timezone - so I can't throw exceptions if for instance an hour is skipped etc. – monkeycsharp Jan 28 '20 at 16:23
  • @Charmander thanks yes - I know it was a fictive method - I would like a method that takes a double and a timezoneinfo as parameters.. – monkeycsharp Jan 28 '20 at 16:24
  • @monkeycsharp you already have your `dayInSpecificTimezone` which was created out of a specific timezone (as the name says). Isnt `AddDays()` taking the passed timezone already into account? – C4d Jan 29 '20 at 09:18

2 Answers2

2

Here are extension methods that you can use for this.

First, this AddDays method matches the signature you were asking about. It operates on DateTime values:

public static DateTime AddDays(this DateTime dt, double days, TimeZoneInfo tz)
{
    // If the kind is Local or Utc, convert that point in time to the given time zone
    DateTimeKind originalKind = dt.Kind;
    if (originalKind != DateTimeKind.Unspecified)
    {
        dt = TimeZoneInfo.ConvertTime(dt, tz);
    }

    // Add days with respect to the wall time only
    DateTime added = dt.AddDays(days);

    // Resolve the added value to a specific point in time
    DateTimeOffset resolved = added.ToDateTimeOffset(tz);

    // Return only the DateTime portion, but take the original kind into account
    switch (originalKind)
    {
        case DateTimeKind.Local:
            return resolved.LocalDateTime;
        case DateTimeKind.Utc:
            return resolved.UtcDateTime;
        default: // DateTimeKind.Unspecified
            return resolved.DateTime;
    }
}

Here is another variation of that extension method. This one operates on DateTimeOffset values:

public static DateTimeOffset AddDays(this DateTimeOffset dto, double days, TimeZoneInfo tz)
{
    // Make sure the input time is in the provided time zone
    dto = TimeZoneInfo.ConvertTime(dto, tz);

    // Add days with respect to the wall time only
    DateTime added = dto.DateTime.AddDays(days);

    // Resolve the added value to a specific point in time
    DateTimeOffset resolved = added.ToDateTimeOffset(tz);

    // Return the fully resolved value
    return resolved;
}

Both of the above methods depend on the following ToDateTimeOffset extension method (which I've used in a few different posts now).

public static DateTimeOffset ToDateTimeOffset(this DateTime dt, TimeZoneInfo tz)
{
    if (dt.Kind != DateTimeKind.Unspecified)
    {
        // Handle UTC or Local kinds (regular and hidden 4th kind)
        DateTimeOffset dto = new DateTimeOffset(dt.ToUniversalTime(), TimeSpan.Zero);
        return TimeZoneInfo.ConvertTime(dto, tz);
    }

    if (tz.IsAmbiguousTime(dt))
    {
        // Prefer the daylight offset, because it comes first sequentially (1:30 ET becomes 1:30 EDT)
        TimeSpan[] offsets = tz.GetAmbiguousTimeOffsets(dt);
        TimeSpan offset = offsets[0] > offsets[1] ? offsets[0] : offsets[1];
        return new DateTimeOffset(dt, offset);
    }

    if (tz.IsInvalidTime(dt))
    {
        // Advance by the gap, and return with the daylight offset  (2:30 ET becomes 3:30 EDT)
        TimeSpan[] offsets = { tz.GetUtcOffset(dt.AddDays(-1)), tz.GetUtcOffset(dt.AddDays(1)) };
        TimeSpan gap = offsets[1] - offsets[0];
        return new DateTimeOffset(dt.Add(gap), offsets[1]);
    }

    // Simple case
    return new DateTimeOffset(dt, tz.GetUtcOffset(dt));
}

Lastly, I'll point out that there is another option to consider: Use the Noda Time library. It's ZoneDateTime.Add method has exactly this purpose.

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • 2
    This is perfection - just what I needed. Thanks a lot! – monkeycsharp Jan 29 '20 at 07:37
  • This is great!, thanks a lot! However, unless I've a mistake in my tests, when calculating the local day lengths at DST-to-ST transitions, I had to fallback to calendarDate and use the standard method on the calendarDate, and then use another ToDateTimeOffset method that prefers ST or: `var calendarDate = DateTime.Parse("2022-10-28 00:59:59.9999999").Date; var tz = TimeZoneInfo.FindSystemTimeZoneById("Asia/Amman"); var s = calendarDate.ToDateTimeOffset(tz); var e = calendarDate.AddDays(1).ToDateTimeOffset(tz);` Expected `s` to be `2022-10-27 21:00` and `e` to be `2022-10-28 22:00`. – Nae Jul 03 '21 at 11:30
  • @Nae - Those values of `s` and `e` would only be if there was a `.ToUniversalTime()` call on each, following the `ToDateTimeOffset`. In other words, the results of the `ToDateTimeOffset` are in respect to the time zone provided, not to UTC. – Matt Johnson-Pint Jul 04 '21 at 17:10
  • @MattJohnson-Pint Sure, I am using `.UtcDateTime`, I just typed the values in UTC rather than their dto representations. At any rate, my test fails because the `calendarDate` is resolved to 01:00 UTC+3 and applying the Add resolves to a day later in the timezone again with 01:00 UTC+3, which is not what I wanted. I think the proposed method just _isn't for my case_. Thanks for your response! – Nae Jul 04 '21 at 18:52
0

Adding a day to a DateTime object and displaying the date in a particular Time Zone are two separate things.

The DateTime.AddDays function can be used to add the day (i.e. Add 24 hours to your current variable). You can then display that date time in any Time Zone you prefer.

For example:

var rst = TimeZoneInfo.FindSystemTimeZoneById("Romance Standard Time");
var dayInSpecificTimezone = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, rst); // Got the datetime in specified timezone

Console.WriteLine("RST datetime now is {0}", dayInSpecificTimezone);

var sameTimeTheDayAfterThat = dayInSpecificTimezone.AddDays(1);

Console.WriteLine("RST datetime in 1 day is {0}", sameTimeTheDayAfterThat);
Console.WriteLine("local datetime in 1 day is {0}", TimeZoneInfo.ConvertTime(sameTimeTheDayAfterThat, rst, TimeZoneInfo.Local));

would give output similar to:

RST datetime now is 29/01/2020 4:31:14 AM
RST datetime in 1 day is 30/01/2020 4:31:14 AM
local datetime in 1 day is 30/01/2020 1:31:14 PM
Cam
  • 116
  • 8
  • Thanks, but I think AddDays does take TimeZone into account (e.g. in some cases it adds 23 hours instead of 24) - the problem is that it operates on "LocalTime" which I cannot specify to a different timezone than the actual "LocalTime" of the server. – monkeycsharp Jan 29 '20 at 07:36
  • 1
    Actually, `AddDays` (the built-in one) completely ignores time zones, even if the `Kind` is set to `Local`, the local time zone is not used in these calculations. It just assumes whole days. Thus if you *need* to work across a transition (DST or otherwise), then you may find the result to be off. Especially if you start or stop during the transition period. The reason the example shown here doesn't seem to have a problem is that it's starting and stopping in January, where there are no transitions occurring in the given time zone. – Matt Johnson-Pint Jan 29 '20 at 19:54
  • @MattJohnson-Pint maybe I am just majorly confused, but in my timezone (romance standard), I can do this: > var a = new DateTime(2020, 3, 28, 10, 0, 0, DateTimeKind.Local).AddDays(1); > var b = new DateTime(2020, 3, 28, 10, 0, 0, DateTimeKind.Local); > (a-b).TotalHours // 24 >(a.ToUniversalTime() - b.ToUniversalTime()).TotalHours // 23 So, doesn't that indicate that it does in fact take timezone into account or am I misreading the results? :-) – monkeycsharp Jan 30 '20 at 13:53
  • `ToUniversalTime` does. `AddDays` does not. – Matt Johnson-Pint Jan 31 '20 at 01:37