2

My understanding is that DateTime.Today gives the time of midnight today in the local timezone.

Does DateTime.Today.AddDays(1) always give midnight tomorrow in the local timezone, or will it sometimes be off due to daylight savings?

i.e is .AddDays(1) == .AddHours(24), or is it sometimes different as it adjusts for daylight savings.

If .AddDays(1) == .AddHours(24), is there a way to get the time of midnight tomorrow local (which may be 23, 24 or 25 hours from now)?

Clinton
  • 22,361
  • 15
  • 67
  • 163
  • `DateTime.Today.AddDays(1)` will return midnight of the next day – Rufus L Oct 08 '19 at 01:47
  • @RufusL so to confirm, there is a case where `x.AddDays(1) == x.AddHours(25)`? – Clinton Oct 08 '19 at 01:51
  • No, not that I'm aware of. `AddDays` literally increments only the day portion of the date (which may in turn affect the month and year, but not the time). You can create a `DateTime` for a time that doesn't exist in daylight savings time, like `03/10/19 02:00:01 AM` (daylight savings started at 2am on 3/10). But I don't claim to be an expert here. I've just never seen `AddDays` affect the time portion of the object. – Rufus L Oct 08 '19 at 02:05
  • Your two statements seem contradictory. How does `DateTime.Today.AddDays(1)` always return midnight the next day but `x.AddDays(1) == x.AddHours(25)` is never true, given that in the first case it's literally 25 hours later? – Clinton Oct 08 '19 at 02:12
  • var y = x.AddHours(n) does **not** answer the question: What time will it be in London n hours from 'x'. – tymtam Oct 08 '19 at 04:32

3 Answers3

2

1. 'pure' DateTime

The key element is to remember that DateTime is 'just' a container which holds so called ticks which are "the number of 100-nanosecond intervals that have elapsed since 1/1/0001 12:00am". DateTime does not keep information which timezone the time it points to refers to.

When you call AddDays you are actually adding a constant TicksPerDay ticks:

public DateTime AddDays(double value) {
    return Add(value, MillisPerDay);
}

private DateTime Add(double value, int scale) {
    (...)
    return AddTicks(millis * TicksPerMillisecond);
}

AddHours also results in ticks being added as well.

When you call AddHours(24) you are saying please add 24 hours to "YYYY-MM-DD HH:mm:SS" (15:00 -> 15:00 next day, independent of the timezone, or DST). You're not asking for the moment of time 24 hours later (24h later could be 14:00 or 16:00).

In order to answer "What time will it be 24h from now?" the timezone information is needed (see NodaTime below).

Similarly, AddHours(25) means: please add 1 day and 1 hour to "YYYY-MM-DD HH:mm:SS". With DateTime 2019-Oct-06 14:07 plus 25 hours is always 2019-Oct-07 15:07.

Please note that DateTime accounts for leap years (which requires only static rules).

2. NodaTime

Noda times allows you to 'pass' time i.e. add an amount of elapsed time. In other words it allows you to answer the question "What time will it be in n hours?"

In Date and time arithmetic we can read.

Time line arithmetic is pretty simple, except you might not always get what you expect when using ZonedDateTime, due to daylight saving transitions:

DateTimeZone london = DateTimeZoneProviders.Tzdb["Europe/London"];
// 12:45am on March 25th 2012
LocalDateTime local = new LocalDateTime(2012, 3, 25, 0, 45, 00);
ZonedDateTime before = london.AtStrictly(local);
ZonedDateTime after = before + Duration.FromMinutes(20);

We start off with a local time of 12.45am. The time that we add is effectively "experienced" time - as if we'd simply waited twenty minutes. However, at 1am on that day, the clocks in the Europe/London time zone go forward by an hour - so we end up with a local time of 2:05am, not the 1:05am you might have expected.

The reverse effect can happen at the other daylight saving transition, when clocks go backward instead of forward: so twenty minutes after 1:45am could easily be 1:05am! So even though we expose the concept of "local time" in ZonedDateTime, any arithmetic performed on it is computed using the underlying time line.

tymtam
  • 31,798
  • 8
  • 86
  • 126
  • So if I call `AddHours(1)` on a local time of 1:30am and 2:30am doesn't exist (as it's when the clocks move forward) then `AddHours(1)` will throw an exception, as this can't be represented as ticks since epoch? – Clinton Oct 08 '19 at 04:13
  • `1:30am` + `1h` will be `2:30am`, because `2:30am` exists and is valid - `2:30am` doesn't exist in only some timezones. – tymtam Oct 08 '19 at 04:19
  • I thought `DateTime` _does_ contain timezone information if it has `DateTimeKind.Local`, no? – Sweeper Oct 08 '19 at 06:15
  • 1
    @Sweeper. No it does not carry the timezone, even when `Local` is specified. Some operations are executed in the context of current timezone (e.g. `ToLocaltime`) but this uses static `TimeZoneInfo.Local` that is not specific to the instance of `DT`, but to the global setting of the machine the code runs on. (A piece of residual data stored alongside the ticks: `A four-state value that describes the DateTimeKind value of the date time, with a 2nd value for the rare case where the date time is local, but is in an overlapped daylight savings time hour and it is in daylight savings time. ...` – tymtam Oct 08 '19 at 06:39
  • ... `This allows distinction of these otherwise ambiguous local times and prevents data loss when round tripping from Local to UTC time.`)` – tymtam Oct 08 '19 at 06:40
1

My understanding is that DateTime.Today gives the time of midnight today in the local timezone.

That is mostly correct, but has an edge case. It will indeed give you the current date, using the local time zone in determining it, with the time set to 00:00:00.0000000. The resulting .Kind will be DateTimeKind.Local. What it does is equivalent to any of the following:

DateTime.Now.Date
DateTime.UtcNow.ToLocalTime().Date
TimeZoneInfo.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Local).Date
// etc.

(See Difference between System.DateTime.Now and System.DateTime.Today for a complete list.)

Notice in all my examples, .Date is last. That property simply sets the time to zero. It keeps the .Kind, but it doesn't evaluate it in any way.

Where the edge case comes into play is if you are dealing with a time zone which has a transition at midnight - where the clocks go from 23:59 to 01:00. Since there is no 00:00, then the result from DateTime.Today will be a local time that doesn't exist. One such example is Santiago, Chile on 2019-09-08. Midnight doesn't exist there on that day.

Does DateTime.Today.AddDays(1) always give midnight tomorrow in the local timezone, or will it sometimes be off due to daylight savings?

i.e is .AddDays(1) == .AddHours(24), or is it sometimes different as it adjusts for daylight savings.

Neither the AddDays or AddHours functions take time zone into account. They both simply add exactly 24 hours. Like the .Date property, these functions retain the .Kind, but they do not evaluate it in way.

If .AddDays(1) == .AddHours(24), is there a way to get the time of midnight tomorrow local (which may be 23, 24 or 25 hours from now)?

Keeping in mind that midnight might not exist, as mentioned earlier, you simply need to adjust for that possibility. Here is an extension method to do that using only DateTime:

static DateTime AdjustToValidTime(this DateTime dt, TimeZoneInfo tz)
{
    if (!tz.IsInvalidTime(dt))
    {
        // The time is already valid
        return dt;
    }

    // Get the adjustment rule that applies
    var rule = tz.GetAdjustmentRules()
        .First(x => x.DateStart <= dt && x.DateEnd > dt);

    // Get the transition gap.  Often will be one hour, but not always
    TimeSpan gap = rule.DaylightDelta;

    // Add the gap to adjust to a valid time
    return dt.Add(gap);
}

You could use it like this (for example):

DateTime tomorrow = DateTime.Today.AddDays(1).AdjustToValidTime(TimeZoneInfo.Local);

If you want to see the case of a day without midnight, you can use it like this:

TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("Pacific SA Standard Time");
DateTime today = new DateTime(2019, 9, 8);
DateTime adjusted = today.AdjustToValidTime(tz); // Will be 2019-09-08 01:00
Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • "Neither the `.AddDays()` or `.AddHours()` functions take time zone into account" This is the quote I keep seeing. But it seems to contradict behaviour. I've tested and found `x.AddDays(1)` == `y.AddHours(1) (25 times)` where `y = x.ToUniversalTime()` and `x` is on a day with an extra hour. If `.AddDays()` or `.AddHours()` didn't "take time zone into account" there's no way this could occur because it wouldn't know the day is a daylight savings day. One of them must be taking it into account, at least at some stage either for the local time or universal time calculations. – Clinton Oct 09 '19 at 00:33
  • 1
    @Clinton - The `ToUniversalTime` method is where that is happening - not in the `Add` methods. – Matt Johnson-Pint Oct 09 '19 at 17:00
  • 1
    Also note, if you switch to methods that use `DateTimeOffset` instead of `DateTime`, you can see this better. And you prevent having ambiguity around fall-back transitions. I didn't cover that in my answer here, because it seemed you were more interested in behavior of `DateTime`. – Matt Johnson-Pint Oct 09 '19 at 17:02
  • 1
    By the way - you can also review [the source code for `System.DateTime`](https://github.com/dotnet/corefx/blob/master/src/Common/src/CoreLib/System/DateTime.cs). .NET is open source. :) Check out the `AddDays`, `AddHours` methods, you'll see how similar they are. Then look at `ToUniversalTime` and you'll see it invokes conversion through `TimeZoneInfo` internally. – Matt Johnson-Pint Oct 09 '19 at 17:06
0

Daylight saving is not adjusted automatically you need to adjust that from code and then you can use

DateTime.Today.AddDays(1)

which will always give you midnight time of the next day even in daylight saving.

To properly manage timezone and daylight saving use Use Noda time .net library

Noda time

Balmohan
  • 54
  • 6