diff options
Diffstat (limited to 'src/System.Private.CoreLib/shared/System/Globalization/TimeSpanFormat.cs')
-rw-r--r-- | src/System.Private.CoreLib/shared/System/Globalization/TimeSpanFormat.cs | 351 |
1 files changed, 247 insertions, 104 deletions
diff --git a/src/System.Private.CoreLib/shared/System/Globalization/TimeSpanFormat.cs b/src/System.Private.CoreLib/shared/System/Globalization/TimeSpanFormat.cs index a66e4600a..5e553d56b 100644 --- a/src/System.Private.CoreLib/shared/System/Globalization/TimeSpanFormat.cs +++ b/src/System.Private.CoreLib/shared/System/Globalization/TimeSpanFormat.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Text; +using System.Buffers.Text; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text; namespace System.Globalization { @@ -35,159 +37,300 @@ namespace System.Globalization internal static readonly FormatLiterals PositiveInvariantFormatLiterals = TimeSpanFormat.FormatLiterals.InitInvariant(isNegative: false); internal static readonly FormatLiterals NegativeInvariantFormatLiterals = TimeSpanFormat.FormatLiterals.InitInvariant(isNegative: true); - internal enum Pattern - { - None = 0, - Minimum = 1, - Full = 2, - } /// <summary>Main method called from TimeSpan.ToString.</summary> - internal static string Format(TimeSpan value, string format, IFormatProvider formatProvider) => - StringBuilderCache.GetStringAndRelease(FormatToBuilder(value, format, formatProvider)); - - /// <summary>Main method called from TimeSpan.TryFormat.</summary> - internal static bool TryFormat(TimeSpan value, Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider formatProvider) + internal static string Format(TimeSpan value, string format, IFormatProvider formatProvider) { - StringBuilder sb = FormatToBuilder(value, format, formatProvider); - if (sb.Length <= destination.Length) + if (string.IsNullOrEmpty(format)) { - charsWritten = sb.Length; - sb.CopyTo(0, destination, sb.Length); - StringBuilderCache.Release(sb); - return true; + return FormatC(value); // formatProvider ignored, as "c" is invariant } - else + + if (format.Length == 1) { - StringBuilderCache.Release(sb); - charsWritten = 0; - return false; + char c = format[0]; + + if (c == 'c' || (c | 0x20) == 't') // special-case to optimize the default TimeSpan format + { + return FormatC(value); // formatProvider ignored, as "c" is invariant + } + + if ((c | 0x20) == 'g') // special-case to optimize the remaining 'g'/'G' standard formats + { + return FormatG(value, DateTimeFormatInfo.GetInstance(formatProvider), c == 'G' ? StandardFormat.G : StandardFormat.g); + } + + throw new FormatException(SR.Format_InvalidString); } + + return StringBuilderCache.GetStringAndRelease(FormatCustomized(value, format, DateTimeFormatInfo.GetInstance(formatProvider), result: null)); } - private static StringBuilder FormatToBuilder(TimeSpan value, ReadOnlySpan<char> format, IFormatProvider formatProvider) + /// <summary>Main method called from TimeSpan.TryFormat.</summary> + internal static bool TryFormat(TimeSpan value, Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider formatProvider) { if (format.Length == 0) { - format = "c"; + return TryFormatStandard(value, StandardFormat.C, null, destination, out charsWritten); } - // Standard formats if (format.Length == 1) { - char f = format[0]; - switch (f) + char c = format[0]; + if (c == 'c' || ((c | 0x20) == 't')) { - case 'c': - case 't': - case 'T': - return FormatStandard( - value, - isInvariant: true, - format: format, - pattern: Pattern.Minimum); - - case 'g': - case 'G': - DateTimeFormatInfo dtfi = DateTimeFormatInfo.GetInstance(formatProvider); - return FormatStandard( - value, - isInvariant: false, - format: value.Ticks < 0 ? dtfi.FullTimeSpanNegativePattern : dtfi.FullTimeSpanPositivePattern, - pattern: f == 'g' ? Pattern.Minimum : Pattern.Full); - - default: + return TryFormatStandard(value, StandardFormat.C, null, destination, out charsWritten); + } + else + { + StandardFormat sf = + c == 'g' ? StandardFormat.g : + c == 'G' ? StandardFormat.G : throw new FormatException(SR.Format_InvalidString); + return TryFormatStandard(value, sf, DateTimeFormatInfo.GetInstance(formatProvider).DecimalSeparator, destination, out charsWritten); } } - // Custom formats - return FormatCustomized(value, format, DateTimeFormatInfo.GetInstance(formatProvider), result: null); + StringBuilder sb = FormatCustomized(value, format, DateTimeFormatInfo.GetInstance(formatProvider), result: null); + + if (sb.Length <= destination.Length) + { + sb.CopyTo(0, destination, sb.Length); + charsWritten = sb.Length; + StringBuilderCache.Release(sb); + return true; + } + + charsWritten = 0; + StringBuilderCache.Release(sb); + return false; } - /// <summary>Format the TimeSpan instance using the specified format.</summary> - private static StringBuilder FormatStandard(TimeSpan value, bool isInvariant, ReadOnlySpan<char> format, Pattern pattern) + internal static string FormatC(TimeSpan value) { - StringBuilder sb = StringBuilderCache.Acquire(InternalGlobalizationHelper.StringBuilderDefaultCapacity); - int day = (int)(value.Ticks / TimeSpan.TicksPerDay); - long time = value.Ticks % TimeSpan.TicksPerDay; + Span<char> destination = stackalloc char[26]; // large enough for any "c" TimeSpan + TryFormatStandard(value, StandardFormat.C, null, destination, out int charsWritten); + return new string(destination.Slice(0, charsWritten)); + } - if (value.Ticks < 0) + private static string FormatG(TimeSpan value, DateTimeFormatInfo dtfi, StandardFormat format) + { + string decimalSeparator = dtfi.DecimalSeparator; + int maxLength = 25 + decimalSeparator.Length; // large enough for any "g"/"G" TimeSpan + Span<char> destination = maxLength < 128 ? + stackalloc char[maxLength] : + new char[maxLength]; // the chances of needing this case are almost 0, as DecimalSeparator.Length will basically always == 1 + TryFormatStandard(value, format, decimalSeparator, destination, out int charsWritten); + return new string(destination.Slice(0, charsWritten)); + } + + private enum StandardFormat { C, G, g } + + private static bool TryFormatStandard(TimeSpan value, StandardFormat format, string decimalSeparator, Span<char> destination, out int charsWritten) + { + Debug.Assert(format == StandardFormat.C || format == StandardFormat.G || format == StandardFormat.g); + + // 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; { - day = -day; - time = -time; + // Turn this into a non-negative TimeSpan if possible. + long ticks = value.Ticks; + if (ticks < 0) + { + requiredOutputLength = 9; // requiredOutputLength + 1 for the leading '-' sign + 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; + } + } + + totalSecondsRemaining = Math.DivRem((ulong)ticks, TimeSpan.TicksPerSecond, out ulong fraction64); + fraction = (uint)fraction64; } - int hours = (int)(time / TimeSpan.TicksPerHour % 24); - int minutes = (int)(time / TimeSpan.TicksPerMinute % 60); - int seconds = (int)(time / TimeSpan.TicksPerSecond % 60); - int fraction = (int)(time % TimeSpan.TicksPerSecond); - FormatLiterals literal; - if (isInvariant) + AfterComputeFraction: + // Only write out the fraction if it's non-zero, and in that + // case write out the entire fraction (all digits). + Debug.Assert(fraction < 10_000_000); + int fractionDigits = 0; + switch (format) { - literal = value.Ticks < 0 ? - NegativeInvariantFormatLiterals : - PositiveInvariantFormatLiterals; + case StandardFormat.C: + // "c": Write out a fraction only if it's non-zero, and write out all 7 digits of it. + if (fraction != 0) + { + fractionDigits = DateTimeFormat.MaxSecondsFractionDigits; + requiredOutputLength += fractionDigits + 1; // digits plus leading decimal separator + } + break; + + case StandardFormat.G: + // "G": Write out a fraction regardless of whether it's 0, and write out all 7 digits of it. + fractionDigits = DateTimeFormat.MaxSecondsFractionDigits; + requiredOutputLength += fractionDigits + 1; // digits plus leading decimal separator + 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); + if (fraction != 0) + { + fractionDigits = DateTimeFormat.MaxSecondsFractionDigits - FormattingHelpers.CountDecimalTrailingZeros(fraction, out fraction); + requiredOutputLength += fractionDigits + 1; // digits plus leading decimal separator + } + break; } - else + + ulong totalMinutesRemaining = 0, seconds = 0; + if (totalSecondsRemaining > 0) { - literal = new FormatLiterals(); - literal.Init(format, pattern == Pattern.Full); + // Only compute minutes if the TimeSpan has an absolute value of >= 1 minute. + totalMinutesRemaining = Math.DivRem(totalSecondsRemaining, 60 /* seconds per minute */, out seconds); + Debug.Assert(seconds < 60); } - if (fraction != 0) + ulong totalHoursRemaining = 0, minutes = 0; + if (totalMinutesRemaining > 0) { - // truncate the partial second to the specified length - fraction = (int)(fraction / TimeSpanParse.Pow10(DateTimeFormat.MaxSecondsFractionDigits - literal.ff)); + // Only compute hours if the TimeSpan has an absolute value of >= 1 hour. + totalHoursRemaining = Math.DivRem(totalMinutesRemaining, 60 /* minutes per hour */, out minutes); + Debug.Assert(minutes < 60); } - // Pattern.Full: [-]dd.hh:mm:ss.fffffff - // Pattern.Minimum: [-][d.]hh:mm:ss[.fffffff] + // At this point, we can switch over to 32-bit DivRem since the data has shrunk far enough. + Debug.Assert(totalHoursRemaining <= uint.MaxValue); - sb.Append(literal.Start); // [-] - if (pattern == Pattern.Full || day != 0) + uint days = 0, hours = 0; + if (totalHoursRemaining > 0) { - sb.Append(day); // [dd] - sb.Append(literal.DayHourSep); // [.] - } // - AppendNonNegativeInt32(sb, hours, literal.hh); // hh - sb.Append(literal.HourMinuteSep); // : - AppendNonNegativeInt32(sb, minutes, literal.mm); // mm - sb.Append(literal.MinuteSecondSep); // : - AppendNonNegativeInt32(sb, seconds, literal.ss); // ss - if (!isInvariant && pattern == Pattern.Minimum) + // Only compute days if the TimeSpan has an absolute value of >= 1 day. + days = Math.DivRem((uint)totalHoursRemaining, 24 /* hours per day */, out hours); + Debug.Assert(hours < 24); + } + + int hourDigits = 2; + if (format == StandardFormat.g && hours < 10) + { + // "g": Only writing a one-digit hour, rather than expected two-digit hour + hourDigits = 1; + requiredOutputLength--; + } + + int dayDigits = 0; + if (days > 0) + { + dayDigits = FormattingHelpers.CountDigits(days); + Debug.Assert(dayDigits <= 8); + requiredOutputLength += dayDigits + 1; // for the leading "d." + } + else if (format == StandardFormat.G) + { + // "G": has a leading "0:" if days is 0 + requiredOutputLength += 2; + dayDigits = 1; + } + + if (destination.Length < requiredOutputLength) { - int effectiveDigits = literal.ff; - while (effectiveDigits > 0) + charsWritten = 0; + return false; + } + + // Write leading '-' if necessary + int idx = 0; + if (value.Ticks < 0) + { + destination[idx++] = '-'; + } + + // Write day and separator, if necessary + if (dayDigits != 0) + { + WriteDigits(days, destination.Slice(idx, dayDigits)); + idx += dayDigits; + destination[idx++] = format == StandardFormat.C ? '.' : ':'; + } + + // Write "[h]h:mm:ss + Debug.Assert(hourDigits == 1 || hourDigits == 2); + if (hourDigits == 2) + { + WriteTwoDigits(hours, destination.Slice(idx)); + idx += 2; + } + else + { + destination[idx++] = (char)('0' + hours); + } + destination[idx++] = ':'; + WriteTwoDigits((uint)minutes, destination.Slice(idx)); + idx += 2; + destination[idx++] = ':'; + WriteTwoDigits((uint)seconds, destination.Slice(idx)); + idx += 2; + + // Write fraction and separator, if necessary + if (fractionDigits != 0) + { + if (format == StandardFormat.C) { - if (fraction % 10 == 0) - { - fraction = fraction / 10; - effectiveDigits--; - } - else - { - break; - } + destination[idx++] = '.'; } - if (effectiveDigits > 0) + else if (decimalSeparator.Length == 1) { - sb.Append(literal.SecondFractionSep); // [.FFFFFFF] - sb.Append((fraction).ToString(DateTimeFormat.fixedNumberFormats[effectiveDigits - 1], CultureInfo.InvariantCulture)); + destination[idx++] = decimalSeparator[0]; } + else + { + decimalSeparator.AsSpan().CopyTo(destination); + idx += decimalSeparator.Length; + } + WriteDigits(fraction, destination.Slice(idx, fractionDigits)); + idx += fractionDigits; } - else if (pattern == Pattern.Full || fraction != 0) + + Debug.Assert(idx == requiredOutputLength); + charsWritten = requiredOutputLength; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteTwoDigits(uint value, Span<char> 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<char> buffer) + { + Debug.Assert(buffer.Length > 0); + + for (int i = buffer.Length - 1; i >= 1; i--) { - sb.Append(literal.SecondFractionSep); // [.] - AppendNonNegativeInt32(sb, fraction, literal.ff); // [fffffff] + uint temp = '0' + value; + value /= 10; + buffer[i] = (char)(temp - (value * 10)); } - sb.Append(literal.End); - return sb; + Debug.Assert(value < 10); + buffer[0] = (char)('0' + value); } /// <summary>Format the TimeSpan instance using the specified format.</summary> - private static StringBuilder FormatCustomized(TimeSpan value, ReadOnlySpan<char> format, DateTimeFormatInfo dtfi, StringBuilder result) + private static StringBuilder FormatCustomized(TimeSpan value, ReadOnlySpan<char> format, DateTimeFormatInfo dtfi, StringBuilder result = null) { Debug.Assert(dtfi != null); |