Open robloo opened 9 months ago
Tagging subscribers to this area: @dotnet/area-system-datetime See info in area-owners.md if you want to be subscribed.
Author: | robloo |
---|---|
Assignees: | - |
Labels: | `api-suggestion`, `untriaged`, `area-System.DateTime` |
Milestone: | - |
Would something like nodatime be expected to expose it too? @mattjohnsonpint
@danmoseley Yes, I was thinking of NodaTime specifically when I mentioned 3rd party libraries.
What exactly is your scenario here? Can you describe the details of the problem you are trying to solve here and how the proposal will solve it? Having some code examples will help here.
We have exposed TimeProvider for time abstraction in general.
This issue has been marked needs-author-action
and may be missing some important information.
@tarekgh I'm not sure why you immediately flagged this as needs authors attention. It also is unexpected that you don't recognize the utility of this. It seems obvious as explained above and the thinking is the same as INumber
.
What exactly is your scenario here?
I explained the background of the scenario above. We have too many Date/Time types and it's difficult to manage the types in both backends and frontends in a general-purpose way.
I would also advise you to speak internally with the team that developed the INumber
interface. Their reasons for doing so are the same as I'm proposing here.
Can you describe the details of the problem you are trying to solve here and how the proposal will solve it?
Yes, this originates here: https://github.com/AvaloniaUI/Avalonia/discussions/13480. In developing UI controls the underlying date/time type has become an issue. Old controls in WPF used DateTime
new controls in WinUI use DateTimeOffset
. However, each is only to select a date and really should be using DateOnly
but that didn't exist until recently. This creates a problem for app developers that now need to manage two different types (DateTime
or DateTimeOffset
) as well as their nullability. We need to clean all of this up and just let the app developer specify the type they want to use. These controls can operate on anything (generally speaking) as long as it has Year/Month/Day components to increment/decrement.
Long term you really need to remove DateTime, add a time zone ID component to DateTimeOffset and then use DateTimeOffset internally for all APIs. That will likely never happen though and is out of scope for this discussion.
We have exposed TimeProvider for time abstraction in general.
This is irrelevant. I'm not talking about providing time. I'm talking about making general-purpose code that can edit/modify any date/time types. I was clear about that above.
I was tagged, so I'll add my opinions. First, let's get a few things out of the way...
Long term you really need to remove DateTime ... and then use DateTimeOffset internally for all APIs.
That would be a bad idea, and defeat the whole purpose of having distinct purpose-specific date/time types. The whole reason DateOnly
and TimeOnly
were added is because there are many needs for having types that just handle one aspect or another. Same with DateTime
and DateTimeOffset
. If DateTime
were removed, then you'd always have to have an offset, and would be unable to handle scenarios where an offset is not available or appropriate.
... add a time zone ID component to DateTimeOffset ...
This has been discussed before. It would be called something like ZonedDateTime
or DateTimeZoned
, DateTimeWithZone
, etc. NodaTime has this, as do date/time libraries in other languages. But as you say, it's not what this issue is about.
... the thinking is the same as
INumber
Sorry, but I strongly disagree. INumber
is about supporting generic math, not UI binding.
OK - now on to the discussion about the interfaces. The main questions I have are, how do they actually solve the UI binding concern you described, and is it actually a real problem? A date-time picker control needs to ultimately bind to or produce a date and time, while a date-only picker control ultimately needs to bind to or produce a date, etc. An offset picker is quite different than a time zone ID picker, etc. It seems to me that the problem manifests only if one is attempting to build a single control to handle all scenarios, rather than scenerio-specific controls to match their scenerio-specific types.
Even if one wanted to support this, I'm not convinced that an interface-centric approach truly solves the problem. How does the control know what concrete type to construct? If it passes it back on an interface, will downstream code know how to handle it? For example, I can see a problem where a control developer decides to use NodaTime but the consuming application doesn't. If the date-picker constructs a NodaTime LocalDate
and passes it back on an IDate
interface, an exception would occur when the app receives it and tries to cast it to a DateOnly
? Even if the app used IDate
throughout their models, what happens when a storage client such as SQL Client receives it? If knows nothing about NodaTime types, it won't be able to work with it unless its internals are also updated to recognize the IDate
interface.
In general, I'm not a fan of interfaces on value types anyway. There's enough confusion as it stands trying to know when to use an interface or a concrete type for collections, but if folks start thinking they have to use IDate
or IDateTime
in their models, it's going to be even more confusing.
The rabbit hole is very deep here.
I suggest rather than trying to cram interfaces into the date-time types, someone who is more saavy on UI binding than I am should suggest another way that UI controls could bind to various similar types to solve the original concern.
If DateTime were removed, then you'd always have to have an offset, and would be unable to handle scenarios where an offset is not available or appropriate.
Ok, I'll give you that one in principle. In practice almost all code I've seen really needs either DateOnly or a DateTimeOffset these days. DateTime does nothing but create complications even when used correctly. Anyway, I shouldn't have mentioned it as it's out of scope.
... the thinking is the same as INumber
Sorry, but I strongly disagree. INumber is about supporting [generic math]
You misunderstand half of my point. Of course it is useful in UI control development but I was clear there are backend uses as well. For example internally in our code base there is a DateRange type with a Start/End member. This has to be duplicated for every single Date/Time type that it needs to operate with which should be unnecessary. So, just like INumber
this class should ideally be DateRange{T} : where T : IDateTime
. There are several other date and time calculations that could be similarly abstracted. So I disagree with your disagreement :)
The main questions I have are, how do they actually solve the UI binding concern you described, and is it actually a real problem? A date-time picker control needs to ultimately bind to or produce a date and time, while a date-only picker control ultimately needs to bind to or produce a date, etc.
I think you are missing my point here.
IDateOnly
type (DateTime, DateTimeOffset, DateOnly). As you said they don't need to care about time.Concerning binding, these controls will choose an internal type to operate with and then copy values over to the bound type supporting the interface. Works just fine as long as we capture the necessary members in the interfaces.
An offset picker is quite different than a time zone ID picker, etc.
No... time zone ID is out of scope. I was clear about that above. It isn't supported in these .NET types anyway... I'm also fully aware of NodaTime.
It seems to me that the problem manifests only if one is attempting to build a single control to handle all scenarios, rather than scenerio-specific controls to match their scenerio-specific types.
Yes, EXACTLY, there is a lot of duplication to support all these types in BOTH control's (frontend) and backend (as I mentioned above). Anyone who has spent time developing related controls will see the benefits here.
How does the control know what concrete type to construct?
default(T)... then copy the memebers from the internal type used (it doesn't matter) to the the external SelectedDate/Time. As long as everyone implements IDateOnly (for example) this works.
If the date-picker constructs a NodaTime LocalDate and passes it back on an IDate interface, an exception would occur when the app receives it and tries to cast it to a DateOnly?
Again, no, you are writing bad code if this happens. We have the type information passed to the control as a generic type argument. If you see my linked proposal about generic controls I'm actually proposing something like DatePicker{T}
so there is a concrete type defined for the corresponding SelectedDate
property. I'm not exposing these as IDateOnly
. where T : IDateOnly
is the constraint on the control itself.
For backend code (for example by DateRange
example) this also isn't an issues. We only need access to the IDateOnly
member information for the calculations.
Even if the app used IDate throughout their models, what happens when a storage client such as SQL Client receives it? If knows nothing about NodaTime types, it won't be able to work with it unless its internals are also updated to recognize the IDate interface.
Yes, of course any backend code unaware of IDateOnly
or IDate
whatever it's called cannot effectively use it. Code must be updated to be aware of IDateOnly
and it's intended ONLY for calculation purposes like INumber
. You can't be passing this to a database without defining a concrete type. So while you recognize a limitation here you are also missing a point. I'm proposing the same as what INumber
allows.
The rabbit hole is very deep here.
Not as deep as you make it sound.
I suggest rather than trying to cram interfaces into the date-time types, someone who is more saavy on UI binding than I am should suggest another way that UI controls could bind to various similar types to solve the original concern.
Don't focus on only UI. That is the example used where this came up. It applies just as much to backend date calculation code.
Every time I interact with the Microsoft team I feel I have to say please look big picture. Please see the larger vision here. If you look at the big picture to start it's clear the direction we need to go. We really need, at minimum, an IDateOnly
interface that implements most members of DateOnly
and then implement that interface on the following types: DateTime
, DateTimeOffset
, DateOnly
. Doing that would solve the majority of the ask here and really isn't that difficult. I could do it myself.
Let me put this another way that is simplified for everyone. In a perfect OOP design (ignoring struct limations) we would have the following types (focusing only on dates for this example).
Note how each derives members from the previous while adding new functionality. Code (unless it need specific members) could be written to target the base type DateOnly and work with all types. We have an enormous amount of examples for general-purpose code targeting base types only.
I'm simply proposing an IDateOnly
interface to effectively support this (analogous to INumber
). This is both because we are working with structs and the types evolved organically not designed from the get-go with each other in mind.
Use cases are:
Now if you expand that line of thinking from dates only in the example above to dates and times. You will also understand why I'm proposing all three interfaces.
If this moves forward, my vote would be for IDate
and ITime
interfaces only. IDateOnly
is redundant wording IMHO, and no need for an IDateTime
interface when DateTime
can just implement both IDate
and ITime
. I'll let others respond to the rest. (FWIW, I left Microsoft a few years ago, and was never part of the core .NET team.)
I agree IDateTime is redundant and it's not much more work to type both. IDateTime was only convenience.
I thought IDateOnly would be more obvious since it's basically abstracting the members in DateOnly. I also agree it's redundant wording there too though and my personal preference is for IDate. Since we went with DateOnly instead of Date elsewhere it might need to follow for the original reasons and for consistency though.
How does the control know what concrete type to construct?
default(T)... then copy the memebers from the internal type used (it doesn't matter) to the the external SelectedDate/Time. As long as everyone implements IDateOnly (for example) this works.
All of the types being discussed here are currently readonly struct
s, and as such cannot be copied into in this fashion. Are you also suggesting this should be changed? If you are not, this proposal probably needs to change to reflect that somehow, maybe by making the interface generic over TSelf with With(Day/Month/Year/Hour/Minute/Second/etc)
methods that return TSelf, similar to INumber
All of the types being discussed here are currently readonly structs, and as such cannot be copied into in this fashion. Are you also suggesting this should be changed? If you are not, this proposal probably needs to change to reflect that somehow, maybe by making the interface generic over TSelf with With(Day/Month/Year/Hour/Minute/Second/etc) methods that return TSelf, similar to INumber?
Ah yea, my mistake and that's an oversight.
In several cases it's only necessary for the interface to constrain a generic type argument to a class or method. The concrete type would still always be defined in usage and IDateOnly/IDate would be used read only... that said you are right.
I will amend the proposal to require TSelf on the interfaces directly.
Let me put this another way that is simplified for everyone. In a perfect OOP design (ignoring struct limations) we would have the following types (focusing only on dates for this example).
- DateOnly
- DateTime : DateOnly
- DateTimeOffset : DateTime
Note how each derives members from the previous while adding new functionality. Code (unless it need specific members) could be written to target the base type DateOnly and work with all types.
... what about time-of-day? eg, why no TimeOnly
type in that hierarchy?
And the answer shouldn't be "because you can only extend one type", because then the question just becomes "Why didn't you choose time as the base type?".
This violates the general principle of "Prefer composition over inheritance". Note that date/time types have very little behavior comparatively, instead (almost) being purely dumb datatypes.
@Clockwork-Muse Read the last part of the comment you quoted after the separator. IT WAS A SIMPLIFIED EXAMPLE. I mentioned this several times in the comment itself and it's also obvious in the issue description at the very top. Of course time is included in the discussion.
If at the end of the day we can only agree on date to be added I would take that by itself though as it's more useful.
What exactly is your scenario here? Can you describe the details of the problem you are trying to solve here and how the proposal will solve it? Having some code examples will help here.
Our use case is Blazor. We are working on MudBlazor and we have components like DatePicker and TimePicker. For example, with DatePickers, we want users to be able to use DateTime
, DateTimeOffset
, and DateOnly
. Currently, this can be achieved in three ways:
DatePickerDateOnly
, DatePickerDateTime
, and DatePickerDateTimeOffset
. This approach is too boilerplate.object
, which requires a lot of casting and type checking.object
.The INumber
interface works very well for us. For example, we use it with slider, allowing users to bind to any type that supports INumber
.
Perhaps if discriminated unions will make into the language it can be solved with it instead.
Background and motivation
.NET now has many different DateTime types:
DateTime
,DateTimeOffset
,DateOnly
, etc. Each of these were added for good reasons.DateTime
: The original but fails to account for time zone offsetsDateTimeOffset
: Accounts for only a numerical DateTimeOffset (not named IANA zones)DateOnly
: Simplified dates without worrying about time and timezone offsets complicating calculationsTimeOnly
: Simplified again with only time-related components.But there is one obvious gap. For UI programming IANA time zone or Windows ID time zones are required for user selection. It's not 1:1 to convert this to/from a numerical time zone offset which means
DateTimeOffset
can't be used in some cases as well. Then 3rd party libraries come into play to fill this gap.Now we have way too many Date/Time types to manage and no way to do it in a generic way. So my proposal is something like the
INumber
interface to bring all of this together.The use cases are not only for backend code. But also for UI controls. For example UI frameworks can start writing generic Date/TimePickers and allow the app to define the type they need to work with. An
IDate
interface is perfect for this to support all known date/time types (since a DatePicker control only cares about the date-related components).API Proposal
I'll flesh this out when I have more time.
API Usage
TBD
Alternative Designs
It needs to be discussed:
IDateOnly
andITimeOnly
instead and model them almost entirely based onDateOnly
andTimeOnly
. ThenIDateTime
will simply be a union of the two interfaces.ITimeOnly
are debatable. Should this be only high-level hours/minutes/seconds (all that usually needed for general-purpose or UI code) or should it include all members down to ticks. The answer is likely all members down to ticks. However, some 3rd party libraries may not support all these members. Therefore, there could be multiple levels to these interfaces like what was done for INumber. BasicallyIBasicTimeOnly
with only hours/minutes/seconds and then anITimeOnly
that derives from that and includes everything.Risks
This seems low risk to me as it is only abstracting common, shared properties already used in all the types.