Best practice for adding/subtracting from universal or local DateTime

寵の児 提交于 2021-02-10 04:36:37

问题


I'm trying to add a wrapper around DateTime to include the time zone information. Here's what I have so far:

public struct DateTimeWithZone {
    private readonly DateTime _utcDateTime;
    private readonly TimeZoneInfo _timeZone;

    public DateTimeWithZone(DateTime dateTime, TimeZoneInfo timeZone) {
        _utcDateTime = TimeZoneInfo.ConvertTimeToUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified), timeZone);
        _timeZone = timeZone;
    }

    public DateTime UniversalTime { get { return _utcDateTime; } }

    public TimeZoneInfo TimeZone { get { return _timeZone; } }

    public DateTime LocalTime { get { return TimeZoneInfo.ConvertTimeFromUtc(_utcDateTime, _timeZone); } }

    public DateTimeWithZone AddDays(int numDays) {
        return new DateTimeWithZone(TimeZoneInfo.ConvertTimeFromUtc(UniversalTime.AddDays(numDays), _timeZone), _timeZone);
    }

    public DateTimeWithZone AddDaysToLocal(int numDays) {
        return new DateTimeWithZone(LocalTime.AddDays(numDays), _timeZone);
    }
}

This has been adapted from an answer @Jon Skeet provided in an earlier question.

I am struggling with with adding/subtracting time due to problems with daylight saving time. According to the following it is best practice to add/subtract the universal time:

https://msdn.microsoft.com/en-us/library/ms973825.aspx#datetime_topic3b

The problem I have is that if I say:

var timeZone = TimeZoneInfo.FindSystemTimeZoneById("Romance Standard Time");            
var date = new DateTimeWithZone(new DateTime(2003, 10, 26, 00, 00, 00), timeZone);
date.AddDays(1).LocalTime.ToString();

This will return 26/10/2003 23:00:00. As you can see the local time has lost an hour (due to daylight saving time ending) so if I was to display this, it would say it's the same day as the day it's just added a day to. However if i was to say:

date.AddDaysToLocal(1).LocalTime.ToString();

I would get back 27/10/2003 00:00:00 and the time is preserved. This looks correct to me but it goes against the best practice to add to the universal time.

I'd appreciate it if someone could help clarify what's the correct way to do this. Please note that I have looked at Noda Time and it's currently going to take too much work to convert to it, also I'd like a better understanding of the problem.


回答1:


Both ways are correct (or incorrect) depending upon what you need to do.

I like to think of these as different types of computations:

  1. Chronological computation.

  2. Calendrical computation.

A chronological computation involves time arithmetic in units that are regular with respect to physical time. For example the addition of seconds, nanoseconds, hours or days.

A calendrical computation involves time arithmetic in units that humans find convenient, but which don't always have the same length of physical time. For example the addition of months or years (each of which have a varying number of days).

A calendrical computation is convenient when you want to add a coarse unit that does not necessarily have a fixed number of seconds in it, and yet you still want to preserve the finer field units in the date, such as days, hours, minutes and seconds.

In your local time computation, you add a day, and presuming a calendrical computation is what you intended, you preserve the local time of day, despite the fact that 1 day is not always 24 hours in the local calendar. Be aware that arithmetic in local time has the potential to result in a local time that has two mappings to UTC, or even zero mappings to UTC. So your code should be constructed such that you know this can never happen, or be able to detect when it does and react in whatever way is correct for your application (e.g. disambiguate an ambiguous mapping).

In your UTC time computation (a chronological computation), you always add 86400 seconds, and the local calendar can react however it may due to UTC offset changes (daylight saving related or otherwise). UTC offset changes can be as large as 24h, and so adding a chronological day may not even bump the local calendar day of the month by one. Chronological computations always have a result which has a unique UTC <-> local mapping (assuming the input has a unique mapping).

Both computations are useful. Both are commonly needed. Know which you need, and know how to use the API to compute whichever you need.




回答2:


Just to add to Howard's great answer, understand that the "best practice" you refer to is about incrementing by an elapsed time. Indeed, if you wanted to add 24 hours, you'd do that in UTC and you'd find you'd end up on 23:00 due to there being an extra hour in that day.

I typically consider adding a day to be a calendrical computation (using Howard's terminology), and thus it doesn't matter how many hours there are on that day or not - you increment the day in local time.

You do then have to verify that the result is a valid time on that day, as it very well may have landed you on an invalid value, in the "gap" of a forward transition. You'll have to decide how to adjust. Likewise, when you convert to UTC, you should test for ambiguous time and adjust accordingly.

Understand that by not doing any adjusting on your own, you're relying on the default behavior of the TimeZoneInfo methods, which adjust backward during an ambiguous time (even though the usually desired behavior is to adjust forward), and that ConvertTimeFromUtc will throw an exception during an invalid time.

This is the reason why ZonedDateTime in Noda Time has the concept of "resolvers" to allow you to control this behavior more specifically. Your code is missing any similar concept.

I'll also add that while you say you've looked at Noda Time and it's too much work to convert to it - I'd encourage you to look again. One doesn't necessarily need to retrofit their entire application to use it. You can, but you can also just introduce it where it's needed. For example, you might want to use it internally in this DateTimeWithZone class, in order to force you down the right path.

One more thing - When you use SpecifyKind in your input, you're basically saying to ignore whatever the input kind is. Since you're designing general purpose code for reuse, you're inviting the potential for bugs. For example, I might pass in DateTime.UtcNow, and you're going to assume it's the timezone-based time. Noda Time avoids this problem by having separate types instead of a "kind". If you're going to continue to use DateTime, then you should evaluate the kind to apply an appropriate action. Just ignoring it is going to get you into trouble for sure.



来源:https://stackoverflow.com/questions/45031306/best-practice-for-adding-subtracting-from-universal-or-local-datetime

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!