1 //------------------------------------------------------------------------------
2 // <copyright file="XslNumber.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 // <owner current="true" primary="true">[....]</owner>
6 //------------------------------------------------------------------------------
8 using System.Collections;
9 using System.Collections.Generic;
10 using System.Diagnostics;
12 using System.Xml.XPath;
14 namespace System.Xml.Xsl.Runtime {
16 internal class TokenInfo {
17 public char startChar; // First element of numbering sequence for format token
18 public int startIdx; // Start index of separator token
19 public string formatString; // Format string for separator token
20 public int length; // Length of separator token, or minimum length of decimal numbers for format token
22 // Instances of this internal class must be created via CreateFormat and CreateSeparator
26 [Conditional("DEBUG")]
27 public void AssertSeparator(bool isSeparator) {
28 Debug.Assert(isSeparator == (formatString != null), "AssertSeparator");
31 // Creates a TokenInfo for a separator token.
32 public static TokenInfo CreateSeparator(string formatString, int startIdx, int tokLen) {
33 Debug.Assert(startIdx >= 0 && tokLen > 0);
34 TokenInfo token = new TokenInfo(); {
35 token.startIdx = startIdx;
36 token.formatString = formatString;
37 token.length = tokLen;
42 // Maps a token of alphanumeric characters to a numbering format ID and a
43 // minimum length bound. Tokens specify the character(s) that begins a Unicode
44 // numbering sequence. For example, "i" specifies lower case roman numeral
45 // numbering. Leading "zeros" specify a minimum length to be maintained by
46 // padding, if necessary.
47 public static TokenInfo CreateFormat(string formatString, int startIdx, int tokLen) {
48 Debug.Assert(startIdx >= 0 && tokLen > 0);
49 TokenInfo token = new TokenInfo();
50 token.formatString = null;
53 bool useDefault = false;
54 char ch = formatString[startIdx];
64 // NOTE: We do not support Tamil and Ethiopic numbering systems having no zeros
65 if (CharUtil.IsDecimalDigitOne(ch)) {
68 if (CharUtil.IsDecimalDigitOne((char)(ch + 1))) {
69 // Leading zeros request padding. Track how much.
73 } while (--tokLen > 0 && ch == formatString[++idx]);
75 // Recognize the token only if the next character is "one"
76 if (formatString[idx] == ++ch) {
85 // If remaining token length is not 1, do not recognize the token
90 // Default to Arabic numbering with no zero padding
91 token.startChar = NumberFormatter.DefaultStartChar;
100 internal class NumberFormatter : NumberFormatterBase {
101 private string formatString;
103 private string letterValue;
104 private string groupingSeparator;
105 private int groupingSize;
107 private List<TokenInfo> tokens;
109 public const char DefaultStartChar = '1';
110 private static readonly TokenInfo DefaultFormat = TokenInfo.CreateFormat ("0", 0, 1);
111 private static readonly TokenInfo DefaultSeparator = TokenInfo.CreateSeparator(".", 0, 1);
113 // Creates a Format object parsing format string into format tokens (alphanumeric) and separators (non-alphanumeric).
114 public NumberFormatter(string formatString, int lang, string letterValue, string groupingSeparator, int groupingSize) {
115 Debug.Assert(groupingSeparator.Length <= 1);
116 this.formatString = formatString;
118 this.letterValue = letterValue;
119 this.groupingSeparator = groupingSeparator;
120 this.groupingSize = groupingSeparator.Length > 0 ? groupingSize : 0;
122 if (formatString == "1" || formatString.Length == 0) {
123 // Special case of the default format
127 this.tokens = new List<TokenInfo>();
129 bool isAlphaNumeric = CharUtil.IsAlphaNumeric(formatString[idxStart]);
131 if (isAlphaNumeric) {
132 // If the first one is alpha num add empty separator as a prefix
136 for (int idx = 0; idx <= formatString.Length; idx++) {
137 // Loop until a switch from formatString token to separator is detected (or vice-versa)
138 if (idx == formatString.Length || isAlphaNumeric != CharUtil.IsAlphaNumeric(formatString[idx])) {
139 if (isAlphaNumeric) {
140 // Just finished a format token
141 tokens.Add(TokenInfo.CreateFormat(formatString, idxStart, idx - idxStart));
143 // Just finished a separator token
144 tokens.Add(TokenInfo.CreateSeparator(formatString, idxStart, idx - idxStart));
147 // Begin parsing the next format token or separator
150 // Flip flag from format token to separator or vice-versa
151 isAlphaNumeric = !isAlphaNumeric;
157 /// Format the given xsl:number place marker
159 /// <param name="val">Place marker - either a sequence of ints, or a double singleton</param>
160 /// <returns>Formatted string</returns>
161 public string FormatSequence(IList<XPathItem> val) {
162 StringBuilder sb = new StringBuilder();
164 // If the value was supplied directly, in the 'value' attribute, check its validity
165 if (val.Count == 1 && val[0].ValueType == typeof(double)) {
166 double dblVal = val[0].ValueAsDouble;
167 if (!(0.5 <= dblVal && dblVal < double.PositiveInfinity)) {
168 // Errata E24: It is an error if the number is NaN, infinite or less than 0.5; an XSLT processor may signal
169 // the error; if it does not signal the error, it must recover by converting the number to a string as if
170 // by a call to the 'string' function and inserting the resulting string into the result tree.
171 return XPathConvert.DoubleToString(dblVal);
175 if (tokens == null) {
176 // Special case of the default format
177 for (int idx = 0; idx < val.Count; idx++) {
181 FormatItem(sb, val[idx], DefaultStartChar, 1);
184 int cFormats = tokens.Count;
185 TokenInfo prefix = tokens[0], suffix;
187 if (cFormats % 2 == 0) {
190 suffix = tokens[--cFormats];
193 TokenInfo periodicSeparator = 2 < cFormats ? tokens[cFormats - 2] : DefaultSeparator;
194 TokenInfo periodicFormat = 0 < cFormats ? tokens[cFormats - 1] : DefaultFormat;
196 if (prefix != null) {
197 prefix.AssertSeparator(true);
198 sb.Append(prefix.formatString, prefix.startIdx, prefix.length);
201 int valCount = val.Count;
202 for (int i = 0; i < valCount; i++ ) {
203 int formatIndex = i * 2;
204 bool haveFormat = formatIndex < cFormats;
207 TokenInfo thisSeparator = haveFormat ? tokens[formatIndex + 0] : periodicSeparator;
208 thisSeparator.AssertSeparator(true);
209 sb.Append(thisSeparator.formatString, thisSeparator.startIdx, thisSeparator.length);
212 TokenInfo thisFormat = haveFormat ? tokens[formatIndex + 1] : periodicFormat;
213 thisFormat.AssertSeparator(false);
214 FormatItem(sb, val[i], thisFormat.startChar, thisFormat.length);
217 if (suffix != null) {
218 suffix.AssertSeparator(true);
219 sb.Append(suffix.formatString, suffix.startIdx, suffix.length);
222 return sb.ToString();
225 private void FormatItem(StringBuilder sb, XPathItem item, char startChar, int length) {
228 if (item.ValueType == typeof(int)) {
229 dblVal = (double)item.ValueAsInt;
231 Debug.Assert(item.ValueType == typeof(double), "Item must be either of type int, or double");
232 dblVal = XsltFunctions.Round(item.ValueAsDouble);
235 Debug.Assert(1 <= dblVal && dblVal < double.PositiveInfinity);
243 if (dblVal <= MaxAlphabeticValue) {
244 ConvertToAlphabetic(sb, dblVal, startChar, 26);
250 if (dblVal <= MaxRomanValue) {
251 ConvertToRoman(sb, dblVal, /*upperCase:*/ startChar == 'I');
256 Debug.Assert(CharUtil.IsDecimalDigitOne(startChar), "Unexpected startChar: " + startChar);
257 zero = (char)(startChar - 1);
261 sb.Append(ConvertToDecimal(dblVal, length, zero, groupingSeparator, groupingSize));
264 private static string ConvertToDecimal(double val, int minLen, char zero, string groupSeparator, int groupSize) {
265 Debug.Assert(val >= 0 && val == Math.Round(val), "ConvertToArabic operates on non-negative integer numbers only");
266 string str = XPathConvert.DoubleToString(val);
267 int shift = zero - '0';
269 // Figure out new string length without separators
270 int oldLen = str.Length;
271 int newLen = Math.Max(oldLen, minLen);
273 // Calculate length of string with separators
274 if (groupSize != 0) {
275 Debug.Assert(groupSeparator.Length == 1);
276 checked { newLen += (newLen - 1) / groupSize; }
279 // If the new number of characters equals the old one, no changes need to be made
280 if (newLen == oldLen && shift == 0) {
284 // If grouping is not needed, add zero padding only
285 if (groupSize == 0 && shift == 0) {
286 return str.PadLeft(newLen, zero);
289 // Add both grouping separators and zero padding to the string representation of a number
292 char *result = stackalloc char[newLen];
293 char separator = (groupSeparator.Length > 0) ? groupSeparator[0] : ' ';
295 fixed (char *pin = str) {
296 char *pOldEnd = pin + oldLen - 1;
297 char *pNewEnd = result + newLen - 1;
301 // Move digit to its new location (zero if we've run out of digits)
302 *pNewEnd-- = (pOldEnd >= pin) ? (char)(*pOldEnd-- + shift) : zero;
303 if (pNewEnd < result) {
306 if (/*groupSize > 0 && */--cnt == 0) {
307 // Every groupSize digits insert the separator
308 *pNewEnd-- = separator;
310 Debug.Assert(pNewEnd >= result, "Separator cannot be the first character");
314 return new string(result, 0, newLen);
317 // Safe version is about 20% slower after NGEN
318 char[] result = new char[newLen];
319 char separator = (groupSeparator.Length > 0) ? groupSeparator[0] : ' ';
321 int oldEnd = oldLen - 1;
322 int newEnd = newLen - 1;
326 // Move digit to its new location (zero if we've run out of digits)
327 result[newEnd--] = (oldEnd >= 0) ? (char)(str[oldEnd--] + shift) : zero;
331 if (/*groupSize > 0 && */--cnt == 0) {
332 // Every groupSize digits insert the separator
333 result[newEnd--] = separator;
335 Debug.Assert(newEnd >= 0, "Separator cannot be the first character");
338 return new string(result, 0, newLen);