| | | 1 | | // Licensed to the .NET Foundation under one or more agreements. |
| | | 2 | | // The .NET Foundation licenses this file to you under the MIT license. |
| | | 3 | | |
| | | 4 | | using System.Buffers.Text; |
| | | 5 | | using System.Collections.Generic; |
| | | 6 | | using System.Diagnostics; |
| | | 7 | | using System.Numerics; |
| | | 8 | | using System.Runtime.InteropServices; |
| | | 9 | | using System.Text; |
| | | 10 | | |
| | | 11 | | namespace System.Globalization |
| | | 12 | | { |
| | | 13 | | internal static class TimeSpanFormat |
| | | 14 | | { |
| | 0 | 15 | | internal static readonly FormatLiterals PositiveInvariantFormatLiterals = FormatLiterals.InitInvariant(isNegativ |
| | 0 | 16 | | internal static readonly FormatLiterals NegativeInvariantFormatLiterals = FormatLiterals.InitInvariant(isNegativ |
| | | 17 | | |
| | | 18 | | /// <summary>Main method called from TimeSpan.ToString.</summary> |
| | | 19 | | internal static string Format(TimeSpan value, string? format, IFormatProvider? formatProvider) |
| | | 20 | | { |
| | 0 | 21 | | if (string.IsNullOrEmpty(format)) |
| | | 22 | | { |
| | 0 | 23 | | return FormatC(value); // formatProvider ignored, as "c" is invariant |
| | | 24 | | } |
| | | 25 | | |
| | 0 | 26 | | if (format.Length == 1) |
| | | 27 | | { |
| | 0 | 28 | | char c = format[0]; |
| | | 29 | | |
| | 0 | 30 | | if (c == 'c' || (c | 0x20) == 't') // special-case to optimize the default TimeSpan format |
| | | 31 | | { |
| | 0 | 32 | | return FormatC(value); // formatProvider ignored, as "c" is invariant |
| | | 33 | | } |
| | | 34 | | |
| | 0 | 35 | | if ((c | 0x20) == 'g') // special-case to optimize the remaining 'g'/'G' standard formats |
| | | 36 | | { |
| | 0 | 37 | | return FormatG(value, DateTimeFormatInfo.GetInstance(formatProvider), c == 'G' ? StandardFormat.G : |
| | | 38 | | } |
| | | 39 | | |
| | 0 | 40 | | throw new FormatException(SR.Format_InvalidString); |
| | | 41 | | } |
| | | 42 | | |
| | 0 | 43 | | var vlb = new ValueListBuilder<char>(stackalloc char[256]); |
| | 0 | 44 | | FormatCustomized(value, format, DateTimeFormatInfo.GetInstance(formatProvider), ref vlb); |
| | 0 | 45 | | string resultString = vlb.AsSpan().ToString(); |
| | 0 | 46 | | vlb.Dispose(); |
| | 0 | 47 | | return resultString; |
| | | 48 | | } |
| | | 49 | | |
| | | 50 | | /// <summary>Main method called from TimeSpan.TryFormat.</summary> |
| | | 51 | | internal static bool TryFormat<TChar>(TimeSpan value, Span<TChar> destination, out int charsWritten, ReadOnlySpa |
| | | 52 | | { |
| | 0 | 53 | | Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); |
| | | 54 | | |
| | 0 | 55 | | if (format.Length == 0) |
| | | 56 | | { |
| | 0 | 57 | | return TryFormatStandard(value, StandardFormat.C, null, destination, out charsWritten); |
| | | 58 | | } |
| | | 59 | | |
| | 0 | 60 | | if (format.Length == 1) |
| | | 61 | | { |
| | 0 | 62 | | char c = format[0]; |
| | 0 | 63 | | if (c == 'c' || ((c | 0x20) == 't')) |
| | | 64 | | { |
| | 0 | 65 | | return TryFormatStandard(value, StandardFormat.C, null, destination, out charsWritten); |
| | | 66 | | } |
| | | 67 | | else |
| | | 68 | | { |
| | 0 | 69 | | StandardFormat sf = |
| | 0 | 70 | | c == 'g' ? StandardFormat.g : |
| | 0 | 71 | | c == 'G' ? StandardFormat.G : |
| | 0 | 72 | | throw new FormatException(SR.Format_InvalidString); |
| | 0 | 73 | | return TryFormatStandard(value, sf, DateTimeFormatInfo.GetInstance(formatProvider).DecimalSeparatorT |
| | | 74 | | } |
| | | 75 | | } |
| | | 76 | | |
| | 0 | 77 | | var vlb = new ValueListBuilder<TChar>(stackalloc TChar[256]); |
| | 0 | 78 | | FormatCustomized(value, format, DateTimeFormatInfo.GetInstance(formatProvider), ref vlb); |
| | 0 | 79 | | bool result = vlb.TryCopyTo(destination, out charsWritten); |
| | 0 | 80 | | vlb.Dispose(); |
| | 0 | 81 | | return result; |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | internal static string FormatC(TimeSpan value) |
| | | 85 | | { |
| | 0 | 86 | | Span<char> destination = stackalloc char[26]; // large enough for any "c" TimeSpan |
| | 0 | 87 | | TryFormatStandard(value, StandardFormat.C, null, destination, out int charsWritten); |
| | 0 | 88 | | return new string(destination.Slice(0, charsWritten)); |
| | | 89 | | } |
| | | 90 | | |
| | | 91 | | private static string FormatG(TimeSpan value, DateTimeFormatInfo dtfi, StandardFormat format) |
| | | 92 | | { |
| | 0 | 93 | | string decimalSeparator = dtfi.DecimalSeparator; |
| | 0 | 94 | | int maxLength = checked(25 + decimalSeparator.Length); // large enough for any "g"/"G" TimeSpan |
| | 0 | 95 | | Span<char> destination = (uint)maxLength < 128 ? |
| | 0 | 96 | | stackalloc char[maxLength] : |
| | 0 | 97 | | new char[maxLength]; // the chances of needing this case are almost 0, as DecimalSeparator.Length will b |
| | 0 | 98 | | TryFormatStandard(value, format, decimalSeparator, destination, out int charsWritten); |
| | 0 | 99 | | return new string(destination.Slice(0, charsWritten)); |
| | | 100 | | } |
| | | 101 | | |
| | | 102 | | internal enum StandardFormat |
| | | 103 | | { |
| | | 104 | | C, |
| | | 105 | | G, |
| | | 106 | | g |
| | | 107 | | } |
| | | 108 | | |
| | | 109 | | internal static bool TryFormatStandard<TChar>(TimeSpan value, StandardFormat format, ReadOnlySpan<TChar> decimal |
| | | 110 | | { |
| | 0 | 111 | | Debug.Assert(format == StandardFormat.C || format == StandardFormat.G || format == StandardFormat.g); |
| | | 112 | | |
| | | 113 | | // First, calculate how large an output buffer is needed to hold the entire output. |
| | 0 | 114 | | int requiredOutputLength = 8; // start with "hh:mm:ss" and adjust as necessary |
| | | 115 | | |
| | | 116 | | uint fraction; |
| | | 117 | | ulong totalSecondsRemaining; |
| | | 118 | | { |
| | | 119 | | // Turn this into a non-negative TimeSpan if possible. |
| | 0 | 120 | | long ticks = value.Ticks; |
| | 0 | 121 | | if (ticks < 0) |
| | | 122 | | { |
| | 0 | 123 | | requiredOutputLength = 9; // requiredOutputLength + 1 for the leading '-' sign |
| | 0 | 124 | | ticks = -ticks; |
| | 0 | 125 | | if (ticks < 0) |
| | | 126 | | { |
| | 0 | 127 | | Debug.Assert(ticks == long.MinValue /* -9223372036854775808 */); |
| | | 128 | | |
| | | 129 | | // We computed these ahead of time; they're straight from the decimal representation of Int64.Mi |
| | 0 | 130 | | fraction = 4775808; |
| | 0 | 131 | | totalSecondsRemaining = 922337203685; |
| | 0 | 132 | | goto AfterComputeFraction; |
| | | 133 | | } |
| | | 134 | | } |
| | | 135 | | |
| | | 136 | | ulong fraction64; |
| | 0 | 137 | | (totalSecondsRemaining, fraction64) = Math.DivRem((ulong)ticks, TimeSpan.TicksPerSecond); |
| | 0 | 138 | | fraction = (uint)fraction64; |
| | | 139 | | } |
| | | 140 | | |
| | | 141 | | AfterComputeFraction: |
| | | 142 | | // Only write out the fraction if it's non-zero, and in that |
| | | 143 | | // case write out the entire fraction (all digits). |
| | 0 | 144 | | Debug.Assert(fraction < 10_000_000); |
| | 0 | 145 | | int fractionDigits = 0; |
| | | 146 | | switch (format) |
| | | 147 | | { |
| | | 148 | | case StandardFormat.C: |
| | | 149 | | // "c": Write out a fraction only if it's non-zero, and write out all 7 digits of it. |
| | 0 | 150 | | if (fraction != 0) |
| | | 151 | | { |
| | 0 | 152 | | Debug.Assert(decimalSeparator.IsEmpty); |
| | 0 | 153 | | fractionDigits = DateTimeFormat.MaxSecondsFractionDigits; |
| | 0 | 154 | | requiredOutputLength += fractionDigits + 1; // digits plus leading decimal separator |
| | | 155 | | } |
| | 0 | 156 | | break; |
| | | 157 | | |
| | | 158 | | case StandardFormat.G: |
| | | 159 | | // "G": Write out a fraction regardless of whether it's 0, and write out all 7 digits of it. |
| | 0 | 160 | | fractionDigits = DateTimeFormat.MaxSecondsFractionDigits; |
| | 0 | 161 | | requiredOutputLength += fractionDigits; |
| | 0 | 162 | | requiredOutputLength += decimalSeparator.Length; |
| | 0 | 163 | | break; |
| | | 164 | | |
| | | 165 | | default: |
| | | 166 | | // "g": Write out a fraction only if it's non-zero, and write out only the most significant digits. |
| | 0 | 167 | | Debug.Assert(format == StandardFormat.g); |
| | 0 | 168 | | if (fraction != 0) |
| | | 169 | | { |
| | 0 | 170 | | fractionDigits = DateTimeFormat.MaxSecondsFractionDigits - FormattingHelpers.CountDecimalTrailin |
| | 0 | 171 | | requiredOutputLength += fractionDigits; |
| | 0 | 172 | | requiredOutputLength += decimalSeparator.Length; |
| | | 173 | | } |
| | | 174 | | break; |
| | | 175 | | } |
| | | 176 | | |
| | 0 | 177 | | ulong totalMinutesRemaining = 0, seconds = 0; |
| | 0 | 178 | | if (totalSecondsRemaining > 0) |
| | | 179 | | { |
| | | 180 | | // Only compute minutes if the TimeSpan has an absolute value of >= 1 minute. |
| | 0 | 181 | | (totalMinutesRemaining, seconds) = Math.DivRem(totalSecondsRemaining, 60 /* seconds per minute */); |
| | 0 | 182 | | Debug.Assert(seconds < 60); |
| | | 183 | | } |
| | | 184 | | |
| | 0 | 185 | | ulong totalHoursRemaining = 0, minutes = 0; |
| | 0 | 186 | | if (totalMinutesRemaining > 0) |
| | | 187 | | { |
| | | 188 | | // Only compute hours if the TimeSpan has an absolute value of >= 1 hour. |
| | 0 | 189 | | (totalHoursRemaining, minutes) = Math.DivRem(totalMinutesRemaining, 60 /* minutes per hour */); |
| | 0 | 190 | | Debug.Assert(minutes < 60); |
| | | 191 | | } |
| | | 192 | | |
| | | 193 | | // At this point, we can switch over to 32-bit DivRem since the data has shrunk far enough. |
| | 0 | 194 | | Debug.Assert(totalHoursRemaining <= uint.MaxValue); |
| | | 195 | | |
| | 0 | 196 | | uint days = 0, hours = 0; |
| | 0 | 197 | | if (totalHoursRemaining > 0) |
| | | 198 | | { |
| | | 199 | | // Only compute days if the TimeSpan has an absolute value of >= 1 day. |
| | 0 | 200 | | (days, hours) = Math.DivRem((uint)totalHoursRemaining, 24 /* hours per day */); |
| | 0 | 201 | | Debug.Assert(hours < 24); |
| | | 202 | | } |
| | | 203 | | |
| | 0 | 204 | | int hourDigits = 2; |
| | 0 | 205 | | if (format == StandardFormat.g && hours < 10) |
| | | 206 | | { |
| | | 207 | | // "g": Only writing a one-digit hour, rather than expected two-digit hour |
| | 0 | 208 | | hourDigits = 1; |
| | 0 | 209 | | requiredOutputLength--; |
| | | 210 | | } |
| | | 211 | | |
| | 0 | 212 | | int dayDigits = 0; |
| | 0 | 213 | | if (days > 0) |
| | | 214 | | { |
| | 0 | 215 | | dayDigits = FormattingHelpers.CountDigits(days); |
| | 0 | 216 | | Debug.Assert(dayDigits <= 8); |
| | 0 | 217 | | requiredOutputLength += dayDigits + 1; // for the leading "d." |
| | | 218 | | } |
| | 0 | 219 | | else if (format == StandardFormat.G) |
| | | 220 | | { |
| | | 221 | | // "G": has a leading "0:" if days is 0 |
| | 0 | 222 | | requiredOutputLength += 2; |
| | 0 | 223 | | dayDigits = 1; |
| | | 224 | | } |
| | | 225 | | |
| | 0 | 226 | | if (destination.Length < requiredOutputLength) |
| | | 227 | | { |
| | 0 | 228 | | written = 0; |
| | 0 | 229 | | return false; |
| | | 230 | | } |
| | | 231 | | |
| | 0 | 232 | | int pos = 0; |
| | | 233 | | |
| | | 234 | | // Write leading '-' if necessary |
| | 0 | 235 | | if (value.Ticks < 0) |
| | | 236 | | { |
| | 0 | 237 | | destination[pos++] = TChar.CastFrom('-'); |
| | | 238 | | } |
| | | 239 | | |
| | | 240 | | // Write day and separator, if necessary |
| | 0 | 241 | | if (dayDigits != 0) |
| | | 242 | | { |
| | 0 | 243 | | Number.WriteDigits(days, destination.Slice(pos, dayDigits)); |
| | 0 | 244 | | pos += dayDigits; |
| | 0 | 245 | | destination[pos++] = TChar.CastFrom(format == StandardFormat.C ? '.' : ':'); |
| | | 246 | | } |
| | | 247 | | |
| | | 248 | | // After writing the variable-length prefix into destination[0..pos), write the |
| | | 249 | | // fixed "[h]h:mm:ss[.fraction]" suffix. We branch on hourDigits (1 or 2) and |
| | | 250 | | // initialize suffixLen to the minimum length (8 or 7) before conditionally adding |
| | | 251 | | // the fraction part, giving the JIT a concrete lower bound to hoist bounds checks |
| | | 252 | | // out of the inner writes. |
| | 0 | 253 | | Debug.Assert(hourDigits == 1 || hourDigits == 2); |
| | | 254 | | int suffixLen; |
| | 0 | 255 | | if (hourDigits == 2) |
| | | 256 | | { |
| | 0 | 257 | | int decSepLen = 0; |
| | 0 | 258 | | suffixLen = 8; // hh:mm:ss |
| | 0 | 259 | | if (fractionDigits != 0) |
| | | 260 | | { |
| | 0 | 261 | | decSepLen = format == StandardFormat.C ? 1 : decimalSeparator.Length; |
| | 0 | 262 | | suffixLen += decSepLen + fractionDigits; |
| | | 263 | | } |
| | | 264 | | // Invariant: suffixLen >= 8 by construction; this check is unreachable but lets |
| | | 265 | | // the JIT prove that all suffix writes at constant offsets 0..7 are in bounds. |
| | 0 | 266 | | if ((uint)suffixLen < 8u) |
| | | 267 | | { |
| | 0 | 268 | | ThrowHelper.ThrowUnreachableException(); |
| | | 269 | | } |
| | 0 | 270 | | Span<TChar> suffix = destination.Slice(pos, suffixLen); |
| | | 271 | | |
| | 0 | 272 | | Number.WriteTwoDigits(hours, suffix.Slice(0, 2)); |
| | 0 | 273 | | suffix[2] = TChar.CastFrom(':'); |
| | 0 | 274 | | Number.WriteTwoDigits((uint)minutes, suffix.Slice(3, 2)); |
| | 0 | 275 | | suffix[5] = TChar.CastFrom(':'); |
| | 0 | 276 | | Number.WriteTwoDigits((uint)seconds, suffix.Slice(6, 2)); |
| | | 277 | | |
| | 0 | 278 | | if (fractionDigits != 0) |
| | | 279 | | { |
| | 0 | 280 | | if (format == StandardFormat.C) |
| | | 281 | | { |
| | 0 | 282 | | suffix[8] = TChar.CastFrom('.'); |
| | | 283 | | } |
| | 0 | 284 | | else if (decSepLen == 1) |
| | | 285 | | { |
| | 0 | 286 | | suffix[8] = decimalSeparator[0]; |
| | | 287 | | } |
| | | 288 | | else |
| | | 289 | | { |
| | 0 | 290 | | decimalSeparator.CopyTo(suffix.Slice(8, decSepLen)); |
| | | 291 | | } |
| | | 292 | | |
| | 0 | 293 | | Number.WriteDigits(fraction, suffix.Slice(8 + decSepLen, fractionDigits)); |
| | | 294 | | } |
| | | 295 | | } |
| | | 296 | | else |
| | | 297 | | { |
| | 0 | 298 | | int decSepLen = 0; |
| | 0 | 299 | | suffixLen = 7; // h:mm:ss |
| | 0 | 300 | | if (fractionDigits != 0) |
| | | 301 | | { |
| | 0 | 302 | | decSepLen = format == StandardFormat.C ? 1 : decimalSeparator.Length; |
| | 0 | 303 | | suffixLen += decSepLen + fractionDigits; |
| | | 304 | | } |
| | | 305 | | // Invariant: suffixLen >= 7 by construction; this check is unreachable but lets |
| | | 306 | | // the JIT prove that all suffix writes at constant offsets 0..6 are in bounds. |
| | 0 | 307 | | if ((uint)suffixLen < 7u) |
| | | 308 | | { |
| | 0 | 309 | | ThrowHelper.ThrowUnreachableException(); |
| | | 310 | | } |
| | 0 | 311 | | Span<TChar> suffix = destination.Slice(pos, suffixLen); |
| | | 312 | | |
| | 0 | 313 | | suffix[0] = TChar.CastFrom('0' + (int)hours); |
| | 0 | 314 | | suffix[1] = TChar.CastFrom(':'); |
| | 0 | 315 | | Number.WriteTwoDigits((uint)minutes, suffix.Slice(2, 2)); |
| | 0 | 316 | | suffix[4] = TChar.CastFrom(':'); |
| | 0 | 317 | | Number.WriteTwoDigits((uint)seconds, suffix.Slice(5, 2)); |
| | | 318 | | |
| | 0 | 319 | | if (fractionDigits != 0) |
| | | 320 | | { |
| | 0 | 321 | | if (format == StandardFormat.C) |
| | | 322 | | { |
| | 0 | 323 | | suffix[7] = TChar.CastFrom('.'); |
| | | 324 | | } |
| | 0 | 325 | | else if (decSepLen == 1) |
| | | 326 | | { |
| | 0 | 327 | | suffix[7] = decimalSeparator[0]; |
| | | 328 | | } |
| | | 329 | | else |
| | | 330 | | { |
| | 0 | 331 | | decimalSeparator.CopyTo(suffix.Slice(7, decSepLen)); |
| | | 332 | | } |
| | | 333 | | |
| | 0 | 334 | | Number.WriteDigits(fraction, suffix.Slice(7 + decSepLen, fractionDigits)); |
| | | 335 | | } |
| | | 336 | | } |
| | | 337 | | |
| | 0 | 338 | | Debug.Assert(pos + suffixLen == requiredOutputLength); |
| | | 339 | | |
| | 0 | 340 | | written = requiredOutputLength; |
| | 0 | 341 | | return true; |
| | | 342 | | } |
| | | 343 | | |
| | | 344 | | /// <summary>Format the TimeSpan instance using the specified format.</summary> |
| | | 345 | | private static void FormatCustomized<TChar>(TimeSpan value, scoped ReadOnlySpan<char> format, DateTimeFormatInfo |
| | | 346 | | { |
| | 0 | 347 | | Debug.Assert(dtfi != null); |
| | | 348 | | |
| | 0 | 349 | | int day = (int)(value.Ticks / TimeSpan.TicksPerDay); |
| | 0 | 350 | | long time = value.Ticks % TimeSpan.TicksPerDay; |
| | | 351 | | |
| | 0 | 352 | | if (value.Ticks < 0) |
| | | 353 | | { |
| | 0 | 354 | | day = -day; |
| | 0 | 355 | | time = -time; |
| | | 356 | | } |
| | 0 | 357 | | int hours = (int)(time / TimeSpan.TicksPerHour % 24); |
| | 0 | 358 | | int minutes = (int)(time / TimeSpan.TicksPerMinute % 60); |
| | 0 | 359 | | int seconds = (int)(time / TimeSpan.TicksPerSecond % 60); |
| | 0 | 360 | | int fraction = (int)(time % TimeSpan.TicksPerSecond); |
| | | 361 | | |
| | | 362 | | int tmp; |
| | 0 | 363 | | int i = 0; |
| | | 364 | | int tokenLen; |
| | | 365 | | |
| | 0 | 366 | | while (i < format.Length) |
| | | 367 | | { |
| | 0 | 368 | | char ch = format[i]; |
| | | 369 | | int nextChar; |
| | | 370 | | switch (ch) |
| | | 371 | | { |
| | | 372 | | case 'h': |
| | 0 | 373 | | tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch); |
| | 0 | 374 | | if (tokenLen > 2) |
| | | 375 | | { |
| | | 376 | | goto default; // to release the builder and throw |
| | | 377 | | } |
| | 0 | 378 | | DateTimeFormat.FormatDigits(ref result, hours, tokenLen); |
| | 0 | 379 | | break; |
| | | 380 | | case 'm': |
| | 0 | 381 | | tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch); |
| | 0 | 382 | | if (tokenLen > 2) |
| | | 383 | | { |
| | | 384 | | goto default; // to release the builder and throw |
| | | 385 | | } |
| | 0 | 386 | | DateTimeFormat.FormatDigits(ref result, minutes, tokenLen); |
| | 0 | 387 | | break; |
| | | 388 | | case 's': |
| | 0 | 389 | | tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch); |
| | 0 | 390 | | if (tokenLen > 2) |
| | | 391 | | { |
| | | 392 | | goto default; // to release the builder and throw |
| | | 393 | | } |
| | 0 | 394 | | DateTimeFormat.FormatDigits(ref result, seconds, tokenLen); |
| | 0 | 395 | | break; |
| | | 396 | | case 'f': |
| | | 397 | | // |
| | | 398 | | // The fraction of a second in single-digit precision. The remaining digits are truncated. |
| | | 399 | | // |
| | 0 | 400 | | tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch); |
| | 0 | 401 | | if (tokenLen > DateTimeFormat.MaxSecondsFractionDigits) |
| | | 402 | | { |
| | | 403 | | goto default; // to release the builder and throw |
| | | 404 | | } |
| | | 405 | | |
| | 0 | 406 | | tmp = fraction; |
| | 0 | 407 | | tmp /= TimeSpanParse.Pow10UpToMaxFractionDigits(DateTimeFormat.MaxSecondsFractionDigits - tokenL |
| | 0 | 408 | | DateTimeFormat.FormatFraction(ref result, tmp, DateTimeFormat.fixedNumberFormats[tokenLen - 1]); |
| | 0 | 409 | | break; |
| | | 410 | | case 'F': |
| | | 411 | | // |
| | | 412 | | // Displays the most significant digit of the seconds fraction. Nothing is displayed if the digi |
| | | 413 | | // |
| | 0 | 414 | | tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch); |
| | 0 | 415 | | if (tokenLen > DateTimeFormat.MaxSecondsFractionDigits) |
| | | 416 | | { |
| | | 417 | | goto default; // to release the builder and throw |
| | | 418 | | } |
| | | 419 | | |
| | 0 | 420 | | tmp = fraction; |
| | 0 | 421 | | tmp /= TimeSpanParse.Pow10UpToMaxFractionDigits(DateTimeFormat.MaxSecondsFractionDigits - tokenL |
| | 0 | 422 | | int effectiveDigits = tokenLen; |
| | 0 | 423 | | while (effectiveDigits > 0) |
| | | 424 | | { |
| | 0 | 425 | | if (tmp % 10 == 0) |
| | | 426 | | { |
| | 0 | 427 | | tmp /= 10; |
| | 0 | 428 | | effectiveDigits--; |
| | | 429 | | } |
| | | 430 | | else |
| | | 431 | | { |
| | | 432 | | break; |
| | | 433 | | } |
| | | 434 | | } |
| | 0 | 435 | | if (effectiveDigits > 0) |
| | | 436 | | { |
| | 0 | 437 | | DateTimeFormat.FormatFraction(ref result, tmp, DateTimeFormat.fixedNumberFormats[effectiveDi |
| | | 438 | | } |
| | 0 | 439 | | break; |
| | | 440 | | case 'd': |
| | | 441 | | // |
| | | 442 | | // tokenLen == 1 : Day as digits with no leading zero. |
| | | 443 | | // tokenLen == 2+: Day as digits with leading zero for single-digit days. |
| | | 444 | | // |
| | 0 | 445 | | tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch); |
| | 0 | 446 | | if (tokenLen > 8) |
| | | 447 | | { |
| | | 448 | | goto default; // to release the builder and throw |
| | | 449 | | } |
| | | 450 | | |
| | 0 | 451 | | DateTimeFormat.FormatDigits(ref result, day, tokenLen); |
| | 0 | 452 | | break; |
| | | 453 | | case '\'': |
| | | 454 | | case '\"': |
| | 0 | 455 | | tokenLen = DateTimeFormat.ParseQuoteString(format, i, ref result); |
| | 0 | 456 | | break; |
| | | 457 | | case '%': |
| | | 458 | | // Optional format character. |
| | | 459 | | // For example, format string "%d" will print day |
| | | 460 | | // Most of the cases, "%" can be ignored. |
| | 0 | 461 | | nextChar = DateTimeFormat.ParseNextChar(format, i); |
| | | 462 | | // nextChar will be -1 if we already reach the end of the format string. |
| | | 463 | | // Besides, we will not allow "%%" appear in the pattern. |
| | 0 | 464 | | if (nextChar >= 0 && nextChar != (int)'%') |
| | | 465 | | { |
| | 0 | 466 | | char nextCharChar = (char)nextChar; |
| | 0 | 467 | | FormatCustomized(value, new ReadOnlySpan<char>(in nextCharChar), dtfi, ref result); |
| | 0 | 468 | | tokenLen = 2; |
| | | 469 | | } |
| | | 470 | | else |
| | | 471 | | { |
| | | 472 | | // |
| | | 473 | | // This means that '%' is at the end of the format string or |
| | | 474 | | // "%%" appears in the format string. |
| | | 475 | | // |
| | | 476 | | goto default; // to release the builder and throw |
| | | 477 | | } |
| | | 478 | | break; |
| | | 479 | | case '\\': |
| | | 480 | | // Escaped character. Can be used to insert character into the format string. |
| | | 481 | | // For example, "\d" will insert the character 'd' into the string. |
| | | 482 | | // |
| | 0 | 483 | | nextChar = DateTimeFormat.ParseNextChar(format, i); |
| | 0 | 484 | | if (nextChar >= 0) |
| | | 485 | | { |
| | 0 | 486 | | char escapedChar = (char)nextChar; |
| | 0 | 487 | | if (char.IsHighSurrogate(escapedChar) && i + 2 < format.Length && char.IsLowSurrogate(format |
| | | 488 | | { |
| | 0 | 489 | | tokenLen = 1 + DateTimeFormat.AppendChar(ref result, format[(i + 1)..]); |
| | | 490 | | } |
| | | 491 | | else |
| | | 492 | | { |
| | 0 | 493 | | DateTimeFormat.AppendChar(ref result, escapedChar); |
| | 0 | 494 | | tokenLen = 2; |
| | | 495 | | } |
| | | 496 | | } |
| | | 497 | | else |
| | | 498 | | { |
| | | 499 | | // |
| | | 500 | | // This means that '\' is at the end of the formatting string. |
| | | 501 | | // |
| | | 502 | | goto default; // to release the builder and throw |
| | | 503 | | } |
| | | 504 | | break; |
| | | 505 | | default: |
| | | 506 | | // Invalid format string |
| | 0 | 507 | | throw new FormatException(SR.Format_InvalidString); |
| | | 508 | | } |
| | 0 | 509 | | i += tokenLen; |
| | | 510 | | } |
| | 0 | 511 | | } |
| | | 512 | | |
| | | 513 | | internal struct FormatLiterals |
| | | 514 | | { |
| | | 515 | | internal string AppCompatLiteral; |
| | | 516 | | internal int dd; |
| | | 517 | | internal int hh; |
| | | 518 | | internal int mm; |
| | | 519 | | internal int ss; |
| | | 520 | | internal int ff; |
| | | 521 | | |
| | | 522 | | private string[] _literals; |
| | | 523 | | |
| | 0 | 524 | | internal string Start => _literals[0]; |
| | 0 | 525 | | internal string DayHourSep => _literals[1]; |
| | 0 | 526 | | internal string HourMinuteSep => _literals[2]; |
| | 0 | 527 | | internal string MinuteSecondSep => _literals[3]; |
| | 0 | 528 | | internal string SecondFractionSep => _literals[4]; |
| | 0 | 529 | | internal string End => _literals[5]; |
| | | 530 | | |
| | | 531 | | /* factory method for static invariant FormatLiterals */ |
| | | 532 | | internal static FormatLiterals InitInvariant(bool isNegative) |
| | | 533 | | { |
| | 0 | 534 | | FormatLiterals x = default; |
| | 0 | 535 | | x._literals = new string[6]; |
| | 0 | 536 | | x._literals[0] = isNegative ? "-" : string.Empty; |
| | 0 | 537 | | x._literals[1] = "."; |
| | 0 | 538 | | x._literals[2] = ":"; |
| | 0 | 539 | | x._literals[3] = ":"; |
| | 0 | 540 | | x._literals[4] = "."; |
| | 0 | 541 | | x._literals[5] = string.Empty; |
| | 0 | 542 | | x.AppCompatLiteral = ":."; // MinuteSecondSep+SecondFractionSep; |
| | 0 | 543 | | x.dd = 2; |
| | 0 | 544 | | x.hh = 2; |
| | 0 | 545 | | x.mm = 2; |
| | 0 | 546 | | x.ss = 2; |
| | 0 | 547 | | x.ff = DateTimeFormat.MaxSecondsFractionDigits; |
| | 0 | 548 | | return x; |
| | | 549 | | } |
| | | 550 | | |
| | | 551 | | // For the "v1" TimeSpan localized patterns, the data is simply literal field separators with |
| | | 552 | | // the constants guaranteed to include DHMSF ordered greatest to least significant. |
| | | 553 | | // Once the data becomes more complex than this we will need to write a proper tokenizer for |
| | | 554 | | // parsing and formatting |
| | | 555 | | internal void Init(ReadOnlySpan<char> format, bool useInvariantFieldLengths) |
| | | 556 | | { |
| | 0 | 557 | | dd = hh = mm = ss = ff = 0; |
| | 0 | 558 | | _literals = new string[6]; |
| | 0 | 559 | | for (int i = 0; i < _literals.Length; i++) |
| | | 560 | | { |
| | 0 | 561 | | _literals[i] = string.Empty; |
| | | 562 | | } |
| | | 563 | | |
| | 0 | 564 | | var sb = new ValueStringBuilder(stackalloc char[256]); |
| | 0 | 565 | | bool inQuote = false; |
| | 0 | 566 | | char quote = '\''; |
| | 0 | 567 | | int field = 0; |
| | | 568 | | |
| | 0 | 569 | | for (int i = 0; i < format.Length; i++) |
| | | 570 | | { |
| | 0 | 571 | | switch (format[i]) |
| | | 572 | | { |
| | | 573 | | case '\'': |
| | | 574 | | case '\"': |
| | 0 | 575 | | if (inQuote && (quote == format[i])) |
| | | 576 | | { |
| | | 577 | | /* we were in a quote and found a matching exit quote, so we are outside a quote now */ |
| | 0 | 578 | | Debug.Assert(field >= 0 && field <= 5); |
| | 0 | 579 | | _literals[field] = sb.AsSpan().ToString(); |
| | 0 | 580 | | sb.Length = 0; |
| | 0 | 581 | | inQuote = false; |
| | | 582 | | } |
| | 0 | 583 | | else if (!inQuote) |
| | | 584 | | { |
| | | 585 | | /* we are at the start of a new quote block */ |
| | 0 | 586 | | quote = format[i]; |
| | 0 | 587 | | inQuote = true; |
| | | 588 | | } |
| | | 589 | | else |
| | | 590 | | { |
| | | 591 | | /* we were in a quote and saw the other type of quote character, so we are still in a qu |
| | | 592 | | } |
| | | 593 | | break; |
| | | 594 | | case '%': |
| | 0 | 595 | | Debug.Fail("Unexpected special token '%', Bug in DateTimeFormatInfo.FullTimeSpan[Positive|Ne |
| | | 596 | | goto default; |
| | | 597 | | case '\\': |
| | 0 | 598 | | if (!inQuote) |
| | | 599 | | { |
| | 0 | 600 | | i++; /* skip next character that is escaped by this backslash or percent sign */ |
| | 0 | 601 | | break; |
| | | 602 | | } |
| | | 603 | | goto default; |
| | | 604 | | case 'd': |
| | 0 | 605 | | if (!inQuote) |
| | | 606 | | { |
| | 0 | 607 | | Debug.Assert((field == 0 && sb.Length == 0) || field == 1, "field == 0 || field == 1, Bu |
| | 0 | 608 | | field = 1; // DayHourSep |
| | 0 | 609 | | dd++; |
| | | 610 | | } |
| | 0 | 611 | | break; |
| | | 612 | | case 'h': |
| | 0 | 613 | | if (!inQuote) |
| | | 614 | | { |
| | 0 | 615 | | Debug.Assert((field == 1 && sb.Length == 0) || field == 2, "field == 1 || field == 2, Bu |
| | 0 | 616 | | field = 2; // HourMinuteSep |
| | 0 | 617 | | hh++; |
| | | 618 | | } |
| | 0 | 619 | | break; |
| | | 620 | | case 'm': |
| | 0 | 621 | | if (!inQuote) |
| | | 622 | | { |
| | 0 | 623 | | Debug.Assert((field == 2 && sb.Length == 0) || field == 3, "field == 2 || field == 3, Bu |
| | 0 | 624 | | field = 3; // MinuteSecondSep |
| | 0 | 625 | | mm++; |
| | | 626 | | } |
| | 0 | 627 | | break; |
| | | 628 | | case 's': |
| | 0 | 629 | | if (!inQuote) |
| | | 630 | | { |
| | 0 | 631 | | Debug.Assert((field == 3 && sb.Length == 0) || field == 4, "field == 3 || field == 4, Bu |
| | 0 | 632 | | field = 4; // SecondFractionSep |
| | 0 | 633 | | ss++; |
| | | 634 | | } |
| | 0 | 635 | | break; |
| | | 636 | | case 'f': |
| | | 637 | | case 'F': |
| | 0 | 638 | | if (!inQuote) |
| | | 639 | | { |
| | 0 | 640 | | Debug.Assert((field == 4 && sb.Length == 0) || field == 5, "field == 4 || field == 5, Bu |
| | 0 | 641 | | field = 5; // End |
| | 0 | 642 | | ff++; |
| | | 643 | | } |
| | 0 | 644 | | break; |
| | | 645 | | default: |
| | 0 | 646 | | sb.Append(format[i]); |
| | | 647 | | break; |
| | | 648 | | } |
| | | 649 | | } |
| | | 650 | | |
| | 0 | 651 | | sb.Dispose(); |
| | | 652 | | |
| | 0 | 653 | | Debug.Assert(field == 5); |
| | 0 | 654 | | AppCompatLiteral = MinuteSecondSep + SecondFractionSep; |
| | | 655 | | |
| | 0 | 656 | | Debug.Assert(0 < dd && dd < 3, "0 < dd && dd < 3, Bug in System.Globalization.DateTimeFormatInfo.FullTim |
| | 0 | 657 | | Debug.Assert(0 < hh && hh < 3, "0 < hh && hh < 3, Bug in System.Globalization.DateTimeFormatInfo.FullTim |
| | 0 | 658 | | Debug.Assert(0 < mm && mm < 3, "0 < mm && mm < 3, Bug in System.Globalization.DateTimeFormatInfo.FullTim |
| | 0 | 659 | | Debug.Assert(0 < ss && ss < 3, "0 < ss && ss < 3, Bug in System.Globalization.DateTimeFormatInfo.FullTim |
| | 0 | 660 | | Debug.Assert(0 < ff && ff < 8, "0 < ff && ff < 8, Bug in System.Globalization.DateTimeFormatInfo.FullTim |
| | | 661 | | |
| | 0 | 662 | | if (useInvariantFieldLengths) |
| | | 663 | | { |
| | 0 | 664 | | dd = 2; |
| | 0 | 665 | | hh = 2; |
| | 0 | 666 | | mm = 2; |
| | 0 | 667 | | ss = 2; |
| | 0 | 668 | | ff = DateTimeFormat.MaxSecondsFractionDigits; |
| | | 669 | | } |
| | | 670 | | else |
| | | 671 | | { |
| | 0 | 672 | | if (dd < 1 || dd > 2) dd = 2; // The DTFI property has a problem. let's try to make the best of th |
| | 0 | 673 | | if (hh < 1 || hh > 2) hh = 2; |
| | 0 | 674 | | if (mm < 1 || mm > 2) mm = 2; |
| | 0 | 675 | | if (ss < 1 || ss > 2) ss = 2; |
| | 0 | 676 | | if (ff < 1 || ff > 7) ff = 7; |
| | | 677 | | } |
| | 0 | 678 | | } |
| | | 679 | | } |
| | | 680 | | } |
| | | 681 | | } |
| | | 682 | | |