summaryrefslogtreecommitdiff
path: root/src/mscorlib/shared/System/Globalization/TimeSpanFormat.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/mscorlib/shared/System/Globalization/TimeSpanFormat.cs')
-rw-r--r--src/mscorlib/shared/System/Globalization/TimeSpanFormat.cs523
1 files changed, 523 insertions, 0 deletions
diff --git a/src/mscorlib/shared/System/Globalization/TimeSpanFormat.cs b/src/mscorlib/shared/System/Globalization/TimeSpanFormat.cs
new file mode 100644
index 0000000000..6801ea8e1b
--- /dev/null
+++ b/src/mscorlib/shared/System/Globalization/TimeSpanFormat.cs
@@ -0,0 +1,523 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// 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.Diagnostics;
+
+namespace System.Globalization
+{
+ internal static class TimeSpanFormat
+ {
+ private static unsafe void AppendNonNegativeInt32(StringBuilder sb, int n, int digits)
+ {
+ Debug.Assert(n >= 0);
+ uint value = (uint)n;
+
+ const int MaxUInt32Digits = 10;
+ char* buffer = stackalloc char[MaxUInt32Digits];
+
+ int index = 0;
+ do
+ {
+ uint div = value / 10;
+ buffer[index++] = (char)(value - (div * 10) + '0');
+ value = div;
+ }
+ while (value != 0);
+ Debug.Assert(index <= MaxUInt32Digits);
+
+ for (int i = digits - index; i > 0; --i) sb.Append('0');
+ for (int i = index - 1; i >= 0; --i) sb.Append(buffer[i]);
+ }
+
+ 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, string format, IFormatProvider formatProvider)
+ {
+ StringBuilder sb = FormatToBuilder(value, format, formatProvider);
+ if (sb.Length <= destination.Length)
+ {
+ charsWritten = sb.Length;
+ sb.CopyTo(0, destination, sb.Length);
+ StringBuilderCache.Release(sb);
+ return true;
+ }
+ else
+ {
+ StringBuilderCache.Release(sb);
+ charsWritten = 0;
+ return false;
+ }
+ }
+
+ private static StringBuilder FormatToBuilder(TimeSpan value, string format, IFormatProvider formatProvider)
+ {
+ if (format == null || format.Length == 0)
+ {
+ format = "c";
+ }
+
+ // Standard formats
+ if (format.Length == 1)
+ {
+ char f = format[0];
+ switch (f)
+ {
+ 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:
+ throw new FormatException(SR.Format_InvalidString);
+ }
+ }
+
+ // Custom formats
+ return FormatCustomized(value, format, DateTimeFormatInfo.GetInstance(formatProvider));
+ }
+
+ /// <summary>Format the TimeSpan instance using the specified format.</summary>
+ private static StringBuilder FormatStandard(TimeSpan value, bool isInvariant, string format, Pattern pattern)
+ {
+ StringBuilder sb = StringBuilderCache.Acquire(InternalGlobalizationHelper.StringBuilderDefaultCapacity);
+ int day = (int)(value.Ticks / TimeSpan.TicksPerDay);
+ long time = value.Ticks % TimeSpan.TicksPerDay;
+
+ if (value.Ticks < 0)
+ {
+ day = -day;
+ time = -time;
+ }
+ 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)
+ {
+ literal = value.Ticks < 0 ?
+ NegativeInvariantFormatLiterals :
+ PositiveInvariantFormatLiterals;
+ }
+ else
+ {
+ literal = new FormatLiterals();
+ literal.Init(format, pattern == Pattern.Full);
+ }
+
+ if (fraction != 0)
+ {
+ // truncate the partial second to the specified length
+ fraction = (int)(fraction / TimeSpanParse.Pow10(DateTimeFormat.MaxSecondsFractionDigits - literal.ff));
+ }
+
+ // Pattern.Full: [-]dd.hh:mm:ss.fffffff
+ // Pattern.Minimum: [-][d.]hh:mm:ss[.fffffff]
+
+ sb.Append(literal.Start); // [-]
+ if (pattern == Pattern.Full || day != 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)
+ {
+ int effectiveDigits = literal.ff;
+ while (effectiveDigits > 0)
+ {
+ if (fraction % 10 == 0)
+ {
+ fraction = fraction / 10;
+ effectiveDigits--;
+ }
+ else
+ {
+ break;
+ }
+ }
+ if (effectiveDigits > 0)
+ {
+ sb.Append(literal.SecondFractionSep); // [.FFFFFFF]
+ sb.Append((fraction).ToString(DateTimeFormat.fixedNumberFormats[effectiveDigits - 1], CultureInfo.InvariantCulture));
+ }
+ }
+ else if (pattern == Pattern.Full || fraction != 0)
+ {
+ sb.Append(literal.SecondFractionSep); // [.]
+ AppendNonNegativeInt32(sb, fraction, literal.ff); // [fffffff]
+ }
+ sb.Append(literal.End);
+
+ return sb;
+ }
+
+ /// <summary>Format the TimeSpan instance using the specified format.</summary>
+ private static StringBuilder FormatCustomized(TimeSpan value, string format, DateTimeFormatInfo dtfi)
+ {
+ Debug.Assert(dtfi != null);
+
+ int day = (int)(value.Ticks / TimeSpan.TicksPerDay);
+ long time = value.Ticks % TimeSpan.TicksPerDay;
+
+ if (value.Ticks < 0)
+ {
+ day = -day;
+ time = -time;
+ }
+ 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);
+
+ long tmp = 0;
+ int i = 0;
+ int tokenLen;
+ StringBuilder result = StringBuilderCache.Acquire(InternalGlobalizationHelper.StringBuilderDefaultCapacity);
+
+ while (i < format.Length)
+ {
+ char ch = format[i];
+ int nextChar;
+ switch (ch)
+ {
+ case 'h':
+ tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
+ if (tokenLen > 2)
+ {
+ throw new FormatException(SR.Format_InvalidString);
+ }
+ DateTimeFormat.FormatDigits(result, hours, tokenLen);
+ break;
+ case 'm':
+ tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
+ if (tokenLen > 2)
+ {
+ throw new FormatException(SR.Format_InvalidString);
+ }
+ DateTimeFormat.FormatDigits(result, minutes, tokenLen);
+ break;
+ case 's':
+ tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
+ if (tokenLen > 2)
+ {
+ throw new FormatException(SR.Format_InvalidString);
+ }
+ DateTimeFormat.FormatDigits(result, seconds, tokenLen);
+ break;
+ case 'f':
+ //
+ // The fraction of a second in single-digit precision. The remaining digits are truncated.
+ //
+ tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
+ if (tokenLen > DateTimeFormat.MaxSecondsFractionDigits)
+ {
+ throw new FormatException(SR.Format_InvalidString);
+ }
+
+ tmp = fraction;
+ tmp /= TimeSpanParse.Pow10(DateTimeFormat.MaxSecondsFractionDigits - tokenLen);
+ result.Append((tmp).ToString(DateTimeFormat.fixedNumberFormats[tokenLen - 1], CultureInfo.InvariantCulture));
+ break;
+ case 'F':
+ //
+ // Displays the most significant digit of the seconds fraction. Nothing is displayed if the digit is zero.
+ //
+ tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
+ if (tokenLen > DateTimeFormat.MaxSecondsFractionDigits)
+ {
+ throw new FormatException(SR.Format_InvalidString);
+ }
+
+ tmp = fraction;
+ tmp /= TimeSpanParse.Pow10(DateTimeFormat.MaxSecondsFractionDigits - tokenLen);
+ int effectiveDigits = tokenLen;
+ while (effectiveDigits > 0)
+ {
+ if (tmp % 10 == 0)
+ {
+ tmp = tmp / 10;
+ effectiveDigits--;
+ }
+ else
+ {
+ break;
+ }
+ }
+ if (effectiveDigits > 0)
+ {
+ result.Append((tmp).ToString(DateTimeFormat.fixedNumberFormats[effectiveDigits - 1], CultureInfo.InvariantCulture));
+ }
+ break;
+ case 'd':
+ //
+ // tokenLen == 1 : Day as digits with no leading zero.
+ // tokenLen == 2+: Day as digits with leading zero for single-digit days.
+ //
+ tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
+ if (tokenLen > 8)
+ {
+ throw new FormatException(SR.Format_InvalidString);
+ }
+
+ DateTimeFormat.FormatDigits(result, day, tokenLen, true);
+ break;
+ case '\'':
+ case '\"':
+ tokenLen = DateTimeFormat.ParseQuoteString(format, i, result);
+ break;
+ case '%':
+ // Optional format character.
+ // For example, format string "%d" will print day
+ // Most of the cases, "%" can be ignored.
+ nextChar = DateTimeFormat.ParseNextChar(format, i);
+ // nextChar will be -1 if we already reach the end of the format string.
+ // Besides, we will not allow "%%" appear in the pattern.
+ if (nextChar >= 0 && nextChar != (int)'%')
+ {
+ result.Append(TimeSpanFormat.FormatCustomized(value, ((char)nextChar).ToString(), dtfi));
+ tokenLen = 2;
+ }
+ else
+ {
+ //
+ // This means that '%' is at the end of the format string or
+ // "%%" appears in the format string.
+ //
+ throw new FormatException(SR.Format_InvalidString);
+ }
+ break;
+ case '\\':
+ // Escaped character. Can be used to insert character into the format string.
+ // For example, "\d" will insert the character 'd' into the string.
+ //
+ nextChar = DateTimeFormat.ParseNextChar(format, i);
+ if (nextChar >= 0)
+ {
+ result.Append(((char)nextChar));
+ tokenLen = 2;
+ }
+ else
+ {
+ //
+ // This means that '\' is at the end of the formatting string.
+ //
+ throw new FormatException(SR.Format_InvalidString);
+ }
+ break;
+ default:
+ throw new FormatException(SR.Format_InvalidString);
+ }
+ i += tokenLen;
+ }
+ return result;
+ }
+
+ internal struct FormatLiterals
+ {
+ internal string AppCompatLiteral;
+ internal int dd;
+ internal int hh;
+ internal int mm;
+ internal int ss;
+ internal int ff;
+
+ private string[] _literals;
+
+ internal string Start => _literals[0];
+ internal string DayHourSep => _literals[1];
+ internal string HourMinuteSep => _literals[2];
+ internal string MinuteSecondSep => _literals[3];
+ internal string SecondFractionSep => _literals[4];
+ internal string End => _literals[5];
+
+ /* factory method for static invariant FormatLiterals */
+ internal static FormatLiterals InitInvariant(bool isNegative)
+ {
+ FormatLiterals x = new FormatLiterals();
+ x._literals = new string[6];
+ x._literals[0] = isNegative ? "-" : string.Empty;
+ x._literals[1] = ".";
+ x._literals[2] = ":";
+ x._literals[3] = ":";
+ x._literals[4] = ".";
+ x._literals[5] = string.Empty;
+ x.AppCompatLiteral = ":."; // MinuteSecondSep+SecondFractionSep;
+ x.dd = 2;
+ x.hh = 2;
+ x.mm = 2;
+ x.ss = 2;
+ x.ff = DateTimeFormat.MaxSecondsFractionDigits;
+ return x;
+ }
+
+ // For the "v1" TimeSpan localized patterns, the data is simply literal field separators with
+ // the constants guaranteed to include DHMSF ordered greatest to least significant.
+ // Once the data becomes more complex than this we will need to write a proper tokenizer for
+ // parsing and formatting
+ internal void Init(string format, bool useInvariantFieldLengths)
+ {
+ dd = hh = mm = ss = ff = 0;
+ _literals = new string[6];
+ for (int i = 0; i < _literals.Length; i++)
+ {
+ _literals[i] = string.Empty;
+ }
+
+ StringBuilder sb = StringBuilderCache.Acquire(InternalGlobalizationHelper.StringBuilderDefaultCapacity);
+ bool inQuote = false;
+ char quote = '\'';
+ int field = 0;
+
+ for (int i = 0; i < format.Length; i++)
+ {
+ switch (format[i])
+ {
+ case '\'':
+ case '\"':
+ if (inQuote && (quote == format[i]))
+ {
+ /* we were in a quote and found a matching exit quote, so we are outside a quote now */
+ if (field >= 0 && field <= 5)
+ {
+ _literals[field] = sb.ToString();
+ sb.Length = 0;
+ inQuote = false;
+ }
+ else
+ {
+ Debug.Fail($"Unexpected field value: {field}");
+ return; // how did we get here?
+ }
+ }
+ else if (!inQuote)
+ {
+ /* we are at the start of a new quote block */
+ quote = format[i];
+ inQuote = true;
+ }
+ else
+ {
+ /* we were in a quote and saw the other type of quote character, so we are still in a quote */
+ }
+ break;
+ case '%':
+ Debug.Fail("Unexpected special token '%', Bug in DateTimeFormatInfo.FullTimeSpan[Positive|Negative]Pattern");
+ goto default;
+ case '\\':
+ if (!inQuote)
+ {
+ i++; /* skip next character that is escaped by this backslash or percent sign */
+ break;
+ }
+ goto default;
+ case 'd':
+ if (!inQuote)
+ {
+ Debug.Assert((field == 0 && sb.Length == 0) || field == 1, "field == 0 || field == 1, Bug in DateTimeFormatInfo.FullTimeSpan[Positive|Negative]Pattern");
+ field = 1; // DayHourSep
+ dd++;
+ }
+ break;
+ case 'h':
+ if (!inQuote)
+ {
+ Debug.Assert((field == 1 && sb.Length == 0) || field == 2, "field == 1 || field == 2, Bug in DateTimeFormatInfo.FullTimeSpan[Positive|Negative]Pattern");
+ field = 2; // HourMinuteSep
+ hh++;
+ }
+ break;
+ case 'm':
+ if (!inQuote)
+ {
+ Debug.Assert((field == 2 && sb.Length == 0) || field == 3, "field == 2 || field == 3, Bug in DateTimeFormatInfo.FullTimeSpan[Positive|Negative]Pattern");
+ field = 3; // MinuteSecondSep
+ mm++;
+ }
+ break;
+ case 's':
+ if (!inQuote)
+ {
+ Debug.Assert((field == 3 && sb.Length == 0) || field == 4, "field == 3 || field == 4, Bug in DateTimeFormatInfo.FullTimeSpan[Positive|Negative]Pattern");
+ field = 4; // SecondFractionSep
+ ss++;
+ }
+ break;
+ case 'f':
+ case 'F':
+ if (!inQuote)
+ {
+ Debug.Assert((field == 4 && sb.Length == 0) || field == 5, "field == 4 || field == 5, Bug in DateTimeFormatInfo.FullTimeSpan[Positive|Negative]Pattern");
+ field = 5; // End
+ ff++;
+ }
+ break;
+ default:
+ sb.Append(format[i]);
+ break;
+ }
+ }
+
+ Debug.Assert(field == 5);
+ AppCompatLiteral = MinuteSecondSep + SecondFractionSep;
+
+ Debug.Assert(0 < dd && dd < 3, "0 < dd && dd < 3, Bug in System.Globalization.DateTimeFormatInfo.FullTimeSpan[Positive|Negative]Pattern");
+ Debug.Assert(0 < hh && hh < 3, "0 < hh && hh < 3, Bug in System.Globalization.DateTimeFormatInfo.FullTimeSpan[Positive|Negative]Pattern");
+ Debug.Assert(0 < mm && mm < 3, "0 < mm && mm < 3, Bug in System.Globalization.DateTimeFormatInfo.FullTimeSpan[Positive|Negative]Pattern");
+ Debug.Assert(0 < ss && ss < 3, "0 < ss && ss < 3, Bug in System.Globalization.DateTimeFormatInfo.FullTimeSpan[Positive|Negative]Pattern");
+ Debug.Assert(0 < ff && ff < 8, "0 < ff && ff < 8, Bug in System.Globalization.DateTimeFormatInfo.FullTimeSpan[Positive|Negative]Pattern");
+
+ if (useInvariantFieldLengths)
+ {
+ dd = 2;
+ hh = 2;
+ mm = 2;
+ ss = 2;
+ ff = DateTimeFormat.MaxSecondsFractionDigits;
+ }
+ else
+ {
+ if (dd < 1 || dd > 2) dd = 2; // The DTFI property has a problem. let's try to make the best of the situation.
+ if (hh < 1 || hh > 2) hh = 2;
+ if (mm < 1 || mm > 2) mm = 2;
+ if (ss < 1 || ss > 2) ss = 2;
+ if (ff < 1 || ff > 7) ff = 7;
+ }
+ StringBuilderCache.Release(sb);
+ }
+ }
+ }
+}