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

Introduce Date and Time only structs #49036

Closed
Tracked by #48253
tarekgh opened this issue Mar 3, 2021 · 159 comments · Fixed by #50980
Closed
Tracked by #48253

Introduce Date and Time only structs #49036

tarekgh opened this issue Mar 3, 2021 · 159 comments · Fixed by #50980
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.DateTime
Milestone

Comments

@tarekgh
Copy link
Member

tarekgh commented Mar 3, 2021

Background and Motivation

.NET expose DateTime and DateTimeOffset for handling date and time. It has been requested multiple time to expose Date only and Time only structs. There are many scenarios users need to handle Dates without caring about time at all. And scenarios need to handle time without caring about Date. Users today try to work around that by using DateTime or DateTimeOffset but users need to truncate the parts they don’t need and want to ensure the remaining values is not affected. This is error prone as DateTime and DateTimeOffset also carry some time zone related things and it is not meant handle absolute date and time.

Also, the interop scenarios with components like ADO.Net, SQL Client,...et.c which having types for Date and Time only require the users to handle the truncation manually.

There are detailed discussions about such requested in the reported issues #14744, #1740 and #14089. Initially we have suggested to expose these features outside the core as a NuGet package. This is implemented in corefxlab https://github.com/dotnet/corefxlab/tree/master/src/System.Time. Users still requesting this functionality to be in core, and they wanted to avoid dependencies on third party libraries and make sense such functionality be in the core.

Here is some text picked up from different issues talking about the user scenarios:

Right now, I'm working on API to store shop working hours.
I'm using TimeSpan type to represent time of day in request types and DB models, but it's quite painful - I need to add separate validations for >0 and <24h etc.
I could use DateTime type, and consider Time component only, but it leads to issues with serialization - DateTime.Parse("07:00").ToString() -> "11/9/2020 7:00:00 AM".
I often need separate Date type as well, but it's a bit less painful to use DateTime type for Date only (comparing to case with Time only).
I'm instead interested in the Date class most of all.
We have a lot of products that is valid from date X to date Y. When we used DateTimeOffset we always had to remember to start at date X 00:00:00 and end at date Y 23:59:59.999. Then we switched to NodaTime and used LocalDate from there and all a sudden we could just compare dates without bother about time. We store Date in the database and map the column to a LocalDate now instead of a DateTimeOffset (which makes no sense since Date in the DB don't have time or offset) which makes our lives even easier.
I'm also interested into the type Date. In my experience it's a fairly common requirements to compare only dates, using a DateTimeOffset implies you have to always remember to ignore the time part whenever you have to do some comparison, having a Date object would make it explicit.

Best names for these types would be Date and Time but there is issue that VB already using these names and if we go with such names can conflict with VB.

Proposed API

namespace System
{
    // The name should be just Date but we couldn't use this name as VB is using the name type.
    // Using DateOnly name but we may accept better names if there is any.
    public readonly struct DateOnly : IComparable, IComparable<DateOnly>, IEquatable<DateOnly>, IFormattable
    {
        public static DateOnly MaxValue { get; }
        public static DateOnly MinValue { get; }

        public DateOnly(int year, int month, int day) { throw null; }
        public DateOnly(int year, int month, int day, Calendar calendar) { throw null; }
        public DateOnly(int dayNumber) { throw null; } // Needed only for serialization as we did in DateTime.Ticks

        public int Year { get { throw null; } }
        public int Month { get { throw null; } }
        public int Day { get { throw null; } }
        public DayOfWeek DayOfWeek { get { throw null; } }
        public int DayOfYear { get { throw null; } }
        public int DayNumber { get { throw null; } } // can be used to calculate the number of days between 2 dates
        public static int DaysInMonth(int year, int month) { throw null; }

        public DateOnly AddDays(int days) { throw null; }
        public DateOnly AddMonths(int months) { throw null; }
        public DateOnly AddYears(int years) { throw null; }
        public static bool IsLeapYear(int year) { throw null; }
        public static bool operator ==(DateOnly left, DateOnly right) { throw null; }
        public static bool operator >(DateOnly left, DateOnly right) { throw null; }
        public static bool operator >=(DateOnly left, DateOnly right) { throw null; }
        public static bool operator !=(DateOnly left, DateOnly right) { throw null; }
        public static bool operator <(DateOnly left, DateOnly right) { throw null; }
        public static bool operator <=(DateOnly left, DateOnly right) { throw null; }
        public static explicit operator DateOnly(DateTime dateTime) { throw null; } // Truncate the time. Guidelines says it must be explicit, can we make it implicit instead?
        public static implicit operator DateTime(DateOnly date) { throw null; }      // add 0:0:0 to the time
        public static DateTime operator +(DateOnly d, TimeOfDay t) { throw null; }

        public static int Compare(DateOnly left, DateOnly right) { throw null; }
        public int CompareTo(DateOnly value) { throw null; }
        public int CompareTo(object? value) { throw null; }

        public bool Equals(DateOnly value) { throw null; }
        public static bool Equals(DateOnly left, DateOnly right) { throw null; }
        public override bool Equals(object? value) { throw null; }
        public override int GetHashCode() { throw null; } // day number

        // Only Allowed DateTimeStyles: AllowWhiteSpaces, AllowTrailingWhite, AllowLeadingWhite, and AllowInnerWhite
        public static DateOnly Parse(ReadOnlySpan<char> s) { throw null; }
        public static DateOnly Parse(ReadOnlySpan<char> s, IFormatProvider? provider, DateTimeStyles styles = DateTimeStyles.None) { throw null; }
        public static DateOnly ParseExact(ReadOnlySpan<char> s, ReadOnlySpan<char> format) { throw null; }
        public static DateOnly ParseExact(ReadOnlySpan<char> s, ReadOnlySpan<char> format, IFormatProvider? provider, DateTimeStyles style = DateTimeStyles.None) { throw null; }
        public static DateOnly ParseExact(ReadOnlySpan<char> s, string [] formats) { throw null; }
        public static DateOnly ParseExact(ReadOnlySpan<char> s, string [] formats, IFormatProvider? provider, DateTimeStyles style = DateTimeStyles.None) { throw null; }

        public static bool TryParse(ReadOnlySpan<char> s, out DateOnly date) { throw null; }
        public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, DateTimeStyles styles, out DateOnly date) { throw null; }
        public static bool TryParseExact(ReadOnlySpan<char> s, ReadOnlySpan<char> format, out DateOnly date) { throw null; }
        public static bool TryParseExact(ReadOnlySpan<char> s, ReadOnlySpan<char> format, IFormatProvider? provider, DateTimeStyles style, out DateOnly date) { throw null; }
        public static bool TryParseExact(ReadOnlySpan<char> s, string [] formats, out DateOnly date) { throw null; }
        public static bool TryParseExact(ReadOnlySpan<char> s, string [] formats, IFormatProvider? provider, DateTimeStyles style, out DateOnly date) { throw null; }

        // Acceptable formats:
        //      Year month pattern              y/Y
        //      Sortable date/time pattern      s   date part only // like O one
        //      RFC1123 pattern                 r/R date part only // include the day of week. based on RFC1123/rfc5322 "ddd, dd MMM yyyy"
        //      round-trip date/time pattern    o/O date part only
        //      Month/day pattern               m/M
        //      Short date pattern              d
        //      Long date pattern               D

        public string ToLongDateString() { throw null; } // use "D"     should we call it ToLongString
        public string ToShortDateString() { throw null; } // us "d"     should we call it ToShortString

        public override string ToString() { throw null; }
        public string ToString(string? format) { throw null; }
        public string ToString(System.IFormatProvider? provider) { throw null; }
        public string ToString(string? format, System.IFormatProvider? provider) { throw null; }
        public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default(ReadOnlySpan<char>), IFormatProvider? provider = null) { throw null; }
    }

    public enum Meridiem
    {
        AM, // Ante meridiem
        PM  // Post meridiem
    }

    public readonly struct TimeOfDay : IComparable, IComparable<TimeOfDay>, IEquatable<TimeOfDay>, IFormattable // TimeOfDay
    {
        public static TimeOfDay MaxValue { get; }
        public static TimeOfDay MinValue { get; }

        public TimeOfDay(int hour, int minutes) { throw null; }
        public TimeOfDay(int hour, int minutes, Meridiem meridiem) { throw null; }
        public TimeOfDay(int hour, int minutes, int seconds) { throw null; }
        public TimeOfDay(int hour, int minutes, int seconds, Meridiem meridiem) { throw null; }
        public TimeOfDay(int hour, int minutes, int seconds, int milliseconds) { throw null; }
        public TimeOfDay(int hour, int minutes, int second, int milliseconds, Meridiem meridiem) { throw null; }
        public TimeOfDay(long ticks) { throw null; }

        public int Hour { get { throw null; } }
        public int Hour12 { get { throw null; } }
        public int Minute { get { throw null; } }
        public int Second { get { throw null; } }
        public int Millisecond { get { throw null; } }
        public long Ticks { get { throw null; } }
        public Meridiem Meridiem { get { throw null; } }

        public TimeOfDay Add(TimeSpan timeSpan) { throw null; } // circular
        public TimeOfDay AddHours(double hours) { throw null; }
        public TimeOfDay AddMinutes(double minutes) { throw null; }
        public TimeOfDay AddSeconds(double seconds) { throw null; }
        public TimeOfDay AddMilliseconds(double milliseconds) { throw null; }
        public TimeOfDay AddTicks(long ticks) { throw null; }
        public bool IsBetween(TimeOfDay t1, TimeOfDay t2) { throw null; }

        public static bool operator ==(TimeOfDay left, TimeOfDay right) { throw null; }
        public static bool operator >(TimeOfDay left, TimeOfDay right) { throw null; }
        public static bool operator >=(TimeOfDay left, TimeOfDay right) { throw null; }
        public static bool operator !=(TimeOfDay left, TimeOfDay right) { throw null; }
        public static bool operator <(TimeOfDay left, TimeOfDay right) { throw null; }
        public static bool operator <=(TimeOfDay left, TimeOfDay right) { throw null; }

        public static TimeSpan operator -(TimeOfDay t1, TimeOfDay t2) { throw null; } // Always positive time span
        public static TimeOfDay operator +(TimeOfDay time, TimeSpan timeSpan) { throw null; }
        public static TimeOfDay operator -(TimeOfDay time, TimeSpan timeSpan) { throw null; }
        public static implicit operator TimeOfDay(TimeSpan timeSpan) { throw null; }
        public static explicit operator TimeOfDay(DateTime dateTime) { throw null; } // Guidelines says it must be explicit, can we make it implicit instead?
        public static DateTime operator +(TimeOfDay t, DateOnly d) { throw null; }

        public static int Compare(TimeOfDay left, TimeOfDay right) { throw null; } // by ticks
        public int CompareTo(TimeOfDay value) { throw null; }
        public int CompareTo(object? value) { throw null; }

        public bool Equals(TimeOfDay value) { throw null; }
        public static bool Equals(TimeOfDay left, TimeOfDay right) { throw null; }
        public override bool Equals(object? value) { throw null; }
        public override int GetHashCode() { throw null; }

        // Only Allowed DateTimeStyles: AllowWhiteSpaces, AllowTrailingWhite, AllowLeadingWhite, and AllowInnerWhite
        public static TimeOfDay Parse(ReadOnlySpan<char> s) { throw null; }
        public static TimeOfDay Parse(ReadOnlySpan<char> s, IFormatProvider? provider, DateTimeStyles styles = DateTimeStyles.None) { throw null; }
        public static TimeOfDay ParseExact(ReadOnlySpan<char> s, ReadOnlySpan<char> format) { throw null; }
        public static TimeOfDay ParseExact(ReadOnlySpan<char> s, ReadOnlySpan<char> format, IFormatProvider? provider, DateTimeStyles style = DateTimeStyles.None) { throw null; }
        public static TimeOfDay ParseExact(ReadOnlySpan<char> s, string [] formats) { throw null; }
        public static TimeOfDay ParseExact(ReadOnlySpan<char> s, string [] formats, IFormatProvider? provider, DateTimeStyles style = DateTimeStyles.None) { throw null; }

        public static bool TryParse(ReadOnlySpan<char> s, out TimeOfDay time) { throw null; }
        public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, DateTimeStyles styles, out TimeOfDay time) { throw null; }
        public static bool TryParseExact(ReadOnlySpan<char> s, ReadOnlySpan<char> format, out TimeOfDay time) { throw null; }
        public static bool TryParseExact(ReadOnlySpan<char> s, ReadOnlySpan<char> format, IFormatProvider? provider, DateTimeStyles style, out TimeOfDay time) { throw null; }
        public static bool TryParseExact(ReadOnlySpan<char> s, string [] formats, out TimeOfDay time) { throw null; }
        public static bool TryParseExact(ReadOnlySpan<char> s, string [] formats, IFormatProvider? provider, DateTimeStyles style, out TimeOfDay time) { throw null; }

        // Acceptable formats:
        //      Sortable date/time pattern      s   Time part only
        //      RFC1123 pattern                 r/R Time part only like o
        //      round-trip date/time pattern    o/O Time part only
        //      Short time pattern              t
        //      Long time pattern               T

        public string ToLongString() { throw null; } // use "T"     ToLongTimeString
        public string ToShortString() { throw null; } // us "t"     ToShortTimeString

        public override string ToString() { throw null; }
        public string ToString(string? format) { throw null; }
        public string ToString(IFormatProvider? provider) { throw null; }
        public string ToString(string? format, IFormatProvider? provider) { throw null; }
        public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default(ReadOnlySpan<char>), IFormatProvider? provider = null) { throw null; }
    }
}
@tarekgh tarekgh added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Mar 3, 2021
@dotnet-issue-labeler
Copy link

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-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Mar 3, 2021
@tarekgh tarekgh removed the untriaged New issue has not been triaged by the area owner label Mar 3, 2021
@tarekgh tarekgh self-assigned this Mar 3, 2021
@tarekgh tarekgh added this to the 6.0.0 milestone Mar 3, 2021
@tarekgh tarekgh changed the title Introduce Date and Timem only structs Introduce Date and Time only structs Mar 3, 2021
@tarekgh tarekgh added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Mar 3, 2021
@scalablecory
Copy link
Contributor

What is the behavior of the conversion operators re: DateTimeKind?

No need for a conversion from DateTimeOffset?

NodaTime uses the name LocalDate.

@jkotas
Copy link
Member

jkotas commented Mar 3, 2021

@jkotas
Copy link
Member

jkotas commented Mar 3, 2021

public static readonly DateOnly MaxValue;
public static readonly DateOnly MinValue;
public static readonly TimeOfDay MaxValue;
public static readonly TimeOfDay MinValue;

These should be properties. Properties have better perf characteristics than readonly fields.

@omariom
Copy link
Contributor

omariom commented Mar 3, 2021

        public static implicit operator DateOnly(DateTime dateTime) { throw null; } // Truncate the time

Operators losing information should be explicit.

@Clockwork-Muse
Copy link
Contributor

public DateOnly(int year, int month, int day, Calendar calendar) { throw null; }

DateTime doesn't retain its reference to the Calendar after initialization. Assuming that to be the case here as well, we should be explicitly calling that out so people aren't completely confused when AddMonth (and a number of other methods) don't do quite what they expect.

public static int operator -(DateOnly d1, DateOnly d2) { throw null; }

Presumably this returns the count of days, but this will hamstring us if we ever introduce a relative offset type (something containing year/month/day differences, like NodaTime's Period. An equivalent operator has also been a consistent source of errors in SQL Server. I'd much rather prefer an explicit method (or set of methods) to get this.

Also, should we add this operator:

public static DateTime operator +(DateOnly d, TimeOnly t) { throw null; }
public static readonly TimeOfDay MaxValue;

I'm assuming this is 23:59:59.9999999. The ability to support times >= 24 is requested on occasion, and is commonly seen in some late-night TV listings (and similar). It can (rarely, possibly on theoretically?) occur in observance of civil time.

public bool IsBetween(TimeOfDay t1, TimeOfDay t2) { throw null; }

BETWEEN is a consistent source of bugs for date/time in SQL, because people keep forgetting about fractional seconds, because they sometimes mean an inclusive bound and sometimes an exclusive bound. I'm waffling about accepting this as a shorthand for t1 <= this < t2 (as opposed to requiring explicit operators).

public static TimeSpan operator -(TimeOfDay t1, TimeOfDay t2) { throw null; } // Always positive time span

Both Java's and NodaTime's equivalent return negative time spans when t2 < t1, which I feel is preferable.

@scalablecory

What is the behavior of the conversion operators re: DateTimeKind?

DateTimeKind.Unspecified would be best, I think. There's no information to make an intelligent decision about the other options.

@tarekgh
Copy link
Member Author

tarekgh commented Mar 3, 2021

@jkotas

we have said no new [Serializable]

right, I am removing it.

These should be properties. Properties have better perf characteristics than readonly fields.

That make sense. thanks. I'll update that.

@tarekgh
Copy link
Member Author

tarekgh commented Mar 3, 2021

@scalablecory

What is the behavior of the conversion operators re: DateTimeKind?

Unspecified.

No need for a conversion from DateTimeOffset?

No, DateTimeOffset is kind offset from UTC. Converting date or time to that would be strange as these not related to the time zones at all. Also, we didn't find any real scenario need that.

NodaTime uses the name LocalDate.

we didn't choose this name for 2 reasons. First, it can be confusing for users will assume it is related to local time zone. Second, we wanted a name that start with the word Date for discoverability.

@tarekgh
Copy link
Member Author

tarekgh commented Mar 3, 2021

@Clockwork-Muse

DateTime doesn't retain its reference to the Calendar after initialization. Assuming that to be the case here as well, we should be explicitly calling that out so people aren't completely confused when AddMonth (and a number of other methods) don't do quite what they expect.

Yes, we'll not retain the calendar. This constructor is provided as a helper to create a Date from any other calendars dates. We can clarify the doc to be explicit about the behavior and not confuse it with the AddMonth.

Presumably this returns the count of days, but this will hamstring us if we ever introduce a relative offset type (something containing year/month/day differences, like NodaTime's Period. An equivalent operator has also been a consistent source of errors in SQL Server. I'd much rather prefer an explicit method (or set of methods) to get this.

Yes, this will return the number of days. The caller of this operator would see it return an integer number and if doesn't understand it would look at the doc. providing explicit method would suffer from the same issue. no? I mean if we have int Subtract(DateOnly d) wouldn't be the same?

Also, should we add this operator:
public static DateTime operator +(DateOnly d, TimeOnly t) { throw null; }

This is interesting idea. will think about but initially it makes sense to me.

public static readonly TimeOfDay MaxValue;

right. I got other comment this should be aa property too.

I'm assuming this is 23:59:59.9999999. The ability to support times >= 24 is requested on occasion, and is commonly seen in some late-night TV listings (and similar). It can (rarely, possibly on theoretically?) occur in observance of civil time.

I assume it cannot be > 24 but the 24 value can be interesting we can think about it. we didn't support 24 in DateTime[Offset] so we may keep that for consistency.

BETWEEN is a consistent source of bugs for date/time in SQL, because people keep forgetting about fractional seconds, because they sometimes mean an inclusive bound and sometimes an exclusive bound. I'm waffling about accepting this as a shorthand for t1 <= this < t2 (as opposed to requiring explicit operators).

IsBetween is different than t1 <= this < t2. Imagine a shop working hours is from 1:00 PM to 2:AM and giving some time want to know if specific time is during working hour or not. IsBetween will help in such scenario.

public static TimeSpan operator -(TimeOfDay t1, TimeOfDay t2) { throw null; } // Always positive time span
Both Java's and NodaTime's equivalent return negative time spans when t2 < t1, which I feel is preferable.

Will try to look at that.

Thanks for the feedback.

@tarekgh
Copy link
Member Author

tarekgh commented Mar 3, 2021

@omariom

    public static implicit operator DateOnly(DateTime dateTime) { throw null; } // Truncate the time

Operators losing information should be explicit.

You are correct. Thanks!

https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/operator-overloads#conversion-operators

@msallin
Copy link
Contributor

msallin commented Mar 3, 2021

@jkotas

public static readonly DateOnly MaxValue;
public static readonly DateOnly MinValue;
public static readonly TimeOfDay MaxValue;
public static readonly TimeOfDay MinValue;

These should be properties. Properties have better perf characteristics than readonly fields.

Might you point me to the resource where I can learn why this is the case or shortly elaborate?

@Clockwork-Muse
Copy link
Contributor

@tarekgh

No, DateTimeOffset is kind offset from UTC. Converting date or time to that would be strange as these not related to the time zones at all. Also, we didn't find any real scenario need that.

The ask was for from. So var result_date = (DateOnly)some_offset_value; instead of var result_date = (DateOnly)((DateTime)some_offset_value);. You can reasonably get date/time components out of a DateTimeOffset. Ignoring NodaTime, I'd be avoiding plain DateTime as much as possible to be explicit about my offsets.

Yes, this will return the number of days. The caller of this operator would see it return an integer number and if doesn't understand it would look at the doc. providing explicit method would suffer from the same issue. no? I mean if we have int Subtract(DateOnly d) wouldn't be the same?

What I meant was that we should have something like the following:

public int YearsDifference(DateOnly value) { throw null; }
public int MonthsDifference(DateOnly value) { throw null; }
public int DaysDifference(DateOnly value) { throw null; }

If I subtract two dates I want their relative difference (years, months, days), not their absolute difference (days only), which we need a separate type for.

I assume it cannot be > 24 but the 24 value can be interesting we can think about it. we didn't support 24 in DateTime[Offset] so we may keep that for consistency.

That was more a musing than a desire, and we probably should stay consistent with the existing types. I would probably avoid having only 24.

IsBetween is different than t1 <= this < t2. Imagine a shop working hours is from 1:00 PM to 2:AM and giving some time want to know if specific time is during working hour or not. IsBetween will help in such scenario.

I'm unsure how to feel about this version. I clearly see what you're going for here, but I'm not sure it's a net benefit. Note that I personally would prefer constructing a full DateTimeOffset for multi-day comparisons, especially as that would also account for things like skipping days.

@jkotas
Copy link
Member

jkotas commented Mar 3, 2021

These should be properties. Properties have better perf characteristics than readonly fields.

Might you point me to the resource where I can learn why this is the case or shortly elaborate?

Static properties that just return a constant value are very easy for the JIT to inline, without special measures.

readonly static fields are not as easy. They can be only inlined in certain cases. It requires either tiered compilation to kick in after the static constructor that initialized the field run. Or it requires a complex global analysis in AOT compiler that attempts to prove that the value of the readonly field can be assumed to be a constant.

To further demonstrate the problem with static readonly field optimizations, consider a program like this:

// This prints "0 1"! It will print "1 1" if you change One to be a static property.

using System;

class Program
{
    static bool Dummy = Method();
    static readonly int One = 1;    

    static bool Method()
    {
        Console.WriteLine(One);
        return false;
    }

    static void Main(string[] args)
    {
        Method();
    }
}

@mattjohnsonpint
Copy link
Contributor

IsBetween is different than t1 <= this < t2. Imagine a shop working hours is from 1:00 PM to 2:AM and giving some time want to know if specific time is during working hour or not. IsBetween will help in such scenario.

I'm unsure how to feel about this version. I clearly see what you're going for here, but I'm not sure it's a net benefit. Note that I personally would prefer constructing a full DateTimeOffset for multi-day comparisons, especially as that would also account for things like skipping days.

It's important with time-of-day operations, because comparison can only be done within a single day. In other words, it is easy to test if "now" is between 1:00 and 3:00, but a lot more challenging to test if "now" is between 23:00 and 01:00, and even more challenging to have a single operation that can do both.

In pseudocode, given a test value, and the half-open interval [start, end) where all three values are a time-of-day on a 24 hour clock:

if start <= end
    isbetween = start <= test AND end > test
else
    isbetween = start <= test OR end > test

You can see how I implemented it in the corefxlab prototype here:

https://github.com/dotnet/corefxlab/blob/3c54492b3c4a8effee31b0700a4979feba9a3394/src/System.Time/System/Time.cs#L492-L507

@ekolis
Copy link

ekolis commented Mar 4, 2021

This would definitely be useful. Sometimes I run into weirdness where I go adding a TimeSpan to a DateTime and get unexpected results because I think the DateTime's time part has been set to midnight (so it can be a "pure" date), but it actually hasn't. Having an explicit Date type would solve these problems; such a type would not be possible to "pollute" with time of day data.

@mattjohnsonpint
Copy link
Contributor

mattjohnsonpint commented Mar 4, 2021

On the usefulness/motivation front, this question was asked 10 years ago.

https://stackoverflow.com/questions/5314309/a-type-for-date-only-in-c-sharp-why-is-there-no-date-type

There are lots of good comments in there, and a detailed reply from yours truly in 2016. In particular, I appreciate this comment very much:

A date only data type is to DateTime as an integer data type is to a decimal. Those who argue we do not need a date because you can just throw away the time part is akin to saying we do not need integers as we can throw away the decimal part. Our world has a concept of a date that does not include a time. 5 March is not 5 March 00:00:00.

@bhood-zorus
Copy link

Best names for these types would be Date and Time but there is issue that VB already using these names and if we go with such names can conflict with VB.

Not to derail with bikeshedding the names 🙂 but I think it would be okay, possibly with a compiler update. There are already such conflicts with String and Enum; you can opt for the CLR type by bracketing the name, e.g. [String], [Enum]

@mattjohnsonpint
Copy link
Contributor

There's no conflict with Time as a class name, but there is precedence for calling it TimeOfDay - we have uses of this name already:

I couldn't find any interesting cases of just Time Part of the problem with "time" is that it has multiple meanings. The question is whether we want to use a name that has precedence for the intended meaning (TimeOfDay), or if we want to pick a name that reduces conflicts with existing code (Time) even if we have to rely on the meaning conveyed via xmlcomments/docs.

For Date, the main conflict is with the Date data type, which is a language reserved keyword, aliased to System.DateTime. Sure, we could have a System.Date and VB.Net developers would either spell out System.Date or would use [Date], but that might not be obvious in how it differs from the Date keyword.

Also it's worth noting that VB.Net has a date literal syntax - regardless of what names we pick, we should consider if such syntax should also be supported for either of the new types. Example:

Dim d As [Date] = #8/13/2002#
Dim t as TimeOfDay = #12:14 PM#

There are a lot of other places that use Date. The interesting ones being:

There are some random projects that have DateOnly properties such as Microsoft.Office.Server.ActivityFeed.DateOnly - but I don't think there's much risk there.

In summary, I'm most concerned with name conflicts of objects (in OData and Graph) and with the Date keyword in VB, but the other proposed names have precedence as well. If we have strategies for dealing with such conflicts, then either choice of name is reasonable, IMHO.

@ekolis
Copy link

ekolis commented Mar 10, 2021

Would it make sense to call the date-only part of a DateTime, a Day instead? I know that sort of implies a day of the week, rather than a day of the month/year, but it would be a unique name with no conflicts that I'm aware of.

Or, you could call it DayOfMonth or DayOfYear, but those sound kind of messy and imply the lack of year/month info respectively (e.g. DayOfMonth could be March 4 without a year, and DayOfYear could be just "Day 32 of the year" which resolves to February 1)...

I wonder if it would make sense to create a general purpose Time class with nullable properties, which could be used to specify a potentially recurring moment in time? So if you set the month to 3 and the day of the month to 4, then you get March 4 which recurs every year, or you could set the weekday to Sunday and the factor (need a better name) to 2 to get "every other Sunday", or the weekday to Thursday, the month to November, and the factor to 4 to get Thanksgiving in the US, or the year to 2020 to get the whole year of 2020... then these objects could be compared to each other using Contains and Intersects...

@drieseng
Copy link
Contributor

Any chance of implementing IXmlSerializable like the structs in corefxlab? This would allow for more scenarios in, for example, WCF.

Why are there no TimeSpan addition and subtraction operators for Date(Only)? Of course, we'd probably have to reject a TimeSpan that does not represent a multiple of a day (or would lead to a negative date). But this is not different than having to reject adding or subtracting a TimeSpan that would lead to a negative time or a time that exceeds 24h in case of TimeOfDay, or is it?

@turowicz
Copy link

turowicz commented Apr 2, 2021

Where is @jskeet when you need him

@jskeet
Copy link

jskeet commented Apr 2, 2021

FWIW, I'm fine with DateOnly and TimeOnly. While they may not be the names we'd have chosen for a blank slate, they're unambiguous and clear in meaning.

@terrajobst
Copy link
Member

FWIW, I'm fine with DateOnly and TimeOnly. While they may not be the names we'd have chosen for a blank slate, they're unambiguous and clear in meaning.

Thanks @jskeet. I think your comment just ended an infinite bike shedding 😄

@Grauenwolf
Copy link

        public static implicit operator DateOnly(DateTime dateTime) { throw null; } // Truncate the time

Operators losing information should be explicit.

Why? DateTime has all of the information that DateOnly needs.

This isn't like the implicit cast from DateTime to DateTimeOffset where it has to invent information that doesn't exist.

@Grauenwolf
Copy link

does it make sense to have a new DateTimeOnly

I would not be in favor of that. It would open up a whole new set of questions about which one to use and how to convert between them.

My answer to that is "always use the new one" and "pretend the old one doesn't exist".

Every since DateTime added the Kind property in .NET 2 it has been the source of countless bugs. Getting rid of that abomination would eliminate many subtle logic errors.

@jskeet
Copy link

jskeet commented Apr 3, 2021

Why? DateTime has all of the information that DateOnly needs.

But it throws information away. A float has all the information that int needs, but it's an explicit conversion precisely because it loses information.

From https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/operator-overloads:

DO NOT provide an implicit conversion operator if the conversion is potentially lossy.

For example, there should not be an implicit conversion from Double to Int32 because Double has a wider range than Int32. An explicit conversion operator can be provided even if the conversion is potentially lossy.

@Grauenwolf
Copy link

I'm not strongly opposed to the -Only suffix, but that suffix is already incongruent with the DateTime because it's not DateTimeOnly (e.g. no offset).

Do note that DateTime does contain an offset via the Kind property.

@Grauenwolf
Copy link

Grauenwolf commented Apr 3, 2021

@jskeet No fair bringing the FDG into this. We can't a proper debate when there's black-letter law already settling the question.

@Clockwork-Muse
Copy link
Contributor

I'm not strongly opposed to the -Only suffix, but that suffix is already incongruent with the DateTime because it's not DateTimeOnly (e.g. no offset).

Do note that DateTime does contain an offset via the Kind property.

Eh. Kind is sometimes an implied offset, but it causes problems if it's Unknown and you didn't keep track of the real one. And you get weird philosophical questions if you call DateTime.Now on a server with the time zone set to UTC...

@mattjohnsonpint
Copy link
Contributor

We're getting a bit off topic, me thinks. 😁

I agree DateTimeKind is an ugly API. However, it's behaviors are known, and applied consistently (mostly). Unspecified kind is even useful in a lot of cases. Sure, if we had a blank slate or could revisit that decision, we likely would go down a different route. But since we are not abandoning the current APIs, then adding the DateOnly and TimeOnly types makes a lot of sense. One can always still choose Noda Time if they prefer a more correct and comprehensive API. Nothing we are doing here prohibits or discourages that.

@Grauenwolf
Copy link

The question was whether or not a DateTimeOnly type should be included as well to address the problems with Kind.

Personally I think the answer is "Yes, but not as part of this effort".

@Grauenwolf
Copy link

I know the issue is settled, but I still like to add one more data point to the naming discussion. VB isn't the only language to use Date to represent a date+time value. Java does it as well.

As for time, in Ruby the Time class means date+time.

So being more explicit with the names will help people coming over from other languages.

@Clockwork-Muse
Copy link
Contributor

Java does it as well.

The existence of that particular type in Java should be forgotten. It's been superseded twice, the more recent (and better) one being JSR310 with Java 8... in 2014. The only reason to be using it is in the case of backwards compatibility.

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Apr 9, 2021
@kkohler2
Copy link

This naming of DateOnly and TimeOnly or TimeOfDay is odd. The existing DateTime obviously contains both date and time. If we wand just a date, call it Date, not DateOnly. If we want just time, call it Time, not TimeOnly or TimeOfDay. Calling it TimeOfDay is kind of redundant. What else would it be time of?

@khellang
Copy link
Member

@kkohler2 I think you missed the 147 comments above detailing why they settled on those names 😅

@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Apr 14, 2021
@drieseng
Copy link
Contributor

@terrajobst No chance of having either an implicit operator or (preferably) a method to convert a DateOnly to DateTime? Having to use DateOnly.ToDateTime(TimeOnly.MinValue)for this purpose is hardly ideal.

@Grauenwolf
Copy link

Please don't. Implicit casting should not add information.

I've lost track of the number of bugs caused by implicitly casting from DateTime to DateTimeOffset.

@ericsampson
Copy link

I agree. There's nothing saying that 00:00:00.000 is the time part of this implicit conversion. A DateOnly is just that, a pure date.
The user has to specify the time component along with a DateOnly to convert to a DateTime.

People are free to make their own extension methods etc if they want this functionality, but it shouldn't be in the BCL.

@drieseng
Copy link
Contributor

I am definitely not advocating an explicit or implicit cast, this was just how it was implemented in CoreFxLab (if I'm not mistaken).
However, I don't see anything harmful in adding a Date.ToDateTime() overload.

Personally, I see much more harm in losing the time component when you invoke Date.FromDateTime(DateTime.Now).
The Date struct in CoreFxLab would at least throw an exception if the DateTime instance had a non-zero time component.

@Grauenwolf
Copy link

That seems ridiculous. Why shouldn't I be able to get a date only value from a date and time value?

I'm not "losing" any information. I'll just taking the subset of the available information that I care about.

Throwing an exception here is just a land mine waiting for someone unfamiliar with the API to trigger.

@mattjohnsonpint
Copy link
Contributor

The CoreFxLab implementation I did was purely experimental. By contrast, the DateOnly and TimeOnly APIs and implementation have gone through extensive debate and review.

Also, at this point, if there are further changes desired, I suggest they get their own GitHub issues, as this one is now completed.

@terrajobst
Copy link
Member

Exactly. I'm locking this thread now, for that reason.

@dotnet dotnet locked as resolved and limited conversation to collaborators Apr 15, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.DateTime
Projects
None yet
Development

Successfully merging a pull request may close this issue.