Skip to content

Introduce Date and Time only structs #49036

Closed

Description

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; }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions