File: System\Xml\Xsl\Runtime\DecimalFormatter.cs
Web Access
Project: src\src\libraries\System.Private.Xml\src\System.Private.Xml.csproj (System.Private.Xml)
// 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;
using System.Text;
 
namespace System.Xml.Xsl.Runtime
{
    internal sealed class DecimalFormat
    {
        public NumberFormatInfo info;
        public char digit;
        public char zeroDigit;
        public char patternSeparator;
 
        internal DecimalFormat(NumberFormatInfo info, char digit, char zeroDigit, char patternSeparator)
        {
            this.info = info;
            this.digit = digit;
            this.zeroDigit = zeroDigit;
            this.patternSeparator = patternSeparator;
        }
    }
 
    internal sealed class DecimalFormatter
    {
        private readonly NumberFormatInfo _posFormatInfo;
        private readonly NumberFormatInfo? _negFormatInfo;
        private readonly string? _posFormat;
        private readonly string? _negFormat;
        private readonly char _zeroDigit;
 
        // These characters have special meaning for CLR and must be escaped
        // <spec>https://learn.microsoft.com/dotnet/standard/base-types/custom-numeric-format-strings</spec>
        private const string ClrSpecialChars = "0#.,%\u2030Ee\\'\";";
 
        // This character is used to escape literal (passive) digits '0'..'9'
        private const char EscChar = '\a';
 
        public DecimalFormatter(string formatPicture, DecimalFormat decimalFormat)
        {
            Debug.Assert(formatPicture != null && decimalFormat != null);
            if (formatPicture.Length == 0)
            {
                throw XsltException.Create(SR.Xslt_InvalidFormat);
            }
 
            _zeroDigit = decimalFormat.zeroDigit;
            _posFormatInfo = (NumberFormatInfo)decimalFormat.info.Clone();
            StringBuilder temp = new StringBuilder();
 
            bool integer = true;
            bool sawPattern = false, sawZeroDigit = false, sawDigit = false, sawDecimalSeparator = false;
            bool digitOrZeroDigit = false;
            char decimalSeparator = _posFormatInfo.NumberDecimalSeparator[0];
            char groupSeparator = _posFormatInfo.NumberGroupSeparator[0];
            char percentSymbol = _posFormatInfo.PercentSymbol[0];
            char perMilleSymbol = _posFormatInfo.PerMilleSymbol[0];
 
            int commaIndex = 0;
            int groupingSize;
            int decimalIndex = -1;
            int lastDigitIndex = -1;
 
            for (int i = 0; i < formatPicture.Length; i++)
            {
                char ch = formatPicture[i];
 
                if (ch == decimalFormat.digit)
                {
                    if (sawZeroDigit && integer)
                    {
                        throw XsltException.Create(SR.Xslt_InvalidFormat1, formatPicture);
                    }
                    lastDigitIndex = temp.Length;
                    sawDigit = digitOrZeroDigit = true;
                    temp.Append('#');
                    continue;
                }
                if (ch == decimalFormat.zeroDigit)
                {
                    if (sawDigit && !integer)
                    {
                        throw XsltException.Create(SR.Xslt_InvalidFormat2, formatPicture);
                    }
                    lastDigitIndex = temp.Length;
                    sawZeroDigit = digitOrZeroDigit = true;
                    temp.Append('0');
                    continue;
                }
                if (ch == decimalFormat.patternSeparator)
                {
                    if (!digitOrZeroDigit)
                    {
                        throw XsltException.Create(SR.Xslt_InvalidFormat8);
                    }
                    if (sawPattern)
                    {
                        throw XsltException.Create(SR.Xslt_InvalidFormat3, formatPicture);
                    }
                    sawPattern = true;
 
                    if (decimalIndex < 0)
                    {
                        decimalIndex = lastDigitIndex + 1;
                    }
                    groupingSize = RemoveTrailingComma(temp, commaIndex, decimalIndex);
 
                    if (groupingSize > 9)
                    {
                        groupingSize = 0;
                    }
                    _posFormatInfo.NumberGroupSizes = new int[] { groupingSize };
                    if (!sawDecimalSeparator)
                    {
                        _posFormatInfo.NumberDecimalDigits = 0;
                    }
 
                    _posFormat = temp.ToString();
 
                    temp.Length = 0;
                    decimalIndex = -1;
                    lastDigitIndex = -1;
                    commaIndex = 0;
                    sawDigit = sawZeroDigit = digitOrZeroDigit = false;
                    sawDecimalSeparator = false;
                    integer = true;
                    _negFormatInfo = (NumberFormatInfo)decimalFormat.info.Clone();
                    _negFormatInfo.NegativeSign = string.Empty;
                    continue;
                }
                if (ch == decimalSeparator)
                {
                    if (sawDecimalSeparator)
                    {
                        throw XsltException.Create(SR.Xslt_InvalidFormat5, formatPicture);
                    }
                    decimalIndex = temp.Length;
                    sawDecimalSeparator = true;
                    sawDigit = sawZeroDigit = integer = false;
                    temp.Append('.');
                    continue;
                }
                if (ch == groupSeparator)
                {
                    commaIndex = temp.Length;
                    lastDigitIndex = commaIndex;
                    temp.Append(',');
                    continue;
                }
                if (ch == percentSymbol)
                {
                    temp.Append('%');
                    continue;
                }
                if (ch == perMilleSymbol)
                {
                    temp.Append('\u2030');
                    continue;
                }
                if (ch == '\'')
                {
                    int pos = formatPicture.IndexOf('\'', i + 1);
                    if (pos < 0)
                    {
                        pos = formatPicture.Length - 1;
                    }
                    temp.Append(formatPicture, i, pos - i + 1);
                    i = pos;
                    continue;
                }
                // Escape literal digits with EscChar, double literal EscChar
                if (char.IsAsciiDigit(ch) || ch == EscChar)
                {
                    if (decimalFormat.zeroDigit != '0')
                    {
                        temp.Append(EscChar);
                    }
                }
                // Escape characters having special meaning for CLR
                if (ClrSpecialChars.Contains(ch))
                {
                    temp.Append('\\');
                }
                temp.Append(ch);
            }
 
            if (!digitOrZeroDigit)
            {
                throw XsltException.Create(SR.Xslt_InvalidFormat8);
            }
            NumberFormatInfo formatInfo = sawPattern ? _negFormatInfo! : _posFormatInfo;
 
            if (decimalIndex < 0)
            {
                decimalIndex = lastDigitIndex + 1;
            }
            groupingSize = RemoveTrailingComma(temp, commaIndex, decimalIndex);
            if (groupingSize > 9)
            {
                groupingSize = 0;
            }
            formatInfo.NumberGroupSizes = new int[] { groupingSize };
            if (!sawDecimalSeparator)
            {
                formatInfo.NumberDecimalDigits = 0;
            }
 
            if (sawPattern)
            {
                _negFormat = temp.ToString();
            }
            else
            {
                _posFormat = temp.ToString();
            }
        }
 
        private static int RemoveTrailingComma(StringBuilder builder, int commaIndex, int decimalIndex)
        {
            if (commaIndex > 0 && commaIndex == (decimalIndex - 1))
            {
                builder.Remove(decimalIndex - 1, 1);
            }
            else if (decimalIndex > commaIndex)
            {
                return decimalIndex - commaIndex - 1;
            }
            return 0;
        }
 
        public string Format(double value)
        {
            NumberFormatInfo formatInfo;
            string? subPicture;
 
            if (value < 0 && _negFormatInfo != null)
            {
                formatInfo = _negFormatInfo;
                subPicture = _negFormat;
            }
            else
            {
                formatInfo = _posFormatInfo;
                subPicture = _posFormat;
            }
 
            string result = value.ToString(subPicture, formatInfo);
 
            if (_zeroDigit != '0')
            {
                StringBuilder builder = new StringBuilder(result.Length);
                int shift = _zeroDigit - '0';
                for (int i = 0; i < result.Length; i++)
                {
                    char ch = result[i];
                    if (char.IsAsciiDigit(ch))
                    {
                        ch += (char)shift;
                    }
                    else if (ch == EscChar)
                    {
                        // This is an escaped literal digit or EscChar, thus unescape it. We make use
                        // of the fact that no extra EscChar could be inserted by value.ToString().
                        Debug.Assert(i + 1 < result.Length);
                        ch = result[++i];
                        Debug.Assert(char.IsAsciiDigit(ch) || ch == EscChar);
                    }
                    builder.Append(ch);
                }
                result = builder.ToString();
            }
            return result;
        }
 
        public static string Format(double value, string formatPicture, DecimalFormat decimalFormat)
        {
            return new DecimalFormatter(formatPicture, decimalFormat).Format(value);
        }
    }
}