private static bool TryFormatInt64MultipleDigits(long value, Span <byte> destination, out int bytesWritten) { if (value < 0) { value = -value; int digitCount = FormattingHelpers.CountDigits((ulong)value); // WriteDigits does not do bounds checks if (digitCount >= destination.Length) { bytesWritten = 0; return(false); } destination[0] = Utf8Constants.Minus; bytesWritten = digitCount + 1; FormattingHelpers.WriteDigits((ulong)value, destination.Slice(1, digitCount)); return(true); } else { return(TryFormatUInt64MultipleDigits((ulong)value, destination, out bytesWritten)); } }
// Split long into two parts that can each fit in a uint - {1-10 digits}{9 digits} private static bool TryFormatInt64MoreThanNegativeBillionMaxUInt(long value, Span <byte> destination, out int bytesWritten) { uint overNineDigits = (uint)(value / Utf8Constants.Billion); uint lastNineDigits = (uint)(value - (overNineDigits * Utf8Constants.Billion)); int digitCountOverNineDigits = FormattingHelpers.CountDigits(overNineDigits); Debug.Assert(digitCountOverNineDigits >= 1 && digitCountOverNineDigits <= 10); int digitCount = digitCountOverNineDigits + 9; // WriteDigits does not do bounds checks if (digitCount >= destination.Length) { bytesWritten = 0; return(false); } destination[0] = Utf8Constants.Minus; bytesWritten = digitCount + 1; FormattingHelpers.WriteDigits(overNineDigits, destination.Slice(1, digitCountOverNineDigits)); FormattingHelpers.WriteDigits(lastNineDigits, destination.Slice(digitCountOverNineDigits + 1, 9)); return(true); }
private static bool TryFormatUInt64N(ulong value, byte precision, Span <byte> buffer, out int bytesWritten) { if (value <= long.MaxValue) { return(TryFormatInt64N((long)value, precision, buffer, out bytesWritten)); } // The ulong path is much slower than the long path here, so we are doing the last group // inside this method plus the zero padding but routing to the long version for the rest. value = FormattingHelpers.DivMod(value, 1000, out ulong lastGroup); if (!TryFormatInt64N((long)value, 0, buffer, out bytesWritten)) { return(false); } if (precision == StandardFormat.NoPrecision) { precision = 2; } int idx = bytesWritten; // Since this method routes entirely to the long version if the number is smaller than // long.MaxValue, we are guaranteed to need to write 3 more digits here before the set // of trailing zeros. bytesWritten += 4; // 3 digits + group separator if (precision > 0) { bytesWritten += precision + 1; // +1 for period. } if (buffer.Length < bytesWritten) { bytesWritten = 0; return(false); } ref byte utf8Bytes = ref buffer.DangerousGetPinnableReference();
private static bool TryFormatUInt64(ulong value, Span <byte> destination, out int bytesWritten, StandardFormat format) { if (format.IsDefault) { return(TryFormatUInt64Default(value, destination, out bytesWritten)); } switch (format.Symbol) { case 'G': case 'g': case 'R': case 'r': // treat 'R'/'r' (which historically was only for floating-point) as being equivalent to 'g', rather than throwing if (format.HasPrecision) { throw new NotSupportedException(SR.Argument_GWithPrecisionNotSupported); // With a precision, 'G' can produce exponential format, even for integers. } return(TryFormatUInt64D(value, format.Precision, destination, insertNegationSign: false, out bytesWritten)); case 'd': case 'D': return(TryFormatUInt64D(value, format.Precision, destination, insertNegationSign: false, out bytesWritten)); case 'n': case 'N': return(TryFormatUInt64N(value, format.Precision, destination, insertNegationSign: false, out bytesWritten)); case 'x': return(TryFormatUInt64X(value, format.Precision, true /* useLower */, destination, out bytesWritten)); case 'X': return(TryFormatUInt64X(value, format.Precision, false /* useLower */, destination, out bytesWritten)); default: return(FormattingHelpers.TryFormatThrowFormatException(out bytesWritten)); } }
private static bool TryFormatUInt64MoreThanBillionMaxUInt(ulong value, Span <byte> destination, out int bytesWritten) { ulong overNineDigits = value / Utf8Constants.Billion; uint lastNineDigits = (uint)(value - (overNineDigits * Utf8Constants.Billion)); uint overEighteenDigits = (uint)(overNineDigits / Utf8Constants.Billion); uint middleNineDigits = (uint)(overNineDigits - (overEighteenDigits * Utf8Constants.Billion)); int digitCountOverEighteenDigits = FormattingHelpers.CountDigits(overEighteenDigits); Debug.Assert(digitCountOverEighteenDigits >= 1 && digitCountOverEighteenDigits <= 2); int digitCount = digitCountOverEighteenDigits + 18; // WriteDigits does not do bounds checks if (digitCount > destination.Length) { bytesWritten = 0; return(false); } bytesWritten = digitCount; FormattingHelpers.WriteDigits(overEighteenDigits, destination.Slice(0, digitCountOverEighteenDigits)); FormattingHelpers.WriteDigits(middleNineDigits, destination.Slice(digitCountOverEighteenDigits, 9)); FormattingHelpers.WriteDigits(lastNineDigits, destination.Slice(digitCountOverEighteenDigits + 9, 9)); return(true); }
/// <summary> /// Formats a Guid as a UTF8 string. /// </summary> /// <param name="value">Value to format</param> /// <param name="destination">Buffer to write the UTF8-formatted value to</param> /// <param name="bytesWritten">Receives the length of the formatted text in bytes</param> /// <param name="format">The standard format to use</param> /// <returns> /// true for success. "bytesWritten" contains the length of the formatted text in bytes. /// false if buffer was too short. Iteratively increase the size of the buffer and retry until it succeeds. /// </returns> /// <remarks> /// Formats supported: /// D (default) nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn /// B {nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn} /// P (nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn) /// N nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn /// </remarks> /// <exceptions> /// <cref>System.FormatException</cref> if the format is not valid for this data type. /// </exceptions> public static bool TryFormat(Guid value, Span <byte> destination, out int bytesWritten, StandardFormat format = default) { const int INSERT_DASHES = unchecked ((int)0x80000000); const int NO_DASHES = 0; const int INSERT_CURLY_BRACES = (CloseBrace << 16) | (OpenBrace << 8); const int INSERT_ROUND_BRACES = (CloseParen << 16) | (OpenParen << 8); const int NO_BRACES = 0; const int LEN_GUID_BASE = 32; const int LEN_ADD_DASHES = 4; const int LEN_ADD_BRACES = 2; // This is a 32-bit value whose contents (where 0 is the low byte) are: // 0th byte: minimum required length of the output buffer, // 1st byte: the ASCII byte to insert for the opening brace position (or 0 if no braces), // 2nd byte: the ASCII byte to insert for the closing brace position (or 0 if no braces), // 3rd byte: high bit set if dashes are to be inserted. // // The reason for keeping a single flag instead of separate vars is that we can avoid register spillage // as we build up the output value. int flags; switch (FormattingHelpers.GetSymbolOrDefault(format, 'D')) { case 'D': // nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn flags = INSERT_DASHES + NO_BRACES + LEN_GUID_BASE + LEN_ADD_DASHES; break; case 'B': // {nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn} flags = INSERT_DASHES + INSERT_CURLY_BRACES + LEN_GUID_BASE + LEN_ADD_DASHES + LEN_ADD_BRACES; break; case 'P': // (nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn) flags = INSERT_DASHES + INSERT_ROUND_BRACES + LEN_GUID_BASE + LEN_ADD_DASHES + LEN_ADD_BRACES; break; case 'N': // nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn flags = NO_BRACES + NO_DASHES + LEN_GUID_BASE; break; default: return(ThrowHelper.TryFormatThrowFormatException(out bytesWritten)); } // At this point, the low byte of flags contains the minimum required length if ((byte)flags > destination.Length) { bytesWritten = 0; return(false); } bytesWritten = (byte)flags; flags >>= 8; // At this point, the low byte of flags contains the opening brace char (if any) if ((byte)flags != 0) { destination[0] = (byte)flags; destination = destination.Slice(1); } flags >>= 8; // At this point, the low byte of flags contains the closing brace char (if any) // And since we're performing arithmetic shifting the high bit of flags is set (flags is negative) if dashes are required DecomposedGuid guidAsBytes = default; guidAsBytes.Guid = value; // When a GUID is blitted, the first three components are little-endian, and the last component is big-endian. // The line below forces the JIT to hoist the bounds check for the following segment. // The JIT will optimize away the read, but it cannot optimize away the bounds check // because it may have an observable side effect (throwing). // We use 8 instead of 7 so that we also capture the dash if we're asked to insert one. { var unused = destination[8]; } FormattingHelpers.WriteHexByte(guidAsBytes.Byte03, destination, 0, FormattingHelpers.HexCasing.Lowercase); FormattingHelpers.WriteHexByte(guidAsBytes.Byte02, destination, 2, FormattingHelpers.HexCasing.Lowercase); FormattingHelpers.WriteHexByte(guidAsBytes.Byte01, destination, 4, FormattingHelpers.HexCasing.Lowercase); FormattingHelpers.WriteHexByte(guidAsBytes.Byte00, destination, 6, FormattingHelpers.HexCasing.Lowercase); if (flags < 0 /* use dash? */) { destination[8] = Dash; destination = destination.Slice(9); } else { destination = destination.Slice(8); } { var unused = destination[4]; } FormattingHelpers.WriteHexByte(guidAsBytes.Byte05, destination, 0, FormattingHelpers.HexCasing.Lowercase); FormattingHelpers.WriteHexByte(guidAsBytes.Byte04, destination, 2, FormattingHelpers.HexCasing.Lowercase); if (flags < 0 /* use dash? */) { destination[4] = Dash; destination = destination.Slice(5); } else { destination = destination.Slice(4); } { var unused = destination[4]; } FormattingHelpers.WriteHexByte(guidAsBytes.Byte07, destination, 0, FormattingHelpers.HexCasing.Lowercase); FormattingHelpers.WriteHexByte(guidAsBytes.Byte06, destination, 2, FormattingHelpers.HexCasing.Lowercase); if (flags < 0 /* use dash? */) { destination[4] = Dash; destination = destination.Slice(5); } else { destination = destination.Slice(4); } { var unused = destination[4]; } FormattingHelpers.WriteHexByte(guidAsBytes.Byte08, destination, 0, FormattingHelpers.HexCasing.Lowercase); FormattingHelpers.WriteHexByte(guidAsBytes.Byte09, destination, 2, FormattingHelpers.HexCasing.Lowercase); if (flags < 0 /* use dash? */) { destination[4] = Dash; destination = destination.Slice(5); } else { destination = destination.Slice(4); } { var unused = destination[11]; } // can't hoist bounds check on the final brace (if exists) FormattingHelpers.WriteHexByte(guidAsBytes.Byte10, destination, 0, FormattingHelpers.HexCasing.Lowercase); FormattingHelpers.WriteHexByte(guidAsBytes.Byte11, destination, 2, FormattingHelpers.HexCasing.Lowercase); FormattingHelpers.WriteHexByte(guidAsBytes.Byte12, destination, 4, FormattingHelpers.HexCasing.Lowercase); FormattingHelpers.WriteHexByte(guidAsBytes.Byte13, destination, 6, FormattingHelpers.HexCasing.Lowercase); FormattingHelpers.WriteHexByte(guidAsBytes.Byte14, destination, 8, FormattingHelpers.HexCasing.Lowercase); FormattingHelpers.WriteHexByte(guidAsBytes.Byte15, destination, 10, FormattingHelpers.HexCasing.Lowercase); if ((byte)flags != 0) { destination[12] = (byte)flags; } return(true); }
/// <summary> /// Formats a TimeSpan as a UTF8 string. /// </summary> /// <param name="value">Value to format</param> /// <param name="destination">Buffer to write the UTF8-formatted value to</param> /// <param name="bytesWritten">Receives the length of the formatted text in bytes</param> /// <param name="format">The standard format to use</param> /// <returns> /// true for success. "bytesWritten" contains the length of the formatted text in bytes. /// false if buffer was too short. Iteratively increase the size of the buffer and retry until it succeeds. /// </returns> /// <remarks> /// Formats supported: /// c/t/T (default) [-][d.]hh:mm:ss[.fffffff] (constant format) /// G [-]d:hh:mm:ss.fffffff (general long) /// g [-][d:][h]h:mm:ss[.f[f[f[f[f[f[f]]]]]] (general short) /// </remarks> /// <exceptions> /// <cref>System.FormatException</cref> if the format is not valid for this data type. /// </exceptions> public static bool TryFormat(TimeSpan value, Span <byte> 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: int fractionDigits = 0; if (symbol == 'c') { // Only write out the fraction if it's non-zero, and in that // case write out the entire fraction (all digits). if (fraction != 0) { fractionDigits = Utf8Constants.DateTimeNumFractionDigits; } } 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) { 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; } } 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); }
/// <summary> /// Formats a Boolean as a UTF8 string. /// </summary> /// <param name="value">Value to format</param> /// <param name="destination">Buffer to write the UTF8-formatted value to</param> /// <param name="bytesWritten">Receives the length of the formatted text in bytes</param> /// <param name="format">The standard format to use</param> /// <returns> /// true for success. "bytesWritten" contains the length of the formatted text in bytes. /// false if buffer was too short. Iteratively increase the size of the buffer and retry until it succeeds. /// </returns> /// <remarks> /// Formats supported: /// G (default) True/False /// l true/false /// </remarks> /// <exceptions> /// <cref>System.FormatException</cref> if the format is not valid for this data type. /// </exceptions> public static bool TryFormat(bool value, Span <byte> destination, out int bytesWritten, StandardFormat format = default) { char symbol = FormattingHelpers.GetSymbolOrDefault(format, 'G'); if (value) { if (symbol == 'G') { // By having each branch perform its own call to TryWriteUInt32BigEndian, we ensure that a // constant value is passed to this routine, which means the compiler can reverse endianness // at compile time instead of runtime if necessary. const uint TrueValueUppercase = ('T' << 24) + ('r' << 16) + ('u' << 8) + ('e' << 0); if (!BinaryPrimitives.TryWriteUInt32BigEndian(destination, TrueValueUppercase)) { goto BufferTooSmall; } } else if (symbol == 'l') { const uint TrueValueLowercase = ('t' << 24) + ('r' << 16) + ('u' << 8) + ('e' << 0); if (!BinaryPrimitives.TryWriteUInt32BigEndian(destination, TrueValueLowercase)) { goto BufferTooSmall; } } else { goto BadFormat; } bytesWritten = 4; return(true); } else { if (symbol == 'G') { // This check can't be performed earlier because we need to throw if an invalid symbol is // provided, even if the buffer is too small. if (destination.Length <= 4) { goto BufferTooSmall; } const uint FalsValueUppercase = ('F' << 24) + ('a' << 16) + ('l' << 8) + ('s' << 0); BinaryPrimitives.WriteUInt32BigEndian(destination, FalsValueUppercase); } else if (symbol == 'l') { if (destination.Length <= 4) { goto BufferTooSmall; } const uint FalsValueLowercase = ('f' << 24) + ('a' << 16) + ('l' << 8) + ('s' << 0); BinaryPrimitives.WriteUInt32BigEndian(destination, FalsValueLowercase); } else { goto BadFormat; } destination[4] = (byte)'e'; bytesWritten = 5; return(true); } BufferTooSmall: bytesWritten = 0; return(false); BadFormat: return(FormattingHelpers.TryFormatThrowFormatException(out bytesWritten)); }
/// <summary> /// Formats a TimeSpan as a UTF8 string. /// </summary> /// <param name="value">Value to format</param> /// <param name="buffer">Buffer to write the UTF8-formatted value to</param> /// <param name="bytesWritten">Receives the length of the formatted text in bytes</param> /// <param name="format">The standard format to use</param> /// <returns> /// true for success. "bytesWritten" contains the length of the formatted text in bytes. /// false if buffer was too short. Iteratively increase the size of the buffer and retry until it succeeds. /// </returns> /// <remarks> /// Formats supported: /// c/t/T (default) [-][d.]hh:mm:ss[.fffffff] (constant format) /// G [-]d:hh:mm:ss.fffffff (general long) /// g [-][d:]h:mm:ss[.f[f[f[f[f[f[f[]]]]]]] (general short) /// </remarks> /// <exceptions> /// <cref>System.FormatException</cref> if the format is not valid for this data type. /// </exceptions> public static bool TryFormat(TimeSpan value, Span <byte> buffer, out int bytesWritten, StandardFormat format = default) { char symbol = format.IsDefault ? 'c' : format.Symbol; switch (symbol) { case 'G': case 'g': case 'c': case 't': case 'T': { bool longForm = (symbol == 'G'); bool constant = (symbol == 't' || symbol == 'T' || symbol == 'c'); long ticks = value.Ticks; int days = (int)FormattingHelpers.DivMod(ticks, TimeSpan.TicksPerDay, out long timeLeft); bool showSign = false; if (ticks < 0) { showSign = true; days = -days; timeLeft = -timeLeft; } int hours = (int)FormattingHelpers.DivMod(timeLeft, TimeSpan.TicksPerHour, out timeLeft); int minutes = (int)FormattingHelpers.DivMod(timeLeft, TimeSpan.TicksPerMinute, out timeLeft); int seconds = (int)FormattingHelpers.DivMod(timeLeft, TimeSpan.TicksPerSecond, out long fraction); int dayDigits = 0; int hourDigits = (constant || longForm || hours > 9) ? 2 : 1; int fractionDigits = 0; bytesWritten = hourDigits + 6; // [h]h:mm:ss if (showSign) { bytesWritten += 1; // [-] } if (longForm || days > 0) { dayDigits = FormattingHelpers.CountDigits(days); bytesWritten += dayDigits + 1; // [d'.'] } if (longForm || fraction > 0) { fractionDigits = (longForm || constant) ? Utf8Constants.DateTimeNumFractionDigits : FormattingHelpers.CountFractionDigits(fraction); bytesWritten += fractionDigits + 1; // ['.'fffffff] or ['.'FFFFFFF] for short-form } if (buffer.Length < bytesWritten) { bytesWritten = 0; return(false); } ref byte utf8Bytes = ref buffer.DangerousGetPinnableReference(); int idx = 0; if (showSign) { Unsafe.Add(ref utf8Bytes, idx++) = Utf8Constants.Minus; } if (dayDigits > 0) { idx += FormattingHelpers.WriteDigits(days, dayDigits, ref utf8Bytes, idx); Unsafe.Add(ref utf8Bytes, idx++) = constant ? Utf8Constants.Period : Utf8Constants.Colon; } idx += FormattingHelpers.WriteDigits(hours, hourDigits, ref utf8Bytes, idx); Unsafe.Add(ref utf8Bytes, idx++) = Utf8Constants.Colon; idx += FormattingHelpers.WriteDigits(minutes, 2, ref utf8Bytes, idx); Unsafe.Add(ref utf8Bytes, idx++) = Utf8Constants.Colon; idx += FormattingHelpers.WriteDigits(seconds, 2, ref utf8Bytes, idx); if (fractionDigits > 0) { Unsafe.Add(ref utf8Bytes, idx++) = Utf8Constants.Period; idx += FormattingHelpers.WriteFractionDigits(fraction, fractionDigits, ref utf8Bytes, idx); } return(true); }
/// <summary> /// Formats a Decimal as a UTF8 string. /// </summary> /// <param name="value">Value to format</param> /// <param name="destination">Buffer to write the UTF8-formatted value to</param> /// <param name="bytesWritten">Receives the length of the formatted text in bytes</param> /// <param name="format">The standard format to use</param> /// <returns> /// true for success. "bytesWritten" contains the length of the formatted text in bytes. /// false if buffer was too short. Iteratively increase the size of the buffer and retry until it succeeds. /// </returns> /// <remarks> /// Formats supported: /// G/g (default) /// F/f 12.45 Fixed point /// E/e 1.245000e1 Exponential /// </remarks> /// <exceptions> /// <cref>System.FormatException</cref> if the format is not valid for this data type. /// </exceptions> public static unsafe bool TryFormat(decimal value, Span <byte> destination, out int bytesWritten, StandardFormat format = default) { if (format.IsDefault) { format = 'G'; } switch (format.Symbol) { case 'g': case 'G': { if (format.Precision != StandardFormat.NoPrecision) { throw new NotSupportedException(SR.Argument_GWithPrecisionNotSupported); } Number.NumberBuffer number = new Number.NumberBuffer(Number.NumberBufferKind.Decimal, stackalloc byte[Number.DecimalNumberBufferLength]); Number.DecimalToNumber(ref value, ref number); if (number.Digits[0] == 0) { number.IsNegative = false; // For Decimals, -0 must print as normal 0. } bool success = TryFormatDecimalG(ref number, destination, out bytesWritten); #if DEBUG // This DEBUG segment exists to close a code coverage hole inside TryFormatDecimalG(). Because we don't call RoundNumber() on this path, we have no way to feed // TryFormatDecimalG() a number where trailing zeros before the decimal point have been cropped. So if the chance comes up, we'll crop the zeroes // ourselves and make a second call to ensure we get the same outcome. if (success) { Span <byte> digits = number.Digits; int numDigits = number.DigitsCount; if (numDigits != 0 && number.Scale == numDigits && digits[numDigits - 1] == '0') { while (numDigits != 0 && digits[numDigits - 1] == '0') { digits[numDigits - 1] = 0; numDigits--; } number.DigitsCount = numDigits; number.CheckConsistency(); byte[] buffer2 = new byte[destination.Length]; bool success2 = TryFormatDecimalG(ref number, buffer2, out int bytesWritten2); Debug.Assert(success2); Debug.Assert(bytesWritten2 == bytesWritten); for (int i = 0; i < bytesWritten; i++) { Debug.Assert(destination[i] == buffer2[i]); } } } #endif // DEBUG return(success); } case 'f': case 'F': { Number.NumberBuffer number = new Number.NumberBuffer(Number.NumberBufferKind.Decimal, stackalloc byte[Number.DecimalNumberBufferLength]); Number.DecimalToNumber(ref value, ref number); byte precision = (format.Precision == StandardFormat.NoPrecision) ? (byte)2 : format.Precision; Number.RoundNumber(ref number, number.Scale + precision, isCorrectlyRounded: false); Debug.Assert((number.Digits[0] != 0) || !number.IsNegative); // For Decimals, -0 must print as normal 0. As it happens, Number.RoundNumber already ensures this invariant. return(TryFormatDecimalF(ref number, destination, out bytesWritten, precision)); } case 'e': case 'E': { Number.NumberBuffer number = new Number.NumberBuffer(Number.NumberBufferKind.Decimal, stackalloc byte[Number.DecimalNumberBufferLength]); Number.DecimalToNumber(ref value, ref number); byte precision = (format.Precision == StandardFormat.NoPrecision) ? (byte)6 : format.Precision; Number.RoundNumber(ref number, precision + 1, isCorrectlyRounded: false); Debug.Assert((number.Digits[0] != 0) || !number.IsNegative); // For Decimals, -0 must print as normal 0. As it happens, Number.RoundNumber already ensures this invariant. return(TryFormatDecimalE(ref number, destination, out bytesWritten, precision, exponentSymbol: (byte)format.Symbol)); } default: return(FormattingHelpers.TryFormatThrowFormatException(out bytesWritten)); } }
private static bool TryFormatFloatingPoint <T>(T value, Span <byte> destination, out int bytesWritten, StandardFormat format) where T : IFormattable, ISpanFormattable { if (format.IsDefault) { format = 'G'; } switch (format.Symbol) { case 'g': case 'G': if (format.Precision != StandardFormat.NoPrecision) { throw new NotSupportedException(SR.Argument_GWithPrecisionNotSupported); } break; case 'f': case 'F': case 'e': case 'E': break; default: return(FormattingHelpers.TryFormatThrowFormatException(out bytesWritten)); } Span <char> formatText = stackalloc char[StandardFormat.FormatStringLength]; int formattedLength = format.Format(formatText); formatText = formatText.Slice(0, formattedLength); // We first try to format into a stack-allocated buffer, and if it succeeds, we can avoid // all allocation. If that fails, we fall back to allocating strings. If it proves impactful, // that allocation (as well as roundtripping from byte to char and back to byte) could be avoided by // calling into a refactored Number.FormatSingle/Double directly. const int StackBufferLength = 128; // large enough to handle the majority cases Span <char> stackBuffer = stackalloc char[StackBufferLength]; ReadOnlySpan <char> utf16Text = stackalloc char[0]; // Try to format into the stack buffer. If we're successful, we can avoid all allocations. if (value.TryFormat(stackBuffer, out formattedLength, formatText, CultureInfo.InvariantCulture)) { utf16Text = stackBuffer.Slice(0, formattedLength); } else { // The stack buffer wasn't large enough. If the destination buffer isn't at least as // big as the stack buffer, we know the whole operation will eventually fail, so we // can just fail now. if (destination.Length <= StackBufferLength) { bytesWritten = 0; return(false); } // Fall back to using a string format and allocating a string for the resulting formatted value. utf16Text = value.ToString(new string(formatText), CultureInfo.InvariantCulture); } // Copy the value to the destination, if it's large enough. if (utf16Text.Length > destination.Length) { bytesWritten = 0; return(false); } for (int i = 0; i < utf16Text.Length; i++) { Debug.Assert(utf16Text[i] < 128, "A culture-invariant ToString() of a floating point expected to produce ASCII characters only."); destination[i] = (byte)utf16Text[i]; } bytesWritten = utf16Text.Length; return(true); }