From ef1ba771347dc1d7d626907e4731b8f2e3cf78b3 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sun, 9 Apr 2023 07:24:15 -0400 Subject: [PATCH] Implement IUtf8SpanFormattable on DateTime, DateTimeOffset, DateOnly, TimeOnly, TimeSpan, Char, Rune (#84469) * Implement IUtf8SpanFormattable on DateTime, DateTimeOffset, DateOnly, TimeOnly, TimeSpan, Char, Rune * Address PR feedback Also dedup Utf8Formatter for TimeSpan with TimeSpan's new IUtf8SpanFormattable implementation and a little more cleanup. And fix parameter name of TryFormat to match approved name. --- .../System.Private.CoreLib.Shared.projitems | 4 +- .../Text/Utf8Formatter/FormattingHelpers.cs | 67 +-- .../Utf8Formatter/Utf8Formatter.Date.G.cs | 16 +- .../Utf8Formatter/Utf8Formatter.Date.L.cs | 16 +- .../Utf8Formatter/Utf8Formatter.Date.O.cs | 109 ----- .../Utf8Formatter/Utf8Formatter.Date.R.cs | 50 -- .../Text/Utf8Formatter/Utf8Formatter.Date.cs | 8 +- .../Utf8Formatter/Utf8Formatter.TimeSpan.cs | 196 +------- .../System.Private.CoreLib/src/System/Char.cs | 6 +- .../Collections/Generic/ValueListBuilder.cs | 43 +- .../src/System/DateOnly.cs | 14 +- .../src/System/DateTime.cs | 6 +- .../src/System/DateTimeOffset.cs | 6 +- .../System/Globalization/DateTimeFormat.cs | 455 +++++++++--------- .../src/System/Globalization/HebrewNumber.cs | 36 +- .../System/Globalization/TimeSpanFormat.cs | 123 ++--- .../src/System/Number.Formatting.cs | 29 +- .../src/System/Text/Rune.cs | 4 + .../src/System/TimeOnly.cs | 23 +- .../src/System/TimeSpan.cs | 14 +- .../System.Runtime/ref/System.Runtime.cs | 21 +- .../tests/System/DateOnlyTests.cs | 56 ++- .../tests/System/DateTimeOffsetTests.cs | 90 +++- .../tests/System/DateTimeTests.cs | 83 +++- .../tests/System/TimeOnlyTests.cs | 86 ++-- .../tests/System/TimeSpanTests.cs | 61 ++- 26 files changed, 767 insertions(+), 855 deletions(-) delete mode 100644 src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Date.O.cs delete mode 100644 src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Date.R.cs 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 ca9f5435d6a1f..a705b2ad72571 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 @@ -130,8 +130,6 @@ - - @@ -401,7 +399,7 @@ - + diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/FormattingHelpers.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/FormattingHelpers.cs index 0f66fa1df7096..a3022370d88bd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/FormattingHelpers.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/FormattingHelpers.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -32,8 +33,6 @@ public static char GetSymbolOrDefault(in StandardFormat format, char defaultSymb return symbol; } - #region UTF-8 Helper methods - /// /// Fills a buffer with the ASCII character '0' (0x30). /// @@ -48,7 +47,7 @@ public static void FillWithAsciiZeros(Span buffer) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteDigits(ulong value, Span buffer) + public static void WriteDigits(ulong value, Span buffer) where TChar : unmanaged, IBinaryInteger { // We can mutate the 'value' parameter since it's a copy-by-value local. // It'll be used to represent the value left over after each division by 10. @@ -57,11 +56,11 @@ public static void WriteDigits(ulong value, Span buffer) { ulong temp = '0' + value; value /= 10; - buffer[i] = (byte)(temp - (value * 10)); + buffer[i] = TChar.CreateTruncating(temp - (value * 10)); } Debug.Assert(value < 10); - buffer[0] = (byte)('0' + value); + buffer[0] = TChar.CreateTruncating('0' + value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -92,66 +91,74 @@ public static void WriteDigitsWithGroupSeparator(ulong value, Span buffer) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteDigits(uint value, Span buffer) + public static void WriteDigits(uint value, Span buffer) where TChar : unmanaged, IBinaryInteger { - // We can mutate the 'value' parameter since it's a copy-by-value local. - // It'll be used to represent the value left over after each division by 10. + Debug.Assert(buffer.Length > 0); for (int i = buffer.Length - 1; i >= 1; i--) { uint temp = '0' + value; value /= 10; - buffer[i] = (byte)(temp - (value * 10)); + buffer[i] = TChar.CreateTruncating(temp - (value * 10)); } Debug.Assert(value < 10); - buffer[0] = (byte)('0' + value); + buffer[0] = TChar.CreateTruncating('0' + value); } /// - /// Writes a value [ 0000 .. 9999 ] to the buffer starting at the specified offset. + /// Writes a value [ 00 .. 99 ] to the buffer starting at the specified offset. /// This method performs best when the starting index is a constant literal. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void WriteFourDecimalDigits(uint value, Span buffer, int startingIndex = 0) + public static unsafe void WriteTwoDigits(uint value, Span buffer, int startingIndex = 0) where TChar : unmanaged, IBinaryInteger { - Debug.Assert(value <= 9999); - Debug.Assert(startingIndex <= buffer.Length - 4); + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); + Debug.Assert(value <= 99); + Debug.Assert(startingIndex <= buffer.Length - 2); - (value, uint remainder) = Math.DivRem(value, 100); - fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer)) + fixed (TChar* bufferPtr = &MemoryMarshal.GetReference(buffer)) { Number.WriteTwoDigits(bufferPtr + startingIndex, value); - Number.WriteTwoDigits(bufferPtr + startingIndex + 2, remainder); } } /// - /// Writes a value [ 00 .. 99 ] to the buffer starting at the specified offset. + /// Writes a value [ 0000 .. 9999 ] to the buffer starting at the specified offset. /// This method performs best when the starting index is a constant literal. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void WriteTwoDecimalDigits(uint value, Span buffer, int startingIndex = 0) + public static unsafe void WriteFourDigits(uint value, Span buffer, int startingIndex = 0) where TChar : unmanaged, IBinaryInteger { - Debug.Assert(value <= 99); - Debug.Assert(startingIndex <= buffer.Length - 2); + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); + Debug.Assert(value <= 9999); + Debug.Assert(startingIndex <= buffer.Length - 4); - fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer)) + (value, uint remainder) = Math.DivRem(value, 100); + fixed (TChar* bufferPtr = &MemoryMarshal.GetReference(buffer)) { Number.WriteTwoDigits(bufferPtr + startingIndex, value); + Number.WriteTwoDigits(bufferPtr + startingIndex + 2, remainder); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void CopyFourBytes(ReadOnlySpan source, Span destination) => - Unsafe.WriteUnaligned(ref MemoryMarshal.GetReference(destination), - Unsafe.ReadUnaligned(ref MemoryMarshal.GetReference(source))); - - #endregion UTF-8 Helper methods + public static void CopyFour(ReadOnlySpan source, Span destination) where TChar : unmanaged, IBinaryInteger + { + if (typeof(TChar) == typeof(byte)) + { + Unsafe.WriteUnaligned(ref Unsafe.As(ref MemoryMarshal.GetReference(destination)), + Unsafe.ReadUnaligned(ref Unsafe.As(ref MemoryMarshal.GetReference(source)))); + } + else + { + Debug.Assert(typeof(TChar) == typeof(char)); + Unsafe.WriteUnaligned(ref Unsafe.As(ref MemoryMarshal.GetReference(destination)), + Unsafe.ReadUnaligned(ref Unsafe.As(ref MemoryMarshal.GetReference(source)))); + } + } - // - // Enable use of ThrowHelper from TryFormat() routines without introducing dozens of non-code-coveraged "bytesWritten = 0; return false" boilerplate. - // + /// Enable use of ThrowHelper from TryFormat() routines without introducing dozens of non-code-coveraged "bytesWritten = 0; return false" boilerplate. public static bool TryFormatThrowFormatException(out int bytesWritten) { bytesWritten = 0; diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Date.G.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Date.G.cs index 37a9329d8a3ad..9c38cb08399c4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Date.G.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Date.G.cs @@ -43,22 +43,22 @@ private static bool TryFormatDateTimeG(DateTime value, TimeSpan offset, Span destination, o value.GetDate(out int year, out int month, out int day); value.GetTime(out int hour, out int minute, out int second); - FormattingHelpers.CopyFourBytes("sun,mon,tue,wed,thu,fri,sat,"u8.Slice(4 * (int)value.DayOfWeek), destination); + FormattingHelpers.CopyFour("sun,mon,tue,wed,thu,fri,sat,"u8.Slice(4 * (int)value.DayOfWeek), destination); destination[4] = Utf8Constants.Space; - FormattingHelpers.WriteTwoDecimalDigits((uint)day, destination, 5); + FormattingHelpers.WriteTwoDigits((uint)day, destination, 5); destination[7] = Utf8Constants.Space; - FormattingHelpers.CopyFourBytes("jan feb mar apr may jun jul aug sep oct nov dec "u8.Slice(4 * (month - 1)), destination.Slice(8)); + FormattingHelpers.CopyFour("jan feb mar apr may jun jul aug sep oct nov dec "u8.Slice(4 * (month - 1)), destination.Slice(8)); - FormattingHelpers.WriteFourDecimalDigits((uint)year, destination, 12); + FormattingHelpers.WriteFourDigits((uint)year, destination, 12); destination[16] = Utf8Constants.Space; - FormattingHelpers.WriteTwoDecimalDigits((uint)hour, destination, 17); + FormattingHelpers.WriteTwoDigits((uint)hour, destination, 17); destination[19] = Utf8Constants.Colon; - FormattingHelpers.WriteTwoDecimalDigits((uint)minute, destination, 20); + FormattingHelpers.WriteTwoDigits((uint)minute, destination, 20); destination[22] = Utf8Constants.Colon; - FormattingHelpers.WriteTwoDecimalDigits((uint)second, destination, 23); + FormattingHelpers.WriteTwoDigits((uint)second, destination, 23); - FormattingHelpers.CopyFourBytes(" gmt"u8, destination.Slice(25)); + FormattingHelpers.CopyFour(" gmt"u8, destination.Slice(25)); bytesWritten = 29; return true; diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Date.O.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Date.O.cs deleted file mode 100644 index db4fb5be054f3..0000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Date.O.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Buffers.Text -{ - public static partial class Utf8Formatter - { - // - // Roundtrippable format. One of - // - // 012345678901234567890123456789012 - // --------------------------------- - // 2017-06-12T05:30:45.7680000-07:00 - // 2017-06-12T05:30:45.7680000Z (Z is short for "+00:00" but also distinguishes DateTimeKind.Utc from DateTimeKind.Local) - // 2017-06-12T05:30:45.7680000 (interpreted as local time wrt to current time zone) - // - private static bool TryFormatDateTimeO(DateTime value, TimeSpan offset, Span destination, out int bytesWritten) - { - const int MinimumBytesNeeded = 27; - - int bytesRequired = MinimumBytesNeeded; - DateTimeKind kind = DateTimeKind.Local; - - if (offset == Utf8Constants.NullUtcOffset) - { - kind = value.Kind; - if (kind == DateTimeKind.Local) - { - offset = TimeZoneInfo.Local.GetUtcOffset(value); - bytesRequired += 6; - } - else if (kind == DateTimeKind.Utc) - { - bytesRequired++; - } - } - else - { - bytesRequired += 6; - } - - if (destination.Length < bytesRequired) - { - bytesWritten = 0; - return false; - } - - bytesWritten = bytesRequired; - - // Hoist most of the bounds checks on buffer. - { _ = destination[MinimumBytesNeeded - 1]; } - - value.GetDate(out int year, out int month, out int day); - value.GetTimePrecise(out int hour, out int minute, out int second, out int ticks); - - FormattingHelpers.WriteFourDecimalDigits((uint)year, destination, 0); - destination[4] = Utf8Constants.Minus; - - FormattingHelpers.WriteTwoDecimalDigits((uint)month, destination, 5); - destination[7] = Utf8Constants.Minus; - - FormattingHelpers.WriteTwoDecimalDigits((uint)day, destination, 8); - destination[10] = (byte)'T'; - - FormattingHelpers.WriteTwoDecimalDigits((uint)hour, destination, 11); - destination[13] = Utf8Constants.Colon; - - FormattingHelpers.WriteTwoDecimalDigits((uint)minute, destination, 14); - destination[16] = Utf8Constants.Colon; - - FormattingHelpers.WriteTwoDecimalDigits((uint)second, destination, 17); - destination[19] = Utf8Constants.Period; - - FormattingHelpers.WriteDigits((uint)ticks, destination.Slice(20, 7)); - - if (kind == DateTimeKind.Local) - { - int offsetTotalMinutes = (int)(offset.Ticks / TimeSpan.TicksPerMinute); - byte sign; - - if (offsetTotalMinutes < 0) - { - sign = Utf8Constants.Minus; - offsetTotalMinutes = -offsetTotalMinutes; - } - else - { - sign = Utf8Constants.Plus; - } - - int offsetHours = Math.DivRem(offsetTotalMinutes, 60, out int offsetMinutes); - - // Writing the value backward allows the JIT to optimize by - // performing a single bounds check against buffer. - - FormattingHelpers.WriteTwoDecimalDigits((uint)offsetMinutes, destination, 31); - destination[30] = Utf8Constants.Colon; - FormattingHelpers.WriteTwoDecimalDigits((uint)offsetHours, destination, 28); - destination[27] = sign; - } - else if (kind == DateTimeKind.Utc) - { - destination[27] = (byte)'Z'; - } - - return true; - } - } -} diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Date.R.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Date.R.cs deleted file mode 100644 index 66c57e5b58b53..0000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Date.R.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Buffers.Text -{ - public static partial class Utf8Formatter - { - // Rfc1123 - // - // 01234567890123456789012345678 - // ----------------------------- - // Tue, 03 Jan 2017 08:08:05 GMT - // - private static bool TryFormatDateTimeR(DateTime value, Span destination, out int bytesWritten) - { - if (destination.Length <= 28) - { - bytesWritten = 0; - return false; - } - - value.GetDate(out int year, out int month, out int day); - value.GetTime(out int hour, out int minute, out int second); - - FormattingHelpers.CopyFourBytes("Sun,Mon,Tue,Wed,Thu,Fri,Sat,"u8.Slice(4 * (int)value.DayOfWeek), destination); - destination[4] = Utf8Constants.Space; - - FormattingHelpers.WriteTwoDecimalDigits((uint)day, destination, 5); - destination[7] = Utf8Constants.Space; - - FormattingHelpers.CopyFourBytes("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec "u8.Slice(4 * (month - 1)), destination.Slice(8)); - - FormattingHelpers.WriteFourDecimalDigits((uint)year, destination, 12); - destination[16] = Utf8Constants.Space; - - FormattingHelpers.WriteTwoDecimalDigits((uint)hour, destination, 17); - destination[19] = Utf8Constants.Colon; - - FormattingHelpers.WriteTwoDecimalDigits((uint)minute, destination, 20); - destination[22] = Utf8Constants.Colon; - - FormattingHelpers.WriteTwoDecimalDigits((uint)second, destination, 23); - - FormattingHelpers.CopyFourBytes(" GMT"u8, destination.Slice(25)); - - bytesWritten = 29; - return true; - } - } -} diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Date.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Date.cs index 2c8a9f882ef79..7c4885eb432e0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Date.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Date.cs @@ -39,9 +39,9 @@ public static bool TryFormat(DateTimeOffset value, Span destination, out i return symbol switch { - 'R' => TryFormatDateTimeR(value.UtcDateTime, destination, out bytesWritten), + 'R' => DateTimeFormat.TryFormatR(value.UtcDateTime, new TimeSpan(DateTimeFormat.NullOffset), destination, out bytesWritten), 'l' => TryFormatDateTimeL(value.UtcDateTime, destination, out bytesWritten), - 'O' => TryFormatDateTimeO(value.DateTime, value.Offset, destination, out bytesWritten), + 'O' => DateTimeFormat.TryFormatO(value.DateTime, value.Offset, destination, out bytesWritten), 'G' => TryFormatDateTimeG(value.DateTime, offset, destination, out bytesWritten), _ => FormattingHelpers.TryFormatThrowFormatException(out bytesWritten), }; @@ -74,9 +74,9 @@ public static bool TryFormat(DateTime value, Span destination, out int byt return symbol switch { - 'R' => TryFormatDateTimeR(value, destination, out bytesWritten), + 'R' => DateTimeFormat.TryFormatR(value, new TimeSpan(DateTimeFormat.NullOffset), destination, out bytesWritten), 'l' => TryFormatDateTimeL(value, destination, out bytesWritten), - 'O' => TryFormatDateTimeO(value, Utf8Constants.NullUtcOffset, destination, out bytesWritten), + 'O' => DateTimeFormat.TryFormatO(value, Utf8Constants.NullUtcOffset, destination, out bytesWritten), 'G' => TryFormatDateTimeG(value, Utf8Constants.NullUtcOffset, destination, out bytesWritten), _ => FormattingHelpers.TryFormatThrowFormatException(out bytesWritten), }; diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.TimeSpan.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.TimeSpan.cs index 3fa8e67c02c5d..aebcc9a154e99 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.TimeSpan.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.TimeSpan.cs @@ -1,7 +1,7 @@ // 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.Buffers.Text { @@ -29,194 +29,28 @@ public static partial class Utf8Formatter /// public static bool TryFormat(TimeSpan value, Span destination, out int bytesWritten, StandardFormat format = default) { - char symbol = FormattingHelpers.GetSymbolOrDefault(format, 'c'); - - switch (symbol) - { - case 'c': - case 'G': - case 'g': - break; - - case 't': - case 'T': - symbol = 'c'; - break; - - default: - return FormattingHelpers.TryFormatThrowFormatException(out bytesWritten); - } - - // First, calculate how large an output buffer is needed to hold the entire output. - - int requiredOutputLength = 8; // start with "hh:mm:ss" and adjust as necessary - - uint fraction; - ulong totalSecondsRemaining; - { - // Turn this into a non-negative TimeSpan if possible. - long ticks = value.Ticks; - if (ticks < 0) - { - ticks = -ticks; - if (ticks < 0) - { - Debug.Assert(ticks == long.MinValue /* -9223372036854775808 */); - - // We computed these ahead of time; they're straight from the decimal representation of Int64.MinValue. - fraction = 4775808; - totalSecondsRemaining = 922337203685; - goto AfterComputeFraction; - } - } - - ulong fraction64; - (totalSecondsRemaining, fraction64) = Math.DivRem((ulong)Math.Abs(value.Ticks), TimeSpan.TicksPerSecond); - fraction = (uint)fraction64; - } - - AfterComputeFraction: + TimeSpanFormat.StandardFormat sf = TimeSpanFormat.StandardFormat.C; + string? decimalSeparator = null; - int fractionDigits = 0; - if (symbol == 'c') + char symbol = FormattingHelpers.GetSymbolOrDefault(format, 'c'); + if (symbol != 'c' && (symbol | 0x20) != 't') { - // Only write out the fraction if it's non-zero, and in that - // case write out the entire fraction (all digits). - if (fraction != 0) + decimalSeparator = DateTimeFormatInfo.InvariantInfo.DecimalSeparator; + if (symbol == 'g') { - fractionDigits = Utf8Constants.DateTimeNumFractionDigits; + sf = TimeSpanFormat.StandardFormat.g; } - } - else if (symbol == 'G') - { - // Always write out the fraction, even if it's zero. - fractionDigits = Utf8Constants.DateTimeNumFractionDigits; - } - else - { - // Only write out the fraction if it's non-zero, and in that - // case write out only the most significant digits. - if (fraction != 0) + else { - fractionDigits = Utf8Constants.DateTimeNumFractionDigits - FormattingHelpers.CountDecimalTrailingZeros(fraction, out fraction); - } - } - - Debug.Assert(fraction < 10_000_000); - - // If we're going to write out a fraction, also need to write the leading decimal. - if (fractionDigits != 0) - { - requiredOutputLength += fractionDigits + 1; - } - - ulong totalMinutesRemaining = 0; - ulong seconds = 0; - if (totalSecondsRemaining > 0) - { - // Only compute minutes if the TimeSpan has an absolute value of >= 1 minute. - (totalMinutesRemaining, seconds) = Math.DivRem(totalSecondsRemaining, 60 /* seconds per minute */); - } - - Debug.Assert(seconds < 60); - - ulong totalHoursRemaining = 0; - ulong minutes = 0; - if (totalMinutesRemaining > 0) - { - // Only compute hours if the TimeSpan has an absolute value of >= 1 hour. - (totalHoursRemaining, minutes) = Math.DivRem(totalMinutesRemaining, 60 /* minutes per hour */); - } - - Debug.Assert(minutes < 60); - - // At this point, we can switch over to 32-bit divmod since the data has shrunk far enough. - Debug.Assert(totalHoursRemaining <= uint.MaxValue); - - uint days = 0; - uint hours = 0; - if (totalHoursRemaining > 0) - { - // Only compute days if the TimeSpan has an absolute value of >= 1 day. - (days, hours) = Math.DivRem((uint)totalHoursRemaining, 24 /* hours per day */); - } - - Debug.Assert(hours < 24); - - int hourDigits = 2; - if (hours < 10 && symbol == 'g') - { - // Only writing a one-digit hour, not a two-digit hour - hourDigits--; - requiredOutputLength--; - } - - int dayDigits = 0; - if (days == 0) - { - if (symbol == 'G') - { - requiredOutputLength += 2; // for the leading "0:" - dayDigits = 1; + sf = TimeSpanFormat.StandardFormat.G; + if (symbol != 'G') + { + ThrowHelper.ThrowFormatException_BadFormatSpecifier(); + } } } - else - { - dayDigits = FormattingHelpers.CountDigits(days); - requiredOutputLength += dayDigits + 1; // for the leading "d:" (or "d.") - } - - if (value.Ticks < 0) - { - requiredOutputLength++; // for the leading '-' sign - } - - if (destination.Length < requiredOutputLength) - { - bytesWritten = 0; - return false; - } - - bytesWritten = requiredOutputLength; - - int idx = 0; - - // Write leading '-' if necessary - if (value.Ticks < 0) - { - destination[idx++] = Utf8Constants.Minus; - } - - // Write day (and separator) if necessary - if (dayDigits > 0) - { - FormattingHelpers.WriteDigits(days, destination.Slice(idx, dayDigits)); - idx += dayDigits; - destination[idx++] = (symbol == 'c') ? Utf8Constants.Period : Utf8Constants.Colon; - } - - // Write "[h]h:mm:ss" - FormattingHelpers.WriteDigits(hours, destination.Slice(idx, hourDigits)); - idx += hourDigits; - destination[idx++] = Utf8Constants.Colon; - FormattingHelpers.WriteDigits((uint)minutes, destination.Slice(idx, 2)); - idx += 2; - destination[idx++] = Utf8Constants.Colon; - FormattingHelpers.WriteDigits((uint)seconds, destination.Slice(idx, 2)); - idx += 2; - - // Write fraction (and separator) if necessary - if (fractionDigits > 0) - { - destination[idx++] = Utf8Constants.Period; - FormattingHelpers.WriteDigits(fraction, destination.Slice(idx, fractionDigits)); - idx += fractionDigits; - } - - // And we're done! - Debug.Assert(idx == requiredOutputLength); - return true; + return TimeSpanFormat.TryFormatStandard(value, sf, decimalSeparator, destination, out bytesWritten); } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Char.cs b/src/libraries/System.Private.CoreLib/src/System/Char.cs index 72e5356007b09..5678c119e0b96 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Char.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Char.cs @@ -34,7 +34,8 @@ public readonly struct Char ISpanFormattable, IBinaryInteger, IMinMaxValue, - IUnsignedNumber + IUnsignedNumber, + IUtf8SpanFormattable { // // Member Variables @@ -191,6 +192,9 @@ bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, Re return false; } + bool IUtf8SpanFormattable.TryFormat(Span utf8Destination, out int bytesWritten, ReadOnlySpan format, IFormatProvider? provider) => + new Rune(this).TryEncodeToUtf8(utf8Destination, out bytesWritten); + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(m_value); public static char Parse(string s) diff --git a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/ValueListBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/ValueListBuilder.cs index 51dcd6ac6bd85..01c19a05b9c58 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/ValueListBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/ValueListBuilder.cs @@ -58,13 +58,38 @@ public void Append(T item) } } + public void Append(scoped ReadOnlySpan source) + { + if ((uint)(_pos + source.Length) > (uint)_span.Length) + { + Grow(source.Length); + } + + source.CopyTo(_span.Slice(_pos)); + _pos += source.Length; + } + + public Span AppendSpan(int length) + { + Debug.Assert(length >= 0); + + int pos = _pos; + if ((uint)(pos + length) > (uint)_span.Length) + { + Grow(length); + } + + _pos += length; + return _span.Slice(pos, length); + } + // Hide uncommon path [MethodImpl(MethodImplOptions.NoInlining)] private void AddWithResize(T item) { Debug.Assert(_pos == _span.Length); int pos = _pos; - Grow(); + Grow(1); _span[pos] = item; _pos = pos + 1; } @@ -74,6 +99,18 @@ public ReadOnlySpan AsSpan() return _span.Slice(0, _pos); } + public bool TryCopyTo(Span destination, out int itemsWritten) + { + if (_span.Slice(0, _pos).TryCopyTo(destination)) + { + itemsWritten = _pos; + return true; + } + + itemsWritten = 0; + return false; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose() { @@ -85,13 +122,13 @@ public void Dispose() } } - private void Grow() + private void Grow(int additionalCapacityRequired = 1) { const int ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength // Double the size of the span. If it's currently empty, default to size 4, // although it'll be increased in Rent to the pool's minimum bucket size. - int nextCapacity = _span.Length != 0 ? _span.Length * 2 : 4; + int nextCapacity = Math.Max(_span.Length != 0 ? _span.Length * 2 : 4, _span.Length + additionalCapacityRequired); // If the computed doubled capacity exceeds the possible length of an array, then we // want to downgrade to either the maximum array length if that's large enough to hold diff --git a/src/libraries/System.Private.CoreLib/src/System/DateOnly.cs b/src/libraries/System.Private.CoreLib/src/System/DateOnly.cs index 1140aeb57c474..13985c540633b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateOnly.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateOnly.cs @@ -18,7 +18,8 @@ public readonly struct DateOnly IComparable, IEquatable, ISpanFormattable, - ISpanParsable + ISpanParsable, + IUtf8SpanFormattable { private readonly int _dayNumber; @@ -87,7 +88,7 @@ public static DateOnly FromDayNumber(int dayNumber) /// /// Gets the month component of the date represented by this instance. /// - public int Month => GetEquivalentDateTime().Month; + public int Month => GetEquivalentDateTime().Month; /// /// Gets the day component of the date represented by this instance. @@ -792,7 +793,14 @@ public string ToString([StringSyntax(StringSyntaxAttribute.DateOnlyFormat)] stri /// 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, [StringSyntax(StringSyntaxAttribute.DateOnlyFormat)] ReadOnlySpan format = default(ReadOnlySpan), IFormatProvider? provider = null) + public bool TryFormat(Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.DateOnlyFormat)] ReadOnlySpan format = default(ReadOnlySpan), IFormatProvider? provider = null) => + TryFormatCore(destination, out charsWritten, format, provider); + + bool IUtf8SpanFormattable.TryFormat(Span utf8Destination, out int bytesWritten, [StringSyntax(StringSyntaxAttribute.DateOnlyFormat)] ReadOnlySpan format, IFormatProvider? provider) => + TryFormatCore(utf8Destination, out bytesWritten, format, provider); + + private bool TryFormatCore(Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.DateOnlyFormat)] ReadOnlySpan format, IFormatProvider? provider = null) + where TChar : unmanaged, IBinaryInteger { if (format.Length == 0) { diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTime.cs b/src/libraries/System.Private.CoreLib/src/System/DateTime.cs index 45a1e8403c18e..8cfa1b480ec62 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTime.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTime.cs @@ -53,7 +53,8 @@ public readonly partial struct DateTime IComparable, IEquatable, ISerializable, - ISpanParsable + ISpanParsable, + IUtf8SpanFormattable { // Number of 100ns ticks per time unit internal const int MicrosecondsPerMillisecond = 1000; @@ -1805,6 +1806,9 @@ public string ToString([StringSyntax(StringSyntaxAttribute.DateTimeFormat)] stri public bool TryFormat(Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] ReadOnlySpan format = default, IFormatProvider? provider = null) => DateTimeFormat.TryFormat(this, destination, out charsWritten, format, provider); + bool IUtf8SpanFormattable.TryFormat(Span utf8Destination, out int bytesWritten, [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] ReadOnlySpan format, IFormatProvider? provider) => + DateTimeFormat.TryFormat(this, utf8Destination, out bytesWritten, format, provider); + public DateTime ToUniversalTime() { return TimeZoneInfo.ConvertTimeToUtc(this, TimeZoneInfoOptions.NoThrowOnInvalidTime); diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs index 614e0ed2d688f..24b4275b3e958 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs @@ -42,7 +42,8 @@ public readonly partial struct DateTimeOffset IEquatable, ISerializable, IDeserializationCallback, - ISpanParsable + ISpanParsable, + IUtf8SpanFormattable { // Constants internal const long MaxOffset = TimeSpan.TicksPerHour * 14; @@ -861,6 +862,9 @@ public string ToString([StringSyntax(StringSyntaxAttribute.DateTimeFormat)] stri public bool TryFormat(Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] ReadOnlySpan format = default, IFormatProvider? formatProvider = null) => DateTimeFormat.TryFormat(ClockDateTime, destination, out charsWritten, format, formatProvider, Offset); + bool IUtf8SpanFormattable.TryFormat(Span utf8Destination, out int bytesWritten, [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] ReadOnlySpan format, IFormatProvider? formatProvider) => + DateTimeFormat.TryFormat(ClockDateTime, utf8Destination, out bytesWritten, format, formatProvider, Offset); + public DateTimeOffset ToUniversalTime() => new DateTimeOffset(UtcDateTime); 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 64c925d8874ed..5a58418c9d624 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs @@ -1,12 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers.Text; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -using System.Text; -using System.Runtime.InteropServices; +using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; namespace System { @@ -133,9 +135,6 @@ internal static class DateTimeFormat private const int DEFAULT_ALL_DATETIMES_SIZE = 132; internal static readonly DateTimeFormatInfo InvariantFormatInfo = CultureInfo.InvariantCulture.DateTimeFormat; - internal static readonly string[] InvariantAbbreviatedMonthNames = InvariantFormatInfo.AbbreviatedMonthNames; - internal static readonly string[] InvariantAbbreviatedDayNames = InvariantFormatInfo.AbbreviatedDayNames; - internal const string Gmt = "GMT"; internal static string[] fixedNumberFormats = new string[] { "0", @@ -161,13 +160,13 @@ internal static class DateTimeFormat // The function can format to int.MaxValue. // //////////////////////////////////////////////////////////////////////////// - internal static void FormatDigits(ref ValueStringBuilder outputBuffer, int value, int len) + internal static void FormatDigits(ref ValueListBuilder outputBuffer, int value, int len) where TChar : unmanaged, IBinaryInteger { Debug.Assert(value >= 0, "DateTimeFormat.FormatDigits(): value >= 0"); FormatDigits(ref outputBuffer, value, len, false); } - internal static unsafe void FormatDigits(ref ValueStringBuilder outputBuffer, int value, int len, bool overrideLengthLimit) + internal static unsafe void FormatDigits(ref ValueListBuilder outputBuffer, int value, int len, bool overrideLengthLimit) where TChar : unmanaged, IBinaryInteger { Debug.Assert(value >= 0, "DateTimeFormat.FormatDigits(): value >= 0"); @@ -178,12 +177,12 @@ internal static unsafe void FormatDigits(ref ValueStringBuilder outputBuffer, in len = 2; } - char* buffer = stackalloc char[16]; - char* p = buffer + 16; + TChar* buffer = stackalloc TChar[16]; + TChar* p = buffer + 16; int n = value; do { - *--p = (char)(n % 10 + '0'); + *--p = TChar.CreateTruncating(n % 10 + '0'); n /= 10; } while ((n != 0) && (p > buffer)); @@ -194,15 +193,10 @@ internal static unsafe void FormatDigits(ref ValueStringBuilder outputBuffer, in // a zero if the string only has one character. while ((digits < len) && (p > buffer)) { - *--p = '0'; + *--p = TChar.CreateTruncating('0'); digits++; } - outputBuffer.Append(p, digits); - } - - private static void HebrewFormatDigits(ref ValueStringBuilder outputBuffer, int digits) - { - HebrewNumber.Append(ref outputBuffer, digits); + new ReadOnlySpan(p, digits).CopyTo(outputBuffer.AppendSpan(digits)); } internal static int ParseRepeatPattern(ReadOnlySpan format, int pos, char patternChar) @@ -293,7 +287,7 @@ private static string FormatHebrewMonthName(DateTime time, int month, int repeat // The pos should point to a quote character. This method will // append to the result StringBuilder the string enclosed by the quote character. // - internal static int ParseQuoteString(scoped ReadOnlySpan format, int pos, ref ValueStringBuilder result) + internal static int ParseQuoteString(scoped ReadOnlySpan format, int pos, ref ValueListBuilder result) where TChar : unmanaged, IBinaryInteger { // // NOTE : pos will be the index of the quote character in the 'format' string. @@ -320,7 +314,7 @@ internal static int ParseQuoteString(scoped ReadOnlySpan format, int pos, // because the second double quote is escaped. if (pos < formatLen) { - result.Append(format[pos++]); + result.Append(TChar.CreateTruncating(format[pos++])); } else { @@ -332,7 +326,7 @@ internal static int ParseQuoteString(scoped ReadOnlySpan format, int pos, } else { - result.Append(ch); + result.Append(TChar.CreateTruncating(ch)); } } @@ -355,11 +349,11 @@ internal static int ParseQuoteString(scoped ReadOnlySpan format, int pos, // internal static int ParseNextChar(ReadOnlySpan format, int pos) { - if (pos >= format.Length - 1) + if ((uint)(pos + 1) >= (uint)format.Length) { return -1; } - return (int)format[pos + 1]; + return format[pos + 1]; } // @@ -437,8 +431,8 @@ private static bool IsUseGenitiveForm(ReadOnlySpan format, int index, int // // Actions: Format the DateTime instance using the specified format. // - private static void FormatCustomized( - DateTime dateTime, scoped ReadOnlySpan format, DateTimeFormatInfo dtfi, TimeSpan offset, ref ValueStringBuilder result) + private static void FormatCustomized( + DateTime dateTime, scoped ReadOnlySpan format, DateTimeFormatInfo dtfi, TimeSpan offset, ref ValueListBuilder result) where TChar : unmanaged, IBinaryInteger { Calendar cal = dtfi.Calendar; @@ -459,7 +453,7 @@ private static void FormatCustomized( { case 'g': tokenLen = ParseRepeatPattern(format, i, ch); - result.Append(dtfi.GetEraName(cal.GetEra(dateTime))); + AppendString(ref result, dtfi.GetEraName(cal.GetEra(dateTime))); break; case 'h': tokenLen = ParseRepeatPattern(format, i, ch); @@ -491,7 +485,7 @@ private static void FormatCustomized( fraction /= (long)Math.Pow(10, 7 - tokenLen); if (ch == 'f') { - result.AppendSpanFormattable((int)fraction, fixedNumberFormats[tokenLen - 1], CultureInfo.InvariantCulture); + FormatFraction(ref result, (int)fraction, fixedNumberFormats[tokenLen - 1]); } else { @@ -510,12 +504,12 @@ private static void FormatCustomized( } if (effectiveDigits > 0) { - result.AppendSpanFormattable((int)fraction, fixedNumberFormats[effectiveDigits - 1], CultureInfo.InvariantCulture); + FormatFraction(ref result, (int)fraction, fixedNumberFormats[effectiveDigits - 1]); } else { // No fraction to emit, so see if we should remove decimal also. - if (result.Length > 0 && result[result.Length - 1] == '.') + if (result.Length > 0 && result[result.Length - 1] == TChar.CreateTruncating('.')) { result.Length--; } @@ -531,24 +525,15 @@ private static void FormatCustomized( tokenLen = ParseRepeatPattern(format, i, ch); if (tokenLen == 1) { - if (dateTime.Hour < 12) - { - if (dtfi.AMDesignator.Length >= 1) - { - result.Append(dtfi.AMDesignator[0]); - } - } - else + string designator = dateTime.Hour < 12 ? dtfi.AMDesignator : dtfi.PMDesignator; + if (designator.Length >= 1) { - if (dtfi.PMDesignator.Length >= 1) - { - result.Append(dtfi.PMDesignator[0]); - } + AppendChar(ref result, designator[0]); } } else { - result.Append(dateTime.Hour < 12 ? dtfi.AMDesignator : dtfi.PMDesignator); + AppendString(ref result, dateTime.Hour < 12 ? dtfi.AMDesignator : dtfi.PMDesignator); } break; case 'd': @@ -565,7 +550,7 @@ private static void FormatCustomized( if (isHebrewCalendar) { // For Hebrew calendar, we need to convert numbers to Hebrew text for yyyy, MM, and dd values. - HebrewFormatDigits(ref result, day); + HebrewNumber.Append(ref result, day); } else { @@ -575,7 +560,7 @@ private static void FormatCustomized( else { int dayOfWeek = (int)cal.GetDayOfWeek(dateTime); - result.Append(FormatDayOfWeek(dayOfWeek, tokenLen, dtfi)); + AppendString(ref result, FormatDayOfWeek(dayOfWeek, tokenLen, dtfi)); } bTimeOnly = false; break; @@ -593,7 +578,7 @@ private static void FormatCustomized( if (isHebrewCalendar) { // For Hebrew calendar, we need to convert numbers to Hebrew text for yyyy, MM, and dd values. - HebrewFormatDigits(ref result, month); + HebrewNumber.Append(ref result, month); } else { @@ -604,13 +589,13 @@ private static void FormatCustomized( { if (isHebrewCalendar) { - result.Append(FormatHebrewMonthName(dateTime, month, tokenLen, dtfi)); + AppendString(ref result, FormatHebrewMonthName(dateTime, month, tokenLen, dtfi)); } else { if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0) { - result.Append( + AppendString(ref result, dtfi.InternalGetMonthName( month, IsUseGenitiveForm(format, i, tokenLen, 'd') ? MonthNameStyles.Genitive : MonthNameStyles.Regular, @@ -618,7 +603,7 @@ private static void FormatCustomized( } else { - result.Append(FormatMonth(month, tokenLen, dtfi)); + AppendString(ref result, FormatMonth(month, tokenLen, dtfi)); } } } @@ -641,7 +626,7 @@ private static void FormatCustomized( // We are formatting a Japanese date with year equals 1 and the year number is followed by the year sign \u5e74 // In Japanese dates, the first year in the era is not formatted as a number 1 instead it is formatted as \u5143 which means // first or beginning of the era. - result.Append(DateTimeFormatInfo.JapaneseEraStart[0]); + AppendChar(ref result, DateTimeFormatInfo.JapaneseEraStart[0]); } else if (dtfi.HasForceTwoDigitYears) { @@ -649,7 +634,7 @@ private static void FormatCustomized( } else if (cal.ID == CalendarId.HEBREW) { - HebrewFormatDigits(ref result, year); + HebrewNumber.Append(ref result, year); } else { @@ -663,7 +648,7 @@ private static void FormatCustomized( } else { - result.Append(year.ToString("D" + tokenLen.ToString(CultureInfo.InvariantCulture), CultureInfo.InvariantCulture)); + AppendString(ref result, year.ToString("D" + tokenLen.ToString(CultureInfo.InvariantCulture), CultureInfo.InvariantCulture)); } } bTimeOnly = false; @@ -677,11 +662,11 @@ private static void FormatCustomized( FormatCustomizedRoundripTimeZone(dateTime, offset, ref result); break; case ':': - result.Append(dtfi.TimeSeparator); + AppendString(ref result, dtfi.TimeSeparator); tokenLen = 1; break; case '/': - result.Append(dtfi.DateSeparator); + AppendString(ref result, dtfi.DateSeparator); tokenLen = 1; break; case '\'': @@ -722,7 +707,7 @@ private static void FormatCustomized( nextChar = ParseNextChar(format, i); if (nextChar >= 0) { - result.Append((char)nextChar); + result.Append(TChar.CreateTruncating(nextChar)); tokenLen = 2; } else @@ -738,7 +723,7 @@ private static void FormatCustomized( // character rule. // That is, if we ask everyone to use single quote or double quote to insert characters, // then we can remove this default block. - result.Append(ch); + result.Append(TChar.CreateTruncating(ch)); tokenLen = 1; break; } @@ -746,8 +731,46 @@ private static void FormatCustomized( } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void AppendChar(ref ValueListBuilder result, char ch) where TChar : unmanaged, IBinaryInteger + { + if (typeof(TChar) == typeof(char) || char.IsAscii(ch)) + { + result.Append(TChar.CreateTruncating(ch)); + } + else + { + Debug.Assert(typeof(TChar) == typeof(byte)); + var r = new Rune(ch); + r.EncodeToUtf8(MemoryMarshal.AsBytes(result.AppendSpan(r.Utf8SequenceLength))); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AppendString(ref ValueListBuilder result, scoped ReadOnlySpan s) where TChar : unmanaged, IBinaryInteger + { + if (typeof(TChar) == typeof(char)) + { + result.Append(MemoryMarshal.Cast(s)); + } + else + { + Debug.Assert(typeof(TChar) == typeof(byte)); + Encoding.UTF8.GetBytes(s, MemoryMarshal.Cast(result.AppendSpan(Encoding.UTF8.GetByteCount(s)))); + } + } + + internal static void FormatFraction(ref ValueListBuilder result, int fraction, ReadOnlySpan fractionFormat) where TChar : unmanaged, IBinaryInteger + { + // TODO https://github.com/dotnet/runtime/issues/84527: Update when Int32 implements IUtf8SpanFormattable + Span chars = stackalloc char[11]; + fraction.TryFormat(chars, out int charsWritten, fractionFormat, CultureInfo.InvariantCulture); + Debug.Assert(charsWritten != 0); + AppendString(ref result, chars.Slice(0, charsWritten)); + } + // output the 'z' family of formats, which output a the offset from UTC, e.g. "-07:30" - private static void FormatCustomizedTimeZone(DateTime dateTime, TimeSpan offset, int tokenLen, bool timeOnly, ref ValueStringBuilder result) + private static void FormatCustomizedTimeZone(DateTime dateTime, TimeSpan offset, int tokenLen, bool timeOnly, ref ValueListBuilder result) where TChar : unmanaged, IBinaryInteger { // See if the instance already has an offset bool dateTimeFormat = (offset.Ticks == NullOffset); @@ -772,11 +795,11 @@ private static void FormatCustomizedTimeZone(DateTime dateTime, TimeSpan offset, } if (offset.Ticks >= 0) { - result.Append('+'); + result.Append(TChar.CreateTruncating('+')); } else { - result.Append('-'); + result.Append(TChar.CreateTruncating('-')); // get a positive offset, so that you don't need a separate code path for the negative numbers. offset = offset.Negate(); } @@ -784,23 +807,27 @@ private static void FormatCustomizedTimeZone(DateTime dateTime, TimeSpan offset, if (tokenLen <= 1) { // 'z' format e.g "-7" - result.AppendSpanFormattable(offset.Hours, "0", CultureInfo.InvariantCulture); + (int tens, int ones) = Math.DivRem(offset.Hours, 10); + if (tens != 0) + { + result.Append(TChar.CreateTruncating('0' + tens)); + } + result.Append(TChar.CreateTruncating('0' + ones)); } else { // 'zz' or longer format e.g "-07" - result.AppendSpanFormattable(offset.Hours, "00", CultureInfo.InvariantCulture); + FormattingHelpers.WriteTwoDigits((uint)offset.Hours, result.AppendSpan(2), 0); if (tokenLen >= 3) { - // 'zzz*' or longer format e.g "-07:30" - result.Append(':'); - result.AppendSpanFormattable(offset.Minutes, "00", CultureInfo.InvariantCulture); + result.Append(TChar.CreateTruncating(':')); + FormattingHelpers.WriteTwoDigits((uint)offset.Minutes, result.AppendSpan(2), 0); } } } // output the 'K' format, which is for round-tripping the data - private static void FormatCustomizedRoundripTimeZone(DateTime dateTime, TimeSpan offset, ref ValueStringBuilder result) + private static void FormatCustomizedRoundripTimeZone(DateTime dateTime, TimeSpan offset, ref ValueListBuilder result) where TChar : unmanaged, IBinaryInteger { // The objective of this format is to round trip the data in the type // For DateTime it should round-trip the Kind value and preserve the time zone. @@ -818,7 +845,7 @@ private static void FormatCustomizedRoundripTimeZone(DateTime dateTime, TimeSpan break; case DateTimeKind.Utc: // The 'Z' constant is a marker for a UTC date - result.Append('Z'); + result.Append(TChar.CreateTruncating('Z')); return; default: // If the kind is unspecified, we output nothing here @@ -827,24 +854,19 @@ private static void FormatCustomizedRoundripTimeZone(DateTime dateTime, TimeSpan } if (offset.Ticks >= 0) { - result.Append('+'); + result.Append(TChar.CreateTruncating('+')); } else { - result.Append('-'); + result.Append(TChar.CreateTruncating('-')); // get a positive offset, so that you don't need a separate code path for the negative numbers. offset = offset.Negate(); } - Append2DigitNumber(ref result, offset.Hours); - result.Append(':'); - Append2DigitNumber(ref result, offset.Minutes); - } - - private static void Append2DigitNumber(ref ValueStringBuilder result, int val) - { - result.Append((char)('0' + (val / 10))); - result.Append((char)('0' + (val % 10))); + Span hoursMinutes = result.AppendSpan(5); + FormattingHelpers.WriteTwoDigits((uint)offset.Hours, hoursMinutes, 0); + hoursMinutes[2] = TChar.CreateTruncating(':'); + FormattingHelpers.WriteTwoDigits((uint)offset.Minutes, hoursMinutes, 3); } internal static string GetRealFormat(ReadOnlySpan format, DateTimeFormatInfo dtfi) @@ -985,16 +1007,20 @@ internal static string Format(DateTime dateTime, string? format, IFormatProvider } } - var vsb = new ValueStringBuilder(stackalloc char[256]); - FormatStringBuilder(dateTime, format, DateTimeFormatInfo.GetInstance(provider), offset, ref vsb); - return vsb.ToString(); + var vlb = new ValueListBuilder(stackalloc char[256]); + FormatIntoBuilder(dateTime, format, DateTimeFormatInfo.GetInstance(provider), offset, ref vlb); + string resultString = vlb.AsSpan().ToString(); + vlb.Dispose(); + return resultString; } - internal static bool TryFormat(DateTime dateTime, Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) => - TryFormat(dateTime, destination, out charsWritten, format, provider, new TimeSpan(NullOffset)); + internal static bool TryFormat(DateTime dateTime, Span destination, out int written, ReadOnlySpan format, IFormatProvider? provider) where TChar : unmanaged, IBinaryInteger => + TryFormat(dateTime, destination, out written, format, provider, new TimeSpan(NullOffset)); - internal static bool TryFormat(DateTime dateTime, Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider, TimeSpan offset) + internal static bool TryFormat(DateTime dateTime, Span destination, out int written, ReadOnlySpan format, IFormatProvider? provider, TimeSpan offset) where TChar : unmanaged, IBinaryInteger { + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); + if (format.Length == 1) { // Optimize for these standard formats that are not affected by culture. @@ -1002,20 +1028,22 @@ internal static bool TryFormat(DateTime dateTime, Span destination, out in { // Round trip format case 'o': - return TryFormatO(dateTime, offset, destination, out charsWritten); + return TryFormatO(dateTime, offset, destination, out written); // RFC1123 case 'r': - return TryFormatR(dateTime, offset, destination, out charsWritten); + return TryFormatR(dateTime, offset, destination, out written); } } - var vsb = new ValueStringBuilder(stackalloc char[256]); - FormatStringBuilder(dateTime, format, DateTimeFormatInfo.GetInstance(provider), offset, ref vsb); - return vsb.TryCopyTo(destination, out charsWritten); + var vlb = new ValueListBuilder(stackalloc TChar[256]); + FormatIntoBuilder(dateTime, format, DateTimeFormatInfo.GetInstance(provider), offset, ref vlb); + bool copied = vlb.TryCopyTo(destination, out written); + vlb.Dispose(); + return copied; } - private static void FormatStringBuilder(DateTime dateTime, ReadOnlySpan format, DateTimeFormatInfo dtfi, TimeSpan offset, ref ValueStringBuilder result) + private static void FormatIntoBuilder(DateTime dateTime, ReadOnlySpan format, DateTimeFormatInfo dtfi, TimeSpan offset, ref ValueListBuilder result) where TChar : unmanaged, IBinaryInteger { Debug.Assert(dtfi != null); if (format.Length == 0) @@ -1210,20 +1238,20 @@ internal static bool IsValidCustomTimeFormat(ReadOnlySpan format, bool thr // 012345678901234567890123456789012 // --------------------------------- // 05:30:45.7680000 - internal static bool TryFormatTimeOnlyO(int hour, int minute, int second, long fraction, Span destination) + internal static bool TryFormatTimeOnlyO(int hour, int minute, int second, long fraction, Span destination) where TChar : unmanaged, IBinaryInteger { 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, 7)); + FormattingHelpers.WriteTwoDigits((uint)hour, destination, 0); + destination[2] = TChar.CreateTruncating(':'); + FormattingHelpers.WriteTwoDigits((uint)minute, destination, 3); + destination[5] = TChar.CreateTruncating(':'); + FormattingHelpers.WriteTwoDigits((uint)second, destination, 6); + destination[8] = TChar.CreateTruncating('.'); + FormattingHelpers.WriteDigits((uint)fraction, destination.Slice(9, 7)); return true; } @@ -1231,18 +1259,18 @@ internal static bool TryFormatTimeOnlyO(int hour, int minute, int second, long f // 012345678901234567890123456789012 // --------------------------------- // 05:30:45 - internal static bool TryFormatTimeOnlyR(int hour, int minute, int second, Span destination) + internal static bool TryFormatTimeOnlyR(int hour, int minute, int second, Span destination) where TChar : unmanaged, IBinaryInteger { 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); + FormattingHelpers.WriteTwoDigits((uint)hour, destination, 0); + destination[2] = TChar.CreateTruncating(':'); + FormattingHelpers.WriteTwoDigits((uint)minute, destination, 3); + destination[5] = TChar.CreateTruncating(':'); + FormattingHelpers.WriteTwoDigits((uint)second, destination, 6); return true; } @@ -1251,18 +1279,18 @@ internal static bool TryFormatTimeOnlyR(int hour, int minute, int second, Span destination) + internal static bool TryFormatDateOnlyO(int year, int month, int day, Span destination) where TChar : unmanaged, IBinaryInteger { 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); + FormattingHelpers.WriteFourDigits((uint)year, destination, 0); + destination[4] = TChar.CreateTruncating('-'); + FormattingHelpers.WriteTwoDigits((uint)month, destination, 5); + destination[7] = TChar.CreateTruncating('-'); + FormattingHelpers.WriteTwoDigits((uint)day, destination, 8); return true; } @@ -1270,31 +1298,39 @@ internal static bool TryFormatDateOnlyO(int year, int month, int day, Span // 01234567890123456789012345678 // ----------------------------- // Tue, 03 Jan 2017 - internal static bool TryFormatDateOnlyR(DayOfWeek dayOfWeek, int year, int month, int day, Span destination) + internal static bool TryFormatDateOnlyR(DayOfWeek dayOfWeek, int year, int month, int day, Span destination) where TChar : unmanaged, IBinaryInteger { + Debug.Assert((uint)dayOfWeek < 7); + 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); + if (typeof(TChar) == typeof(char)) + { + Span dest = MemoryMarshal.Cast(destination); + + FormattingHelpers.CopyFour("Sun,Mon,Tue,Wed,Thu,Fri,Sat,".AsSpan(4 * (int)dayOfWeek), dest); + dest[4] = ' '; + FormattingHelpers.WriteTwoDigits((uint)day, dest, 5); + dest[7] = ' '; + FormattingHelpers.CopyFour("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec ".AsSpan(4 * (month - 1)), dest.Slice(8)); + FormattingHelpers.WriteFourDigits((uint)year, dest, 12); + } + else + { + Debug.Assert(typeof(TChar) == typeof(byte)); + Span dest = MemoryMarshal.Cast(destination); + + FormattingHelpers.CopyFour("Sun,Mon,Tue,Wed,Thu,Fri,Sat,"u8.Slice(4 * (int)dayOfWeek), dest); + dest[4] = (byte)' '; + FormattingHelpers.WriteTwoDigits((uint)day, dest, 5); + dest[7] = (byte)' '; + FormattingHelpers.CopyFour("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec "u8.Slice(4 * (month - 1)), dest.Slice(8)); + FormattingHelpers.WriteFourDigits((uint)year, dest, 12); + } + return true; } @@ -1304,7 +1340,7 @@ internal static bool TryFormatDateOnlyR(DayOfWeek dayOfWeek, int year, int month // 2017-06-12T05:30:45.7680000-07:00 // 2017-06-12T05:30:45.7680000Z (Z is short for "+00:00" but also distinguishes DateTimeKind.Utc from DateTimeKind.Local) // 2017-06-12T05:30:45.7680000 (interpreted as local time wrt to current time zone) - private static bool TryFormatO(DateTime dateTime, TimeSpan offset, Span destination, out int charsWritten) + internal static bool TryFormatO(DateTime dateTime, TimeSpan offset, Span destination, out int charsWritten) where TChar : unmanaged, IBinaryInteger { const int MinimumBytesNeeded = 27; @@ -1342,47 +1378,43 @@ private static bool TryFormatO(DateTime dateTime, TimeSpan offset, Span de dateTime.GetDate(out int year, out int month, out int day); dateTime.GetTimePrecise(out int hour, out int minute, out int second, out int tick); - WriteFourDecimalDigits((uint)year, destination, 0); - destination[4] = '-'; - WriteTwoDecimalDigits((uint)month, destination, 5); - destination[7] = '-'; - WriteTwoDecimalDigits((uint)day, destination, 8); - destination[10] = 'T'; - WriteTwoDecimalDigits((uint)hour, destination, 11); - destination[13] = ':'; - WriteTwoDecimalDigits((uint)minute, destination, 14); - destination[16] = ':'; - WriteTwoDecimalDigits((uint)second, destination, 17); - destination[19] = '.'; - WriteDigits((uint)tick, destination.Slice(20, 7)); + FormattingHelpers.WriteFourDigits((uint)year, destination, 0); + destination[4] = TChar.CreateTruncating('-'); + FormattingHelpers.WriteTwoDigits((uint)month, destination, 5); + destination[7] = TChar.CreateTruncating('-'); + FormattingHelpers.WriteTwoDigits((uint)day, destination, 8); + destination[10] = TChar.CreateTruncating('T'); + FormattingHelpers.WriteTwoDigits((uint)hour, destination, 11); + destination[13] = TChar.CreateTruncating(':'); + FormattingHelpers.WriteTwoDigits((uint)minute, destination, 14); + destination[16] = TChar.CreateTruncating(':'); + FormattingHelpers.WriteTwoDigits((uint)second, destination, 17); + destination[19] = TChar.CreateTruncating('.'); + FormattingHelpers.WriteDigits((uint)tick, destination.Slice(20, 7)); if (kind == DateTimeKind.Local) { int offsetTotalMinutes = (int)(offset.Ticks / TimeSpan.TicksPerMinute); - char sign; + char sign = '+'; if (offsetTotalMinutes < 0) { sign = '-'; offsetTotalMinutes = -offsetTotalMinutes; } - else - { - sign = '+'; - } int offsetHours = Math.DivRem(offsetTotalMinutes, 60, out int offsetMinutes); // Writing the value backward allows the JIT to optimize by // performing a single bounds check against buffer. - WriteTwoDecimalDigits((uint)offsetMinutes, destination, 31); - destination[30] = ':'; - WriteTwoDecimalDigits((uint)offsetHours, destination, 28); - destination[27] = sign; + FormattingHelpers.WriteTwoDigits((uint)offsetMinutes, destination, 31); + destination[30] = TChar.CreateTruncating(':'); + FormattingHelpers.WriteTwoDigits((uint)offsetHours, destination, 28); + destination[27] = TChar.CreateTruncating(sign); } else if (kind == DateTimeKind.Utc) { - destination[27] = 'Z'; + destination[27] = TChar.CreateTruncating('Z'); } return true; @@ -1392,7 +1424,7 @@ private static bool TryFormatO(DateTime dateTime, TimeSpan offset, Span de // 01234567890123456789012345678 // ----------------------------- // Tue, 03 Jan 2017 08:08:05 GMT - private static bool TryFormatR(DateTime dateTime, TimeSpan offset, Span destination, out int charsWritten) + internal static bool TryFormatR(DateTime dateTime, TimeSpan offset, Span destination, out int charsWritten) where TChar : unmanaged, IBinaryInteger { if (destination.Length <= 28) { @@ -1409,93 +1441,46 @@ private static bool TryFormatR(DateTime dateTime, TimeSpan offset, Span de dateTime.GetDate(out int year, out int month, out int day); dateTime.GetTime(out int hour, out int minute, out int second); - string dayAbbrev = InvariantAbbreviatedDayNames[(int)dateTime.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); - destination[16] = ' '; - WriteTwoDecimalDigits((uint)hour, destination, 17); - destination[19] = ':'; - WriteTwoDecimalDigits((uint)minute, destination, 20); - destination[22] = ':'; - WriteTwoDecimalDigits((uint)second, destination, 23); - destination[25] = ' '; - destination[26] = 'G'; - destination[27] = 'M'; - destination[28] = 'T'; - - charsWritten = 29; - return true; - } - - /// - /// Writes a value [ 00 .. 99 ] to the buffer starting at the specified offset. - /// This method performs best when the starting index is a constant literal. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteTwoDecimalDigits(uint value, Span destination, int offset) - { - Debug.Assert(value <= 99); - - uint temp = '0' + value; - value /= 10; - destination[offset + 1] = (char)(temp - (value * 10)); - destination[offset] = (char)('0' + value); - } - - /// - /// Writes a value [ 0000 .. 9999 ] to the buffer starting at the specified offset. - /// This method performs best when the starting index is a constant literal. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteFourDecimalDigits(uint value, Span buffer, int startingIndex = 0) - { - Debug.Assert(value <= 9999); - - uint temp = '0' + value; - value /= 10; - buffer[startingIndex + 3] = (char)(temp - (value * 10)); - - temp = '0' + value; - value /= 10; - buffer[startingIndex + 2] = (char)(temp - (value * 10)); - - temp = '0' + value; - value /= 10; - buffer[startingIndex + 1] = (char)(temp - (value * 10)); - - buffer[startingIndex] = (char)('0' + value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteDigits(ulong value, Span buffer) - { - // We can mutate the 'value' parameter since it's a copy-by-value local. - // It'll be used to represent the value left over after each division by 10. - - for (int i = buffer.Length - 1; i >= 1; i--) + if (typeof(TChar) == typeof(char)) { - ulong temp = '0' + value; - value /= 10; - buffer[i] = (char)(temp - (value * 10)); + Span dest = MemoryMarshal.Cast(destination); + + FormattingHelpers.CopyFour("Sun,Mon,Tue,Wed,Thu,Fri,Sat,".AsSpan(4 * (int)dateTime.DayOfWeek), dest); + dest[4] = ' '; + FormattingHelpers.WriteTwoDigits((uint)day, dest, 5); + dest[7] = ' '; + FormattingHelpers.CopyFour("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec ".AsSpan(4 * (month - 1)), dest.Slice(8)); + FormattingHelpers.WriteFourDigits((uint)year, dest, 12); + dest[16] = ' '; + FormattingHelpers.WriteTwoDigits((uint)hour, dest, 17); + dest[19] = ':'; + FormattingHelpers.WriteTwoDigits((uint)minute, dest, 20); + dest[22] = ':'; + FormattingHelpers.WriteTwoDigits((uint)second, dest, 23); + FormattingHelpers.CopyFour(" GMT", dest.Slice(25)); + } + else + { + Debug.Assert(typeof(TChar) == typeof(byte)); + Span dest = MemoryMarshal.Cast(destination); + + FormattingHelpers.CopyFour("Sun,Mon,Tue,Wed,Thu,Fri,Sat,"u8.Slice(4 * (int)dateTime.DayOfWeek), dest); + dest[4] = (byte)' '; + FormattingHelpers.WriteTwoDigits((uint)day, dest, 5); + dest[7] = (byte)' '; + FormattingHelpers.CopyFour("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec "u8.Slice(4 * (month - 1)), dest.Slice(8)); + FormattingHelpers.WriteFourDigits((uint)year, dest, 12); + dest[16] = (byte)' '; + FormattingHelpers.WriteTwoDigits((uint)hour, dest, 17); + dest[19] = (byte)':'; + FormattingHelpers.WriteTwoDigits((uint)minute, dest, 20); + dest[22] = (byte)':'; + FormattingHelpers.WriteTwoDigits((uint)second, dest, 23); + FormattingHelpers.CopyFour(" GMT"u8, dest.Slice(25)); } - Debug.Assert(value < 10); - buffer[0] = (char)('0' + value); + charsWritten = 29; + return true; } internal static string[] GetAllDateTimes(DateTime dateTime, char format, DateTimeFormatInfo dtfi) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/HebrewNumber.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/HebrewNumber.cs index e38002f17802e..dddbaba644777 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/HebrewNumber.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/HebrewNumber.cs @@ -1,8 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Text; +using System.Collections.Generic; using System.Diagnostics; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; namespace System.Globalization { @@ -80,7 +83,7 @@ internal static class HebrewNumber // //////////////////////////////////////////////////////////////////////////// - internal static void Append(ref ValueStringBuilder outputBuffer, int Number) + internal static void Append(ref ValueListBuilder outputBuffer, int Number) where TChar : unmanaged, IBinaryInteger { int outputBufferStartingLength = outputBuffer.Length; @@ -113,13 +116,13 @@ internal static void Append(ref ValueStringBuilder outputBuffer, int Number) // If the number is greater than 400, use the multiples of 400. for (int i = 0; i < (Hundreds / 4); i++) { - outputBuffer.Append('\x05ea'); + DateTimeFormat.AppendChar(ref outputBuffer, '\x05ea'); } int remains = Hundreds % 4; if (remains > 0) { - outputBuffer.Append((char)((int)'\x05e6' + remains)); + DateTimeFormat.AppendChar(ref outputBuffer, (char)('\x05e6' + remains)); } } @@ -188,24 +191,35 @@ internal static void Append(ref ValueStringBuilder outputBuffer, int Number) if (cTens != '\x0') { - outputBuffer.Append(cTens); + DateTimeFormat.AppendChar(ref outputBuffer, cTens); } if (cUnits != '\x0') { - outputBuffer.Append(cUnits); + DateTimeFormat.AppendChar(ref outputBuffer, cUnits); } if (outputBuffer.Length - outputBufferStartingLength > 1) { - char last = outputBuffer[outputBuffer.Length - 1]; - outputBuffer.Length--; - outputBuffer.Append('"'); - outputBuffer.Append(last); + if (typeof(TChar) == typeof(char)) + { + TChar last = outputBuffer[outputBuffer.Length - 1]; + outputBuffer.Length--; + outputBuffer.Append(TChar.CreateTruncating('"')); + outputBuffer.Append(last); + } + else + { + Debug.Assert(typeof(TChar) == typeof(byte)); + Rune.DecodeLastFromUtf8(MemoryMarshal.AsBytes(outputBuffer.AsSpan()), out Rune value, out int bytesConsumed); + outputBuffer.Length -= bytesConsumed; + outputBuffer.Append(TChar.CreateTruncating('"')); + DateTimeFormat.AppendChar(ref outputBuffer, (char)value.Value); + } } else { - outputBuffer.Append('\''); + DateTimeFormat.AppendChar(ref outputBuffer, '\''); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanFormat.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanFormat.cs index f66e5482c36d9..aa891eb81846a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanFormat.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanFormat.cs @@ -2,8 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers.Text; +using System.Collections.Generic; using System.Diagnostics; -using System.Runtime.CompilerServices; +using System.Numerics; using System.Runtime.InteropServices; using System.Text; @@ -39,14 +40,18 @@ internal static string Format(TimeSpan value, string? format, IFormatProvider? f throw new FormatException(SR.Format_InvalidString); } - var vsb = new ValueStringBuilder(stackalloc char[256]); - FormatCustomized(value, format, DateTimeFormatInfo.GetInstance(formatProvider), ref vsb); - return vsb.ToString(); + var vlb = new ValueListBuilder(stackalloc char[256]); + FormatCustomized(value, format, DateTimeFormatInfo.GetInstance(formatProvider), ref vlb); + string resultString = vlb.AsSpan().ToString(); + vlb.Dispose(); + return resultString; } /// Main method called from TimeSpan.TryFormat. - internal static bool TryFormat(TimeSpan value, Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? formatProvider) + internal static bool TryFormat(TimeSpan value, Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? formatProvider) where TChar : unmanaged, IBinaryInteger { + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); + if (format.Length == 0) { return TryFormatStandard(value, StandardFormat.C, null, destination, out charsWritten); @@ -69,9 +74,11 @@ internal static bool TryFormat(TimeSpan value, Span destination, out int c } } - var vsb = new ValueStringBuilder(stackalloc char[256]); - FormatCustomized(value, format, DateTimeFormatInfo.GetInstance(formatProvider), ref vsb); - return vsb.TryCopyTo(destination, out charsWritten); + var vlb = new ValueListBuilder(stackalloc TChar[256]); + FormatCustomized(value, format, DateTimeFormatInfo.GetInstance(formatProvider), ref vlb); + bool result = vlb.TryCopyTo(destination, out charsWritten); + vlb.Dispose(); + return result; } internal static string FormatC(TimeSpan value) @@ -92,9 +99,9 @@ private static string FormatG(TimeSpan value, DateTimeFormatInfo dtfi, StandardF return new string(destination.Slice(0, charsWritten)); } - private enum StandardFormat { C, G, g } + internal enum StandardFormat { C, G, g } - private static bool TryFormatStandard(TimeSpan value, StandardFormat format, string? decimalSeparator, Span destination, out int charsWritten) + internal static bool TryFormatStandard(TimeSpan value, StandardFormat format, string? decimalSeparator, Span destination, out int written) where TChar : unmanaged, IBinaryInteger { Debug.Assert(format == StandardFormat.C || format == StandardFormat.G || format == StandardFormat.g); @@ -137,6 +144,7 @@ private static bool TryFormatStandard(TimeSpan value, StandardFormat format, str // "c": Write out a fraction only if it's non-zero, and write out all 7 digits of it. if (fraction != 0) { + Debug.Assert(decimalSeparator is null); fractionDigits = DateTimeFormat.MaxSecondsFractionDigits; requiredOutputLength += fractionDigits + 1; // digits plus leading decimal separator } @@ -144,17 +152,25 @@ private static bool TryFormatStandard(TimeSpan value, StandardFormat format, str case StandardFormat.G: // "G": Write out a fraction regardless of whether it's 0, and write out all 7 digits of it. + Debug.Assert(decimalSeparator is not null); fractionDigits = DateTimeFormat.MaxSecondsFractionDigits; - requiredOutputLength += fractionDigits + 1; // digits plus leading decimal separator + requiredOutputLength += fractionDigits; + requiredOutputLength += typeof(TChar) == typeof(char) || (decimalSeparator.Length == 1 && char.IsAscii(decimalSeparator[0])) ? + decimalSeparator.Length : + Encoding.UTF8.GetByteCount(decimalSeparator); break; default: // "g": Write out a fraction only if it's non-zero, and write out only the most significant digits. Debug.Assert(format == StandardFormat.g); + Debug.Assert(decimalSeparator is not null); if (fraction != 0) { fractionDigits = DateTimeFormat.MaxSecondsFractionDigits - FormattingHelpers.CountDecimalTrailingZeros(fraction, out fraction); - requiredOutputLength += fractionDigits + 1; // digits plus leading decimal separator + requiredOutputLength += fractionDigits; + requiredOutputLength += typeof(TChar) == typeof(char) || (decimalSeparator.Length == 1 && char.IsAscii(decimalSeparator[0])) ? + decimalSeparator.Length : + Encoding.UTF8.GetByteCount(decimalSeparator); } break; } @@ -210,7 +226,7 @@ private static bool TryFormatStandard(TimeSpan value, StandardFormat format, str if (destination.Length < requiredOutputLength) { - charsWritten = 0; + written = 0; return false; } @@ -218,33 +234,33 @@ private static bool TryFormatStandard(TimeSpan value, StandardFormat format, str int idx = 0; if (value.Ticks < 0) { - destination[idx++] = '-'; + destination[idx++] = TChar.CreateTruncating('-'); } // Write day and separator, if necessary if (dayDigits != 0) { - WriteDigits(days, destination.Slice(idx, dayDigits)); + FormattingHelpers.WriteDigits(days, destination.Slice(idx, dayDigits)); idx += dayDigits; - destination[idx++] = format == StandardFormat.C ? '.' : ':'; + destination[idx++] = TChar.CreateTruncating(format == StandardFormat.C ? '.' : ':'); } // Write "[h]h:mm:ss Debug.Assert(hourDigits == 1 || hourDigits == 2); if (hourDigits == 2) { - WriteTwoDigits(hours, destination.Slice(idx)); + FormattingHelpers.WriteTwoDigits(hours, destination, idx); idx += 2; } else { - destination[idx++] = (char)('0' + hours); + destination[idx++] = TChar.CreateTruncating('0' + hours); } - destination[idx++] = ':'; - WriteTwoDigits((uint)minutes, destination.Slice(idx)); + destination[idx++] = TChar.CreateTruncating(':'); + FormattingHelpers.WriteTwoDigits((uint)minutes, destination, idx); idx += 2; - destination[idx++] = ':'; - WriteTwoDigits((uint)seconds, destination.Slice(idx)); + destination[idx++] = TChar.CreateTruncating(':'); + FormattingHelpers.WriteTwoDigits((uint)seconds, destination, idx); idx += 2; // Write fraction and separator, if necessary @@ -253,54 +269,43 @@ private static bool TryFormatStandard(TimeSpan value, StandardFormat format, str Debug.Assert(format == StandardFormat.C || decimalSeparator != null); if (format == StandardFormat.C) { - destination[idx++] = '.'; + destination[idx++] = TChar.CreateTruncating('.'); } - else if (decimalSeparator!.Length == 1) + else if (typeof(TChar) == typeof(char)) { - destination[idx++] = decimalSeparator[0]; + if (decimalSeparator!.Length == 1) + { + destination[idx++] = TChar.CreateTruncating(decimalSeparator[0]); + } + else + { + decimalSeparator.CopyTo(MemoryMarshal.Cast(destination).Slice(idx)); + idx += decimalSeparator.Length; + } } else { - decimalSeparator.CopyTo(destination); - idx += decimalSeparator.Length; + Debug.Assert(typeof(TChar) == typeof(byte)); + if (decimalSeparator!.Length == 1 && char.IsAscii(decimalSeparator[0])) + { + destination[idx++] = TChar.CreateTruncating(decimalSeparator[0]); + } + else + { + idx += Encoding.UTF8.GetBytes(decimalSeparator, MemoryMarshal.Cast(destination).Slice(idx)); + } } - WriteDigits(fraction, destination.Slice(idx, fractionDigits)); + FormattingHelpers.WriteDigits(fraction, destination.Slice(idx, fractionDigits)); idx += fractionDigits; } Debug.Assert(idx == requiredOutputLength); - charsWritten = requiredOutputLength; + written = requiredOutputLength; return true; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteTwoDigits(uint value, Span buffer) - { - Debug.Assert(buffer.Length >= 2); - uint temp = '0' + value; - value /= 10; - buffer[1] = (char)(temp - (value * 10)); - buffer[0] = (char)('0' + value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteDigits(uint value, Span buffer) - { - Debug.Assert(buffer.Length > 0); - - for (int i = buffer.Length - 1; i >= 1; i--) - { - uint temp = '0' + value; - value /= 10; - buffer[i] = (char)(temp - (value * 10)); - } - - Debug.Assert(value < 10); - buffer[0] = (char)('0' + value); - } - /// Format the TimeSpan instance using the specified format. - private static void FormatCustomized(TimeSpan value, scoped ReadOnlySpan format, DateTimeFormatInfo dtfi, ref ValueStringBuilder result) + private static void FormatCustomized(TimeSpan value, scoped ReadOnlySpan format, DateTimeFormatInfo dtfi, ref ValueListBuilder result) where TChar : unmanaged, IBinaryInteger { Debug.Assert(dtfi != null); @@ -363,7 +368,7 @@ private static void FormatCustomized(TimeSpan value, scoped ReadOnlySpan f tmp = fraction; tmp /= TimeSpanParse.Pow10(DateTimeFormat.MaxSecondsFractionDigits - tokenLen); - result.AppendSpanFormattable(tmp, DateTimeFormat.fixedNumberFormats[tokenLen - 1], CultureInfo.InvariantCulture); + DateTimeFormat.FormatFraction(ref result, (int)tmp, DateTimeFormat.fixedNumberFormats[tokenLen - 1]); break; case 'F': // @@ -392,7 +397,7 @@ private static void FormatCustomized(TimeSpan value, scoped ReadOnlySpan f } if (effectiveDigits > 0) { - result.AppendSpanFormattable(tmp, DateTimeFormat.fixedNumberFormats[effectiveDigits - 1], CultureInfo.InvariantCulture); + DateTimeFormat.FormatFraction(ref result, (int)tmp, DateTimeFormat.fixedNumberFormats[effectiveDigits - 1]); } break; case 'd': @@ -441,7 +446,7 @@ private static void FormatCustomized(TimeSpan value, scoped ReadOnlySpan f nextChar = DateTimeFormat.ParseNextChar(format, i); if (nextChar >= 0) { - result.Append((char)nextChar); + result.Append(TChar.CreateTruncating(nextChar)); tokenLen = 2; } else diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs index 078c671db55e0..5b106f5063147 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs @@ -4,6 +4,7 @@ using System.Buffers.Text; using System.Diagnostics; using System.Globalization; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -1581,22 +1582,24 @@ private static unsafe void UInt32ToNumber(uint value, ref NumberBuffer number) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe void WriteTwoDigits(char* ptr, uint value) + internal static unsafe void WriteTwoDigits(TChar* ptr, uint value) where TChar : unmanaged, IBinaryInteger { + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); Debug.Assert(value <= 99); - Unsafe.WriteUnaligned(ptr, - Unsafe.ReadUnaligned( - ref Unsafe.As( - ref Unsafe.Add(ref TwoDigitsChars.GetRawStringData(), (int)value * 2)))); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe void WriteTwoDigits(byte* ptr, uint value) - { - Debug.Assert(value <= 99); - Unsafe.WriteUnaligned(ptr, - Unsafe.ReadUnaligned( - ref Unsafe.Add(ref MemoryMarshal.GetReference(TwoDigitsBytes), (int)value * 2))); + if (typeof(TChar) == typeof(char)) + { + Unsafe.WriteUnaligned(ptr, + Unsafe.ReadUnaligned( + ref Unsafe.As( + ref Unsafe.Add(ref TwoDigitsChars.GetRawStringData(), (int)value * 2)))); + } + else + { + Unsafe.WriteUnaligned(ptr, + Unsafe.ReadUnaligned( + ref Unsafe.Add(ref MemoryMarshal.GetReference(TwoDigitsBytes), (int)value * 2))); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs b/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs index 182863b8dcd65..d99947dd09cfb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs @@ -22,6 +22,7 @@ namespace System.Text #if SYSTEM_PRIVATE_CORELIB #pragma warning disable SA1001 // Commas should be spaced correctly , ISpanFormattable + , IUtf8SpanFormattable #pragma warning restore SA1001 #endif { @@ -915,6 +916,9 @@ public override string ToString() bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) => TryEncodeToUtf16(destination, out charsWritten); + bool IUtf8SpanFormattable.TryFormat(Span utf8Destination, out int bytesWritten, ReadOnlySpan format, IFormatProvider? provider) => + TryEncodeToUtf8(utf8Destination, out bytesWritten); + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); #endif diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs b/src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs index 4f29d170160df..5a6eeb8d1c0c7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs @@ -17,7 +17,8 @@ public readonly struct TimeOnly IComparable, IEquatable, ISpanFormattable, - ISpanParsable + ISpanParsable, + IUtf8SpanFormattable { // represent the number of ticks map to the time of the day. 1 ticks = 100-nanosecond in time measurements. private readonly long _ticks; @@ -964,7 +965,13 @@ public string ToString([StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] stri /// 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, [StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] ReadOnlySpan format = default(ReadOnlySpan), IFormatProvider? provider = null) + public bool TryFormat(Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] ReadOnlySpan format = default(ReadOnlySpan), IFormatProvider? provider = null) => + TryFormatCore(destination, out charsWritten, format, provider); + + bool IUtf8SpanFormattable.TryFormat(Span utf8Destination, out int bytesWritten, [StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] ReadOnlySpan format, IFormatProvider? provider) => + TryFormatCore(utf8Destination, out bytesWritten, format, provider); + + private bool TryFormatCore(Span destination, out int written, [StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] ReadOnlySpan format, IFormatProvider? provider) where TChar : unmanaged, IBinaryInteger { if (format.Length == 0) { @@ -979,25 +986,25 @@ public string ToString([StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] stri case 'O': if (!DateTimeFormat.TryFormatTimeOnlyO(Hour, Minute, Second, _ticks % TimeSpan.TicksPerSecond, destination)) { - charsWritten = 0; + written = 0; return false; } - charsWritten = 16; + written = 16; return true; case 'r': case 'R': if (!DateTimeFormat.TryFormatTimeOnlyR(Hour, Minute, Second, destination)) { - charsWritten = 0; + written = 0; return false; } - charsWritten = 8; + written = 8; return true; case 't': case 'T': - return DateTimeFormat.TryFormat(ToDateTime(), destination, out charsWritten, format, provider); + return DateTimeFormat.TryFormat(ToDateTime(), destination, out written, format, provider); default: throw new FormatException(SR.Argument_BadFormatSpecifier); @@ -1009,7 +1016,7 @@ public string ToString([StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] stri throw new FormatException(SR.Format(SR.Format_DateTimeOnlyContainsNoneDateParts, format.ToString(), nameof(TimeOnly))); } - return DateTimeFormat.TryFormat(ToDateTime(), destination, out charsWritten, format, provider); + return DateTimeFormat.TryFormat(ToDateTime(), destination, out written, format, provider); } // diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeSpan.cs b/src/libraries/System.Private.CoreLib/src/System/TimeSpan.cs index 11025c6137190..0a13fe4c42f4b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeSpan.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeSpan.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.Numerics; using System.Runtime.CompilerServices; -using System.Runtime.Versioning; namespace System { @@ -32,7 +31,8 @@ public readonly struct TimeSpan IComparable, IEquatable, ISpanFormattable, - ISpanParsable + ISpanParsable, + IUtf8SpanFormattable { /// /// Represents the number of nanoseconds per tick. This field is constant. @@ -561,10 +561,12 @@ public string ToString([StringSyntax(StringSyntaxAttribute.TimeSpanFormat)] stri return TimeSpanFormat.Format(this, format, formatProvider); } - public bool TryFormat(Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.TimeSpanFormat)] ReadOnlySpan format = default, IFormatProvider? formatProvider = null) - { - return TimeSpanFormat.TryFormat(this, destination, out charsWritten, format, formatProvider); - } + public bool TryFormat(Span destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.TimeSpanFormat)] ReadOnlySpan format = default, IFormatProvider? formatProvider = null) => + TimeSpanFormat.TryFormat(this, destination, out charsWritten, format, formatProvider); + + bool IUtf8SpanFormattable.TryFormat(Span utf8Destination, out int bytesWritten, [StringSyntax(StringSyntaxAttribute.TimeSpanFormat)] ReadOnlySpan format, IFormatProvider? formatProvider) => + TimeSpanFormat.TryFormat(this, utf8Destination, out bytesWritten, format, formatProvider); + #endregion public static TimeSpan operator -(TimeSpan t) diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 86305a208d9be..d6c76849d8bb2 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -865,7 +865,7 @@ protected CannotUnloadAppDomainException(System.Runtime.Serialization.Serializat public CannotUnloadAppDomainException(string? message) { } public CannotUnloadAppDomainException(string? message, System.Exception? innerException) { } } - public readonly partial struct Char : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.Numerics.IAdditionOperators, System.Numerics.IAdditiveIdentity, System.Numerics.IBinaryInteger, System.Numerics.IBinaryNumber, System.Numerics.IBitwiseOperators, System.Numerics.IComparisonOperators, System.Numerics.IDecrementOperators, System.Numerics.IDivisionOperators, System.Numerics.IEqualityOperators, System.Numerics.IIncrementOperators, System.Numerics.IMinMaxValue, System.Numerics.IModulusOperators, System.Numerics.IMultiplicativeIdentity, System.Numerics.IMultiplyOperators, System.Numerics.INumber, System.Numerics.INumberBase, System.Numerics.IShiftOperators, System.Numerics.ISubtractionOperators, System.Numerics.IUnaryNegationOperators, System.Numerics.IUnaryPlusOperators, System.Numerics.IUnsignedNumber + public readonly partial struct Char : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.Numerics.IAdditionOperators, System.Numerics.IAdditiveIdentity, System.Numerics.IBinaryInteger, System.Numerics.IBinaryNumber, System.Numerics.IBitwiseOperators, System.Numerics.IComparisonOperators, System.Numerics.IDecrementOperators, System.Numerics.IDivisionOperators, System.Numerics.IEqualityOperators, System.Numerics.IIncrementOperators, System.Numerics.IMinMaxValue, System.Numerics.IModulusOperators, System.Numerics.IMultiplicativeIdentity, System.Numerics.IMultiplyOperators, System.Numerics.INumber, System.Numerics.INumberBase, System.Numerics.IShiftOperators, System.Numerics.ISubtractionOperators, System.Numerics.IUnaryNegationOperators, System.Numerics.IUnaryPlusOperators, System.Numerics.IUnsignedNumber, System.IUtf8SpanFormattable { private readonly char _dummyPrimitive; public const char MaxValue = '\uFFFF'; @@ -951,6 +951,7 @@ public CannotUnloadAppDomainException(string? message, System.Exception? innerEx static char System.IParsable.Parse(string s, System.IFormatProvider? provider) { throw null; } static bool System.IParsable.TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, System.IFormatProvider? provider, out char result) { throw null; } bool System.ISpanFormattable.TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } + bool System.IUtf8SpanFormattable.TryFormat(System.Span utf8Destination, out int bytesWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } static char System.ISpanParsable.Parse(System.ReadOnlySpan s, System.IFormatProvider? provider) { throw null; } static bool System.ISpanParsable.TryParse(System.ReadOnlySpan s, System.IFormatProvider? provider, out char result) { throw null; } static char System.Numerics.IAdditionOperators.operator +(char left, char right) { throw null; } @@ -1524,7 +1525,7 @@ 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 partial struct DateOnly : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable + public readonly partial struct DateOnly : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.IUtf8SpanFormattable { private readonly int _dummyPrimitive; public DateOnly(int year, int month, int day) { throw null; } @@ -1576,6 +1577,7 @@ public static partial class Convert public string ToString([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("DateOnlyFormat")] string? format) { throw null; } public string ToString([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("DateOnlyFormat")] string? format, System.IFormatProvider? provider) { throw null; } public bool TryFormat(System.Span destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("DateOnlyFormat")] System.ReadOnlySpan format = default(System.ReadOnlySpan), System.IFormatProvider? provider = null) { throw null; } + bool System.IUtf8SpanFormattable.TryFormat(System.Span utf8Destination, out int bytesWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("TimeOnlyFormat")] System.ReadOnlySpan format, System.IFormatProvider? provider) { 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, 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; } @@ -1591,7 +1593,7 @@ public static partial class Convert public static bool TryParseExact([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true), System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("DateOnlyFormat")] string?[]? formats, out System.DateOnly result) { throw null; } public static bool TryParseExact([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true), System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("DateOnlyFormat")] string?[]? formats, System.IFormatProvider? provider, System.Globalization.DateTimeStyles style, out System.DateOnly result) { throw null; } } - public readonly partial struct DateTime : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.Runtime.Serialization.ISerializable + public readonly partial struct DateTime : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.Runtime.Serialization.ISerializable, System.IUtf8SpanFormattable { private readonly int _dummyPrimitive; public static readonly System.DateTime MaxValue; @@ -1718,6 +1720,7 @@ void System.Runtime.Serialization.ISerializable.GetObjectData(System.Runtime.Ser public string ToString([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("DateTimeFormat")] string? format, System.IFormatProvider? provider) { throw null; } public System.DateTime ToUniversalTime() { throw null; } public bool TryFormat(System.Span destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("DateTimeFormat")] System.ReadOnlySpan format = default(System.ReadOnlySpan), System.IFormatProvider? provider = null) { throw null; } + bool System.IUtf8SpanFormattable.TryFormat(System.Span utf8Destination, out int bytesWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("TimeOnlyFormat")] System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } public static bool TryParse(System.ReadOnlySpan s, out System.DateTime result) { throw null; } public static bool TryParse(System.ReadOnlySpan s, System.IFormatProvider? provider, out System.DateTime result) { throw null; } public static bool TryParse(System.ReadOnlySpan s, System.IFormatProvider? provider, System.Globalization.DateTimeStyles styles, out System.DateTime result) { throw null; } @@ -1735,7 +1738,7 @@ public enum DateTimeKind Utc = 1, Local = 2, } - public readonly partial struct DateTimeOffset : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.Runtime.Serialization.IDeserializationCallback, System.Runtime.Serialization.ISerializable + public readonly partial struct DateTimeOffset : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.Runtime.Serialization.IDeserializationCallback, System.Runtime.Serialization.ISerializable, System.IUtf8SpanFormattable { private readonly int _dummyPrimitive; public static readonly System.DateTimeOffset MaxValue; @@ -1830,6 +1833,7 @@ void System.Runtime.Serialization.ISerializable.GetObjectData(System.Runtime.Ser public long ToUnixTimeMilliseconds() { throw null; } public long ToUnixTimeSeconds() { throw null; } public bool TryFormat(System.Span destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("DateTimeFormat")] System.ReadOnlySpan format = default(System.ReadOnlySpan), System.IFormatProvider? formatProvider = null) { throw null; } + bool System.IUtf8SpanFormattable.TryFormat(System.Span utf8Destination, out int bytesWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("TimeOnlyFormat")] System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } public static bool TryParse(System.ReadOnlySpan input, out System.DateTimeOffset result) { throw null; } public static bool TryParse(System.ReadOnlySpan s, System.IFormatProvider? provider, out System.DateTimeOffset result) { throw null; } public static bool TryParse(System.ReadOnlySpan input, System.IFormatProvider? formatProvider, System.Globalization.DateTimeStyles styles, out System.DateTimeOffset result) { throw null; } @@ -5257,7 +5261,7 @@ public partial class ThreadStaticAttribute : System.Attribute { public ThreadStaticAttribute() { } } - public readonly partial struct TimeOnly : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable + public readonly partial struct TimeOnly : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.IUtf8SpanFormattable { private readonly int _dummyPrimitive; public TimeOnly(int hour, int minute) { throw null; } @@ -5323,6 +5327,7 @@ public ThreadStaticAttribute() { } public string ToString([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("TimeOnlyFormat")] string? format, System.IFormatProvider? provider) { throw null; } public System.TimeSpan ToTimeSpan() { throw null; } public bool TryFormat(System.Span destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("TimeOnlyFormat")] System.ReadOnlySpan format = default(System.ReadOnlySpan), System.IFormatProvider? provider = null) { throw null; } + bool System.IUtf8SpanFormattable.TryFormat(System.Span utf8Destination, out int bytesWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("TimeOnlyFormat")] System.ReadOnlySpan format, System.IFormatProvider? provider) { 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 TryParse(System.ReadOnlySpan s, System.IFormatProvider? provider, out System.TimeOnly result) { throw null; } public static bool TryParse(System.ReadOnlySpan s, out System.TimeOnly result) { throw null; } @@ -5345,7 +5350,7 @@ protected TimeoutException(System.Runtime.Serialization.SerializationInfo info, public TimeoutException(string? message) { } public TimeoutException(string? message, System.Exception? innerException) { } } - public readonly partial struct TimeSpan : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable + public readonly partial struct TimeSpan : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable, System.IUtf8SpanFormattable { private readonly int _dummyPrimitive; public static readonly System.TimeSpan MaxValue; @@ -5426,6 +5431,7 @@ public TimeoutException(string? message, System.Exception? innerException) { } public string ToString([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("TimeSpanFormat")] string? format) { throw null; } public string ToString([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("TimeSpanFormat")] string? format, System.IFormatProvider? formatProvider) { throw null; } public bool TryFormat(System.Span destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("TimeSpanFormat")] System.ReadOnlySpan format = default(System.ReadOnlySpan), System.IFormatProvider? formatProvider = null) { throw null; } + bool System.IUtf8SpanFormattable.TryFormat(System.Span utf8Destination, out int bytesWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("TimeSpanFormat")] System.ReadOnlySpan format, System.IFormatProvider? formatProvider) { throw null; } public static bool TryParse(System.ReadOnlySpan input, System.IFormatProvider? formatProvider, out System.TimeSpan result) { throw null; } public static bool TryParse(System.ReadOnlySpan s, out System.TimeSpan result) { throw null; } public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? input, System.IFormatProvider? formatProvider, out System.TimeSpan result) { throw null; } @@ -14218,7 +14224,7 @@ public enum NormalizationForm [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] FormKD = 6, } - public readonly partial struct Rune : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.ISpanFormattable + public readonly partial struct Rune : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.ISpanFormattable, System.IUtf8SpanFormattable { private readonly int _dummyPrimitive; public Rune(char ch) { throw null; } @@ -14273,6 +14279,7 @@ public enum NormalizationForm int System.IComparable.CompareTo(object? obj) { throw null; } string System.IFormattable.ToString(string? format, System.IFormatProvider? formatProvider) { throw null; } bool System.ISpanFormattable.TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } + bool System.IUtf8SpanFormattable.TryFormat(System.Span utf8Destination, out int bytesWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } public static System.Text.Rune ToLower(System.Text.Rune value, System.Globalization.CultureInfo culture) { throw null; } public static System.Text.Rune ToLowerInvariant(System.Text.Rune value) { throw null; } public override string ToString() { throw null; } diff --git a/src/libraries/System.Runtime/tests/System/DateOnlyTests.cs b/src/libraries/System.Runtime/tests/System/DateOnlyTests.cs index c045aa9fc882a..9a844baac2365 100644 --- a/src/libraries/System.Runtime/tests/System/DateOnlyTests.cs +++ b/src/libraries/System.Runtime/tests/System/DateOnlyTests.cs @@ -516,27 +516,43 @@ public static void AllCulturesTest() [Fact] public static void TryFormatTest() { - Span buffer = stackalloc char[100]; - DateOnly dateOnly = DateOnly.FromDateTime(DateTime.Today); + // UTF16 + { + 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")); + Assert.Throws(() => dateOnly.TryFormat(stackalloc char[100], out charsWritten, "u")); + Assert.Throws(() => dateOnly.TryFormat(stackalloc char[100], out charsWritten, "hh-ss")); + Assert.Throws(() => $"{dateOnly:u}"); + Assert.Throws(() => $"{dateOnly:hh-ss}"); + } - 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")); - Assert.Throws(() => { - Span buff = stackalloc char[100]; - dateOnly.TryFormat(buff, out charsWritten, "u"); - }); - Assert.Throws(() => { - Span buff = stackalloc char[100]; - dateOnly.TryFormat(buff, out charsWritten, "hh-ss"); - }); - Assert.Throws(() => $"{dateOnly:u}"); - Assert.Throws(() => $"{dateOnly:hh-ss}"); + // UTF8 + { + Span buffer = stackalloc byte[100]; + DateOnly dateOnly = DateOnly.FromDateTime(DateTime.Today); + + Assert.True(((IUtf8SpanFormattable)dateOnly).TryFormat(buffer, out int charsWritten, default, null)); + Assert.True(((IUtf8SpanFormattable)dateOnly).TryFormat(buffer, out charsWritten, "o", null)); + Assert.Equal(10, charsWritten); + Assert.True(((IUtf8SpanFormattable)dateOnly).TryFormat(buffer, out charsWritten, "R", null)); + Assert.Equal(16, charsWritten); + Assert.False(((IUtf8SpanFormattable)dateOnly).TryFormat(buffer.Slice(0, 3), out charsWritten, default, null)); + Assert.False(((IUtf8SpanFormattable)dateOnly).TryFormat(buffer.Slice(0, 3), out charsWritten, "r", null)); + Assert.False(((IUtf8SpanFormattable)dateOnly).TryFormat(buffer.Slice(0, 3), out charsWritten, "O", null)); + Assert.Throws(() => ((IUtf8SpanFormattable)dateOnly).TryFormat(stackalloc byte[100], out charsWritten, "u", null)); + Assert.Throws(() => ((IUtf8SpanFormattable)dateOnly).TryFormat(stackalloc byte[100], out charsWritten, "hh-ss", null)); + Assert.Throws(() => $"{dateOnly:u}"); + Assert.Throws(() => $"{dateOnly:hh-ss}"); + } } } } diff --git a/src/libraries/System.Runtime/tests/System/DateTimeOffsetTests.cs b/src/libraries/System.Runtime/tests/System/DateTimeOffsetTests.cs index 4b38a10818a57..aff32a78d2aae 100644 --- a/src/libraries/System.Runtime/tests/System/DateTimeOffsetTests.cs +++ b/src/libraries/System.Runtime/tests/System/DateTimeOffsetTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text; using System.Threading; using Xunit; @@ -1442,26 +1443,53 @@ public static void TotalNumberOfMinutesNowTest() [Fact] public static void TryFormat_ToString_EqualResults() { - DateTimeOffset expected = DateTimeOffset.MaxValue; - string expectedString = expected.ToString(); - - // Just the right amount of space, succeeds - Span actual = new char[expectedString.Length]; - Assert.True(expected.TryFormat(actual, out int charsWritten)); - Assert.Equal(expectedString.Length, charsWritten); - Assert.Equal(expectedString.ToCharArray(), actual.ToArray()); - - // Too little space, fails - actual = new char[expectedString.Length - 1]; - Assert.False(expected.TryFormat(actual, out charsWritten)); - Assert.Equal(0, charsWritten); + // UTF16 + { + DateTimeOffset expected = DateTimeOffset.MaxValue; + string expectedString = expected.ToString(); + + // Just the right amount of space, succeeds + Span actual = new char[expectedString.Length]; + Assert.True(expected.TryFormat(actual, out int charsWritten)); + Assert.Equal(expectedString.Length, charsWritten); + Assert.Equal(expectedString.ToCharArray(), actual.ToArray()); + + // Too little space, fails + actual = new char[expectedString.Length - 1]; + Assert.False(expected.TryFormat(actual, out charsWritten)); + Assert.Equal(0, charsWritten); + + // More than enough space, succeeds + actual = new char[expectedString.Length + 1]; + Assert.True(expected.TryFormat(actual, out charsWritten)); + Assert.Equal(expectedString.Length, charsWritten); + Assert.Equal(expectedString.ToCharArray(), actual.Slice(0, expectedString.Length).ToArray()); + Assert.Equal(0, actual[actual.Length - 1]); + } - // More than enough space, succeeds - actual = new char[expectedString.Length + 1]; - Assert.True(expected.TryFormat(actual, out charsWritten)); - Assert.Equal(expectedString.Length, charsWritten); - Assert.Equal(expectedString.ToCharArray(), actual.Slice(0, expectedString.Length).ToArray()); - Assert.Equal(0, actual[actual.Length - 1]); + // UTF8 + { + DateTimeOffset expected = DateTimeOffset.MaxValue; + string expectedString = expected.ToString(); + + // Just the right amount of space, succeeds + Span actual = new byte[expectedString.Length]; + Assert.True(((IUtf8SpanFormattable)expected).TryFormat(actual, out int charsWritten, default, null)); + Assert.Equal(expectedString.Length, charsWritten); + Assert.Equal(expectedString, Encoding.UTF8.GetString(actual)); + + // Too little space, fails + actual = new byte[expectedString.Length - 1]; + Assert.False(((IUtf8SpanFormattable)expected).TryFormat(actual, out charsWritten, default, null)); + Assert.Equal(0, charsWritten); + + // More than enough space, succeeds + actual = new byte[expectedString.Length + 1]; + Assert.True(((IUtf8SpanFormattable)expected).TryFormat(actual, out charsWritten, default, null)); + Assert.Equal(expectedString.Length, charsWritten); + Assert.Equal(expectedString, Encoding.UTF8.GetString(actual.Slice(0, expectedString.Length))); + Assert.Equal(0, actual[actual.Length - 1]); + } } [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotInvariantGlobalization))] @@ -1469,13 +1497,27 @@ public static void TryFormat_ToString_EqualResults() [ActiveIssue("https://github.com/dotnet/runtime/issues/60562", TestPlatforms.Android | TestPlatforms.LinuxBionic)] public static void TryFormat_MatchesExpected(DateTimeOffset dateTimeOffset, string format, IFormatProvider provider, string expected) { - var destination = new char[expected.Length]; + // UTF16 + { + var destination = new char[expected.Length]; + + Assert.False(dateTimeOffset.TryFormat(destination.AsSpan(0, destination.Length - 1), out _, format, provider)); - Assert.False(dateTimeOffset.TryFormat(destination.AsSpan(0, destination.Length - 1), out _, format, provider)); + Assert.True(dateTimeOffset.TryFormat(destination, out int charsWritten, format, provider)); + Assert.Equal(destination.Length, charsWritten); + Assert.Equal(expected, new string(destination)); + } - Assert.True(dateTimeOffset.TryFormat(destination, out int charsWritten, format, provider)); - Assert.Equal(destination.Length, charsWritten); - Assert.Equal(expected, new string(destination)); + // UTF8 + { + var destination = new byte[expected.Length]; + + Assert.False(((IUtf8SpanFormattable)dateTimeOffset).TryFormat(destination.AsSpan(0, destination.Length - 1), out _, format, provider)); + + Assert.True(((IUtf8SpanFormattable)dateTimeOffset).TryFormat(destination, out int charsWritten, format, provider)); + Assert.Equal(destination.Length, charsWritten); + Assert.Equal(expected, Encoding.UTF8.GetString(destination)); + } } [Fact] diff --git a/src/libraries/System.Runtime/tests/System/DateTimeTests.cs b/src/libraries/System.Runtime/tests/System/DateTimeTests.cs index d83adc35b8dec..469c10a8cb436 100644 --- a/src/libraries/System.Runtime/tests/System/DateTimeTests.cs +++ b/src/libraries/System.Runtime/tests/System/DateTimeTests.cs @@ -5,8 +5,9 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Runtime.Serialization; using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Text; using Xunit; namespace System.Tests @@ -2638,23 +2639,47 @@ public static void TryFormat_MatchesToString(string format) DateTime dt = DateTime.UtcNow; string expected = dt.ToString(format); - // Just the right length, succeeds - Span dest = new char[expected.Length]; - Assert.True(dt.TryFormat(dest, out int charsWritten, format)); - Assert.Equal(expected.Length, charsWritten); - Assert.Equal(expected.ToCharArray(), dest.ToArray()); - - // Too short, fails - dest = new char[expected.Length - 1]; - Assert.False(dt.TryFormat(dest, out charsWritten, format)); - Assert.Equal(0, charsWritten); + // UTF16 + { + // Just the right length, succeeds + Span dest = new char[expected.Length]; + Assert.True(dt.TryFormat(dest, out int charsWritten, format)); + Assert.Equal(expected.Length, charsWritten); + Assert.Equal(expected.ToCharArray(), dest.ToArray()); + + // Too short, fails + dest = new char[expected.Length - 1]; + Assert.False(dt.TryFormat(dest, out charsWritten, format)); + Assert.Equal(0, charsWritten); + + // Longer than needed, succeeds + dest = new char[expected.Length + 1]; + Assert.True(dt.TryFormat(dest, out charsWritten, format)); + Assert.Equal(expected.Length, charsWritten); + Assert.Equal(expected.ToCharArray(), dest.Slice(0, expected.Length).ToArray()); + Assert.Equal(0, dest[dest.Length - 1]); + } - // Longer than needed, succeeds - dest = new char[expected.Length + 1]; - Assert.True(dt.TryFormat(dest, out charsWritten, format)); - Assert.Equal(expected.Length, charsWritten); - Assert.Equal(expected.ToCharArray(), dest.Slice(0, expected.Length).ToArray()); - Assert.Equal(0, dest[dest.Length - 1]); + // UTF8 + { + // Just the right length, succeeds + Span dest = new byte[expected.Length]; + Assert.True(((IUtf8SpanFormattable)dt).TryFormat(dest, out int bytesWritten, format, null)); + Assert.Equal(expected.Length, bytesWritten); + Assert.Equal(expected, Encoding.UTF8.GetString(dest)); + + // Too short, fails + dest = new byte[expected.Length - 1]; + Assert.False(((IUtf8SpanFormattable)dt).TryFormat(dest, out bytesWritten, format, null)); + Assert.Equal(0, bytesWritten); + + // Longer than needed, succeeds + dest = new byte[expected.Length + 1]; + Assert.True(((IUtf8SpanFormattable)dt).TryFormat(dest, out bytesWritten, format, null)); + Assert.Equal(expected.Length, bytesWritten); + Assert.Equal(expected, Encoding.UTF8.GetString(dest.Slice(0, expected.Length))); + Assert.Equal(0, dest[dest.Length - 1]); + } } [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotInvariantGlobalization))] @@ -2662,13 +2687,27 @@ public static void TryFormat_MatchesToString(string format) [ActiveIssue("https://github.com/dotnet/runtime/issues/60562", TestPlatforms.Android | TestPlatforms.LinuxBionic)] public static void TryFormat_MatchesExpected(DateTime dateTime, string format, IFormatProvider provider, string expected) { - var destination = new char[expected.Length]; + // UTF16 + { + var destination = new char[expected.Length]; + + Assert.False(dateTime.TryFormat(destination.AsSpan(0, destination.Length - 1), out _, format, provider)); - Assert.False(dateTime.TryFormat(destination.AsSpan(0, destination.Length - 1), out _, format, provider)); + Assert.True(dateTime.TryFormat(destination, out int charsWritten, format, provider)); + Assert.Equal(destination.Length, charsWritten); + Assert.Equal(expected, new string(destination)); + } - Assert.True(dateTime.TryFormat(destination, out int charsWritten, format, provider)); - Assert.Equal(destination.Length, charsWritten); - Assert.Equal(expected, new string(destination)); + // UTF8 + { + var destination = new byte[expected.Length]; + + Assert.False(((IUtf8SpanFormattable)dateTime).TryFormat(destination.AsSpan(0, destination.Length - 1), out _, format, provider)); + + Assert.True(((IUtf8SpanFormattable)dateTime).TryFormat(destination, out int byteWritten, format, provider)); + Assert.Equal(destination.Length, byteWritten); + Assert.Equal(expected, Encoding.UTF8.GetString(destination)); + } } [Fact] diff --git a/src/libraries/System.Runtime/tests/System/TimeOnlyTests.cs b/src/libraries/System.Runtime/tests/System/TimeOnlyTests.cs index d201a9bbdab4f..cc833cfe5c27d 100644 --- a/src/libraries/System.Runtime/tests/System/TimeOnlyTests.cs +++ b/src/libraries/System.Runtime/tests/System/TimeOnlyTests.cs @@ -562,37 +562,63 @@ public static void AllCulturesTest() [Fact] public static void TryFormatTest() { - Span buffer = stackalloc char[100]; - TimeOnly timeOnly = TimeOnly.FromDateTime(DateTime.Now); + // UTF16 + { + Span buffer = stackalloc char[100]; + TimeOnly timeOnly = TimeOnly.FromDateTime(DateTime.Now); + + buffer.Fill(' '); + Assert.True(timeOnly.TryFormat(buffer, out int charsWritten)); + Assert.Equal(charsWritten, buffer.TrimEnd().Length); + + buffer.Fill(' '); + Assert.True(timeOnly.TryFormat(buffer, out charsWritten, "o")); + Assert.Equal(16, charsWritten); + Assert.Equal(16, buffer.TrimEnd().Length); + + buffer.Fill(' '); + Assert.True(timeOnly.TryFormat(buffer, out charsWritten, "R")); + Assert.Equal(8, charsWritten); + Assert.Equal(8, buffer.TrimEnd().Length); + + 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")); + + Assert.Throws(() => timeOnly.TryFormat(stackalloc char[100], out charsWritten, "u")); + Assert.Throws(() => timeOnly.TryFormat(stackalloc char[100], out charsWritten, "dd-yyyy")); + Assert.Throws(() => $"{timeOnly:u}"); + Assert.Throws(() => $"{timeOnly:dd-yyyy}"); + } - buffer.Fill(' '); - Assert.True(timeOnly.TryFormat(buffer, out int charsWritten)); - Assert.Equal(charsWritten, buffer.TrimEnd().Length); - - buffer.Fill(' '); - Assert.True(timeOnly.TryFormat(buffer, out charsWritten, "o")); - Assert.Equal(16, charsWritten); - Assert.Equal(16, buffer.TrimEnd().Length); - - buffer.Fill(' '); - Assert.True(timeOnly.TryFormat(buffer, out charsWritten, "R")); - Assert.Equal(8, charsWritten); - Assert.Equal(8, buffer.TrimEnd().Length); - - 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")); - - Assert.Throws(() => { - Span buff = stackalloc char[100]; - timeOnly.TryFormat(buff, out charsWritten, "u"); - }); - Assert.Throws(() => { - Span buff = stackalloc char[100]; - timeOnly.TryFormat(buff, out charsWritten, "dd-yyyy"); - }); - Assert.Throws(() => $"{timeOnly:u}"); - Assert.Throws(() => $"{timeOnly:dd-yyyy}"); + // UTF8 + { + Span buffer = stackalloc byte[100]; + TimeOnly timeOnly = TimeOnly.FromDateTime(DateTime.Now); + + buffer.Fill((byte)' '); + Assert.True(((IUtf8SpanFormattable)timeOnly).TryFormat(buffer, out int charsWritten, default, null)); + Assert.Equal(charsWritten, buffer.TrimEnd(" "u8).Length); + + buffer.Fill((byte)' '); + Assert.True(((IUtf8SpanFormattable)timeOnly).TryFormat(buffer, out charsWritten, "o", null)); + Assert.Equal(16, charsWritten); + Assert.Equal(16, buffer.TrimEnd(" "u8).Length); + + buffer.Fill((byte)' '); + Assert.True(((IUtf8SpanFormattable)timeOnly).TryFormat(buffer, out charsWritten, "R", null)); + Assert.Equal(8, charsWritten); + Assert.Equal(8, buffer.TrimEnd(" "u8).Length); + + Assert.False(((IUtf8SpanFormattable)timeOnly).TryFormat(buffer.Slice(0, 3), out charsWritten, default, null)); + Assert.False(((IUtf8SpanFormattable)timeOnly).TryFormat(buffer.Slice(0, 3), out charsWritten, "r", null)); + Assert.False(((IUtf8SpanFormattable)timeOnly).TryFormat(buffer.Slice(0, 3), out charsWritten, "O", null)); + + Assert.Throws(() => ((IUtf8SpanFormattable)timeOnly).TryFormat(new byte[100], out charsWritten, "u", null)); + Assert.Throws(() => ((IUtf8SpanFormattable)timeOnly).TryFormat(new byte[100], out charsWritten, "dd-yyyy", null)); + Assert.Throws(() => $"{((IUtf8SpanFormattable)timeOnly):u}"); + Assert.Throws(() => $"{((IUtf8SpanFormattable)timeOnly):dd-yyyy}"); + } } } } diff --git a/src/libraries/System.Runtime/tests/System/TimeSpanTests.cs b/src/libraries/System.Runtime/tests/System/TimeSpanTests.cs index 494eddaadd747..6bcd82c3e3128 100644 --- a/src/libraries/System.Runtime/tests/System/TimeSpanTests.cs +++ b/src/libraries/System.Runtime/tests/System/TimeSpanTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Text; using Xunit; namespace System.Tests @@ -1422,31 +1423,55 @@ public static void NamedNaNDivision() [MemberData(nameof(ToString_TestData))] public static void TryFormat_Valid(TimeSpan input, string format, CultureInfo info, string expected) { - int charsWritten; - Span dst; - - dst = new char[expected.Length - 1]; - Assert.False(input.TryFormat(dst, out charsWritten, format, info)); - Assert.Equal(0, charsWritten); - - dst = new char[expected.Length]; - Assert.True(input.TryFormat(dst, out charsWritten, format, info)); - Assert.Equal(expected.Length, charsWritten); - Assert.Equal(expected, new string(dst)); + // UTF16 + { + int charsWritten; + Span dst; + + dst = new char[expected.Length - 1]; + Assert.False(input.TryFormat(dst, out charsWritten, format, info)); + Assert.Equal(0, charsWritten); + + dst = new char[expected.Length]; + Assert.True(input.TryFormat(dst, out charsWritten, format, info)); + Assert.Equal(expected.Length, charsWritten); + Assert.Equal(expected, new string(dst)); + + dst = new char[expected.Length + 1]; + Assert.True(input.TryFormat(dst, out charsWritten, format, info)); + Assert.Equal(expected.Length, charsWritten); + Assert.Equal(expected, new string(dst.Slice(0, dst.Length - 1))); + Assert.Equal(0, dst[dst.Length - 1]); + } - dst = new char[expected.Length + 1]; - Assert.True(input.TryFormat(dst, out charsWritten, format, info)); - Assert.Equal(expected.Length, charsWritten); - Assert.Equal(expected, new string(dst.Slice(0, dst.Length - 1))); - Assert.Equal(0, dst[dst.Length - 1]); + // UTF8 + { + int bytesWritten; + Span dst; + + dst = new byte[expected.Length - 1]; + Assert.False(((IUtf8SpanFormattable)input).TryFormat(dst, out bytesWritten, format, info)); + Assert.Equal(0, bytesWritten); + + dst = new byte[expected.Length]; + Assert.True(((IUtf8SpanFormattable)input).TryFormat(dst, out bytesWritten, format, info)); + Assert.Equal(expected.Length, bytesWritten); + Assert.Equal(expected, Encoding.UTF8.GetString(dst)); + + dst = new byte[expected.Length + 1]; + Assert.True(((IUtf8SpanFormattable)input).TryFormat(dst, out bytesWritten, format, info)); + Assert.Equal(expected.Length, bytesWritten); + Assert.Equal(expected, Encoding.UTF8.GetString(dst.Slice(0, dst.Length - 1))); + Assert.Equal(0, dst[dst.Length - 1]); + } } [Theory] [MemberData(nameof(ToString_InvalidFormat_TestData))] public void TryFormat_InvalidFormat_ThrowsFormatException(string invalidFormat) { - char[] dst = new char[1]; - Assert.Throws(() => new TimeSpan().TryFormat(dst.AsSpan(), out int charsWritten, invalidFormat, null)); + Assert.Throws(() => new TimeSpan().TryFormat(new char[1], out int charsWritten, invalidFormat, null)); + Assert.Throws(() => ((IUtf8SpanFormattable)new TimeSpan()).TryFormat(new byte[1], out int bytesWritten, invalidFormat, null)); } [Fact]