Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Eliminate ambiguous date and time values #39816

Open
sgryphon opened this issue Jul 23, 2020 · 28 comments
Open

Eliminate ambiguous date and time values #39816

sgryphon opened this issue Jul 23, 2020 · 28 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.DateTime
Milestone

Comments

@sgryphon
Copy link

sgryphon commented Jul 23, 2020

aka The Date project

Background and Motivation

Dotnet Core contains several types for representing dates and times. As noted in the guidance documentation, developers should "consider DateTimeOffset as the default date and time type for application development" (https://docs.microsoft.com/en-us/dotnet/standard/datetime/choosing-between-datetime)

However in many cases, in examples, component packages, and business software, DateTime is used, even though it is the incorrect type -- part of this is historical because DateTime existed prior to DateTimeOffset. This issue even affects Dotnet and many examples from Microsoft, where DateTime is often used where DateTimeOffset would be more appropriate.

This proposal - Everywhere that an ambigious DateTime has been used in Dotnet, it should be supplemented with an unambiguous DateTimeOffset, and these values should be promoted as the default for future use.

It is currently difficult to follow the guidance advice and use DateTimeOffset when basic classes like FileInfo only use DateTime.

There are still some uses for DateTime. For some, such as working with UTC dates and times, DateTimeOffset can also be used, and should still be considered the default, or for working with times only then TimeSpan can be used.

The cases where DateTime is the correct class are:

  • working with dates only, for example a birthdate for calculating something like drinking age only the date part is relevant -- it is generally not relevant which timezone you were born in or your current timezone.

  • working with abstract dates and times, for example in a calendar application; a common example is having a meeting at 09:00 on the first of each month, where the instance of time will vary in line with timezones. In these cases the date and time components need to be treated separately.

In both these cases, only the date part is used, not the full DateTime, and would be better served by a Date only structure; for all other cases either DateTimeOffset or another type (TimeSpan) is more appropriate. In some cases (e.g. calendar) both a Date and TimeSpan value would be needed, but also need to be handled independently (and not combined into a single DateTime value).

Promoting the use of Date, for date only, and DateTimeOffset, for other scenarios, will help eliminate the issues that can arise from using ambiguous dates, e.g. where a field is supposed to be date only bug a bug has introduce a time component into the DateTime field -- such a bug is not possible with a date only structure.

The long term goal is to eventually be able to mark DateTime as Obsolete -- something that I have proposed before.

Proposed API

The new API changes would consist of adding a Date struct, and adding DateTimeOffset properties through Dotnet to supplement where there is currently only a DateTime.

To implement unambiguous single points of time across the entire Dotnet is a large, long term project, that can be done incrementally, with the following roadmap:

  1. Implement a Date structure for those scenarios that require only date. This will eliminate the need to ambiguously use DateTime with a time component, where only date is needed.

A basic structure can be introduced, and then incrementally added to as needed.

  1. Incrementally add supplementary DateTimeOffset properties anywhere that DateTime is used in Dotnet. This would allow the guidance of "consider DateTimeOffset as the default" to be acted upon.

In some cases these values may already exist, e.g. FileSystemInfo already uses DateTimeOffset internally but just doesn't expose it in the API.

Searching the code for something like "[\s.()=+-/!&[]{}]DateTime[\s.()=+-/!&[]{}]" within .cs files gives an idea of the scope -- 10,000 references across 1,000 files.

  1. Remove the dependency from DateTimeOffset on DateTime. Internally DateTimeOffset uses DateTime, plus the offset. This should be changed to a ulong, and make DateTimeOffset a stand alone structure without dependency on DateTIme limited to conversions.

Initially, this would involve duplication of some code from DateTime to DateTimeOffset (there is also duplication in some places like comdatetime). Further incremental changes may reverse the dependency, e.g. DateTime could call an IsValidTimeWithLeapSeconds in DateTimeOffset, to then remove the duplication.

  1. As an additional aid, any example code, training material, documentation, and other guidance can also be incrementally updated, e.g. any examples that reference FileInfo should be updated to show examples using DateTimeOffset fields, not date time.

Steps 1-4 can be done in parallel, incrementally.

  1. Once all functionality has been moved across, places where DateTime is used can be incrementally marked Obsolete and/or hidden from editors (e.g. Intellisense), via [EditorBrowsable(EditorBrowsableState.Never)].

Note that marking something Obsolete is not a directly breaking change, the API is the same and does not break any runtime dependencies. There may, however, be some indirect issues when recompiling any dependencies if "treat warnings as errors" is turned on -- these would be good indicators to fix those dependencies to also use the DateTimeOffset alternative. You could potentially start this before steps 1-4 are complete, but it may be clearer to wait.

  1. Eventually, DateTime itself can be marked Obsolete, and/or hidden, having been fully replaced by DateTimeOffset and, in some cases, Date.

There may be opposition to marking DateTime Obsolete, due to the possibility of breaking build systems that have treat warnings as errors turned on (not a runtime breaking change), but if all instances within Dotnet have been replaced, and after sufficient time, this should be possible. Once this is achieved, ambiguous dates and times will have been eliminated from Dotnet.

A compromise may be to hide the class from editors, which will not break builds but help encourage future developers follow the guidance and use DateTimeOffset.

Note: I have previously raised a suggestion to mark DateTime as Obsolete.

Usage Examples

The overview example of How to get information about files, folders, and drives, (https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/file-system/how-to-get-information-about-files-folders-and-drives) would be changed to use the DateTimeOffset version of the property.

        foreach (System.IO.FileInfo fi in fileNames)
        {
            Console.WriteLine("{0}: {1}: {2}", fi.Name, fi.LastAccessAt, fi.Length);
        }

The corresponding current properties on FileInfo (FileSystemInfo) are LastAccessTime and LastAccessTimeUtc. The specific pattern used for DateTimeOffset properties can be discussed and agreed.

Other possible variations could be LastAccess, LastAccessTimeAt, LastAccessedAt, or LastAccessedTimeAt. Another possible variation is a past tense form LastAccessed similar to the IFileInfo interface from Microsoft.Extensions.FileProviders, although this could be confused with the conventions for event names.

Alternative Designs

The main well known alternative is probably Noda Time (https://nodatime.org/).

Risks

There is probably a lot of opposition to trying to replace a core construct such as DateTime, although the professional guidance is that in most cases DateTimeOffset is far more appropriate to use.

Marking it Obsolete could also indirectly cause issues with build processes in some cases, although it does not break runtime API compatibility.

@sgryphon sgryphon added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Jul 23, 2020
@Dotnet-GitSync-Bot
Copy link
Collaborator

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added the untriaged New issue has not been triaged by the area owner label Jul 23, 2020
@Clockwork-Muse
Copy link
Contributor

... we've had System.Time.Date and System.Time.Time types in corefxlab for years, although they've never been pulled in.

Personally, I rather wish that we'd get a good, complete, first-party date/time library, like I proposed years ago. For one thing, it allows representations much closer to conceptual/semantic than the limited types you've proposed. Although the migration plan you've outlined here is essentially what I imagined.

Note that DateTimeOffset is often not the correct semantic type - normally you want either an equivalent to NodaTime's Instant or DateTimeZoned (it often ends up being the correct "solution" type, since it's what's available). For instance, file access times should really be represented as an Instant (since any zone, including UTC, is irrelevant).

@sgryphon
Copy link
Author

While NodaTime is good, I think simplicity is more important. DateTimeOffset already exists in the framework, and doesn't have the drawbacks of DateTime.

DateTimeOffset can perform all the functions of Instant, such as comparison, without any of the drawbacks of DateTime. In theory a DateTime fixed to UTC could do similar, but there is a high risk of getting out of sync with any manipulation that involves local times.

DateTimeOffset does contain additional information -- not just the instance of time, but the system offset at the time it was captured / or desired to be displayed in. Whilst this takes up additional storage, it doesn't lead to any problems.

equivalent to NodaTime's Instant... file access times should really be represented as an Instant (since any zone, including UTC, is irrelevant).

The problem with Instant is that while it is okay from a programmatic side, usually at some point you want to display values to users, and showing the UTC instant would be a bit strange. DateTimeOffset is a compromise as you can, mostly, show local times to users, similar to what DateTime would, but without the ambiguity in code.

As a default, e.g. a simple programming example for new programmers, displaying a file time, or clock, in the "local" time is probably easier to understand. Plus, DateTimeOffset already exists in the framework, and is a smaller, incremental change, that could be made.

A change to Instant would be more complicated, and I question whether the slight improvement in semantics is worth the additional complexity.

or DateTimeZoned (it often ends up being the correct "solution" type, since it's what's available). For instance,

I consider DateTimeZoned a much more complicated type that, while on the surface seems good, is generally ambiguous for most uses of date and time, i.e. figuring out when something did or should happen. To correlate back to something in the real world, you are always correlating to an instant of time.

The NodaTime Instance is never ambiguous about that; likewise DateTimeOffset is never ambiguous about the instance that it represents.

In contrast, to resolve DateTimeZoned to a real world instance is dependent upon on how up to date the associated timezone database is, and ensuring that everything accessing the value is using the same database.

While DateTimeOffset could display wrong (showing 05:00 +11 instead of 04:00 +10) due to an incorrect database, the real world instance of time it represents is never ambiguous.

Maybe as a display value (user input/output) a zoned time is useful, but internally you would in most cases want to store something that represents an instance.

Where you do use it, such as a calendaring app, you probably need all the individual parts - time, timezone, date (or even day of month separate from month).

While maybe it could be useful, I think using what we already have (DateTimeOffset) is more achievable, and in an incremental fashion.

@Clockwork-Muse
Copy link
Contributor

The problem with Instant is that while it is okay from a programmatic side, usually at some point you want to display values to users, and showing the UTC instant would be a bit strange. DateTimeOffset is a compromise as you can, mostly, show local times to users, similar to what DateTime would, but without the ambiguity in code.

... Instant, in theory, doesn't have a "friendly" display (although both NodaTime and Java give it a default UTC output). You're supposed to translate it to some configured zone. This is best handled by the application itself, not automatically (ie, file times can't be translated to the "local" zone), since the local zone of the process may not be the zone desired for display (ie, on a server).

I consider DateTimeZoned a much more complicated type that, while on the surface seems good, is generally ambiguous for most uses of date and time, i.e. figuring out when something did or should happen. To correlate back to something in the real world, you are always correlating to an instant of time.

Generally uses of date/time values are split into two buckets:

  • Logged Instants, representing the exact time something (was reported to have) happened in the past or "right now". This is never ambiguous.
  • Future DateTimeZoneds, which have an estimated instant. This is, essentially, deliberately ambiguous.

This cannot be emphasized enough - when dealing with future date/time values, you need (in almost all cases) to be working with the civil time. If I put a time in my calendar for an appointment, the civil time shouldn't change if the rules do. The primary value isn't the Instant - that part should be derived.

In contrast, to resolve DateTimeZoned to a real world instance is dependent upon on how up to date the associated timezone database is, and ensuring that everything accessing the value is using the same database.

Yes, but for most real-world use cases DateTimeOffset is in the same boat, because you have to figure out what the offset for the "destination" timezone is anyways (ie, "What time is it in Sydney, Australia?").

While DateTimeOffset could display wrong (showing 05:00 +11 instead of 04:00 +10) due to an incorrect database, the real world instance of time it represents is never ambiguous.

nit: This can't display wrong, because the offset is stored. It can be recorded wrong, or estimated wrong, if the database isn't correct/up to date, but the display will reflect the "true" value.

Where you do use it, such as a calendaring app, you probably need all the individual parts - time, timezone, date (or even day of month separate from month).

.... I can't see any reason this would be beneficial. Note that date/time are not separate values for things like this, they represent a singular combined value. Note that two dates with the same "date" aren't guaranteed to share the same 24-hour period. In addition, your scheme ignores the fact that it would put the same day-of-month from every month+year combination into the same bucket, which seems strange - and will be in the wrong bucket if you're trying to find everything in "your" day-of-month, rather than "origin" day-of-month. If you're trying to make it faster to query in a database, you're going to have better results by querying with a dynamic range.

@joperezr joperezr removed the untriaged New issue has not been triaged by the area owner label Aug 24, 2020
@joperezr joperezr added this to the Future milestone Aug 24, 2020
@joperezr joperezr added the api-needs-work API needs work before it is approved, it is NOT ready for implementation label Aug 24, 2020
@joperezr
Copy link
Member

cc: @tarekgh

@tarekgh tarekgh removed the api-needs-work API needs work before it is approved, it is NOT ready for implementation label Aug 24, 2020
@tannergooding
Copy link
Member

@tarekgh, were you able to take a look at this? Is it something we should look at moving forward around or is it something we can close as not planned at this point?

@tarekgh
Copy link
Member

tarekgh commented Sep 8, 2022

@tannergooding we have already done some of this by exposing DateOnly, TimeOnly, educating user directly when to use DateTimeOffset and when avoid using DateTime. To be honest, I don't think we can obsolete DateTime in near future. DateTime mostly a problem when used for time zone related operations. We have some thoughts exposing a new type like ZonedDateTime which can be used in general for date and time with time zones. I believe this can better step to take than just trying to update DateTimeOffset and obsolete DateTime. Anyway, this is a long-term issue and with every release we add more support to the date and time area.

@Clockwork-Muse
Copy link
Contributor

DateTime mostly a problem when used for time zone related operations.

... which is mostly a problem due to DateTimeKind. You actually want a no-zone combined type like DateTime for certain scenarios (so you wouldn't deprecate DateTime, but instead DateTimeKind), but in most cases you need other/different types, depending on what your application is doing.

@mattjohnsonpint
Copy link
Contributor

@tarekgh - I don't believe this proposal was for obsoleting DateTime, but rather to systematically look for properties on .NET classes that are exposed only as DateTime where they would be better exposed as DateTimeOffset (or DateOnly or TimeOnly)

The clear example is System.IO.FileSystemInfo, which has pairs of DateTime properties for creation, last access, and last write, all of which would be better served by DateTimeOffset properties.

@tarekgh
Copy link
Member

tarekgh commented Sep 9, 2022

I don't believe this proposal was for obsoleting DateTime

from the proposal:

6. Eventually, DateTime itself can be marked Obsolete, and/or hidden, having been fully replaced by DateTimeOffset and, in some cases, Date.

but rather to systematically look for properties on .NET classes that are exposed only as DateTime where they would be better exposed as DateTimeOffset (or DateOnly or TimeOnly)

That is fine, I am not opposing that :-)

@mattjohnsonpint
Copy link
Contributor

Sure. To be more precise on the 6 steps proposed in this issue

  1. Implement a Date structure for those scenarios that require only date.

    Already completed with DateOnly / TimeOnly

  2. Incrementally add supplementary DateTimeOffset properties anywhere that DateTime is used in Dotnet

    I believe this has the most value. Similar work was done with adding TimeSpan properties/parameters in places that only had int seconds/milliseconds.

  3. Remove the dependency from DateTimeOffset on DateTime

    I'm not opposed to this, but I'm not sure that its necessary or actually adds perf benefit. Some work would need to be done to test that assumption. (The public APIs shouldn't change either way, but perhaps the internals could.)

  4. As an additional aid, any example code, training material, documentation, and other guidance can also be incrementally updated

    Always a good idea

  5. Once all functionality has been moved across, places where DateTime is used can be incrementally marked Obsolete and/or hidden from editors (e.g. Intellisense), via [EditorBrowsable(EditorBrowsableState.Never)].

    Debatable. May make sense in some areas, but not in others.

  6. Eventually, DateTime itself can be marked Obsolete ...

    I'm opposed to this.

@tarekgh
Copy link
Member

tarekgh commented Sep 9, 2022

Mostly I agree with you @mattjohnsonpint. for no. 2, do you have any candidate list for that? I am aware about System.IO.FileSystemInfo but wondering if you already have any list in mind?

@mattjohnsonpint
Copy link
Contributor

Not off hand. Someone should do a comprehensive search.

@sgryphon
Copy link
Author

  1. Eventually, DateTime itself can be marked Obsolete ...
    I'm opposed to this.

Great work with the progress on things like DateOnly (removing one of the reasons to use DateTime)

Once steps 1-5 have been done, what is the reasoning for not Obsoleting DateTime (note that obsolete doesn't mean remove now, but does mean an intention to remove in the future).

Is it because builds that have "treat warnings as errors" will fail? Or is it because you don't think that we could even remove DateTime, so marking it obsolete is misleading.

Or is it for zoned operations, e.g. a future calendar agreement to meet in Brisbane at 06:00 on 01 March 2040, which should remain valid in civil time even if daylights savings rules change? In my experience this is generally better represented as separate columns for TimeOnly and then a separate DateOnly (or something more complex) because you might be applying different rules to them (not for a one off, but for something recurring like "last day of the month")

@Clockwork-Muse
Copy link
Contributor

Or is it because you don't think that we could even remove DateTime, so marking it obsolete is misleading.

This one. Because DateTime is occasionally useful on its own.
That said, I wouldn't be opposed to marking DateTimeKind (and a few related properties/methods) obsolete.

Or is it for zoned operations, e.g. a future calendar agreement to meet in Brisbane at 06:00 on 01 March 2040, which should remain valid in civil time even if daylights savings rules change?

If you're doing this sort of work, you'd be better off using NodaTime, as we currently lack a DateTimeZoned type. That aside, even if you had the type you're still generally going to need to keep track of the offset for some use-cases; for instance if the DST rules change, you have to notify everybody not in that zone, because for them the civil time did change.

separate columns for TimeOnly and then a separate DateOnly (or something more complex)

Recurring events is probably best represented with DateTimeZoned + Period or similar (neither of which we have currently), or possibly a different set of columns for each different type of recurrence/event.

@mattjohnsonpint
Copy link
Contributor

The main reason I'm opposed is that DateTime has many valid use cases, and is widely used in real-world applications. Obsoleting it would create too much friction.

With regard to the cited advice:

... "consider DateTimeOffset as the default date and time type for application development"

This is wrong in my opinion. We should not recommend any type to be the default. Rather, each type should be described and developers should be advised to pick the type that best fits their scenario.

After all, we have 13 different numeric types to choose from, and we don't tell developers to consider one of them as the "default" numeric type. Sometimes your working with fractional values and need a double. Sometimes you're working with currency it would be better to use decimal to avoid rounding issues. Should we obsolete double and make everyone use decimal? Or advise that decimal should be the default type because it doesn't have floating-point rounding issues? I don't think so. Developers are smart enough to choose the numeric type that fits their use case. We should treat date and time types likewise.

@sgryphon
Copy link
Author

Because DateTime is occasionally useful on its own.
The main reason I'm opposed is that DateTime has many valid use cases,

I'm not sure it is even occasionally, as I struggle to come up with a scenario when you actually have a valid use for DateTime?

The one I used to use was always to store date only, e.g. for calendaring type applications; but with DateOnly that is no longer the case.

Is there any other valid use case for DateTime remaining? (i.e. that doesn't have a hidden implicit offset assumption)

and is widely used in real-world applications. Obsoleting it would create too much friction.

I agree it is widely used.

Removing it outright would create some friction; obsoleting it would create less, and help hide/prevent further use.

Should we obsolete double and make everyone use decimal? <<<

Probably not a bad idea. Double was a product of a time with limited computing resources where we had to accept imprecision in return for adequate performance. If decimal was the default then it would probably prevent a lot of bugs and mistakes.

(Although in this case I think the performance for things like graphics rendering is probably still a significant enough consideration... although they usually aren't done in C# though)

@mattjohnsonpint
Copy link
Contributor

mattjohnsonpint commented Sep 11, 2022

Is there any other valid use case for DateTime remaining?

Yes, many - and they are almost always in the future. Here are some that are easy to remember:

  • "On Christmas day this year, all our stores will close early at 2:00 PM" - ex., a national/global retailer.
    • DateTime value: 2022-12-25T14:00
    • It's not valid to include a time zone or offset, because there could be stores in different time zones.
  • "Set an alarm for next Tuesday at 8:00 AM" - ex., on a phone.
    • DateTime value: 2022-09-20T08:00
    • It's not valid to include a time zone or offset because the person could carry the phone to another time zone, such as while traveling.
  • "The law changes / contract expires / sale ends / (etc.) on July 31, 2025 at 6:00 PM Pacific Time".
    • DateTime value: 2025-01-31T18:00 and time zone ID string America/Los_Angeles
    • It's not valid to pre-compute the offset because we can't know ahead of time whether or not that offset will still be correct when that day comes around.

The last one is particularly interesting. Say that we took the "prefer DateTimeOffset" advice, and had stored 2025-01-03T18:00-08:00. Well, the US Senate already passed a bill to make DST permanent. The bill still needs to pass the US House of Representatives to become law. If it does, then the actual offset would be -07:00. Our timestamp would be an hour late. The point is - we can't actually know now whether that will be the case or not.

Consider also that on a global scale, time zones offsets and DST rules change somewhat frequently. While some countries are more stable than others, logically we still cannot be certain of what the time zone offset for any future event will be until we are actually at that moment in time. Sometimes changes happen with very little notice (see my blog post) - which can wreak havoc on pre-computed offsets or UTC equivalents.

If I had to give one "rule of thumb" to follow, it would be to pick the type that most accurately stores the original values that you have. If you know the date, store the date. If you know the date and time, then store the date and time. If you know the time zone and it's not variable, then store the time zone ID. If you know the offset (because it is either fixed or has already come to pass), then store the offset. If you don't know these things, then don't invent them. Don't try to pre-determine an offset, or convert a future time to or from UTC based on data that might change.

@mattjohnsonpint
Copy link
Contributor

Oh, and I forgot to mention - just because one should use a DateTimeOffset in a given scenario doesn't mean that everything will fall apart if you don't. There are many cases where you can get by just fine using a DateTime and never have any problems.

Say, for example, that you stored DateTime.UtcNow into a database field and then queried that field. The Kind is probably DateTimeKind.Unspecified after the round-trip, unless you went out of your way to set it as Utc during loading. So now you want to show that value in local time and call theDateTime.ToLocalTime(). Guess what, everything works correctly! Why? Because DateTime.ToLocalTime presumes that Unspecified means Utc.

Sure, if I called instead .ToUniversalTime(), it would assume Local kind and I'd have converted in the wrong direction. But probably I won't call that method in this scenario, so I never observe the problem.

My point is, deprecating DateTime all out would mean tons of red or yellow flags showing up to developers saying that they've got problems they might not actually have.

@mattjohnsonpint
Copy link
Contributor

mattjohnsonpint commented Sep 11, 2022

Thinking about this more, if we were to flag potentially problematic usage with an analyzer, I would flag DateTime.Now and DateTime.UtcNow, recommending DateTimeOffset.Now or DateTimeOffset.UtcNow instead. There's very little purpose in storing a current date and time without also storing the offset.

I suppose DateTime.Today could be flagged and replaced with DateOnly.FromDateTime(DateTimeOffset.Now.Date)

I'd also flag the DateTime to DateTimeOffset implicit operator - that one is just flat out dangerous.

@Clockwork-Muse
Copy link
Contributor

It's not valid to include a time zone or offset, because there could be stores in different time zones.

.... yeesss, although probably this gets turned into per-store date/time values pretty quickly.

It's not valid to pre-compute the offset because we can't know ahead of time whether or not that offset will still be correct when that day comes around.

I want DateTimeZoned, for this reason.
That said, sometimes you still want to estimate/store the offset (usually, query optimization).

@tarekgh
Copy link
Member

tarekgh commented Sep 12, 2022

@Clockwork-Muse

I want DateTimeZoned, for this reason.

Are you interested in submitting a proposal for that? It is in our radar, but we haven't gotten into it yet.

@sgryphon
Copy link
Author

Thanks for the expanded examples.

Calendaring (future dates) is the one item that I had flagged, but usually for more complex scenarios like "Set an alarm for every Tuesday at 08:00", where you need a more complex type to store than just DateTime. In that scenario the single date time becomes a specialised version of the more complex schedule, i..e you probably wouldn't use the DateTime type.

Maybe what we need is a FutureDateTime (not a serious suggestion) that has date and time, but does not have things like Now or implicit conversion to/from DateTimeOffset, or all those other things that cause problems.

Obsoleting (or removing) all those functions from DateTime would satisfy me; and effectively make DateTime for the future calendar dates. So I guess it is not so much the DateTime struct itself that I dislike, but the operations like Now(), conversion, etc.

Let's change step 6 to: Obsolete all of the problematic functions of DateTime (such as Now, implicit conversion, etc), and leave it for calendaring/future date operations where offset or zone are not known or applicable.

I'm pretty sure removing/hiding DateTime.Now() would solve 95% of the times DateTime is used incorrectly.


Interesting with the ToLocalTime() and ToUniversalTime() where it has the opposite effect on Unspecified times; I've seen code that got caught out with that where different paths might or might not call the functions in different orders and end up in a complete mess. A complete mess that could not happen with DateTimeOffset.

Maybe rather than default a more subtle wording is needed, such as: Usually you will be dealing with instants in time, such as "now", or when an event occurred such as a file change, log entry, or other historical record -- in these cases use the DateTimeOffset type.

If you are doing calendaring or other future date operations, consider the DateTime type.

@Clockwork-Muse
Copy link
Contributor

Maybe what we need is a FutureDateTime (not a serious suggestion)

The reason you don't want something like this is that then it makes your code dependent on the current time of execution, as opposed to using the same standard type all the time and writing methods that take a parameter for the "current" time.

I'm pretty sure removing/hiding DateTime.Now() would solve 95% of the times DateTime is used incorrectly.

.... Personally, you should actually ban all the static Now/UtcNow properties, in preference for passing in some sort of current time factory or source (or in leaf methods, just the current time value), because it immediately makes your code far more testable.
There's justification to ban the call in server-related code, but in most cases you should likely be using DateTimeOffset there...

@sgryphon
Copy link
Author

passing in some sort of current time factory

Pretty much all my code uses ISystemClock from Microsoft.AspNetCore.Authentication.

And code reviews check for any static use of Now, UtcNow, or DateTime and flag them as wrong. Many applications use "current time", e.g. for recording logs or timestamps, and very few use calendaring / future dates.

Even then, calendaring functions usually require something more complicated than a single datetime, e.g. checking a recent project it has a crontab like schedule + offset (for the schedule) ... although it could just as easily be crontab + timezone. The calendar specification then gets turned into/checked against DateTimeOffset for when to run, with no need to use the DateTime struct itself.

But as mentioned above, I realise it is not so much the struct I want to stop being used, but Now, implicit conversion, etc; sure they might sometimes work by coincidence -- i.e. keeping in the same timezone with no code changes -- but the code is inherently unsafe if you put on a different server/machine, or call methods in a different order, e.g. d.ToLocalTime() may get a different result than d.ToUniversalTime().ToLocalTime(), depending on the original value, which is weird.

@sethzollinger
Copy link

I hope this is still pursued. While the ambiguity of DateTime or rather the effects of having DateTime.Kind can be problematic, I see that this can’t be fixed any time soon.

I believe what was not discussed is the ambiguity or rather the expressiveness of DateTimeOffset. I would welcome a simpler type like NodaTime.Instant just expressing a point in time, a UTC timestamp (maybe simply named Timestamp). At least in the projects I have worked on, the actual “offset” was never relevant (and can easily be calculated). Only if it was a timestamp/instant or a generic DateTime. I know, creating yet another type seems like making things worse and maybe it is.

But for me as a dev, more specific minimalistic types (like DateOnly and TimeOnly) simplify things.

Also I can see library authors somewhat struggling with DateTimeOffset and DateTime.Kind as they are expressive and maybe hard to map to other tools. For instance Postgres has no native type capable of storing an offset. Because of that the type is not fully supported (roundtripping with offset = 0): npgsql release notes 6.0 npgsql/efcore.pg#2108 (comment)

As a side note, if a type like Timestamp is considered, I would suggest it behave mostly like and have the capabilities of a DateTime with Kind=UTC. With one exception, when ToString is called it should display its time not as UTC but in local/machine time (with the same default formatting DateTime uses). I believe this improves dev experience when printing or debugging, especially for beginners. All serializing should however be in the standard form of "…T…Z".

@Clockwork-Muse
Copy link
Contributor

With one exception, when ToString is called it should display its time not as UTC but in local/machine time (with the same default formatting DateTime uses). I believe this improves dev experience when printing or debugging, especially for beginners. All serializing should however be in the standard form of "…T…Z".

These two requirements are in conflict, ToString() is how serialization is done. Pick UTC. There isn't a good way to do a local timezone for "developer only debugging".

Pedantically, a timestamp type like this is ignorant to all timezones, they're irrelevant to the thing being talked about. UTC is commonly chosen because it's well known and "safe". A pure seconds-since serialization is also common.
For most practical purposes DateTimeOffset is sufficient. A more beneficial new type would be DateTimeZoned.

@sethzollinger
Copy link

I agree, maybe wanting a different value displayed, than is stored is wanting to much, or misleading. So that is definitely no requirement for me.

But ToString (to me this is just for display / printing) is not how serialization (to me this refers to a standardized roundtrippable format) is done. Because most of the time the default ToString produces an inadequate string representation for deserialization. The standard string representations in JSON, XML, Excel differ from what ToString would produce. This is especially true for most date/time value types. This is besides the culture specific nature of ToString. I was primarily referring to the requirement that a Timestamp needs to be exchanged as a UTC timestamp, suffixed with Z, and not with an offset.

What is usually required is to record a point in time, irrespective of the time zone or offset that the server which originally created the timestamp had. And at the moment the runtime gives me only two somewhat ambiguous options to do that: a DateTime with Kind = UTC; or a DateTimeOffset with Offset = 0.

What I am proposing, because I believe it would make things simpler, more expressive and because I value type safety. Is only having one definitive option, a specific Timestamp type to do that, store an exact point in time.

Personally I believe DateTimeZoned would be the ideal replacement for what DateTimeOffset was intended to be. Because the offset in and of itself is very inexact. As was described in previous examples (i.e. the example with America/Los_Angeles: #39816 (comment)), DateTimeOffset it is not ideal for storing points in time in the future. And I do not see how storing an offset is beneficial to points in time in the past, because it yields almost no relevant information. Which again leads me to believe that a DateTimeZoned would be much more relevant than a DateTimeOffset.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.DateTime
Projects
None yet
Development

No branches or pull requests

10 participants