diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 807cca91a37ae5..5706b4543dcb65 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -1,17 +1,17 @@  - @@ -478,6 +478,14 @@ Object must be of type Char. + + Object must be of type DateOnly. + {Locked="DateOnly"} + + + Object must be of type TimeOnly. + {Locked="TimeOnly"} + Object must be of type DateTime. @@ -1123,6 +1131,10 @@ An undefined DateTimeStyles value is being used. + + The only allowed values for the styles are AllowWhiteSpaces, AllowTrailingWhite, AllowLeadingWhite, and AllowInnerWhite. + {Locked="AllowWhiteSpaces, AllowTrailingWhite, AllowLeadingWhite, and AllowInnerWhite"} + The DigitSubstitution property must be of a valid member of the DigitShapes enumeration. Valid entries include Context, NativeNational or None. @@ -1651,6 +1663,9 @@ Ticks must be between DateTime.MinValue.Ticks and DateTime.MaxValue.Ticks. + + Ticks must be between 0 and and TimeOnly.MaxValue.Ticks. + Years value must be between +/-10000. @@ -1747,6 +1762,10 @@ Month must be between one and twelve. + + Day number must be between 0 and DateOnly.MaxValue.DayNumber. + {Locked="DateOnly.MaxValue.DayNumber"} + The Month parameter must be in the range 1 through 12. @@ -2173,6 +2192,17 @@ String '{0}' was not recognized as a valid DateTime. + + String '{0}' was not recognized as a valid DateOnly. + {Locked="DateOnly"} + + + String '{0}' was not recognized as a valid TimeOnly. + {Locked="TimeOnly"} + + + String '{0}' contains parts which are not specific to the {1}. + The DateTime represented by the string '{0}' is not supported in calendar '{1}'. diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 9256e41f8a6332..165fd526f8a3fa 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -230,6 +230,7 @@ + @@ -1040,6 +1041,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/DateOnly.cs b/src/libraries/System.Private.CoreLib/src/System/DateOnly.cs new file mode 100644 index 00000000000000..6cee176a31d103 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/DateOnly.cs @@ -0,0 +1,846 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Globalization; + +namespace System +{ + internal enum ParseOperationResult + { + Success, + WrongStyles, + ParseFailure, + WrongParts, + BadFormatSpecifier + } + + /// + /// Represents dates with values ranging from January 1, 0001 Anno Domini (Common Era) through December 31, 9999 A.D. (C.E.) in the Gregorian calendar. + /// + public readonly struct DateOnly : IComparable, IComparable, IEquatable, IFormattable, ISpanFormattable + { + private readonly int _dayNumber; + + // Maps to Jan 1st year 1 + private const int MinDayNumber = 0; + + // Maps to December 31 year 9999. The value calculated from "new DateTime(9999, 12, 31).Ticks / TimeSpan.TicksPerDay" + private const int MaxDayNumber = 3_652_058; + + private static int DayNumberFromDateTime(DateTime dt) => (int)(dt.Ticks / TimeSpan.TicksPerDay); + + private DateTime GetEquivalentDateTime() => DateTime.UnsafeCreate(_dayNumber * TimeSpan.TicksPerDay); + + private DateOnly(int dayNumber) + { + Debug.Assert((uint)dayNumber <= MaxDayNumber); + _dayNumber = dayNumber; + } + + /// + /// Gets the earliest possible date that can be created. + /// + public static DateOnly MinValue => new DateOnly(MinDayNumber); + + /// + /// Gets the latest possible date that can be created. + /// + public static DateOnly MaxValue => new DateOnly(MaxDayNumber); + + /// + /// Creates a new instance of the DateOnly structure to the specified year, month, and day. + /// + /// The year (1 through 9999). + /// The month (1 through 12). + /// The day (1 through the number of days in ). + public DateOnly(int year, int month, int day) => _dayNumber = DayNumberFromDateTime(new DateTime(year, month, day)); + + /// + /// Creates a new instance of the DateOnly structure to the specified year, month, and day for the specified calendar. + /// + /// The year (1 through the number of years in calendar). + /// The month (1 through the number of months in calendar). + /// The day (1 through the number of days in ). + /// The calendar that is used to interpret year, month, and day.. + public DateOnly(int year, int month, int day, Calendar calendar) => _dayNumber = DayNumberFromDateTime(new DateTime(year, month, day, calendar)); + + /// + /// Creates a new instance of the DateOnly structure to the specified number of days. + /// + /// The number of days since January 1, 0001 in the Proleptic Gregorian calendar. + public static DateOnly FromDayNumber(int dayNumber) + { + if ((uint)dayNumber > MaxDayNumber) + { + ThrowHelper.ThrowArgumentOutOfRange_DayNumber(dayNumber); + } + + return new DateOnly(dayNumber); + } + + /// + /// Gets the year component of the date represented by this instance. + /// + public int Year => GetEquivalentDateTime().Year; + + /// + /// Gets the month component of the date represented by this instance. + /// + public int Month => GetEquivalentDateTime().Month; + + /// + /// Gets the day component of the date represented by this instance. + /// + public int Day => GetEquivalentDateTime().Day; + + /// + /// Gets the day of the week represented by this instance. + /// + public DayOfWeek DayOfWeek => GetEquivalentDateTime().DayOfWeek; + + /// + /// Gets the day of the year represented by this instance. + /// + public int DayOfYear => GetEquivalentDateTime().DayOfYear; + + /// + /// Gets the number of days since January 1, 0001 in the Proleptic Gregorian calendar represented by this instance. + /// + public int DayNumber => _dayNumber; + + /// + /// Adds the specified number of days to the value of this instance. + /// + /// The number of days to add. To subtract days, specify a negative number. + /// An instance whose value is the sum of the date represented by this instance and the number of days represented by value. + public DateOnly AddDays(int value) + { + int newDayNumber = _dayNumber + value; + if ((uint)newDayNumber > MaxDayNumber) + { + ThrowOutOfRange(); + } + + return new DateOnly(newDayNumber); + + static void ThrowOutOfRange() => throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_AddValue); + } + + /// + /// Adds the specified number of months to the value of this instance. + /// + /// A number of months. The months parameter can be negative or positive. + /// An object whose value is the sum of the date represented by this instance and months. + public DateOnly AddMonths(int value) => new DateOnly(DayNumberFromDateTime(GetEquivalentDateTime().AddMonths(value))); + + /// + /// Adds the specified number of years to the value of this instance. + /// + /// A number of years. The value parameter can be negative or positive. + /// An object whose value is the sum of the date represented by this instance and the number of years represented by value. + public DateOnly AddYears(int value) => new DateOnly(DayNumberFromDateTime(GetEquivalentDateTime().AddYears(value))); + + /// + /// Determines whether two specified instances of DateOnly are equal. + /// + /// The first object to compare. + /// The second object to compare. + /// true if left and right represent the same date; otherwise, false. + public static bool operator ==(DateOnly left, DateOnly right) => left._dayNumber == right._dayNumber; + + /// + /// Determines whether two specified instances of DateOnly are not equal. + /// + /// The first object to compare. + /// The second object to compare. + /// true if left and right do not represent the same date; otherwise, false. + public static bool operator !=(DateOnly left, DateOnly right) => left._dayNumber != right._dayNumber; + + /// + /// Determines whether one specified DateOnly is later than another specified DateTime. + /// + /// The first object to compare. + /// The second object to compare. + /// true if left is later than right; otherwise, false. + public static bool operator >(DateOnly left, DateOnly right) => left._dayNumber > right._dayNumber; + + /// + /// Determines whether one specified DateOnly represents a date that is the same as or later than another specified DateOnly. + /// + /// The first object to compare. + /// The second object to compare. + /// true if left is the same as or later than right; otherwise, false. + public static bool operator >=(DateOnly left, DateOnly right) => left._dayNumber >= right._dayNumber; + + /// + /// Determines whether one specified DateOnly is earlier than another specified DateOnly. + /// + /// The first object to compare. + /// The second object to compare. + /// true if left is earlier than right; otherwise, false. + public static bool operator <(DateOnly left, DateOnly right) => left._dayNumber < right._dayNumber; + + /// + /// Determines whether one specified DateOnly represents a date that is the same as or earlier than another specified DateOnly. + /// + /// The first object to compare. + /// The second object to compare. + /// true if left is the same as or earlier than right; otherwise, false. + public static bool operator <=(DateOnly left, DateOnly right) => left._dayNumber <= right._dayNumber; + + /// + /// Returns a DateTime that is set to the date of this DateOnly instance and the time of specified input time. + /// + /// The time of the day. + /// The DateTime instance composed of the date of the current DateOnly instance and the time specified by the input time. + public DateTime ToDateTime(TimeOnly time) => new DateTime(_dayNumber * TimeSpan.TicksPerDay + time.Ticks); + + /// + /// Returns a DateTime instance with the specified input kind that is set to the date of this DateOnly instance and the time of specified input time. + /// + /// The time of the day. + /// One of the enumeration values that indicates whether ticks specifies a local time, Coordinated Universal Time (UTC), or neither. + /// The DateTime instance composed of the date of the current DateOnly instance and the time specified by the input time. + public DateTime ToDateTime(TimeOnly time, DateTimeKind kind) => new DateTime(_dayNumber * TimeSpan.TicksPerDay + time.Ticks, kind); + + /// + /// Returns a DateOnly instance that is set to the date part of the specified dateTime. + /// + /// The DateTime instance. + /// The DateOnly instance composed of the date part of the specified input time dateTime instance. + public static DateOnly FromDateTime(DateTime dateTime) => new DateOnly(DayNumberFromDateTime(dateTime)); + + /// + /// Compares the value of this instance to a specified DateOnly value and returns an integer that indicates whether this instance is earlier than, the same as, or later than the specified DateTime value. + /// + /// The object to compare to the current instance. + /// Less than zero if this instance is earlier than value. Greater than zero if this instance is later than value. Zero if this instance is the same as value. + public int CompareTo(DateOnly value) => _dayNumber.CompareTo(value._dayNumber); + + /// + /// Compares the value of this instance to a specified object that contains a specified DateOnly value, and returns an integer that indicates whether this instance is earlier than, the same as, or later than the specified DateOnly value. + /// + /// A boxed object to compare, or null. + /// Less than zero if this instance is earlier than value. Greater than zero if this instance is later than value. Zero if this instance is the same as value. + public int CompareTo(object? value) + { + if (value == null) return 1; + if (value is not DateOnly dateOnly) + { + throw new ArgumentException(SR.Arg_MustBeDateOnly); + } + + return CompareTo(dateOnly); + } + + /// + /// Returns a value indicating whether the value of this instance is equal to the value of the specified DateOnly instance. + /// + /// The object to compare to this instance. + /// true if the value parameter equals the value of this instance; otherwise, false. + public bool Equals(DateOnly value) => _dayNumber == value._dayNumber; + + /// + /// Returns a value indicating whether this instance is equal to a specified object. + /// + /// The object to compare to this instance. + /// true if value is an instance of DateOnly and equals the value of this instance; otherwise, false. + public override bool Equals(object? value) => value is DateOnly dateOnly && _dayNumber == dateOnly._dayNumber; + + /// + /// Returns the hash code for this instance. + /// + /// A 32-bit signed integer hash code. + public override int GetHashCode() => _dayNumber; + + private const ParseFlags ParseFlagsDateMask = ParseFlags.HaveHour | ParseFlags.HaveMinute | ParseFlags.HaveSecond | ParseFlags.HaveTime | ParseFlags.TimeZoneUsed | + ParseFlags.TimeZoneUtc | ParseFlags.CaptureOffset | ParseFlags.UtcSortPattern; + + /// + /// Converts a memory span that contains string representation of a date to its DateOnly equivalent by using culture-specific format information and a formatting style. + /// + /// The memory span that contains the string to parse. + /// An object that supplies culture-specific format information about s. + /// A bitwise combination of enumeration values that indicates the permitted format of s. A typical value to specify is None. + /// An object that is equivalent to the date contained in s, as specified by provider and styles. + public static DateOnly Parse(ReadOnlySpan s, IFormatProvider? provider = default, DateTimeStyles style = DateTimeStyles.None) + { + ParseOperationResult result = TryParseInternal(s, provider, style, out DateOnly dateOnly); + if (result != ParseOperationResult.Success) + { + switch (result) + { + case ParseOperationResult.WrongStyles: throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + case ParseOperationResult.ParseFailure: throw new FormatException(SR.Format(SR.Format_BadDateOnly, s.ToString())); + default: + Debug.Assert(result == ParseOperationResult.WrongParts); + throw new FormatException(SR.Format(SR.Format_DateTimeOnlyContainsNoneDateParts, s.ToString(), nameof(DateOnly))); + } + } + + return dateOnly; + } + + private const string OFormat = "yyyy'-'MM'-'dd"; + private const string RFormat = "ddd, dd MMM yyyy"; + + /// + /// Converts the specified span representation of a date to its DateOnly equivalent using the specified format, culture-specific format information, and style. + /// The format of the string representation must match the specified format exactly or an exception is thrown. + /// + /// A span containing the characters that represent a date to convert. + /// A span containing the characters that represent a format specifier that defines the required format of s. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of enumeration values that indicates the permitted format of s. A typical value to specify is None. + /// An object that is equivalent to the date contained in s, as specified by format, provider, and style. + public static DateOnly ParseExact(ReadOnlySpan s, ReadOnlySpan format, IFormatProvider? provider = default, DateTimeStyles style = DateTimeStyles.None) + { + ParseOperationResult result = TryParseExactInternal(s, format, provider, style, out DateOnly dateOnly); + + if (result != ParseOperationResult.Success) + { + switch (result) + { + case ParseOperationResult.WrongStyles: throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + case ParseOperationResult.ParseFailure: throw new FormatException(SR.Format(SR.Format_BadDateOnly, s.ToString())); + default: + Debug.Assert(result == ParseOperationResult.WrongParts); + throw new FormatException(SR.Format(SR.Format_DateTimeOnlyContainsNoneDateParts, s.ToString(), nameof(DateOnly))); + + } + } + + return dateOnly; + } + + /// + /// Converts the specified span representation of a date to its DateOnly equivalent using the specified array of formats. + /// The format of the string representation must match at least one of the specified formats exactly or an exception is thrown. + /// + /// A span containing the characters that represent a date to convert. + /// An array of allowable formats of s. + /// An object that is equivalent to the date contained in s, as specified by format, provider, and style. + public static DateOnly ParseExact(ReadOnlySpan s, string[] formats) => ParseExact(s, formats, null, DateTimeStyles.None); + + /// + /// Converts the specified span representation of a date to its DateOnly equivalent using the specified array of formats, culture-specific format information, and style. + /// The format of the string representation must match at least one of the specified formats exactly or an exception is thrown. + /// + /// A span containing the characters that represent a date to convert. + /// An array of allowable formats of s. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of enumeration values that indicates the permitted format of s. A typical value to specify is None. + /// An object that is equivalent to the date contained in s, as specified by format, provider, and style. + public static DateOnly ParseExact(ReadOnlySpan s, string[] formats, IFormatProvider? provider, DateTimeStyles style = DateTimeStyles.None) + { + ParseOperationResult result = TryParseExactInternal(s, formats, provider, style, out DateOnly dateOnly); + if (result != ParseOperationResult.Success) + { + switch (result) + { + case ParseOperationResult.WrongStyles: throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + case ParseOperationResult.ParseFailure: throw new FormatException(SR.Format(SR.Format_BadDateOnly, s.ToString())); + default: + Debug.Assert(result == ParseOperationResult.BadFormatSpecifier); + throw new FormatException(SR.Argument_BadFormatSpecifier); + } + } + + return dateOnly; + } + + /// + /// Converts a string that contains string representation of a date to its DateOnly equivalent by using the conventions of the current culture. + /// + /// The string that contains the string to parse. + /// An object that is equivalent to the date contained in s. + public static DateOnly Parse(string s) => Parse(s, null, DateTimeStyles.None); + + /// + /// Converts a string that contains string representation of a date to its DateOnly equivalent by using culture-specific format information and a formatting style. + /// + /// The string that contains the string to parse. + /// An object that supplies culture-specific format information about s. + /// A bitwise combination of the enumeration values that indicates the style elements that can be present in s for the parse operation to succeed, and that defines how to interpret the parsed date. A typical value to specify is None. + /// An object that is equivalent to the date contained in s, as specified by provider and styles. + public static DateOnly Parse(string s, IFormatProvider? provider, DateTimeStyles style = DateTimeStyles.None) + { + if (s == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.s); + return Parse(s.AsSpan(), provider, style); + } + + /// + /// Converts the specified string representation of a date to its DateOnly equivalent using the specified format. + /// The format of the string representation must match the specified format exactly or an exception is thrown. + /// + /// A string containing the characters that represent a date to convert. + /// A string that represent a format specifier that defines the required format of s. + /// An object that is equivalent to the date contained in s, as specified by format. + public static DateOnly ParseExact(string s, string format) => ParseExact(s, format, null, DateTimeStyles.None); + + /// + /// Converts the specified string representation of a date to its DateOnly equivalent using the specified format, culture-specific format information, and style. + /// The format of the string representation must match the specified format exactly or an exception is thrown. + /// + /// A string containing the characters that represent a date to convert. + /// A string containing the characters that represent a format specifier that defines the required format of s. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of the enumeration values that provides additional information about s, about style elements that may be present in s, or about the conversion from s to a DateOnly value. A typical value to specify is None. + /// An object that is equivalent to the date contained in s, as specified by format, provider, and style. + public static DateOnly ParseExact(string s, string format, IFormatProvider? provider, DateTimeStyles style = DateTimeStyles.None) + { + if (s == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.s); + if (format == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.format); + return ParseExact(s.AsSpan(), format.AsSpan(), provider, style); + } + + /// + /// Converts the specified span representation of a date to its DateOnly equivalent using the specified array of formats. + /// The format of the string representation must match at least one of the specified formats exactly or an exception is thrown. + /// + /// A span containing the characters that represent a date to convert. + /// An array of allowable formats of s. + /// An object that is equivalent to the date contained in s, as specified by format, provider, and style. + public static DateOnly ParseExact(string s, string[] formats) => ParseExact(s, formats, null, DateTimeStyles.None); + + /// + /// Converts the specified string representation of a date to its DateOnly equivalent using the specified array of formats, culture-specific format information, and style. + /// The format of the string representation must match at least one of the specified formats exactly or an exception is thrown. + /// + /// A string containing the characters that represent a date to convert. + /// An array of allowable formats of s. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of enumeration values that indicates the permitted format of s. A typical value to specify is None. + /// An object that is equivalent to the date contained in s, as specified by format, provider, and style. + public static DateOnly ParseExact(string s, string[] formats, IFormatProvider? provider, DateTimeStyles style = DateTimeStyles.None) + { + if (s == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.s); + return ParseExact(s.AsSpan(), formats, provider, style); + } + + /// + /// Converts the specified span representation of a date to its DateOnly equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// A span containing the characters representing the date to convert. + /// When this method returns, contains the DateOnly value equivalent to the date contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is empty string, or does not contain a valid string representation of a date. This parameter is passed uninitialized. + /// true if the s parameter was converted successfully; otherwise, false. + public static bool TryParse(ReadOnlySpan s, out DateOnly result) => TryParse(s, null, DateTimeStyles.None, out result); + + /// + /// Converts the specified span representation of a date to its DateOnly equivalent using the specified array of formats, culture-specific format information, and style. And returns a value that indicates whether the conversion succeeded. + /// + /// A string containing the characters that represent a date to convert. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of enumeration values that indicates the permitted format of s. A typical value to specify is None. + /// When this method returns, contains the DateOnly value equivalent to the date contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is empty string, or does not contain a valid string representation of a date. This parameter is passed uninitialized. + /// true if the s parameter was converted successfully; otherwise, false. + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) => TryParseInternal(s, provider, style, out result) == ParseOperationResult.Success; + + private static ParseOperationResult TryParseInternal(ReadOnlySpan s, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) + { + if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0) + { + result = default; + return ParseOperationResult.WrongStyles; + } + + DateTimeResult dtResult = default; + dtResult.Init(s); + + if (!DateTimeParse.TryParse(s, DateTimeFormatInfo.GetInstance(provider), style, ref dtResult)) + { + result = default; + return ParseOperationResult.ParseFailure; + } + + if ((dtResult.flags & ParseFlagsDateMask) != 0) + { + result = default; + return ParseOperationResult.WrongParts; + } + + result = new DateOnly(DayNumberFromDateTime(dtResult.parsedDate)); + return ParseOperationResult.Success; + } + + /// + /// Converts the specified span representation of a date to its DateOnly equivalent using the specified format and style. + /// The format of the string representation must match the specified format exactly. The method returns a value that indicates whether the conversion succeeded. + /// + /// A span containing the characters representing a date to convert. + /// The required format of s. + /// When this method returns, contains the DateOnly value equivalent to the date contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s is empty string, or does not contain a date that correspond to the pattern specified in format. This parameter is passed uninitialized. + /// true if s was converted successfully; otherwise, false. + public static bool TryParseExact(ReadOnlySpan s, ReadOnlySpan format, out DateOnly result) => TryParseExact(s, format, null, DateTimeStyles.None, out result); + + /// + /// Converts the specified span representation of a date to its DateOnly equivalent using the specified format, culture-specific format information, and style. + /// The format of the string representation must match the specified format exactly. The method returns a value that indicates whether the conversion succeeded. + /// + /// A span containing the characters representing a date to convert. + /// The required format of s. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of one or more enumeration values that indicate the permitted format of s. + /// When this method returns, contains the DateOnly value equivalent to the date contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s is empty string, or does not contain a date that correspond to the pattern specified in format. This parameter is passed uninitialized. + /// true if s was converted successfully; otherwise, false. + public static bool TryParseExact(ReadOnlySpan s, ReadOnlySpan format, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) => + TryParseExactInternal(s, format, provider, style, out result) == ParseOperationResult.Success; + private static ParseOperationResult TryParseExactInternal(ReadOnlySpan s, ReadOnlySpan format, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) + { + if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0) + { + result = default; + return ParseOperationResult.WrongStyles; + } + + if (format.Length == 1) + { + switch (format[0]) + { + case 'o': + case 'O': + format = OFormat; + provider = CultureInfo.InvariantCulture.DateTimeFormat; + break; + + case 'r': + case 'R': + format = RFormat; + provider = CultureInfo.InvariantCulture.DateTimeFormat; + break; + } + } + + DateTimeResult dtResult = default; + dtResult.Init(s); + + if (!DateTimeParse.TryParseExact(s, format, DateTimeFormatInfo.GetInstance(provider), style, ref dtResult)) + { + result = default; + return ParseOperationResult.ParseFailure; + } + + if ((dtResult.flags & ParseFlagsDateMask) != 0) + { + result = default; + return ParseOperationResult.WrongParts; + } + + result = new DateOnly(DayNumberFromDateTime(dtResult.parsedDate)); + + return ParseOperationResult.Success; + } + + /// + /// Converts the specified char span of a date to its DateOnly equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// The span containing the string to parse. + /// An array of allowable formats of s. + /// When this method returns, contains the DateOnly value equivalent to the date contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is Empty, or does not contain a valid string representation of a date. This parameter is passed uninitialized. + /// true if the s parameter was converted successfully; otherwise, false. + public static bool TryParseExact(ReadOnlySpan s, string[] formats, out DateOnly result) => TryParseExact(s, formats, null, DateTimeStyles.None, out result); + + /// + /// Converts the specified char span of a date to its DateOnly equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// The span containing the string to parse. + /// An array of allowable formats of s. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of enumeration values that defines how to interpret the parsed date. A typical value to specify is None. + /// When this method returns, contains the DateOnly value equivalent to the date contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is Empty, or does not contain a valid string representation of a date. This parameter is passed uninitialized. + /// true if the s parameter was converted successfully; otherwise, false. + public static bool TryParseExact(ReadOnlySpan s, string[] formats, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) => + TryParseExactInternal(s, formats, provider, style, out result) == ParseOperationResult.Success; + + private static ParseOperationResult TryParseExactInternal(ReadOnlySpan s, string[] formats, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) + { + if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0 || formats == null) + { + result = default; + return ParseOperationResult.WrongStyles; + } + + DateTimeFormatInfo dtfi = DateTimeFormatInfo.GetInstance(provider); + + for (int i = 0; i < formats.Length; i++) + { + DateTimeFormatInfo dtfiToUse = dtfi; + string? format = formats[i]; + if (string.IsNullOrEmpty(format)) + { + result = default; + return ParseOperationResult.BadFormatSpecifier; + } + + if (format.Length == 1) + { + switch (format[0]) + { + case 'o': + case 'O': + format = OFormat; + dtfiToUse = CultureInfo.InvariantCulture.DateTimeFormat; + break; + + case 'r': + case 'R': + format = RFormat; + dtfiToUse = CultureInfo.InvariantCulture.DateTimeFormat; + break; + } + } + + // Create a new result each time to ensure the runs are independent. Carry through + // flags from the caller and return the result. + DateTimeResult dtResult = default; + dtResult.Init(s); + if (DateTimeParse.TryParseExact(s, format, dtfiToUse, style, ref dtResult) && ((dtResult.flags & ParseFlagsDateMask) == 0)) + { + result = new DateOnly(DayNumberFromDateTime(dtResult.parsedDate)); + return ParseOperationResult.Success; + } + } + + result = default; + return ParseOperationResult.ParseFailure; + } + + /// + /// Converts the specified string representation of a date to its DateOnly equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// A string containing the characters representing the date to convert. + /// When this method returns, contains the DateOnly value equivalent to the date contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is empty string, or does not contain a valid string representation of a date. This parameter is passed uninitialized. + /// true if the s parameter was converted successfully; otherwise, false. + public static bool TryParse(string s, out DateOnly result) => TryParse(s, null, DateTimeStyles.None, out result); + + /// + /// Converts the specified string representation of a date to its DateOnly equivalent using the specified array of formats, culture-specific format information, and style. And returns a value that indicates whether the conversion succeeded. + /// + /// A string containing the characters that represent a date to convert. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of enumeration values that indicates the permitted format of s. A typical value to specify is None. + /// When this method returns, contains the DateOnly value equivalent to the date contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is empty string, or does not contain a valid string representation of a date. This parameter is passed uninitialized. + /// true if the s parameter was converted successfully; otherwise, false. + public static bool TryParse(string s, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) + { + result = default; + if (s == null) + { + return false; + } + + return TryParse(s.AsSpan(), provider, style, out result); + } + + /// + /// Converts the specified string representation of a date to its DateOnly equivalent using the specified format and style. + /// The format of the string representation must match the specified format exactly. The method returns a value that indicates whether the conversion succeeded. + /// + /// A string containing the characters representing a date to convert. + /// The required format of s. + /// When this method returns, contains the DateOnly value equivalent to the date contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s is empty string, or does not contain a date that correspond to the pattern specified in format. This parameter is passed uninitialized. + /// true if s was converted successfully; otherwise, false. + public static bool TryParseExact(string s, string format, out DateOnly result) => TryParseExact(s, format, null, DateTimeStyles.None, out result); + + /// + /// Converts the specified span representation of a date to its DateOnly equivalent using the specified format, culture-specific format information, and style. + /// The format of the string representation must match the specified format exactly. The method returns a value that indicates whether the conversion succeeded. + /// + /// A span containing the characters representing a date to convert. + /// The required format of s. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of one or more enumeration values that indicate the permitted format of s. + /// When this method returns, contains the DateOnly value equivalent to the date contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s is empty string, or does not contain a date that correspond to the pattern specified in format. This parameter is passed uninitialized. + /// true if s was converted successfully; otherwise, false. + public static bool TryParseExact(string s, string format, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) + { + if (s == null || format == null) + { + result = default; + return false; + } + + return TryParseExact(s.AsSpan(), format.AsSpan(), provider, style, out result); + } + + /// + /// Converts the specified string of a date to its DateOnly equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// The string containing date to parse. + /// An array of allowable formats of s. + /// When this method returns, contains the DateOnly value equivalent to the date contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is Empty, or does not contain a valid string representation of a date. This parameter is passed uninitialized. + /// true if the s parameter was converted successfully; otherwise, false. + public static bool TryParseExact(string s, string[] formats, out DateOnly result) => TryParseExact(s, formats, null, DateTimeStyles.None, out result); + + /// + /// Converts the specified string of a date to its DateOnly equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// The string containing the date to parse. + /// An array of allowable formats of s. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of enumeration values that defines how to interpret the parsed date. A typical value to specify is None. + /// When this method returns, contains the DateOnly value equivalent to the date contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is Empty, or does not contain a valid string representation of a date. This parameter is passed uninitialized. + /// true if the s parameter was converted successfully; otherwise, false. + public static bool TryParseExact(string s, string[] formats, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) + { + if (s == null) + { + result = default; + return false; + } + + return TryParseExact(s.AsSpan(), formats, provider, style, out result); + } + + /// + /// Converts the value of the current DateOnly object to its equivalent long date string representation. + /// + /// A string that contains the long date string representation of the current DateOnly object. + public string ToLongDateString() => ToString("D"); + + /// + /// Converts the value of the current DateOnly object to its equivalent short date string representation. + /// + /// A string that contains the short date string representation of the current DateOnly object. + public string ToShortDateString() => ToString(); + + /// + /// Converts the value of the current DateOnly object to its equivalent string representation using the formatting conventions of the current culture. + /// The DateOnly object will be formatted in short form. + /// + /// A string that contains the short date string representation of the current DateOnly object. + public override string ToString() => ToString("d"); + + /// + /// Converts the value of the current DateOnly object to its equivalent string representation using the specified format and the formatting conventions of the current culture. + /// + /// A standard or custom date format string. + /// A string representation of value of the current DateOnly object as specified by format. + public string ToString(string? format) => ToString(format, null); + + /// + /// Converts the value of the current DateOnly object to its equivalent string representation using the specified culture-specific format information. + /// + /// An object that supplies culture-specific formatting information. + /// A string representation of value of the current DateOnly object as specified by provider. + public string ToString(IFormatProvider? provider) => ToString("d", provider); + + /// + /// Converts the value of the current DateOnly object to its equivalent string representation using the specified culture-specific format information. + /// + /// A standard or custom date format string. + /// An object that supplies culture-specific formatting information. + /// A string representation of value of the current DateOnly object as specified by format and provider. + public string ToString(string? format, IFormatProvider? provider) + { + if (format == null || format.Length == 0) + { + format = "d"; + } + + if (format.Length == 1) + { + switch (format[0]) + { + case 'o': + case 'O': + { + return string.Create(10, this, (destination, value) => + { + bool b = DateTimeFormat.TryFormatDateOnlyO(value.Year, value.Month, value.Day, destination); + Debug.Assert(b); + }); + } + + case 'r': + case 'R': + { + return string.Create(16, this, (destination, value) => + { + bool b = DateTimeFormat.TryFormatDateOnlyR(value.DayOfWeek, value.Year, value.Month, value.Day, destination); + Debug.Assert(b); + }); + } + + case 'm': + case 'M': + case 'd': + case 'D': + case 'y': + case 'Y': + return DateTimeFormat.Format(GetEquivalentDateTime(), format, provider); + + default: + throw new FormatException(SR.Format_InvalidString); + } + } + + DateTimeFormat.IsValidCustomDateFormat(format.AsSpan(), throwOnError: true); + return DateTimeFormat.Format(GetEquivalentDateTime(), format, provider); + } + + /// + /// Tries to format the value of the current DateOnly instance into the provided span of characters. + /// + /// When this method returns, this instance's value formatted as a span of characters. + /// When this method returns, the number of characters that were written in destination. + /// A span containing the characters that represent a standard or custom format string that defines the acceptable format for destination. + /// An optional object that supplies culture-specific formatting information for destination. + /// true if the formatting was successful; otherwise, false. + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format = default(ReadOnlySpan), IFormatProvider? provider = null) + { + if (format.Length == 0) + { + format = "d"; + } + + if (format.Length == 1) + { + switch (format[0]) + { + case 'o': + case 'O': + if (!DateTimeFormat.TryFormatDateOnlyO(Year, Month, Day, destination)) + { + charsWritten = 0; + return false; + } + charsWritten = 10; + return true; + + case 'r': + case 'R': + + if (!DateTimeFormat.TryFormatDateOnlyR(DayOfWeek, Year, Month, Day, destination)) + { + charsWritten = 0; + return false; + } + charsWritten = 16; + return true; + + case 'm': + case 'M': + case 'd': + case 'D': + case 'y': + case 'Y': + return DateTimeFormat.TryFormat(GetEquivalentDateTime(), destination, out charsWritten, format, provider); + + default: + charsWritten = 0; + return false; + } + } + + if (!DateTimeFormat.IsValidCustomDateFormat(format, throwOnError: false)) + { + charsWritten = 0; + return false; + } + + return DateTimeFormat.TryFormat(GetEquivalentDateTime(), destination, out charsWritten, format, provider); + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTime.cs b/src/libraries/System.Private.CoreLib/src/System/DateTime.cs index 3afc0e8d7b23ca..aee82eb98c608f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTime.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTime.cs @@ -145,6 +145,8 @@ private DateTime(ulong dateData) this._dateData = dateData; } + internal static DateTime UnsafeCreate(long ticks) => new DateTime((ulong) ticks); + public DateTime(long ticks, DateTimeKind kind) { if ((ulong)ticks > MaxTicks) ThrowTicksOutOfRange(); @@ -611,6 +613,19 @@ private static ulong TimeToTicks(int hour, int minute, int second) return (uint)totalSeconds * (ulong)TicksPerSecond; } + internal static ulong TimeToTicks(int hour, int minute, int second, int millisecond) + { + ulong ticks = TimeToTicks(hour, minute, second); + + if ((uint)millisecond >= MillisPerSecond) ThrowMillisecondOutOfRange(); + + ticks += (uint)millisecond * (uint)TicksPerMillisecond; + + Debug.Assert(ticks <= MaxTicks, "Input parameters validated already"); + + return ticks; + } + // Returns the number of days in the month given by the year and // month arguments. // diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs index 8f4d8c4d749d6e..35e7f18a593ceb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs @@ -1112,6 +1112,234 @@ private static StringBuilder FormatStringBuilder(DateTime dateTime, ReadOnlySpan return FormatCustomized(dateTime, format, dtfi, offset, result: null); } + internal static bool IsValidCustomDateFormat(ReadOnlySpan format, bool throwOnError) + { + int i = 0; + + while (i < format.Length) + { + switch (format[i]) + { + case '\\': + if (i == format.Length - 1) + { + if (throwOnError) + { + throw new FormatException(SR.Format_InvalidString); + } + + return false; + } + + i += 2; + break; + + case '\'': + case '"': + char quoteChar = format[i++]; + while (i < format.Length && format[i] != quoteChar) + { + i++; + } + + if (i >= format.Length) + { + if (throwOnError) + { + throw new FormatException(SR.Format(SR.Format_BadQuote, quoteChar)); + } + + return false; + } + + i++; + break; + + case ':': + case 't': + case 'f': + case 'F': + case 'h': + case 'H': + case 'm': + case 's': + case 'z': + case 'K': + // reject non-date formats + if (throwOnError) + { + throw new FormatException(SR.Format_InvalidString); + } + + return false; + + default: + i++; + break; + } + } + + return true; + } + + + internal static bool IsValidCustomTimeFormat(ReadOnlySpan format, bool throwOnError) + { + int length = format.Length; + int i = 0; + + while (i < length) + { + switch (format[i]) + { + case '\\': + if (i == length - 1) + { + if (throwOnError) + { + throw new FormatException(SR.Format_InvalidString); + } + + return false; + } + + i += 2; + break; + + case '\'': + case '"': + char quoteChar = format[i++]; + while (i < length && format[i] != quoteChar) + { + i++; + } + + if (i >= length) + { + if (throwOnError) + { + throw new FormatException(SR.Format(SR.Format_BadQuote, quoteChar)); + } + + return false; + } + + i++; + break; + + case 'd': + case 'M': + case 'y': + case '/': + case 'z': + case 'k': + if (throwOnError) + { + throw new FormatException(SR.Format_InvalidString); + } + + return false; + + default: + i++; + break; + } + } + + return true; + } + + // 012345678901234567890123456789012 + // --------------------------------- + // 05:30:45.7680000 + internal static bool TryFormatTimeOnlyO(int hour, int minute, int second, long fraction, Span destination) + { + if (destination.Length < 16) + { + return false; + } + + WriteTwoDecimalDigits((uint)hour, destination, 0); + destination[2] = ':'; + WriteTwoDecimalDigits((uint)minute, destination, 3); + destination[5] = ':'; + WriteTwoDecimalDigits((uint)second, destination, 6); + destination[8] = '.'; + WriteDigits((uint)fraction, destination.Slice(9)); + + return true; + } + + // 012345678901234567890123456789012 + // --------------------------------- + // 05:30:45 + internal static bool TryFormatTimeOnlyR(int hour, int minute, int second, Span destination) + { + if (destination.Length < 8) + { + return false; + } + + WriteTwoDecimalDigits((uint)hour, destination, 0); + destination[2] = ':'; + WriteTwoDecimalDigits((uint)minute, destination, 3); + destination[5] = ':'; + WriteTwoDecimalDigits((uint)second, destination, 6); + + return true; + } + + // Roundtrippable format. One of + // 012345678901234567890123456789012 + // --------------------------------- + // 2017-06-12 + internal static bool TryFormatDateOnlyO(int year, int month, int day, Span destination) + { + if (destination.Length < 10) + { + return false; + } + + WriteFourDecimalDigits((uint)year, destination, 0); + destination[4] = '-'; + WriteTwoDecimalDigits((uint)month, destination, 5); + destination[7] = '-'; + WriteTwoDecimalDigits((uint)day, destination, 8); + return true; + } + + // Rfc1123 + // 01234567890123456789012345678 + // ----------------------------- + // Tue, 03 Jan 2017 + internal static bool TryFormatDateOnlyR(DayOfWeek dayOfWeek, int year, int month, int day, Span destination) + { + if (destination.Length < 16) + { + return false; + } + + string dayAbbrev = InvariantAbbreviatedDayNames[(int)dayOfWeek]; + Debug.Assert(dayAbbrev.Length == 3); + + string monthAbbrev = InvariantAbbreviatedMonthNames[month - 1]; + Debug.Assert(monthAbbrev.Length == 3); + + destination[0] = dayAbbrev[0]; + destination[1] = dayAbbrev[1]; + destination[2] = dayAbbrev[2]; + destination[3] = ','; + destination[4] = ' '; + WriteTwoDecimalDigits((uint)day, destination, 5); + destination[7] = ' '; + destination[8] = monthAbbrev[0]; + destination[9] = monthAbbrev[1]; + destination[10] = monthAbbrev[2]; + destination[11] = ' '; + WriteFourDecimalDigits((uint)year, destination, 12); + return true; + } + // Roundtrippable format. One of // 012345678901234567890123456789012 // --------------------------------- diff --git a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs index 117e60fdd06171..dd2cc2742975da 100644 --- a/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs +++ b/src/libraries/System.Private.CoreLib/src/System/ThrowHelper.cs @@ -156,6 +156,12 @@ internal static void ThrowArgumentOutOfRange_Month(int month) throw new ArgumentOutOfRangeException(nameof(month), month, SR.ArgumentOutOfRange_Month); } + [DoesNotReturn] + internal static void ThrowArgumentOutOfRange_DayNumber(int dayNumber) + { + throw new ArgumentOutOfRangeException(nameof(dayNumber), dayNumber, SR.ArgumentOutOfRange_DayNumber); + } + [DoesNotReturn] internal static void ThrowArgumentOutOfRange_BadYearMonthDay() { @@ -639,6 +645,8 @@ private static string GetArgumentName(ExceptionArgument argument) return "start"; case ExceptionArgument.format: return "format"; + case ExceptionArgument.formats: + return "formats"; case ExceptionArgument.culture: return "culture"; case ExceptionArgument.comparer: @@ -960,6 +968,7 @@ internal enum ExceptionArgument pointer, start, format, + formats, culture, comparer, comparable, diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs b/src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs new file mode 100644 index 00000000000000..291bc47f73d56d --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs @@ -0,0 +1,906 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Globalization; + +namespace System +{ + /// + /// Represents a time of day, as would be read from a clock, within the range 00:00:00 to 23:59:59.9999999. + /// + public readonly struct TimeOnly : IComparable, IComparable, IEquatable, IFormattable, ISpanFormattable + { + // represent the number of ticks map to the time of the day. 1 ticks = 100-nanosecond in time measurements. + private readonly long _ticks; + + // MinTimeTicks is the ticks for the midnight time 00:00:00.000 AM + private const long MinTimeTicks = 0; + + // MaxTimeTicks is the max tick value for the time in the day. It is calculated using DateTime.Today.AddTicks(-1).TimeOfDay.Ticks. + private const long MaxTimeTicks = 863_999_999_999; + + /// + /// Represents the smallest possible value of TimeOnly. + /// + public static TimeOnly MinValue => new TimeOnly((ulong)MinTimeTicks); + + /// + /// Represents the largest possible value of TimeOnly. + /// + public static TimeOnly MaxValue => new TimeOnly((ulong)MaxTimeTicks); + + /// + /// Initializes a new instance of the timeOnly structure to the specified hour and the minute. + /// + /// The hours (0 through 23). + /// The minutes (0 through 59). + public TimeOnly(int hour, int minute) : this(DateTime.TimeToTicks(hour, minute, 0, 0)) {} + + /// + /// Initializes a new instance of the timeOnly structure to the specified hour, minute, and second. + /// + /// The hours (0 through 23). + /// The minutes (0 through 59). + /// The seconds (0 through 59). + public TimeOnly(int hour, int minute, int second) : this(DateTime.TimeToTicks(hour, minute, second, 0)) {} + + /// + /// Initializes a new instance of the timeOnly structure to the specified hour, minute, second, and millisecond. + /// + /// The hours (0 through 23). + /// The minutes (0 through 59). + /// The seconds (0 through 59). + /// The millisecond (0 through 999). + public TimeOnly(int hour, int minute, int second, int millisecond) : this(DateTime.TimeToTicks(hour, minute, second, millisecond)) {} + + /// + /// Initializes a new instance of the TimeOnly structure using a specified number of ticks. + /// + /// A time of day expressed in the number of 100-nanosecond units since 00:00:00.0000000. + public TimeOnly(long ticks) + { + if ((ulong)ticks > MaxTimeTicks) + { + throw new ArgumentOutOfRangeException(nameof(ticks), SR.ArgumentOutOfRange_TimeOnlyBadTicks); + } + + _ticks = ticks; + } + + // exist to bypass the check in the public constructor. + internal TimeOnly(ulong ticks) => _ticks = (long)ticks; + + /// + /// Gets the hour component of the time represented by this instance. + /// + public int Hour => new TimeSpan(_ticks).Hours; + + /// + /// Gets the minute component of the time represented by this instance. + /// + public int Minute => new TimeSpan(_ticks).Minutes; + + /// + /// Gets the second component of the time represented by this instance. + /// + public int Second => new TimeSpan(_ticks).Seconds; + + /// + /// Gets the millisecond component of the time represented by this instance. + /// + public int Millisecond => new TimeSpan(_ticks).Milliseconds; + + /// + /// Gets the number of ticks that represent the time of this instance. + /// + public long Ticks => _ticks; + + private TimeOnly AddTicks(long ticks) => new TimeOnly((_ticks + TimeSpan.TicksPerDay + (ticks % TimeSpan.TicksPerDay)) % TimeSpan.TicksPerDay); + + private TimeOnly AddTicks(long ticks, out int wrappedDays) + { + wrappedDays = (int)(ticks / TimeSpan.TicksPerDay); + long newTicks = _ticks + ticks % TimeSpan.TicksPerDay; + if (newTicks < 0) + { + wrappedDays--; + newTicks += TimeSpan.TicksPerDay; + } + else + { + if (newTicks >= TimeSpan.TicksPerDay) + { + wrappedDays++; + newTicks -= TimeSpan.TicksPerDay; + } + } + + return new TimeOnly(newTicks); + } + + /// + /// Returns a new TimeOnly that adds the value of the specified TimeSpan to the value of this instance. + /// + /// A positive or negative time interval. + /// An object whose value is the sum of the time represented by this instance and the time interval represented by value. + public TimeOnly Add(TimeSpan value) => AddTicks(value.Ticks); + + /// + /// Returns a new TimeOnly that adds the value of the specified TimeSpan to the value of this instance. + /// If the added value circulate though the day, this method will out the number of the circulated days. + /// + /// A positive or negative time interval. + /// When this method returns, contains the number of circulated days resulted from this addition operation. + /// An object whose value is the sum of the time represented by this instance and the time interval represented by value. + public TimeOnly Add(TimeSpan value, out int wrappedDays) => AddTicks(value.Ticks, out wrappedDays); + + /// + /// Returns a new TimeOnly that adds the specified number of hours to the value of this instance. + /// + /// A number of whole and fractional hours. The value parameter can be negative or positive. + /// An object whose value is the sum of the time represented by this instance and the number of hours represented by value. + public TimeOnly AddHours(double value) => AddTicks((long)(value * TimeSpan.TicksPerHour)); + + /// + /// Returns a new TimeOnly that adds the specified number of hours to the value of this instance. + /// If the added value circulate though the day, this method will out the number of the circulated days. + /// + /// A number of whole and fractional hours. The value parameter can be negative or positive. + /// When this method returns, contains the number of circulated days resulted from this addition operation. + /// An object whose value is the sum of the time represented by this instance and the number of hours represented by value. + public TimeOnly AddHours(double value, out int wrappedDays) => AddTicks((long)(value * TimeSpan.TicksPerHour), out wrappedDays); + + /// + /// Returns a new TimeOnly that adds the specified number of minutes to the value of this instance. + /// + /// A number of whole and fractional minutes. The value parameter can be negative or positive. + /// An object whose value is the sum of the time represented by this instance and the number of minutes represented by value. + public TimeOnly AddMinutes(double value) => AddTicks((long)(value * TimeSpan.TicksPerMinute)); + + /// + /// Returns a new TimeOnly that adds the specified number of minutes to the value of this instance. + /// If the added value circulate though the day, this method will out the number of the circulated days. + /// + /// A number of whole and fractional minutes. The value parameter can be negative or positive. + /// When this method returns, contains the number of circulated days resulted from this addition operation. + /// An object whose value is the sum of the time represented by this instance and the number of minutes represented by value. + public TimeOnly AddMinutes(double value, out int wrappedDays) => AddTicks((long)(value * TimeSpan.TicksPerMinute), out wrappedDays); + + /// + /// Determines if a time falls within the range provided. + /// Supports both "normal" ranges such as 10:00-12:00, and ranges that span midnight such as 23:00-01:00. + /// + /// The starting time of day, inclusive. + /// The ending time of day, exclusive. + /// True, if the time falls within the range, false otherwise. + public bool IsBetween(TimeOnly start, TimeOnly end) + { + long startTicks = start._ticks; + long endTicks = end._ticks; + + return startTicks <= endTicks + ? (startTicks <= _ticks && endTicks > _ticks) + : (startTicks <= _ticks || endTicks > _ticks); + } + + /// + /// Determines whether two specified instances of TimeOnly are equal. + /// + /// The first object to compare. + /// The second object to compare. + /// true if left and right represent the same time; otherwise, false. + public static bool operator ==(TimeOnly left, TimeOnly right) => left._ticks == right._ticks; + + /// + /// Determines whether two specified instances of TimeOnly are not equal. + /// + /// The first object to compare. + /// The second object to compare. + /// true if left and right do not represent the same time; otherwise, false. + public static bool operator !=(TimeOnly left, TimeOnly right) => left._ticks != right._ticks; + + /// + /// Determines whether one specified TimeOnly is later than another specified TimeOnly. + /// + /// The first object to compare. + /// The second object to compare. + /// true if left is later than right; otherwise, false. + public static bool operator >(TimeOnly left, TimeOnly right) => left._ticks > right._ticks; + + /// + /// Determines whether one specified TimeOnly represents a time that is the same as or later than another specified TimeOnly. + /// + /// The first object to compare. + /// The second object to compare. + /// true if left is the same as or later than right; otherwise, false. + public static bool operator >=(TimeOnly left, TimeOnly right) => left._ticks >= right._ticks; + + /// + /// Determines whether one specified TimeOnly is earlier than another specified TimeOnly. + /// + /// The first object to compare. + /// The second object to compare. + /// true if left is earlier than right; otherwise, false. + public static bool operator <(TimeOnly left, TimeOnly right) => left._ticks < right._ticks; + + /// + /// Determines whether one specified TimeOnly represents a time that is the same as or earlier than another specified TimeOnly. + /// + /// The first object to compare. + /// The second object to compare. + /// true if left is the same as or earlier than right; otherwise, false. + public static bool operator <=(TimeOnly left, TimeOnly right) => left._ticks <= right._ticks; + + /// + /// Gives the elapsed time between two points on a circular clock, which will always be a positive value. + /// + /// The first TimeOnly instance. + /// The second TimeOnly instance.. + /// The elapsed time between t1 and t2. + public static TimeSpan operator -(TimeOnly t1, TimeOnly t2) => new TimeSpan((t1._ticks - t2._ticks + TimeSpan.TicksPerDay) % TimeSpan.TicksPerDay); + + /// + /// Constructs a TimeOnly object from a TimeSpan representing the time elapsed since midnight. + /// + /// The time interval measured since midnight. This value has to be positive and not exceeding the time of the day. + /// A TimeOnly object representing the time elapsed since midnight using the timeSpan value. + public static TimeOnly FromTimeSpan(TimeSpan timeSpan) => new TimeOnly(timeSpan._ticks); + + /// + /// Constructs a TimeOnly object from a DateTime representing the time of the day in this DateTime object. + /// + /// The time DateTime object to extract the time of the day from. + /// A TimeOnly object representing time of the day specified in the DateTime object. + public static TimeOnly FromDateTime(DateTime dateTime) => new TimeOnly(dateTime.TimeOfDay.Ticks); + + /// + /// Convert the current TimeOnly instance to a TimeSpan object. + /// + /// A TimeSpan object spanning to the time specified in the current TimeOnly object. + public TimeSpan ToTimeSpan() => new TimeSpan(_ticks); + + internal DateTime ToDateTime() => new DateTime(_ticks); + + /// + /// Compares the value of this instance to a specified TimeOnly value and indicates whether this instance is earlier than, the same as, or later than the specified TimeOnly value. + /// + /// The object to compare to the current instance. + /// + /// A signed number indicating the relative values of this instance and the value parameter. + /// Less than zero if this instance is earlier than value. + /// Zero if this instance is the same as value. + /// Greater than zero if this instance is later than value. + /// + public int CompareTo(TimeOnly value) => _ticks.CompareTo(value._ticks); + + /// + /// Compares the value of this instance to a specified object that contains a specified TimeOnly value, and returns an integer that indicates whether this instance is earlier than, the same as, or later than the specified TimeOnly value. + /// + /// A boxed object to compare, or null. + /// + /// A signed number indicating the relative values of this instance and the value parameter. + /// Less than zero if this instance is earlier than value. + /// Zero if this instance is the same as value. + /// Greater than zero if this instance is later than value. + /// + public int CompareTo(object? value) + { + if (value == null) return 1; + if (value is not TimeOnly timeOnly) + { + throw new ArgumentException(SR.Arg_MustBeTimeOnly); + } + + return CompareTo(timeOnly); + } + + /// + /// Returns a value indicating whether the value of this instance is equal to the value of the specified TimeOnly instance. + /// + /// The object to compare to this instance. + /// true if the value parameter equals the value of this instance; otherwise, false. + public bool Equals(TimeOnly value) => _ticks == value._ticks; + + /// + /// Returns a value indicating whether this instance is equal to a specified object. + /// + /// The object to compare to this instance. + /// true if value is an instance of TimeOnly and equals the value of this instance; otherwise, false. + public override bool Equals(object? value) => value is TimeOnly timeOnly && _ticks == timeOnly._ticks; + + /// + /// Returns the hash code for this instance. + /// + /// A 32-bit signed integer hash code. + public override int GetHashCode() + { + long ticks = _ticks; + return unchecked((int)ticks) ^ (int)(ticks >> 32); + } + + private const ParseFlags ParseFlagsTimeMask = ParseFlags.HaveYear | ParseFlags.HaveMonth | ParseFlags.HaveDay | ParseFlags.HaveDate | ParseFlags.TimeZoneUsed | + ParseFlags.TimeZoneUtc | ParseFlags.ParsedMonthName | ParseFlags.CaptureOffset | ParseFlags.UtcSortPattern; + + /// + /// Converts a memory span that contains string representation of a time to its TimeOnly equivalent by using culture-specific format information and a formatting style. + /// + /// The memory span that contains the string to parse. + /// An object that supplies culture-specific format information about s. + /// A bitwise combination of enumeration values that indicates the permitted format of s. A typical value to specify is None. + /// An object that is equivalent to the time contained in s, as specified by provider and styles. + public static TimeOnly Parse(ReadOnlySpan s, IFormatProvider? provider = default, DateTimeStyles style = DateTimeStyles.None) + { + ParseOperationResult result = TryParseInternal(s, provider, style, out TimeOnly timeOnly); + if (result != ParseOperationResult.Success) + { + switch (result) + { + case ParseOperationResult.WrongStyles: throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + case ParseOperationResult.ParseFailure: throw new FormatException(SR.Format(SR.Format_BadTimeOnly, s.ToString())); + default: + Debug.Assert(result == ParseOperationResult.WrongParts); + throw new FormatException(SR.Format(SR.Format_DateTimeOnlyContainsNoneDateParts, s.ToString(), nameof(TimeOnly))); + } + } + + return timeOnly; + } + + private const string OFormat = "HH':'mm':'ss'.'fffffff"; + private const string RFormat = "HH':'mm':'ss"; + + /// + /// Converts the specified span representation of a time to its TimeOnly equivalent using the specified format, culture-specific format information, and style. + /// The format of the string representation must match the specified format exactly or an exception is thrown. + /// + /// A span containing the characters that represent a time to convert. + /// A span containing the characters that represent a format specifier that defines the required format of s. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of enumeration values that indicates the permitted format of s. A typical value to specify is None. + /// An object that is equivalent to the time contained in s, as specified by format, provider, and style. + public static TimeOnly ParseExact(ReadOnlySpan s, ReadOnlySpan format, IFormatProvider? provider = default, DateTimeStyles style = DateTimeStyles.None) + { + ParseOperationResult result = TryParseExactInternal(s, format, provider, style, out TimeOnly timeOnly); + if (result != ParseOperationResult.Success) + { + switch (result) + { + case ParseOperationResult.WrongStyles: throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + case ParseOperationResult.ParseFailure: throw new FormatException(SR.Format(SR.Format_BadTimeOnly, s.ToString())); + default: + Debug.Assert(result == ParseOperationResult.WrongParts); + throw new FormatException(SR.Format(SR.Format_DateTimeOnlyContainsNoneDateParts, s.ToString(), nameof(TimeOnly))); + } + } + + return timeOnly; + } + + /// + /// Converts the specified span representation of a time to its TimeOnly equivalent using the specified array of formats. + /// The format of the string representation must match at least one of the specified formats exactly or an exception is thrown. + /// + /// A span containing the characters that represent a time to convert. + /// An array of allowable formats of s. + /// An object that is equivalent to the time contained in s, as specified by format, provider, and style. + public static TimeOnly ParseExact(ReadOnlySpan s, string[] formats) => ParseExact(s, formats, null, DateTimeStyles.None); + + /// + /// Converts the specified span representation of a time to its TimeOnly equivalent using the specified array of formats, culture-specific format information, and style. + /// The format of the string representation must match at least one of the specified formats exactly or an exception is thrown. + /// + /// A span containing the characters that represent a time to convert. + /// An array of allowable formats of s. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of enumeration values that indicates the permitted format of s. A typical value to specify is None. + /// An object that is equivalent to the time contained in s, as specified by format, provider, and style. + public static TimeOnly ParseExact(ReadOnlySpan s, string[] formats, IFormatProvider? provider, DateTimeStyles style = DateTimeStyles.None) + { + ParseOperationResult result = TryParseExactInternal(s, formats, provider, style, out TimeOnly timeOnly); + if (result != ParseOperationResult.Success) + { + switch (result) + { + case ParseOperationResult.WrongStyles: throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + case ParseOperationResult.ParseFailure: throw new FormatException(SR.Format(SR.Format_BadTimeOnly, s.ToString())); + default: + Debug.Assert(result == ParseOperationResult.BadFormatSpecifier); + throw new FormatException(SR.Argument_BadFormatSpecifier); + } + } + + return timeOnly; + } + + /// + /// Converts a string that contains string representation of a time to its TimeOnly equivalent by using the conventions of the current culture. + /// + /// The string that contains the string to parse. + /// An object that is equivalent to the time contained in s. + public static TimeOnly Parse(string s) => Parse(s, null, DateTimeStyles.None); + + /// + /// Converts a string that contains string representation of a time to its TimeOnly equivalent by using culture-specific format information and a formatting style. + /// + /// The string that contains the string to parse. + /// An object that supplies culture-specific format information about s. + /// A bitwise combination of the enumeration values that indicates the style elements that can be present in s for the parse operation to succeed, and that defines how to interpret the parsed date. A typical value to specify is None. + /// An object that is equivalent to the time contained in s, as specified by provider and styles. + public static TimeOnly Parse(string s, IFormatProvider? provider, DateTimeStyles style = DateTimeStyles.None) + { + if (s == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.s); + return Parse(s.AsSpan(), provider, style); + } + + /// + /// Converts the specified string representation of a time to its TimeOnly equivalent using the specified format. + /// The format of the string representation must match the specified format exactly or an exception is thrown. + /// + /// A string containing the characters that represent a time to convert. + /// A string that represent a format specifier that defines the required format of s. + /// An object that is equivalent to the time contained in s, as specified by format. + public static TimeOnly ParseExact(string s, string format) => ParseExact(s, format, null, DateTimeStyles.None); + + /// + /// Converts the specified string representation of a time to its TimeOnly equivalent using the specified format, culture-specific format information, and style. + /// The format of the string representation must match the specified format exactly or an exception is thrown. + /// + /// A string containing the characters that represent a time to convert. + /// A string containing the characters that represent a format specifier that defines the required format of s. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of the enumeration values that provides additional information about s, about style elements that may be present in s, or about the conversion from s to a TimeOnly value. A typical value to specify is None. + /// An object that is equivalent to the time contained in s, as specified by format, provider, and style. + public static TimeOnly ParseExact(string s, string format, IFormatProvider? provider, DateTimeStyles style = DateTimeStyles.None) + { + if (s == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.s); + if (format == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.format); + return ParseExact(s.AsSpan(), format.AsSpan(), provider, style); + } + + /// + /// Converts the specified span representation of a time to its TimeOnly equivalent using the specified array of formats. + /// The format of the string representation must match at least one of the specified formats exactly or an exception is thrown. + /// + /// A span containing the characters that represent a time to convert. + /// An array of allowable formats of s. + /// An object that is equivalent to the time contained in s, as specified by format, provider, and style. + public static TimeOnly ParseExact(string s, string[] formats) => ParseExact(s, formats, null, DateTimeStyles.None); + + /// + /// Converts the specified string representation of a time to its TimeOnly equivalent using the specified array of formats, culture-specific format information, and style. + /// The format of the string representation must match at least one of the specified formats exactly or an exception is thrown. + /// + /// A string containing the characters that represent a time to convert. + /// An array of allowable formats of s. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of enumeration values that indicates the permitted format of s. A typical value to specify is None. + /// An object that is equivalent to the time contained in s, as specified by format, provider, and style. + public static TimeOnly ParseExact(string s, string[] formats, IFormatProvider? provider, DateTimeStyles style = DateTimeStyles.None) + { + if (s == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.s); + return ParseExact(s.AsSpan(), formats, provider, style); + } + + /// + /// Converts the specified span representation of a time to its TimeOnly equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// A span containing the characters representing the time to convert. + /// When this method returns, contains the TimeOnly value equivalent to the time contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is empty string, or does not contain a valid string representation of a time. This parameter is passed uninitialized. + /// true if the s parameter was converted successfully; otherwise, false. + public static bool TryParse(ReadOnlySpan s, out TimeOnly result) => TryParse(s, null, DateTimeStyles.None, out result); + + /// + /// Converts the specified span representation of a time to its TimeOnly equivalent using the specified array of formats, culture-specific format information, and style. And returns a value that indicates whether the conversion succeeded. + /// + /// A string containing the characters that represent a time to convert. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of enumeration values that indicates the permitted format of s. A typical value to specify is None. + /// When this method returns, contains the TimeOnly value equivalent to the time contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is empty string, or does not contain a valid string representation of a date. This parameter is passed uninitialized. + /// true if the s parameter was converted successfully; otherwise, false. + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) => + TryParseInternal(s, provider, style, out result) == ParseOperationResult.Success; + private static ParseOperationResult TryParseInternal(ReadOnlySpan s, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) + { + if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0) + { + result = default; + return ParseOperationResult.WrongStyles; + } + + DateTimeResult dtResult = default; + + dtResult.Init(s); + + if (!DateTimeParse.TryParse(s, DateTimeFormatInfo.GetInstance(provider), style, ref dtResult)) + { + result = default; + return ParseOperationResult.ParseFailure; + } + + if ((dtResult.flags & ParseFlagsTimeMask) != 0) + { + result = default; + return ParseOperationResult.WrongParts; + } + + result = new TimeOnly(dtResult.parsedDate.TimeOfDay.Ticks); + + return ParseOperationResult.Success; + } + + /// + /// Converts the specified span representation of a time to its TimeOnly equivalent using the specified format and style. + /// The format of the string representation must match the specified format exactly. The method returns a value that indicates whether the conversion succeeded. + /// + /// A span containing the characters representing a time to convert. + /// The required format of s. + /// When this method returns, contains the TimeOnly value equivalent to the time contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s is empty string, or does not contain a time that correspond to the pattern specified in format. This parameter is passed uninitialized. + /// true if s was converted successfully; otherwise, false. + public static bool TryParseExact(ReadOnlySpan s, ReadOnlySpan format, out TimeOnly result) => TryParseExact(s, format, null, DateTimeStyles.None, out result); + + /// + /// Converts the specified span representation of a time to its TimeOnly equivalent using the specified format, culture-specific format information, and style. + /// The format of the string representation must match the specified format exactly. The method returns a value that indicates whether the conversion succeeded. + /// + /// A span containing the characters representing a time to convert. + /// The required format of s. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of one or more enumeration values that indicate the permitted format of s. + /// When this method returns, contains the TimeOnly value equivalent to the time contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s is empty string, or does not contain a time that correspond to the pattern specified in format. This parameter is passed uninitialized. + /// true if s was converted successfully; otherwise, false. + public static bool TryParseExact(ReadOnlySpan s, ReadOnlySpan format, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) => + TryParseExactInternal(s, format, provider, style, out result) == ParseOperationResult.Success; + + private static ParseOperationResult TryParseExactInternal(ReadOnlySpan s, ReadOnlySpan format, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) + { + if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0) + { + result = default; + return ParseOperationResult.WrongStyles; + } + + if (format.Length == 1) + { + switch (format[0]) + { + case 'o': + case 'O': + format = OFormat; + provider = CultureInfo.InvariantCulture.DateTimeFormat; + break; + + case 'r': + case 'R': + format = RFormat; + provider = CultureInfo.InvariantCulture.DateTimeFormat; + break; + } + } + + DateTimeResult dtResult = default; + dtResult.Init(s); + + if (!DateTimeParse.TryParseExact(s, format, DateTimeFormatInfo.GetInstance(provider), style, ref dtResult)) + { + result = default; + return ParseOperationResult.ParseFailure; + } + + if ((dtResult.flags & ParseFlagsTimeMask) != 0) + { + result = default; + return ParseOperationResult.WrongParts; + } + + result = new TimeOnly(dtResult.parsedDate.TimeOfDay.Ticks); + + return ParseOperationResult.Success; + } + + /// + /// Converts the specified char span of a time to its TimeOnly equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// The span containing the string to parse. + /// An array of allowable formats of s. + /// When this method returns, contains the TimeOnly value equivalent to the time contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is Empty, or does not contain a valid string representation of a time. This parameter is passed uninitialized. + /// true if the s parameter was converted successfully; otherwise, false. + public static bool TryParseExact(ReadOnlySpan s, string[] formats, out TimeOnly result) => TryParseExact(s, formats, null, DateTimeStyles.None, out result); + + /// + /// Converts the specified char span of a time to its TimeOnly equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// The span containing the string to parse. + /// An array of allowable formats of s. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of enumeration values that defines how to interpret the parsed time. A typical value to specify is None. + /// When this method returns, contains the TimeOnly value equivalent to the time contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is Empty, or does not contain a valid string representation of a time. This parameter is passed uninitialized. + /// true if the s parameter was converted successfully; otherwise, false. + public static bool TryParseExact(ReadOnlySpan s, string[] formats, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) => + TryParseExactInternal(s, formats, provider, style, out result) == ParseOperationResult.Success; + + private static ParseOperationResult TryParseExactInternal(ReadOnlySpan s, string[] formats, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) + { + if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0 || formats == null) + { + result = default; + return ParseOperationResult.WrongStyles; + } + + DateTimeFormatInfo dtfi = DateTimeFormatInfo.GetInstance(provider); + + for (int i = 0; i < formats.Length; i++) + { + DateTimeFormatInfo dtfiToUse = dtfi; + string? format = formats[i]; + if (string.IsNullOrEmpty(format)) + { + result = default; + return ParseOperationResult.BadFormatSpecifier; + } + + if (format.Length == 1) + { + switch (format[0]) + { + case 'o': + case 'O': + format = OFormat; + dtfiToUse = CultureInfo.InvariantCulture.DateTimeFormat; + break; + + case 'r': + case 'R': + format = RFormat; + dtfiToUse = CultureInfo.InvariantCulture.DateTimeFormat; + break; + } + } + + // Create a new result each time to ensure the runs are independent. Carry through + // flags from the caller and return the result. + DateTimeResult dtResult = default; + dtResult.Init(s); + if (DateTimeParse.TryParseExact(s, format, dtfiToUse, style, ref dtResult) && ((dtResult.flags & ParseFlagsTimeMask) == 0)) + { + result = new TimeOnly(dtResult.parsedDate.TimeOfDay.Ticks); + return ParseOperationResult.Success; + } + } + + result = default; + return ParseOperationResult.ParseFailure; + } + + /// + /// Converts the specified string representation of a time to its TimeOnly equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// A string containing the characters representing the time to convert. + /// When this method returns, contains the TimeOnly value equivalent to the time contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is empty string, or does not contain a valid string representation of a time. This parameter is passed uninitialized. + /// true if the s parameter was converted successfully; otherwise, false. + public static bool TryParse(string s, out TimeOnly result) => TryParse(s, null, DateTimeStyles.None, out result); + + /// + /// Converts the specified string representation of a time to its TimeOnly equivalent using the specified array of formats, culture-specific format information, and style. And returns a value that indicates whether the conversion succeeded. + /// + /// A string containing the characters that represent a time to convert. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of enumeration values that indicates the permitted format of s. A typical value to specify is None. + /// When this method returns, contains the TimeOnly value equivalent to the time contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is empty string, or does not contain a valid string representation of a time. This parameter is passed uninitialized. + /// true if the s parameter was converted successfully; otherwise, false. + public static bool TryParse(string s, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) + { + if (s == null) + { + result = default; + return false; + } + + return TryParse(s.AsSpan(), provider, style, out result); + } + + /// + /// Converts the specified string representation of a time to its TimeOnly equivalent using the specified format and style. + /// The format of the string representation must match the specified format exactly. The method returns a value that indicates whether the conversion succeeded. + /// + /// A string containing the characters representing a time to convert. + /// The required format of s. + /// When this method returns, contains the TimeOnly value equivalent to the time contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s is empty string, or does not contain a time that correspond to the pattern specified in format. This parameter is passed uninitialized. + /// true if s was converted successfully; otherwise, false. + public static bool TryParseExact(string s, string format, out TimeOnly result) => TryParseExact(s, format, null, DateTimeStyles.None, out result); + + /// + /// Converts the specified span representation of a time to its TimeOnly equivalent using the specified format, culture-specific format information, and style. + /// The format of the string representation must match the specified format exactly. The method returns a value that indicates whether the conversion succeeded. + /// + /// A span containing the characters representing a time to convert. + /// The required format of s. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of one or more enumeration values that indicate the permitted format of s. + /// When this method returns, contains the TimeOnly value equivalent to the time contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s is empty string, or does not contain a time that correspond to the pattern specified in format. This parameter is passed uninitialized. + /// true if s was converted successfully; otherwise, false. + public static bool TryParseExact(string s, string format, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) + { + if (s == null || format == null) + { + result = default; + return false; + } + + return TryParseExact(s.AsSpan(), format.AsSpan(), provider, style, out result); + } + + /// + /// Converts the specified string of a time to its TimeOnly equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// The string containing time to parse. + /// An array of allowable formats of s. + /// When this method returns, contains the timeOnly value equivalent to the time contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is Empty, or does not contain a valid string representation of a time. This parameter is passed uninitialized. + /// true if the s parameter was converted successfully; otherwise, false. + public static bool TryParseExact(string s, string[] formats, out TimeOnly result) => TryParseExact(s, formats, null, DateTimeStyles.None, out result); + + /// + /// Converts the specified string of a time to its TimeOnly equivalent and returns a value that indicates whether the conversion succeeded. + /// + /// The string containing the time to parse. + /// An array of allowable formats of s. + /// An object that supplies culture-specific formatting information about s. + /// A bitwise combination of enumeration values that defines how to interpret the parsed date. A typical value to specify is None. + /// When this method returns, contains the TimeOnly value equivalent to the time contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is Empty, or does not contain a valid string representation of a time. This parameter is passed uninitialized. + /// true if the s parameter was converted successfully; otherwise, false. + public static bool TryParseExact(string s, string[] formats, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) + { + if (s == null) + { + result = default; + return false; + } + + return TryParseExact(s.AsSpan(), formats, provider, style, out result); + } + + /// + /// Converts the value of the current TimeOnly object to its equivalent long date string representation. + /// + /// A string that contains the long time string representation of the current TimeOnly object. + public string ToLongTimeString() => ToString("T"); + + /// + /// Converts the value of the current TimeOnly object to its equivalent short time string representation. + /// + /// A string that contains the short time string representation of the current TimeOnly object. + public string ToShortTimeString() => ToString(); + + /// + /// Converts the value of the current TimeOnly object to its equivalent string representation using the formatting conventions of the current culture. + /// The TimeOnly object will be formatted in short form. + /// + /// A string that contains the short time string representation of the current TimeOnly object. + public override string ToString() => ToString("t"); + + /// + /// Converts the value of the current TimeOnly object to its equivalent string representation using the specified format and the formatting conventions of the current culture. + /// + /// A standard or custom time format string. + /// A string representation of value of the current TimeOnly object as specified by format. + /// The accepted standard formats are 'r', 'R', 'o', 'O', 't' and 'T'. + public string ToString(string? format) => ToString(format, null); + + /// + /// Converts the value of the current TimeOnly object to its equivalent string representation using the specified culture-specific format information. + /// + /// An object that supplies culture-specific formatting information. + /// A string representation of value of the current TimeOnly object as specified by provider. + public string ToString(IFormatProvider? provider) => ToString("t", provider); + + /// + /// Converts the value of the current TimeOnly object to its equivalent string representation using the specified culture-specific format information. + /// + /// A standard or custom time format string. + /// An object that supplies culture-specific formatting information. + /// A string representation of value of the current TimeOnly object as specified by format and provider. + /// The accepted standard formats are 'r', 'R', 'o', 'O', 't' and 'T'. + public string ToString(string? format, IFormatProvider? provider) + { + if (format == null || format.Length == 0) + { + format = "t"; + } + + if (format.Length == 1) + { + switch (format[0]) + { + case 'o': + case 'O': + { + return string.Create(16, this, (destination, value) => + { + bool b = DateTimeFormat.TryFormatTimeOnlyO(value.Hour, value.Minute, value.Second, value._ticks % TimeSpan.TicksPerSecond, destination); + Debug.Assert(b); + }); + } + + case 'r': + case 'R': + { + return string.Create(8, this, (destination, value) => + { + bool b = DateTimeFormat.TryFormatTimeOnlyR(value.Hour, value.Minute, value.Second, destination); + Debug.Assert(b); + }); + } + + case 't': + case 'T': + return DateTimeFormat.Format(ToDateTime(), format, provider); + + default: + throw new FormatException(SR.Format_InvalidString); + } + } + + DateTimeFormat.IsValidCustomTimeFormat(format.AsSpan(), throwOnError: true); + return DateTimeFormat.Format(ToDateTime(), format, provider); + } + + /// + /// Tries to format the value of the current TimeOnly instance into the provided span of characters. + /// + /// When this method returns, this instance's value formatted as a span of characters. + /// When this method returns, the number of characters that were written in destination. + /// A span containing the characters that represent a standard or custom format string that defines the acceptable format for destination. + /// An optional object that supplies culture-specific formatting information for destination. + /// true if the formatting was successful; otherwise, false. + /// The accepted standard formats are 'r', 'R', 'o', 'O', 't' and 'T'. + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format = default(ReadOnlySpan), IFormatProvider? provider = null) + { + if (format.Length == 0) + { + format = "t"; + } + + if (format.Length == 1) + { + switch (format[0]) + { + case 'o': + case 'O': + if (!DateTimeFormat.TryFormatTimeOnlyO(Hour, Minute, Second, _ticks % TimeSpan.TicksPerSecond, destination)) + { + charsWritten = 0; + return false; + } + charsWritten = 16; + return true; + + case 'r': + case 'R': + if (!DateTimeFormat.TryFormatTimeOnlyR(Hour, Minute, Second, destination)) + { + charsWritten = 0; + return false; + } + charsWritten = 8; + return true; + + case 't': + case 'T': + return DateTimeFormat.TryFormat(ToDateTime(), destination, out charsWritten, format, provider); + + default: + charsWritten = 0; + return false; + } + } + + if (!DateTimeFormat.IsValidCustomTimeFormat(format, throwOnError: false)) + { + charsWritten = 0; + return false; + } + + return DateTimeFormat.TryFormat(ToDateTime(), destination, out charsWritten, format, provider); + } + } +} diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 6292cc6b7e7db0..707e8f389e0287 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -1294,6 +1294,66 @@ public static partial class Convert public static bool TryToBase64Chars(System.ReadOnlySpan bytes, System.Span chars, out int charsWritten, System.Base64FormattingOptions options = System.Base64FormattingOptions.None) { throw null; } } public delegate TOutput Converter(TInput input); + public readonly struct DateOnly : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable + { + public static DateOnly MinValue { get { throw null; } } + public static DateOnly MaxValue { get { throw null; } } + public DateOnly(int year, int month, int day) { throw null; } + public DateOnly(int year, int month, int day, System.Globalization.Calendar calendar) { throw null; } + public static DateOnly FromDayNumber(int dayNumber) { throw null; } + public int Year { get { throw null; } } + public int Month { get { throw null; } } + public int Day { get { throw null; } } + public System.DayOfWeek DayOfWeek { get { throw null; } } + public int DayOfYear { get { throw null; } } + public int DayNumber { get { throw null; } } + public System.DateOnly AddDays(int value) { throw null; } + public System.DateOnly AddMonths(int value) { throw null; } + public System.DateOnly AddYears(int value) { throw null; } + public static bool operator ==(System.DateOnly left, System.DateOnly right) { throw null; } + public static bool operator >(System.DateOnly left, System.DateOnly right) { throw null; } + public static bool operator >=(System.DateOnly left, System.DateOnly right) { throw null; } + public static bool operator !=(System.DateOnly left, System.DateOnly right) { throw null; } + public static bool operator <(System.DateOnly left, System.DateOnly right) { throw null; } + public static bool operator <=(System.DateOnly left, System.DateOnly right) { throw null; } + public System.DateTime ToDateTime(System.TimeOnly time) { throw null; } + public System.DateTime ToDateTime(System.TimeOnly time, System.DateTimeKind kind) { throw null; } + public static System.DateOnly FromDateTime(System.DateTime dateTime) { throw null; } + public int CompareTo(System.DateOnly value) { throw null; } + public int CompareTo(object? value) { throw null; } + public bool Equals(System.DateOnly value) { throw null; } + public override bool Equals(object? value) { throw null; } + public override int GetHashCode() { throw null; } + public static System.DateOnly Parse(System.ReadOnlySpan s, System.IFormatProvider? provider = default, System.Globalization.DateTimeStyles style = System.Globalization.DateTimeStyles.None) { throw null; } + public static System.DateOnly ParseExact(System.ReadOnlySpan s, System.ReadOnlySpan format, System.IFormatProvider? provider = default, System.Globalization.DateTimeStyles style = System.Globalization.DateTimeStyles.None) { throw null; } + public static System.DateOnly ParseExact(System.ReadOnlySpan s, string[] formats) { throw null; } + public static System.DateOnly ParseExact(System.ReadOnlySpan s, string[] formats, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style = System.Globalization.DateTimeStyles.None) { throw null; } + public static System.DateOnly Parse(string s) { throw null; } + public static System.DateOnly Parse(string s, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style = System.Globalization.DateTimeStyles.None) { throw null; } + public static System.DateOnly ParseExact(string s, string format) { throw null; } + public static System.DateOnly ParseExact(string s, string format, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style = System.Globalization.DateTimeStyles.None) { throw null; } + public static System.DateOnly ParseExact(string s, string[] formats) { throw null; } + public static System.DateOnly ParseExact(string s, string[] formats, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style = System.Globalization.DateTimeStyles.None) { throw null; } + public static bool TryParse(System.ReadOnlySpan s, out System.DateOnly result) { throw null; } + public static bool TryParse(System.ReadOnlySpan s, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style, out System.DateOnly result) { throw null; } + public static bool TryParseExact(System.ReadOnlySpan s, System.ReadOnlySpan format, out System.DateOnly result) { throw null; } + public static bool TryParseExact(System.ReadOnlySpan s, System.ReadOnlySpan format, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style, out System.DateOnly result) { throw null; } + public static bool TryParseExact(System.ReadOnlySpan s, string[] formats, out System.DateOnly result) { throw null; } + public static bool TryParseExact(System.ReadOnlySpan s, string[] formats, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style, out System.DateOnly result) { throw null; } + public static bool TryParse(string s, out System.DateOnly result) { throw null; } + public static bool TryParse(string s, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style, out System.DateOnly result) { throw null; } + public static bool TryParseExact(string s, string format, out System.DateOnly result) { throw null; } + public static bool TryParseExact(string s, string format, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style, out System.DateOnly result) { throw null; } + public static bool TryParseExact(string s, string[] formats, out System.DateOnly result) { throw null; } + public static bool TryParseExact(string s, string[] formats, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style, out System.DateOnly result) { throw null; } + public string ToLongDateString() { throw null; } + public string ToShortDateString() { throw null; } + 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(System.Span destination, out int charsWritten, System.ReadOnlySpan format = default(System.ReadOnlySpan), System.IFormatProvider? provider = null) { throw null; } + } public readonly partial struct DateTime : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable, System.Runtime.Serialization.ISerializable { private readonly int _dummyPrimitive; @@ -3738,6 +3798,71 @@ public partial class ThreadStaticAttribute : System.Attribute { public ThreadStaticAttribute() { } } + public readonly struct TimeOnly : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable + { + public static System.TimeOnly MinValue { get { throw null; } } + public static System.TimeOnly MaxValue { get { throw null; } } + public TimeOnly(int hour, int minute) { throw null; } + public TimeOnly(int hour, int minute, int second) { throw null; } + public TimeOnly(int hour, int minute, int second, int millisecond) { throw null; } + public TimeOnly(long ticks) { throw null; } + public int Hour { 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 System.TimeOnly Add(System.TimeSpan value) { throw null; } + public System.TimeOnly Add(System.TimeSpan value, out int wrappedDays) { throw null; } + public System.TimeOnly AddHours(double value) { throw null; } + public System.TimeOnly AddHours(double value, out int wrappedDays) { throw null; } + public System.TimeOnly AddMinutes(double value) { throw null; } + public System.TimeOnly AddMinutes(double value, out int wrappedDays) { throw null; } + public bool IsBetween(System.TimeOnly start, System.TimeOnly end) { throw null; } + public static bool operator ==(System.TimeOnly left, System.TimeOnly right) { throw null; } + public static bool operator >(System.TimeOnly left, System.TimeOnly right) { throw null; } + public static bool operator >=(System.TimeOnly left, System.TimeOnly right) { throw null; } + public static bool operator !=(System.TimeOnly left, System.TimeOnly right) { throw null; } + public static bool operator <(System.TimeOnly left, System.TimeOnly right) { throw null; } + public static bool operator <=(System.TimeOnly left, System.TimeOnly right) { throw null; } + public static System.TimeSpan operator -(System.TimeOnly t1, System.TimeOnly t2) { throw null; } + public static System.TimeOnly FromTimeSpan(System.TimeSpan timeSpan) { throw null; } + public static System.TimeOnly FromDateTime(System.DateTime dateTime) { throw null; } + public System.TimeSpan ToTimeSpan() { throw null; } + public int CompareTo(System.TimeOnly value) { throw null; } + public int CompareTo(object? value) { throw null; } + public bool Equals(System.TimeOnly value) { throw null; } + public override bool Equals(object? value) { throw null; } + public override int GetHashCode() { throw null; } + public static System.TimeOnly Parse(System.ReadOnlySpan s, System.IFormatProvider? provider = default, System.Globalization.DateTimeStyles style = System.Globalization.DateTimeStyles.None) { throw null; } + public static System.TimeOnly ParseExact(System.ReadOnlySpan s, System.ReadOnlySpan format, System.IFormatProvider? provider = default, System.Globalization.DateTimeStyles style = System.Globalization.DateTimeStyles.None) { throw null; } + public static System.TimeOnly ParseExact(System.ReadOnlySpan s, string[] formats) { throw null; } + public static System.TimeOnly ParseExact(System.ReadOnlySpan s, string[] formats, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style = System.Globalization.DateTimeStyles.None) { throw null; } + public static System.TimeOnly Parse(string s) { throw null; } + public static System.TimeOnly Parse(string s, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style = System.Globalization.DateTimeStyles.None) { throw null; } + public static System.TimeOnly ParseExact(string s, string format) { throw null; } + public static System.TimeOnly ParseExact(string s, string format, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style = System.Globalization.DateTimeStyles.None) { throw null; } + public static System.TimeOnly ParseExact(string s, string[] formats) { throw null; } + public static System.TimeOnly ParseExact(string s, string[] formats, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style = System.Globalization.DateTimeStyles.None) { throw null; } + public static bool TryParse(System.ReadOnlySpan s, out System.TimeOnly result) { throw null; } + public static bool TryParse(System.ReadOnlySpan s, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style, out System.TimeOnly result) { throw null; } + public static bool TryParseExact(System.ReadOnlySpan s, System.ReadOnlySpan format, out System.TimeOnly result) { throw null; } + public static bool TryParseExact(System.ReadOnlySpan s, System.ReadOnlySpan format, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style, out System.TimeOnly result) { throw null; } + public static bool TryParseExact(System.ReadOnlySpan s, string[] formats, out System.TimeOnly result) { throw null; } + public static bool TryParseExact(System.ReadOnlySpan s, string[] formats, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style, out System.TimeOnly result) { throw null; } + public static bool TryParse(string s, out System.TimeOnly result) { throw null; } + public static bool TryParse(string s, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style, out System.TimeOnly result) { throw null; } + public static bool TryParseExact(string s, string format, out System.TimeOnly result) { throw null; } + public static bool TryParseExact(string s, string format, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style, out System.TimeOnly result) { throw null; } + public static bool TryParseExact(string s, string[] formats, out System.TimeOnly result) { throw null; } + public static bool TryParseExact(string s, string[] formats, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style, out System.TimeOnly result) { throw null; } + public string ToLongTimeString() { throw null; } + public string ToShortTimeString() { throw null; } + 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(System.Span destination, out int charsWritten, System.ReadOnlySpan format = default(System.ReadOnlySpan), System.IFormatProvider? provider = null) { throw null; } + } public partial class TimeoutException : System.SystemException { public TimeoutException() { } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj index e4b0b3580f5853..d9272e44addeae 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj @@ -59,6 +59,7 @@ + @@ -127,6 +128,7 @@ + diff --git a/src/libraries/System.Runtime/tests/System/DateOnlyTests.cs b/src/libraries/System.Runtime/tests/System/DateOnlyTests.cs new file mode 100644 index 00000000000000..935c75e01bb7c3 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System/DateOnlyTests.cs @@ -0,0 +1,518 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Globalization; +using Xunit; + +namespace System.Tests +{ + public class DateOnlyTests + { + [Fact] + public static void MinMaxValuesTest() + { + DateOnly date = DateOnly.MinValue; + Assert.Equal(0, date.DayNumber); + Assert.Equal(1, date.Year); + Assert.Equal(1, date.Month); + Assert.Equal(1, date.Day); + + date = DateOnly.MaxValue; + Assert.Equal(3652058, date.DayNumber); + Assert.Equal(9999, date.Year); + Assert.Equal(12, date.Month); + Assert.Equal(31, date.Day); + } + + public static IEnumerable Constructor_TestData() + { + yield return new object[] { 1, 1, 1, null }; + yield return new object[] { 9999, 12, 31, null }; + yield return new object[] { 2001, 4, 7, null }; + yield return new object[] { 1, 1, 1, new HijriCalendar() }; + yield return new object[] { 1, 1, 1, new JapaneseCalendar() }; + } + + [Theory] + [MemberData(nameof(Constructor_TestData))] + public static void ConstructorsTest(int year, int month, int day, Calendar calendar) + { + if (calendar == null) + { + DateOnly dateOnly = new DateOnly(year, month, day); + Assert.Equal(year, dateOnly.Year); + Assert.Equal(month, dateOnly.Month); + Assert.Equal(day, dateOnly.Day); + } + else + { + DateTime dt = calendar.ToDateTime(year, month, day, 0, 0, 0, 0); + DateOnly dateOnly = new DateOnly(year, month, day, calendar); + Assert.Equal(dt.Year, dateOnly.Year); + Assert.Equal(dt.Month, dateOnly.Month); + Assert.Equal(dt.Day, dateOnly.Day); + } + } + + [Fact] + public static void ConstructorsNegativeCasesTest() + { + Assert.Throws(() => new DateOnly(10000, 1, 1)); + Assert.Throws(() => new DateOnly(-2021, 1, 1)); + Assert.Throws(() => new DateOnly(2020, 13, 1)); + Assert.Throws(() => new DateOnly(2020, -1, 1)); + Assert.Throws(() => new DateOnly(2020, 0, 1)); + Assert.Throws(() => new DateOnly(2020, 1, 0)); + Assert.Throws(() => new DateOnly(2020, 1, 32)); + Assert.Throws(() => new DateOnly(2020, 1, -1)); + Assert.Throws(() => new DateOnly(2003, 2, 29)); + } + + [Fact] + public static void FromDayNumberTest() + { + DateOnly dateOnly = DateOnly.FromDayNumber(DateOnly.MinValue.DayNumber); + Assert.Equal(1, dateOnly.Year); + Assert.Equal(1, dateOnly.Month); + Assert.Equal(1, dateOnly.Day); + Assert.Equal(DateOnly.MinValue.DayNumber, dateOnly.DayNumber); + + dateOnly = DateOnly.FromDayNumber(DateOnly.MaxValue.DayNumber); + Assert.Equal(9999, dateOnly.Year); + Assert.Equal(12, dateOnly.Month); + Assert.Equal(31, dateOnly.Day); + Assert.Equal(DateOnly.MaxValue.DayNumber, dateOnly.DayNumber); + + DateTime dt = DateTime.Today; + int dayNumber = (int) (dt.Ticks / TimeSpan.TicksPerDay); + dateOnly = DateOnly.FromDayNumber(dayNumber); + Assert.Equal(dt.Year, dateOnly.Year); + Assert.Equal(dt.Month, dateOnly.Month); + Assert.Equal(dt.Day, dateOnly.Day); + Assert.Equal(dayNumber, dateOnly.DayNumber); + + Assert.Throws(() => DateOnly.FromDayNumber(-1)); + Assert.Throws(() => DateOnly.FromDayNumber(DateOnly.MaxValue.DayNumber + 1)); + } + + [Fact] + public static void DayOfWeekAndDayOfYearTest() + { + DateTime dt = DateTime.Today; + DateOnly dateOnly = DateOnly.FromDayNumber((int) (dt.Ticks / TimeSpan.TicksPerDay)); + Assert.Equal(dt.DayOfWeek, dateOnly.DayOfWeek); + Assert.Equal(dt.DayOfYear, dateOnly.DayOfYear); + } + + + [Fact] + public static void AddDaysTest() + { + DateOnly dateOnly = DateOnly.MinValue.AddDays(1); + Assert.Equal(1, dateOnly.DayNumber); + dateOnly = dateOnly.AddDays(1); + Assert.Equal(2, dateOnly.DayNumber); + dateOnly = dateOnly.AddDays(100); + Assert.Equal(102, dateOnly.DayNumber); + + dateOnly = DateOnly.MaxValue.AddDays(-1); + Assert.Equal(DateOnly.MaxValue.DayNumber - 1, dateOnly.DayNumber); + dateOnly = dateOnly.AddDays(-1); + Assert.Equal(DateOnly.MaxValue.DayNumber - 2, dateOnly.DayNumber); + dateOnly = dateOnly.AddDays(-100); + Assert.Equal(DateOnly.MaxValue.DayNumber - 102, dateOnly.DayNumber); + + Assert.Throws(() => DateOnly.MinValue.AddDays(-1)); + Assert.Throws(() => DateOnly.MaxValue.AddDays(1)); + } + + [Fact] + public static void AddMonthsTest() + { + DateOnly dateOnly = new DateOnly(2021, 1, 31); + for (int i = 1; i < 12; i++) + { + Assert.Equal(i, dateOnly.Month); + dateOnly = dateOnly.AddMonths(1); + } + + for (int i = 12; i > 1; i--) + { + Assert.Equal(i, dateOnly.Month); + dateOnly = dateOnly.AddMonths(-1); + } + + DateTime dt = DateTime.Today; + dateOnly = DateOnly.FromDayNumber((int) (dt.Ticks / TimeSpan.TicksPerDay)); + + Assert.Equal(dt.Year, dateOnly.Year); + Assert.Equal(dt.Month, dateOnly.Month); + Assert.Equal(dt.Day, dateOnly.Day); + + dt = dt.AddMonths(1); + dateOnly = dateOnly.AddMonths(1); + Assert.Equal(dt.Month, dateOnly.Month); + + dt = dt.AddMonths(50); + dateOnly = dateOnly.AddMonths(50); + Assert.Equal(dt.Month, dateOnly.Month); + + dt = dt.AddMonths(-150); + dateOnly = dateOnly.AddMonths(-150); + Assert.Equal(dt.Month, dateOnly.Month); + + Assert.Throws(() => DateOnly.MinValue.AddMonths(-1)); + Assert.Throws(() => DateOnly.MaxValue.AddMonths(1)); + } + + [Fact] + public static void AddYearsTest() + { + DateOnly dateOnly = new DateOnly(2021, 1, 31); + for (int i = 2021; i < 2040; i++) + { + Assert.Equal(i, dateOnly.Year); + dateOnly = dateOnly.AddYears(1); + } + + for (int i = dateOnly.Year; i > 2020; i--) + { + Assert.Equal(i, dateOnly.Year); + dateOnly = dateOnly.AddYears(-1); + } + + DateTime dt = DateTime.Today; + dateOnly = DateOnly.FromDayNumber((int) (dt.Ticks / TimeSpan.TicksPerDay)); + + Assert.Equal(dt.Year, dateOnly.Year); + Assert.Equal(dt.Month, dateOnly.Month); + Assert.Equal(dt.Day, dateOnly.Day); + + dt = dt.AddYears(1); + dateOnly = dateOnly.AddYears(1); + Assert.Equal(dt.Year, dateOnly.Year); + + dt = dt.AddYears(50); + dateOnly = dateOnly.AddYears(50); + Assert.Equal(dt.Year, dateOnly.Year); + + dt = dt.AddYears(-150); + dateOnly = dateOnly.AddYears(-150); + Assert.Equal(dt.Year, dateOnly.Year); + + Assert.Throws(() => DateOnly.MinValue.AddYears(-1)); + Assert.Throws(() => DateOnly.MaxValue.AddYears(1)); + } + + [Fact] + public static void OperatorsTest() + { + Assert.True(DateOnly.MinValue != DateOnly.MaxValue); + Assert.True(DateOnly.MinValue < DateOnly.MaxValue); + Assert.True(DateOnly.MinValue <= DateOnly.MaxValue); + Assert.True(DateOnly.MaxValue > DateOnly.MinValue); + Assert.True(DateOnly.MaxValue >= DateOnly.MinValue); + + DateOnly dateOnly1 = new DateOnly(2021, 10, 10); + DateOnly dateOnly2 = new DateOnly(2021, 10, 11); + DateOnly dateOnly3 = new DateOnly(2021, 10, 10); + + Assert.True(dateOnly1 == dateOnly3); + Assert.True(dateOnly1 >= dateOnly3); + Assert.True(dateOnly1 <= dateOnly3); + Assert.True(dateOnly1 != dateOnly2); + Assert.True(dateOnly1 < dateOnly2); + Assert.True(dateOnly1 <= dateOnly2); + Assert.True(dateOnly2 > dateOnly1); + Assert.True(dateOnly2 >= dateOnly1); + } + + [Fact] + public static void DateTimeConversionTest() + { + DateTime dt = DateTime.Today; + DateOnly dateOnly = DateOnly.FromDateTime(dt); + Assert.Equal(dt.Year, dateOnly.Year); + Assert.Equal(dt.Month, dateOnly.Month); + Assert.Equal(dt.Day, dateOnly.Day); + + dt = dateOnly.ToDateTime(new TimeOnly(1, 10, 20)); + Assert.Equal(dateOnly.Year, dt.Year); + Assert.Equal(dateOnly.Month, dt.Month); + Assert.Equal(dateOnly.Day, dt.Day); + + Assert.Equal(1, dt.Hour); + Assert.Equal(10, dt.Minute); + Assert.Equal(20, dt.Second); + Assert.Equal(DateTimeKind.Unspecified, dt.Kind); + + + dt = dateOnly.ToDateTime(new TimeOnly(23, 59, 59), DateTimeKind.Utc); + Assert.Equal(dateOnly.Year, dt.Year); + Assert.Equal(dateOnly.Month, dt.Month); + Assert.Equal(dateOnly.Day, dt.Day); + + Assert.Equal(23, dt.Hour); + Assert.Equal(59, dt.Minute); + Assert.Equal(59, dt.Second); + Assert.Equal(DateTimeKind.Utc, dt.Kind); + + dt = dateOnly.ToDateTime(new TimeOnly(23, 59, 59), DateTimeKind.Local); + Assert.Equal(DateTimeKind.Local, dt.Kind); + + dateOnly = DateOnly.FromDateTime(dt); + Assert.Equal(dt.Year, dateOnly.Year); + Assert.Equal(dt.Month, dateOnly.Month); + Assert.Equal(dt.Day, dateOnly.Day); + } + + [Fact] + public static void ComparisonsTest() + { + DateOnly dateOnly1 = DateOnly.FromDateTime(DateTime.Today); + DateOnly dateOnly2 = DateOnly.FromDateTime(DateTime.Today); + DateOnly dateOnly3 = dateOnly1.AddYears(-10); + + Assert.Equal(0, dateOnly1.CompareTo(dateOnly2)); + Assert.True(dateOnly1.Equals(dateOnly2)); + Assert.True(dateOnly1.Equals((object)dateOnly2)); + Assert.Equal(0, dateOnly2.CompareTo(dateOnly1)); + Assert.True(dateOnly2.Equals(dateOnly1)); + Assert.True(dateOnly2.Equals((object)dateOnly1)); + Assert.Equal(1, dateOnly1.CompareTo(dateOnly3)); + Assert.False(dateOnly1.Equals(dateOnly3)); + Assert.False(dateOnly1.Equals((object)dateOnly3)); + Assert.Equal(-1, dateOnly3.CompareTo(dateOnly1)); + Assert.False(dateOnly3.Equals(dateOnly1)); + Assert.False(dateOnly3.Equals((object)dateOnly1)); + + Assert.Equal(0, dateOnly1.CompareTo((object)dateOnly2)); + Assert.Equal(0, dateOnly2.CompareTo((object)dateOnly1)); + Assert.Equal(1, dateOnly1.CompareTo((object)dateOnly3)); + Assert.Equal(-1, dateOnly3.CompareTo((object)dateOnly1)); + + Assert.Equal(1, dateOnly1.CompareTo(null)); + + Assert.Throws(() => dateOnly1.CompareTo(new object())); + Assert.False(dateOnly3.Equals(new object())); + } + + [Fact] + public static void GetHashCodeTest() + { + Assert.Equal(DateOnly.MinValue.DayNumber, DateOnly.MinValue.GetHashCode()); + Assert.Equal(DateOnly.MaxValue.DayNumber, DateOnly.MaxValue.GetHashCode()); + DateOnly dateOnly = DateOnly.FromDateTime(DateTime.Today); + Assert.Equal(dateOnly.DayNumber, dateOnly.GetHashCode()); + } + + // Arabic cultures uses zero width characters in the date formatting which cause a problem with the DateTime parsing in general. + // We still test these cultures parsing but with ParseExact instead. + internal static bool IsNotArabicCulture => !CultureInfo.CurrentCulture.Name.StartsWith("ar", StringComparison.OrdinalIgnoreCase); + + [ConditionalFact(nameof(IsNotArabicCulture))] + public static void BasicFormatParseTest() + { + DateOnly dateOnly = DateOnly.FromDateTime(DateTime.Today); + string s = dateOnly.ToString(); + DateOnly parsedDateOnly = DateOnly.Parse(s); + Assert.True(DateOnly.TryParse(s, out DateOnly parsedDateOnly1)); + Assert.Equal(dateOnly, parsedDateOnly); + Assert.Equal(dateOnly, parsedDateOnly1); + parsedDateOnly = DateOnly.Parse(s.AsSpan()); + Assert.True(DateOnly.TryParse(s.AsSpan(), out parsedDateOnly1)); + Assert.Equal(dateOnly, parsedDateOnly); + Assert.Equal(dateOnly, parsedDateOnly1); + + s = dateOnly.ToString(CultureInfo.InvariantCulture); + parsedDateOnly = DateOnly.Parse(s, CultureInfo.InvariantCulture); + Assert.True(DateOnly.TryParse(s.AsSpan(), CultureInfo.InvariantCulture, DateTimeStyles.None, out parsedDateOnly1)); + Assert.Equal(dateOnly, parsedDateOnly); + Assert.Equal(dateOnly, parsedDateOnly1); + parsedDateOnly = DateOnly.Parse(s.AsSpan(), CultureInfo.InvariantCulture); + Assert.True(DateOnly.TryParse(s.AsSpan(), CultureInfo.InvariantCulture, DateTimeStyles.None, out parsedDateOnly1)); + Assert.Equal(dateOnly, parsedDateOnly); + Assert.Equal(dateOnly, parsedDateOnly1); + + Assert.False(DateOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out parsedDateOnly1)); + Assert.Throws(() => DateOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal)); + Assert.False(DateOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out parsedDateOnly1)); + Assert.Throws(() => DateOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal)); + Assert.False(DateOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out parsedDateOnly1)); + Assert.Throws(() => DateOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)); + Assert.False(DateOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out parsedDateOnly1)); + Assert.Throws(() => DateOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault)); + + s = " " + s + " "; + parsedDateOnly = DateOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces); + Assert.Equal(dateOnly, parsedDateOnly); + parsedDateOnly = DateOnly.Parse(s.AsSpan(), CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces); + Assert.Equal(dateOnly, parsedDateOnly); + } + + [ConditionalFact(nameof(IsNotArabicCulture))] + public static void FormatParseTest() + { + string[] patterns = new string[] { CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern, CultureInfo.CurrentCulture.DateTimeFormat.LongDatePattern, "d", "D", "o", "r" }; + + DateOnly dateOnly = DateOnly.FromDateTime(DateTime.Today); + + foreach (string format in patterns) + { + string formattedDate = dateOnly.ToString(format); + DateOnly parsedDateOnly = DateOnly.Parse(formattedDate); + Assert.True(DateOnly.TryParse(formattedDate, out DateOnly parsedDateOnly1)); + Assert.Equal(dateOnly, parsedDateOnly); + Assert.Equal(dateOnly, parsedDateOnly1); + parsedDateOnly = DateOnly.Parse(formattedDate.AsSpan()); + Assert.True(DateOnly.TryParse(formattedDate.AsSpan(), out parsedDateOnly1)); + Assert.Equal(dateOnly, parsedDateOnly); + Assert.Equal(dateOnly, parsedDateOnly1); + + parsedDateOnly = DateOnly.Parse(formattedDate, CultureInfo.CurrentCulture); + Assert.True(DateOnly.TryParse(formattedDate, CultureInfo.CurrentCulture, DateTimeStyles.None, out parsedDateOnly1)); + Assert.Equal(dateOnly, parsedDateOnly); + Assert.Equal(dateOnly, parsedDateOnly1); + parsedDateOnly = DateOnly.Parse(formattedDate.AsSpan(), CultureInfo.CurrentCulture); + Assert.True(DateOnly.TryParse(formattedDate.AsSpan(), CultureInfo.CurrentCulture, DateTimeStyles.None, out parsedDateOnly1)); + Assert.Equal(dateOnly, parsedDateOnly); + Assert.Equal(dateOnly, parsedDateOnly1); + + parsedDateOnly = DateOnly.ParseExact(formattedDate, format); + Assert.True(DateOnly.TryParseExact(formattedDate, format, out parsedDateOnly1)); + Assert.Equal(dateOnly, parsedDateOnly); + Assert.Equal(dateOnly, parsedDateOnly1); + parsedDateOnly = DateOnly.ParseExact(formattedDate.AsSpan(), format.AsSpan()); + Assert.True(DateOnly.TryParseExact(formattedDate.AsSpan(), format.AsSpan(), out parsedDateOnly1)); + Assert.Equal(dateOnly, parsedDateOnly); + Assert.Equal(dateOnly, parsedDateOnly1); + + parsedDateOnly = DateOnly.ParseExact(formattedDate, format, CultureInfo.CurrentCulture); + Assert.True(DateOnly.TryParseExact(formattedDate, format, CultureInfo.CurrentCulture, DateTimeStyles.None, out parsedDateOnly1)); + Assert.Equal(dateOnly, parsedDateOnly); + Assert.Equal(dateOnly, parsedDateOnly1); + parsedDateOnly = DateOnly.ParseExact(formattedDate.AsSpan(), format.AsSpan(), CultureInfo.CurrentCulture); + Assert.True(DateOnly.TryParseExact(formattedDate.AsSpan(), format.AsSpan(), CultureInfo.CurrentCulture, DateTimeStyles.None, out parsedDateOnly1)); + Assert.Equal(dateOnly, parsedDateOnly); + Assert.Equal(dateOnly, parsedDateOnly1); + + parsedDateOnly = DateOnly.ParseExact(formattedDate, patterns); + Assert.True(DateOnly.TryParseExact(formattedDate, patterns, out parsedDateOnly1)); + Assert.Equal(dateOnly, parsedDateOnly); + Assert.Equal(dateOnly, parsedDateOnly1); + parsedDateOnly = DateOnly.ParseExact(formattedDate.AsSpan(), patterns); + Assert.True(DateOnly.TryParseExact(formattedDate.AsSpan(), patterns, out parsedDateOnly1)); + Assert.Equal(dateOnly, parsedDateOnly); + Assert.Equal(dateOnly, parsedDateOnly1); + + parsedDateOnly = DateOnly.ParseExact(formattedDate, patterns, CultureInfo.CurrentCulture); + Assert.True(DateOnly.TryParseExact(formattedDate, patterns, CultureInfo.CurrentCulture, DateTimeStyles.None, out parsedDateOnly1)); + Assert.Equal(dateOnly, parsedDateOnly); + Assert.Equal(dateOnly, parsedDateOnly1); + parsedDateOnly = DateOnly.ParseExact(formattedDate.AsSpan(), patterns, CultureInfo.CurrentCulture); + Assert.True(DateOnly.TryParseExact(formattedDate.AsSpan(), patterns, CultureInfo.CurrentCulture, DateTimeStyles.None, out parsedDateOnly1)); + Assert.Equal(dateOnly, parsedDateOnly); + Assert.Equal(dateOnly, parsedDateOnly1); + } + } + + [Fact] + public static void OAndRFormatsTest() + { + DateOnly dateOnly = DateOnly.FromDateTime(DateTime.Today); + string formattedDate = dateOnly.ToString("o"); + Assert.Equal(10, formattedDate.Length); + Assert.Equal('-', formattedDate[4]); + Assert.Equal('-', formattedDate[7]); + DateOnly parsedDateOnly = DateOnly.Parse(formattedDate); + Assert.Equal(dateOnly, parsedDateOnly); + parsedDateOnly = DateOnly.Parse(formattedDate.AsSpan()); + Assert.Equal(dateOnly, parsedDateOnly); + parsedDateOnly = DateOnly.ParseExact(formattedDate, "O"); + Assert.Equal(dateOnly, parsedDateOnly); + parsedDateOnly = DateOnly.ParseExact(formattedDate.AsSpan(), "O".AsSpan()); + Assert.Equal(dateOnly, parsedDateOnly); + + formattedDate = dateOnly.ToString("r"); + Assert.Equal(16, formattedDate.Length); + Assert.Equal(',', formattedDate[3]); + Assert.Equal(' ', formattedDate[4]); + Assert.Equal(' ', formattedDate[7]); + Assert.Equal(' ', formattedDate[11]); + parsedDateOnly = DateOnly.Parse(formattedDate); + Assert.Equal(dateOnly, parsedDateOnly); + parsedDateOnly = DateOnly.Parse(formattedDate.AsSpan()); + Assert.Equal(dateOnly, parsedDateOnly); + parsedDateOnly = DateOnly.ParseExact(formattedDate, "R"); + Assert.Equal(dateOnly, parsedDateOnly); + parsedDateOnly = DateOnly.ParseExact(formattedDate.AsSpan(), "R".AsSpan()); + Assert.Equal(dateOnly, parsedDateOnly); + } + + [Fact] + public static void InvalidFormatsTest() + { + DateTime dt = DateTime.Now; + string formatted = dt.ToString(); + Assert.Throws(() => DateOnly.Parse(formatted)); + Assert.Throws(() => DateOnly.Parse(formatted.AsSpan())); + Assert.False(DateOnly.TryParse(formatted, out DateOnly dateOnly)); + Assert.False(DateOnly.TryParse(formatted.AsSpan(), out dateOnly)); + formatted = dt.ToString("t"); + Assert.Throws(() => DateOnly.Parse(formatted)); + Assert.Throws(() => DateOnly.Parse(formatted.AsSpan())); + Assert.False(DateOnly.TryParse(formatted, out dateOnly)); + Assert.False(DateOnly.TryParse(formatted.AsSpan(), out dateOnly)); + } + + [Fact] + public static void CustomFormattingTest() + { + DateOnly dateOnly = DateOnly.FromDateTime(DateTime.Today); + string format = "dd, ddd 'dash' MMMM \"dash\" yyyy"; + string formatted = dateOnly.ToString(format); + DateOnly parsedDateOnly = DateOnly.ParseExact(formatted, format); + Assert.Equal(dateOnly, parsedDateOnly); + parsedDateOnly = DateOnly.ParseExact(formatted.AsSpan(), format.AsSpan()); + Assert.Equal(dateOnly, parsedDateOnly); + + Assert.Throws(() => dateOnly.ToString("dd-MM-yyyy hh")); + Assert.Throws(() => dateOnly.ToString("dd-MM-yyyy m")); + Assert.Throws(() => dateOnly.ToString("dd-MM-yyyy s")); + Assert.Throws(() => dateOnly.ToString("dd-MM-yyyy z")); + } + + [Fact] + public static void AllCulturesTest() + { + DateOnly dateOnly = DateOnly.FromDateTime(DateTime.Today); + foreach (CultureInfo ci in CultureInfo.GetCultures(CultureTypes.SpecificCultures)) + { + string formatted = dateOnly.ToString("d", ci); + DateOnly parsedDateOnly = DateOnly.ParseExact(formatted, "d", ci); + Assert.Equal(dateOnly, parsedDateOnly); + + formatted = dateOnly.ToString("D", ci); + parsedDateOnly = DateOnly.ParseExact(formatted, "D", ci); + Assert.Equal(dateOnly, parsedDateOnly); + } + } + + [Fact] + public static void TryFormatTest() + { + Span buffer = stackalloc char[100]; + DateOnly dateOnly = DateOnly.FromDateTime(DateTime.Today); + + Assert.True(dateOnly.TryFormat(buffer, out int charsWritten)); + Assert.True(dateOnly.TryFormat(buffer, out charsWritten, "o")); + Assert.Equal(10, charsWritten); + Assert.True(dateOnly.TryFormat(buffer, out charsWritten, "R")); + Assert.Equal(16, charsWritten); + Assert.False(dateOnly.TryFormat(buffer.Slice(0, 3), out charsWritten)); + Assert.False(dateOnly.TryFormat(buffer.Slice(0, 3), out charsWritten, "r")); + Assert.False(dateOnly.TryFormat(buffer.Slice(0, 3), out charsWritten, "O")); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System/TimeOnlyTests.cs b/src/libraries/System.Runtime/tests/System/TimeOnlyTests.cs new file mode 100644 index 00000000000000..a2d6cecac91da7 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System/TimeOnlyTests.cs @@ -0,0 +1,479 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Globalization; +using Xunit; + +namespace System.Tests +{ + public class TimeOnlyTests + { + [Fact] + public static void MinMaxValuesTest() + { + Assert.Equal(0, TimeOnly.MinValue.Ticks); + Assert.Equal(0, TimeOnly.MinValue.Hour); + Assert.Equal(0, TimeOnly.MinValue.Minute); + Assert.Equal(0, TimeOnly.MinValue.Second); + Assert.Equal(0, TimeOnly.MinValue.Millisecond); + + Assert.Equal(DateTime.Today.AddTicks(-1).TimeOfDay.Ticks, TimeOnly.MaxValue.Ticks); // ticks should be 863999999999; + Assert.Equal(23, TimeOnly.MaxValue.Hour); + Assert.Equal(59, TimeOnly.MaxValue.Minute); + Assert.Equal(59, TimeOnly.MaxValue.Second); + Assert.Equal(999, TimeOnly.MaxValue.Millisecond); + } + + [Fact] + public static void ConstructorsTest() + { + TimeOnly to = new TimeOnly(14, 35); + Assert.Equal(14, to.Hour); + Assert.Equal(35, to.Minute); + Assert.Equal(0, to.Second); + Assert.Equal(0, to.Millisecond); + Assert.Equal(new DateTime(1, 1, 1, to.Hour, to.Minute, to.Second, to.Millisecond).Ticks, to.Ticks); + + to = new TimeOnly(10, 20, 30); + Assert.Equal(10, to.Hour); + Assert.Equal(20, to.Minute); + Assert.Equal(30, to.Second); + Assert.Equal(0, to.Millisecond); + Assert.Equal(new DateTime(1, 1, 1, to.Hour, to.Minute, to.Second, to.Millisecond).Ticks, to.Ticks); + + to = new TimeOnly(23, 59, 59, 999); + Assert.Equal(23, to.Hour); + Assert.Equal(59, to.Minute); + Assert.Equal(59, to.Second); + Assert.Equal(999, to.Millisecond); + Assert.Equal(new DateTime(1, 1, 1, to.Hour, to.Minute, to.Second, to.Millisecond).Ticks, to.Ticks); + + DateTime dt = DateTime.Now; + to = new TimeOnly(dt.TimeOfDay.Ticks); + Assert.Equal(dt.Hour, to.Hour); + Assert.Equal(dt.Minute, to.Minute); + Assert.Equal(dt.Second, to.Second); + Assert.Equal(dt.Millisecond, to.Millisecond); + + Assert.Throws(() => new TimeOnly(24, 10)); + Assert.Throws(() => new TimeOnly(-1, 10)); + Assert.Throws(() => new TimeOnly(10, 60)); + Assert.Throws(() => new TimeOnly(10, -2)); + Assert.Throws(() => new TimeOnly(10, 10, 60)); + Assert.Throws(() => new TimeOnly(10, 10, -3)); + AssertExtensions.Throws("millisecond", () => new TimeOnly(10, 10, 10, 1000)); + AssertExtensions.Throws("millisecond", () => new TimeOnly(10, 10, 10, -4)); + AssertExtensions.Throws("ticks", () => new TimeOnly(TimeOnly.MaxValue.Ticks + 1)); + AssertExtensions.Throws("ticks", () => new TimeOnly(-1)); + } + + [Fact] + public static void AddTest() + { + TimeOnly to = new TimeOnly(1, 10, 20, 900); + to = to.Add(new TimeSpan(TimeSpan.TicksPerMillisecond)); + Assert.Equal(901, to.Millisecond); + to = to.Add(new TimeSpan(TimeSpan.TicksPerSecond)); + Assert.Equal(21, to.Second); + to = to.Add(new TimeSpan(TimeSpan.TicksPerMinute)); + Assert.Equal(11, to.Minute); + to = to.Add(new TimeSpan(TimeSpan.TicksPerHour)); + Assert.Equal(2, to.Hour); + + to = TimeOnly.MinValue.Add(new TimeSpan(-1), out int wrappedDays); + Assert.Equal(23, to.Hour); + Assert.Equal(59, to.Minute); + Assert.Equal(59, to.Second); + Assert.Equal(999, to.Millisecond); + Assert.Equal(-1, wrappedDays); + + to = TimeOnly.MinValue.Add(new TimeSpan(48, 0, 0), out wrappedDays); + Assert.Equal(0, to.Hour); + Assert.Equal(0, to.Minute); + Assert.Equal(0, to.Second); + Assert.Equal(0, to.Millisecond); + Assert.Equal(2, wrappedDays); + to = to.Add(new TimeSpan(1, 0, 0), out wrappedDays); + Assert.Equal(0, wrappedDays); + + to = TimeOnly.MinValue.AddHours(1.5); + Assert.Equal(1, to.Hour); + Assert.Equal(30, to.Minute); + Assert.Equal(0, to.Second); + Assert.Equal(0, to.Millisecond); + to = to.AddHours(1.5, out wrappedDays); + Assert.Equal(3, to.Hour); + Assert.Equal(0, to.Minute); + Assert.Equal(0, to.Second); + Assert.Equal(0, to.Millisecond); + Assert.Equal(0, wrappedDays); + to = to.AddHours(-28, out wrappedDays); + Assert.Equal(23, to.Hour); + Assert.Equal(0, to.Minute); + Assert.Equal(-2, wrappedDays); + to = to.AddHours(1, out wrappedDays); + Assert.Equal(1, wrappedDays); + Assert.Equal(0, to.Hour); + Assert.Equal(0, to.Minute); + + to = to.AddMinutes(190.5); + Assert.Equal(3, to.Hour); + Assert.Equal(10, to.Minute); + Assert.Equal(30, to.Second); + + to = to.AddMinutes(-4 * 60, out wrappedDays); + Assert.Equal(23, to.Hour); + Assert.Equal(10, to.Minute); + Assert.Equal(30, to.Second); + Assert.Equal(-1, wrappedDays); + + to = to.AddMinutes(60.5, out wrappedDays); + Assert.Equal(0, to.Hour); + Assert.Equal(11, to.Minute); + Assert.Equal(0, to.Second); + Assert.Equal(1, wrappedDays); + } + + [Fact] + public static void IsBetweenTest() + { + TimeOnly to1 = new TimeOnly(14, 30); + TimeOnly to2 = new TimeOnly(2, 0); + TimeOnly to3 = new TimeOnly(12, 0); + + Assert.True(to3.IsBetween(to2, to1)); + Assert.True(to1.IsBetween(to3, to2)); + + Assert.True(to3.IsBetween(to3, to1)); + Assert.True(to1.IsBetween(to1, to2)); + + Assert.False(to1.IsBetween(to3, to1)); + Assert.False(to2.IsBetween(to3, to2)); + + Assert.True(to1.IsBetween(to3, to1.Add(new TimeSpan(1)))); + Assert.True(to2.IsBetween(to3, to2.Add(new TimeSpan(1)))); + } + + [Fact] + public static void CompareOperatorsTest() + { + TimeOnly to1 = new TimeOnly(14, 30); + TimeOnly to2 = new TimeOnly(14, 30); + TimeOnly to3 = new TimeOnly(14, 30, 1); + + Assert.True(to1 == to2); + Assert.True(to1 >= to2); + Assert.True(to1 <= to2); + + Assert.True(to1 != to3); + Assert.True(to1 < to3); + Assert.True(to1 <= to3); + + Assert.True(to3 > to1); + Assert.True(to3 >= to1); + + Assert.False(to1 == to3); + Assert.False(to1 > to3); + Assert.False(to3 < to1); + Assert.False(to1 != to2); + } + + [Fact] + public static void SubtractOperatorTest() + { + TimeOnly to1 = new TimeOnly(10, 30, 40); + TimeOnly to2 = new TimeOnly(14, 0); + + Assert.Equal(new TimeSpan(3, 29, 20), to2 - to1); + Assert.Equal(new TimeSpan(20,30, 40), to1 - to2); + Assert.Equal(TimeSpan.Zero, to1 - to1); + Assert.Equal(new TimeSpan(2,0, 0), new TimeOnly(1, 0) - new TimeOnly(23, 0)); + } + + [Fact] + public static void FromToTimeSpanTest() + { + Assert.Equal(TimeOnly.MinValue, TimeOnly.FromTimeSpan(TimeSpan.Zero)); + Assert.Equal(TimeSpan.Zero, TimeOnly.MinValue.ToTimeSpan()); + + Assert.Equal(new TimeOnly(10, 20, 30), TimeOnly.FromTimeSpan(new TimeSpan(10, 20, 30))); + Assert.Equal(new TimeSpan(14, 10, 50), new TimeOnly(14, 10, 50).ToTimeSpan()); + + Assert.Equal(TimeOnly.MaxValue, TimeOnly.FromTimeSpan(TimeOnly.MaxValue.ToTimeSpan())); + + AssertExtensions.Throws("ticks", () => TimeOnly.FromTimeSpan(new TimeSpan(24, 0, 0))); + AssertExtensions.Throws("ticks", () => TimeOnly.FromTimeSpan(new TimeSpan(-1, 0, 0))); + } + + [Fact] + public static void FromDateTimeTest() + { + DateTime dt = DateTime.Now; + TimeOnly timeOnly = TimeOnly.FromDateTime(dt); + + Assert.Equal(dt.Hour, timeOnly.Hour); + Assert.Equal(dt.Minute, timeOnly.Minute); + Assert.Equal(dt.Second, timeOnly.Second); + Assert.Equal(dt.Millisecond, timeOnly.Millisecond); + Assert.Equal(dt.TimeOfDay.Ticks, timeOnly.Ticks); + } + + [Fact] + public static void ComparisonsTest() + { + TimeOnly timeOnly1 = TimeOnly.FromDateTime(DateTime.Now); + TimeOnly timeOnly2 = timeOnly1.Add(new TimeSpan(1)); + TimeOnly timeOnly3 = new TimeOnly(timeOnly1.Ticks); + + Assert.Equal(-1, timeOnly1.CompareTo(timeOnly2)); + Assert.Equal(1, timeOnly2.CompareTo(timeOnly1)); + Assert.Equal(-1, timeOnly1.CompareTo(timeOnly2)); + Assert.Equal(0, timeOnly1.CompareTo(timeOnly3)); + + Assert.Equal(-1, timeOnly1.CompareTo((object)timeOnly2)); + Assert.Equal(1, timeOnly2.CompareTo((object)timeOnly1)); + Assert.Equal(-1, timeOnly1.CompareTo((object)timeOnly2)); + Assert.Equal(0, timeOnly1.CompareTo((object)timeOnly3)); + + Assert.True(timeOnly1.Equals(timeOnly3)); + Assert.True(timeOnly1.Equals((object)timeOnly3)); + Assert.False(timeOnly2.Equals(timeOnly3)); + Assert.False(timeOnly2.Equals((object)timeOnly3)); + + Assert.False(timeOnly2.Equals(null)); + Assert.False(timeOnly2.Equals(new object())); + } + + [Fact] + public static void GetHashCodeTest() + { + TimeOnly timeOnly1 = TimeOnly.FromDateTime(DateTime.Now); + TimeOnly timeOnly2 = timeOnly1.Add(new TimeSpan(1)); + TimeOnly timeOnly3 = new TimeOnly(timeOnly1.Ticks); + + Assert.True(timeOnly1.GetHashCode() == timeOnly3.GetHashCode()); + Assert.False(timeOnly1.GetHashCode() == timeOnly2.GetHashCode()); + } + + // Arabic cultures uses zero width characters in the date formatting which cause a problem with the DateTime parsing in general. + // We still test these cultures parsing but with ParseExact instead. + internal static bool IsNotArabicCulture => !CultureInfo.CurrentCulture.Name.StartsWith("ar", StringComparison.OrdinalIgnoreCase); + + [ConditionalFact(nameof(IsNotArabicCulture))] + public static void BasicFormatParseTest() + { + string pattern = "hh:mm:ss tt"; + DateTime dt = DateTime.Now; + TimeOnly timeOnly = new TimeOnly(dt.Hour, dt.Minute, dt.Second); + string s = timeOnly.ToString(pattern); + TimeOnly parsedTimeOnly = TimeOnly.Parse(s); + Assert.True(TimeOnly.TryParse(s, out TimeOnly parsedTimeOnly1)); + Assert.Equal(timeOnly.Hour % 12, parsedTimeOnly.Hour % 12); + Assert.Equal(timeOnly.Minute, parsedTimeOnly.Minute); + Assert.Equal(timeOnly.Hour % 12, parsedTimeOnly1.Hour % 12); + Assert.Equal(timeOnly.Minute, parsedTimeOnly1.Minute); + parsedTimeOnly = TimeOnly.Parse(s.AsSpan()); + Assert.True(TimeOnly.TryParse(s.AsSpan(), out parsedTimeOnly1)); + Assert.Equal(timeOnly.Hour % 12, parsedTimeOnly.Hour % 12); + Assert.Equal(timeOnly.Minute, parsedTimeOnly.Minute); + Assert.Equal(timeOnly.Hour % 12, parsedTimeOnly1.Hour % 12); + Assert.Equal(timeOnly.Minute, parsedTimeOnly1.Minute); + + s = timeOnly.ToString(pattern, CultureInfo.InvariantCulture); + parsedTimeOnly = TimeOnly.Parse(s, CultureInfo.InvariantCulture); + Assert.True(TimeOnly.TryParse(s.AsSpan(), CultureInfo.InvariantCulture, DateTimeStyles.None, out parsedTimeOnly1)); + Assert.Equal(timeOnly, parsedTimeOnly); + Assert.Equal(timeOnly, parsedTimeOnly1); + parsedTimeOnly = TimeOnly.Parse(s.AsSpan(), CultureInfo.InvariantCulture); + Assert.True(TimeOnly.TryParse(s.AsSpan(), CultureInfo.InvariantCulture, DateTimeStyles.None, out parsedTimeOnly1)); + Assert.Equal(parsedTimeOnly, parsedTimeOnly1); + + Assert.False(TimeOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out parsedTimeOnly1)); + AssertExtensions.Throws("style", () => TimeOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal)); + Assert.False(TimeOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out parsedTimeOnly1)); + AssertExtensions.Throws("style", () => TimeOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal)); + Assert.False(TimeOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out parsedTimeOnly1)); + AssertExtensions.Throws("style", () => TimeOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)); + Assert.False(TimeOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out parsedTimeOnly1)); + AssertExtensions.Throws("style", () => TimeOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault)); + + s = " " + s + " "; + parsedTimeOnly = TimeOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces); + Assert.Equal(timeOnly, parsedTimeOnly); + parsedTimeOnly = TimeOnly.Parse(s.AsSpan(), CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces); + Assert.Equal(timeOnly, parsedTimeOnly); + } + + [ConditionalFact(nameof(IsNotArabicCulture))] + public static void FormatParseTest() + { + string[] patterns = new string[] { CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern, CultureInfo.CurrentCulture.DateTimeFormat.LongTimePattern, "t", "T", "o", "r" }; + + TimeOnly timeOnly = TimeOnly.FromDateTime(DateTime.Now); + + foreach (string format in patterns) + { + string formattedTime = timeOnly.ToString(format); + timeOnly = TimeOnly.Parse(formattedTime); + + Assert.True(TimeOnly.TryParse(formattedTime, out TimeOnly parsedTimeOnly1)); + Assert.Equal(timeOnly, parsedTimeOnly1); + TimeOnly parsedTimeOnly = TimeOnly.Parse(formattedTime.AsSpan()); + Assert.True(TimeOnly.TryParse(formattedTime.AsSpan(), out parsedTimeOnly1)); + Assert.Equal(timeOnly, parsedTimeOnly); + Assert.Equal(timeOnly, parsedTimeOnly1); + + parsedTimeOnly = TimeOnly.Parse(formattedTime, CultureInfo.CurrentCulture); + Assert.True(TimeOnly.TryParse(formattedTime, CultureInfo.CurrentCulture, DateTimeStyles.None, out parsedTimeOnly1)); + Assert.Equal(timeOnly, parsedTimeOnly); + Assert.Equal(timeOnly, parsedTimeOnly1); + parsedTimeOnly = TimeOnly.Parse(formattedTime.AsSpan(), CultureInfo.CurrentCulture); + Assert.True(TimeOnly.TryParse(formattedTime.AsSpan(), CultureInfo.CurrentCulture, DateTimeStyles.None, out parsedTimeOnly1)); + Assert.Equal(timeOnly, parsedTimeOnly); + Assert.Equal(timeOnly, parsedTimeOnly1); + + parsedTimeOnly = TimeOnly.ParseExact(formattedTime, format); + Assert.True(TimeOnly.TryParseExact(formattedTime, format, out parsedTimeOnly1)); + Assert.Equal(timeOnly, parsedTimeOnly); + Assert.Equal(timeOnly, parsedTimeOnly1); + parsedTimeOnly = TimeOnly.ParseExact(formattedTime.AsSpan(), format.AsSpan()); + Assert.True(TimeOnly.TryParseExact(formattedTime.AsSpan(), format.AsSpan(), out parsedTimeOnly1)); + Assert.Equal(timeOnly, parsedTimeOnly); + Assert.Equal(timeOnly, parsedTimeOnly1); + + parsedTimeOnly = TimeOnly.ParseExact(formattedTime, format, CultureInfo.CurrentCulture); + Assert.True(TimeOnly.TryParseExact(formattedTime, format, CultureInfo.CurrentCulture, DateTimeStyles.None, out parsedTimeOnly1)); + Assert.Equal(timeOnly, parsedTimeOnly); + Assert.Equal(timeOnly, parsedTimeOnly1); + parsedTimeOnly = TimeOnly.ParseExact(formattedTime.AsSpan(), format.AsSpan(), CultureInfo.CurrentCulture); + Assert.True(TimeOnly.TryParseExact(formattedTime.AsSpan(), format.AsSpan(), CultureInfo.CurrentCulture, DateTimeStyles.None, out parsedTimeOnly1)); + Assert.Equal(timeOnly, parsedTimeOnly); + Assert.Equal(timeOnly, parsedTimeOnly1); + + parsedTimeOnly = TimeOnly.ParseExact(formattedTime, patterns); + Assert.True(TimeOnly.TryParseExact(formattedTime, patterns, out parsedTimeOnly1)); + Assert.Equal(timeOnly, parsedTimeOnly); + Assert.Equal(timeOnly, parsedTimeOnly1); + parsedTimeOnly = TimeOnly.ParseExact(formattedTime.AsSpan(), patterns); + Assert.True(TimeOnly.TryParseExact(formattedTime.AsSpan(), patterns, out parsedTimeOnly1)); + Assert.Equal(timeOnly, parsedTimeOnly); + Assert.Equal(timeOnly, parsedTimeOnly1); + + parsedTimeOnly = TimeOnly.ParseExact(formattedTime, patterns, CultureInfo.CurrentCulture); + Assert.True(TimeOnly.TryParseExact(formattedTime, patterns, CultureInfo.CurrentCulture, DateTimeStyles.None, out parsedTimeOnly1)); + Assert.Equal(timeOnly, parsedTimeOnly); + Assert.Equal(timeOnly, parsedTimeOnly1); + parsedTimeOnly = TimeOnly.ParseExact(formattedTime.AsSpan(), patterns, CultureInfo.CurrentCulture); + Assert.True(TimeOnly.TryParseExact(formattedTime.AsSpan(), patterns, CultureInfo.CurrentCulture, DateTimeStyles.None, out parsedTimeOnly1)); + Assert.Equal(timeOnly, parsedTimeOnly); + Assert.Equal(timeOnly, parsedTimeOnly1); + } + } + + [Fact] + public static void OAndRFormatsTest() + { + TimeOnly timeOnly = TimeOnly.FromDateTime(DateTime.Today); + string formattedDate = timeOnly.ToString("o"); + Assert.Equal(16, formattedDate.Length); + Assert.Equal(':', formattedDate[2]); + Assert.Equal(':', formattedDate[5]); + Assert.Equal('.', formattedDate[8]); + TimeOnly parsedTimeOnly = TimeOnly.Parse(formattedDate); + Assert.Equal(timeOnly, parsedTimeOnly); + parsedTimeOnly = TimeOnly.Parse(formattedDate.AsSpan()); + Assert.Equal(timeOnly, parsedTimeOnly); + parsedTimeOnly = TimeOnly.ParseExact(formattedDate, "O"); + Assert.Equal(timeOnly, parsedTimeOnly); + parsedTimeOnly = TimeOnly.ParseExact(formattedDate.AsSpan(), "O".AsSpan()); + Assert.Equal(timeOnly, parsedTimeOnly); + + formattedDate = timeOnly.ToString("r"); + Assert.Equal(8, formattedDate.Length); + Assert.Equal(':', formattedDate[2]); + Assert.Equal(':', formattedDate[5]); + parsedTimeOnly = TimeOnly.Parse(formattedDate); + Assert.Equal(timeOnly, parsedTimeOnly); + parsedTimeOnly = TimeOnly.Parse(formattedDate.AsSpan()); + Assert.Equal(timeOnly, parsedTimeOnly); + parsedTimeOnly = TimeOnly.ParseExact(formattedDate, "R"); + Assert.Equal(timeOnly, parsedTimeOnly); + parsedTimeOnly = TimeOnly.ParseExact(formattedDate.AsSpan(), "R".AsSpan()); + Assert.Equal(timeOnly, parsedTimeOnly); + } + + [Fact] + public static void InvalidFormatsTest() + { + DateTime dt = DateTime.Now; + string formatted = dt.ToString(); + Assert.Throws(() => TimeOnly.Parse(formatted)); + Assert.Throws(() => TimeOnly.Parse(formatted.AsSpan())); + Assert.False(TimeOnly.TryParse(formatted, out TimeOnly timeOnly)); + Assert.False(TimeOnly.TryParse(formatted.AsSpan(), out timeOnly)); + formatted = dt.ToString("d"); + Assert.Throws(() => TimeOnly.Parse(formatted)); + Assert.Throws(() => TimeOnly.Parse(formatted.AsSpan())); + Assert.False(TimeOnly.TryParse(formatted, out timeOnly)); + Assert.False(TimeOnly.TryParse(formatted.AsSpan(), out timeOnly)); + } + + [Fact] + public static void CustomFormattingTest() + { + TimeOnly timeOnly = TimeOnly.FromDateTime(DateTime.Now); + string format = "HH 'dash' mm \"dash\" ss'....'fffffff"; + string formatted = timeOnly.ToString(format); + TimeOnly parsedTimeOnly = TimeOnly.ParseExact(formatted, format); + Assert.Equal(timeOnly, parsedTimeOnly); + parsedTimeOnly = TimeOnly.ParseExact(formatted.AsSpan(), format.AsSpan()); + Assert.Equal(timeOnly, parsedTimeOnly); + + Assert.Throws(() => timeOnly.ToString("hh:mm:ss dd")); + Assert.Throws(() => timeOnly.ToString("hh:mm:ss MM")); + Assert.Throws(() => timeOnly.ToString("hh:mm:ss yy")); + } + + [Fact] + public static void AllCulturesTest() + { + TimeOnly timeOnly = new TimeOnly((DateTime.Now.TimeOfDay.Ticks / TimeSpan.TicksPerMinute) * TimeSpan.TicksPerMinute); + foreach (CultureInfo ci in CultureInfo.GetCultures(CultureTypes.SpecificCultures)) + { + if (string.IsNullOrEmpty(ci.DateTimeFormat.TimeSeparator)) + { + // cannot parse concatenated time part numbers. + continue; + } + + string formatted = timeOnly.ToString("t", ci); + TimeOnly parsedTimeOnly = TimeOnly.ParseExact(formatted, "t", ci); + Assert.Equal(timeOnly.Hour % 12, parsedTimeOnly.Hour % 12); + Assert.Equal(timeOnly.Minute, parsedTimeOnly.Minute); + + formatted = timeOnly.ToString("T", ci); + parsedTimeOnly = TimeOnly.ParseExact(formatted, "T", ci); + Assert.Equal(timeOnly.Hour % 12, parsedTimeOnly.Hour % 12); + Assert.Equal(timeOnly.Minute, parsedTimeOnly.Minute); + } + } + + [Fact] + public static void TryFormatTest() + { + Span buffer = stackalloc char[100]; + TimeOnly timeOnly = TimeOnly.FromDateTime(DateTime.Now); + + Assert.True(timeOnly.TryFormat(buffer, out int charsWritten)); + Assert.True(timeOnly.TryFormat(buffer, out charsWritten, "o")); + Assert.Equal(16, charsWritten); + Assert.True(timeOnly.TryFormat(buffer, out charsWritten, "R")); + Assert.Equal(8, charsWritten); + Assert.False(timeOnly.TryFormat(buffer.Slice(0, 3), out charsWritten)); + Assert.False(timeOnly.TryFormat(buffer.Slice(0, 3), out charsWritten, "r")); + Assert.False(timeOnly.TryFormat(buffer.Slice(0, 3), out charsWritten, "O")); + } + + } +}