diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Xsl/Runtime/XslNumber.cs b/src/libraries/System.Private.Xml/src/System/Xml/Xsl/Runtime/XslNumber.cs index cc897eaa574937..9a269e1e756420 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Xsl/Runtime/XslNumber.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Xsl/Runtime/XslNumber.cs @@ -333,36 +333,30 @@ private static string ConvertToDecimal(double val, int minLen, char zero, string } // Add both grouping separators and zero padding to the string representation of a number - unsafe + char separator = groupSeparator.Length > 0 ? groupSeparator[0] : ' '; + return string.Create(newLen, (str, shift, zero, separator, groupSize), static (result, state) => { - char* result = stackalloc char[newLen]; - char separator = (groupSeparator.Length > 0) ? groupSeparator[0] : ' '; + var (str, shift, zero, separator, groupSize) = state; + int cnt = groupSize; + int oldPos = str.Length - 1; - fixed (char* pin = str) + for (int newPos = result.Length - 1; newPos >= 0; newPos--) { - char* pOldEnd = pin + oldLen - 1; - char* pNewEnd = result + newLen - 1; - int cnt = groupSize; - - while (true) + if (groupSize != 0 && cnt == 0) + { + // Every groupSize digits insert the separator + result[newPos] = separator; + cnt = groupSize; + Debug.Assert(newPos > 0, "Separator cannot be the first character"); + } + else { // Move digit to its new location (zero if we've run out of digits) - *pNewEnd-- = (pOldEnd >= pin) ? (char)(*pOldEnd-- + shift) : zero; - if (pNewEnd < result) - { - break; - } - if (/*groupSize > 0 && */--cnt == 0) - { - // Every groupSize digits insert the separator - *pNewEnd-- = separator; - cnt = groupSize; - Debug.Assert(pNewEnd >= result, "Separator cannot be the first character"); - } + result[newPos] = oldPos >= 0 ? (char)(str[oldPos--] + shift) : zero; + cnt--; } } - return new string(result, 0, newLen); - } + }); } } } diff --git a/src/libraries/System.Private.Xml/tests/Xslt/XslCompiledTransformApi/XslCompilerTests.cs b/src/libraries/System.Private.Xml/tests/Xslt/XslCompiledTransformApi/XslCompilerTests.cs index 0c6cfcd7a6e5c3..69101e290d2f42 100644 --- a/src/libraries/System.Private.Xml/tests/Xslt/XslCompiledTransformApi/XslCompilerTests.cs +++ b/src/libraries/System.Private.Xml/tests/Xslt/XslCompiledTransformApi/XslCompilerTests.cs @@ -111,6 +111,68 @@ public void FormatTimeWithEmptyFormatString() } } + public static TheoryData XslNumberDecimalData => new TheoryData + { + // (value, format, grouping-separator + grouping-size attributes, expectedOutput) + // Basic decimal (fast path: no changes needed) + { "1", "1", "", "1" }, + { "1234", "1", "", "1234" }, + // Padding only (fast path: PadLeft) + { "42", "001", "", "042" }, + { "5", "0001", "", "0005" }, + // Grouping separator (string.Create path) + { "1234567", "1", @"grouping-separator="","" grouping-size=""3""", "1,234,567" }, + { "1000", "1", @"grouping-separator="","" grouping-size=""3""", "1,000" }, + { "100", "1", @"grouping-separator="","" grouping-size=""3""", "100" }, + { "1234", "1", @"grouping-separator="","" grouping-size=""3""", "1,234" }, + // Padding with grouping separator (string.Create path) + { "42", "001", @"grouping-separator="","" grouping-size=""3""", "042" }, + { "42", "00001", @"grouping-separator="","" grouping-size=""3""", "00,042" }, + // Non-ASCII digit system - Arabic-Indic digits (string.Create path, shift != 0) + { "123", "\u0661", "", "\u0661\u0662\u0663" }, + { "1234", "\u0661", "", "\u0661\u0662\u0663\u0664" }, + // Non-ASCII digits with grouping separator (shift != 0 AND groupSize != 0) + { "1234", "\u0661", @"grouping-separator="","" grouping-size=""3""", "\u0661,\u0662\u0663\u0664" }, + // Non-ASCII digits with zero padding (shift != 0, pad with non-ASCII zero) + { "5", "\u0660\u0661", "", "\u0660\u0665" }, + // Non-standard grouping size (grouping-size="2") + { "12345", "1", @"grouping-separator="","" grouping-size=""2""", "1,23,45" }, + // Padding that creates additional groups + { "42", "0000001", @"grouping-separator="","" grouping-size=""3""", "0,000,042" }, + // Single digit with grouping enabled (no separator should appear) + { "5", "1", @"grouping-separator="","" grouping-size=""3""", "5" }, + // Number already exceeds format token length (no padding needed) + { "100", "01", "", "100" }, + // Thai digits (another non-ASCII decimal system, shift != 0) + { "42", "\u0E51", "", "\u0E54\u0E52" }, + // Non-ASCII digits with grouping AND padding + { "42", "\u0660\u0660\u0660\u0660\u0661", @"grouping-separator="","" grouping-size=""3""", "\u0660\u0660,\u0660\u0664\u0662" }, + }; + + [Theory] + [MemberData(nameof(XslNumberDecimalData))] + public void XslNumberDecimalFormatting(string value, string format, string groupingAttrs, string expected) + { + string xml = @""; + string xsl = $@" + + + + +"; + using var outWriter = new StringWriter(); + using (var xslStringReader = new StringReader(xsl)) + using (var xmlStringReader = new StringReader(xml)) + using (var xslReader = XmlReader.Create(xslStringReader)) + using (var xmlReader = XmlReader.Create(xmlStringReader)) + { + var transform = new XslCompiledTransform(); + transform.Load(xslReader); + transform.Transform(xmlReader, null, outWriter); + } + + Assert.Equal(expected, outWriter.ToString()); + } } }