Update Reference Sources to .NET Framework 4.6.1
[mono.git] / mcs / class / referencesource / System.Data.SqlXml / System / Xml / Xsl / Runtime / XslNumber.cs
1 //------------------------------------------------------------------------------
2 // <copyright file="XslNumber.cs" company="Microsoft">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 // <owner current="true" primary="true">[....]</owner>
6 //------------------------------------------------------------------------------
7
8 using System.Collections;
9 using System.Collections.Generic;
10 using System.Diagnostics;
11 using System.Text;
12 using System.Xml.XPath;
13
14 namespace System.Xml.Xsl.Runtime {
15
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
21
22         // Instances of this internal class must be created via CreateFormat and CreateSeparator
23         private TokenInfo() {
24         }
25
26         [Conditional("DEBUG")]
27         public void AssertSeparator(bool isSeparator) {
28             Debug.Assert(isSeparator == (formatString != null), "AssertSeparator");
29         }
30
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;
38             }
39             return token;
40         }
41
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;
51             token.length = 1;
52
53             bool useDefault = false;
54             char ch = formatString[startIdx];
55
56             switch (ch) {
57             case '1':
58             case 'A':
59             case 'I':
60             case 'a':
61             case 'i':
62                 break;
63             default:
64                 // NOTE: We do not support Tamil and Ethiopic numbering systems having no zeros
65                 if (CharUtil.IsDecimalDigitOne(ch)) {
66                     break;
67                 }
68                 if (CharUtil.IsDecimalDigitOne((char)(ch + 1))) {
69                     // Leading zeros request padding.  Track how much.
70                     int idx = startIdx;
71                     do {
72                         token.length++;
73                     } while (--tokLen > 0 && ch == formatString[++idx]);
74
75                     // Recognize the token only if the next character is "one"
76                     if (formatString[idx] == ++ch) {
77                         break;
78                     }
79                 }
80                 useDefault = true;
81                 break;
82             }
83
84             if (tokLen != 1) {
85                 // If remaining token length is not 1, do not recognize the token
86                 useDefault = true;
87             }
88
89             if (useDefault) {
90                 // Default to Arabic numbering with no zero padding
91                 token.startChar = NumberFormatter.DefaultStartChar;
92                 token.length = 1;
93             } else {
94                 token.startChar = ch;
95             }
96             return token;
97         }
98     }
99
100     internal class NumberFormatter : NumberFormatterBase {
101         private string          formatString;
102         private int             lang;
103         private string          letterValue;
104         private string          groupingSeparator;
105         private int             groupingSize;
106
107         private List<TokenInfo> tokens;
108
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);
112
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;
117             this.lang               = lang;
118             this.letterValue        = letterValue;
119             this.groupingSeparator  = groupingSeparator;
120             this.groupingSize       = groupingSeparator.Length > 0 ? groupingSize : 0;
121
122             if (formatString == "1" || formatString.Length == 0) {
123                 // Special case of the default format
124                 return;
125             }
126
127             this.tokens = new List<TokenInfo>();
128             int idxStart = 0;
129             bool isAlphaNumeric = CharUtil.IsAlphaNumeric(formatString[idxStart]);
130
131             if (isAlphaNumeric) {
132                 // If the first one is alpha num add empty separator as a prefix
133                 tokens.Add(null);
134             }
135
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));
142                     } else {
143                         // Just finished a separator token
144                         tokens.Add(TokenInfo.CreateSeparator(formatString, idxStart, idx - idxStart));
145                     }
146
147                     // Begin parsing the next format token or separator
148                     idxStart = idx;
149
150                     // Flip flag from format token to separator or vice-versa
151                     isAlphaNumeric = !isAlphaNumeric;
152                 }
153             }
154         }
155
156         /// <summary>
157         /// Format the given xsl:number place marker
158         /// </summary>
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();
163
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);
172                 }
173             }
174
175             if (tokens == null) {
176                 // Special case of the default format
177                 for (int idx = 0; idx < val.Count; idx++) {
178                     if (idx > 0) {
179                         sb.Append('.');
180                     }
181                     FormatItem(sb, val[idx], DefaultStartChar, 1);
182                 }
183             } else {
184                 int cFormats = tokens.Count;
185                 TokenInfo prefix = tokens[0], suffix;
186
187                 if (cFormats % 2 == 0) {
188                     suffix = null;
189                 } else {
190                     suffix = tokens[--cFormats];
191                 }
192
193                 TokenInfo periodicSeparator = 2 < cFormats ? tokens[cFormats - 2] : DefaultSeparator;
194                 TokenInfo periodicFormat    = 0 < cFormats ? tokens[cFormats - 1] : DefaultFormat;
195
196                 if (prefix != null) {
197                     prefix.AssertSeparator(true);
198                     sb.Append(prefix.formatString, prefix.startIdx, prefix.length);
199                 }
200
201                 int valCount = val.Count;
202                 for (int i = 0; i < valCount; i++ ) {
203                     int formatIndex = i * 2;
204                     bool haveFormat = formatIndex < cFormats;
205
206                     if (i > 0) {
207                         TokenInfo thisSeparator = haveFormat ? tokens[formatIndex + 0] : periodicSeparator;
208                         thisSeparator.AssertSeparator(true);
209                         sb.Append(thisSeparator.formatString, thisSeparator.startIdx, thisSeparator.length);
210                     }
211
212                     TokenInfo thisFormat = haveFormat ? tokens[formatIndex + 1] : periodicFormat;
213                     thisFormat.AssertSeparator(false);
214                     FormatItem(sb, val[i], thisFormat.startChar, thisFormat.length);
215                 }
216
217                 if (suffix != null) {
218                     suffix.AssertSeparator(true);
219                     sb.Append(suffix.formatString, suffix.startIdx, suffix.length);
220                 }
221             }
222             return sb.ToString();
223         }
224
225         private void FormatItem(StringBuilder sb, XPathItem item, char startChar, int length) {
226             double dblVal;
227
228             if (item.ValueType == typeof(int)) {
229                 dblVal = (double)item.ValueAsInt;
230             } else {
231                 Debug.Assert(item.ValueType == typeof(double), "Item must be either of type int, or double");
232                 dblVal = XsltFunctions.Round(item.ValueAsDouble);
233             }
234
235             Debug.Assert(1 <= dblVal && dblVal < double.PositiveInfinity);
236             char zero = '0';
237
238             switch (startChar) {
239             case '1':
240                 break;
241             case 'A':
242             case 'a':
243                 if (dblVal <= MaxAlphabeticValue) {
244                     ConvertToAlphabetic(sb, dblVal, startChar, 26);
245                     return;
246                 }
247                 break;
248             case 'I':
249             case 'i':
250                 if (dblVal <= MaxRomanValue) {
251                     ConvertToRoman(sb, dblVal, /*upperCase:*/ startChar == 'I');
252                     return;
253                 }
254                 break;
255             default:
256                 Debug.Assert(CharUtil.IsDecimalDigitOne(startChar), "Unexpected startChar: " + startChar);
257                 zero = (char)(startChar - 1);
258                 break;
259             }
260
261             sb.Append(ConvertToDecimal(dblVal, length, zero, groupingSeparator, groupingSize));
262         }
263
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';
268
269             // Figure out new string length without separators
270             int oldLen = str.Length;
271             int newLen = Math.Max(oldLen, minLen);
272
273             // Calculate length of string with separators
274             if (groupSize != 0) {
275                 Debug.Assert(groupSeparator.Length == 1);
276                 checked { newLen += (newLen - 1) / groupSize; }
277             }
278
279             // If the new number of characters equals the old one, no changes need to be made
280             if (newLen == oldLen && shift == 0) {
281                 return str;
282             }
283
284             // If grouping is not needed, add zero padding only
285             if (groupSize == 0 && shift == 0) {
286                 return str.PadLeft(newLen, zero);
287             }
288
289             // Add both grouping separators and zero padding to the string representation of a number
290         #if true
291             unsafe {
292                 char *result = stackalloc char[newLen];
293                 char separator = (groupSeparator.Length > 0) ? groupSeparator[0] : ' ';
294
295                 fixed (char *pin = str) {
296                     char *pOldEnd = pin + oldLen - 1;
297                     char *pNewEnd = result + newLen - 1;
298                     int cnt = groupSize;
299
300                     while (true) {
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) {
304                             break;
305                         }
306                         if (/*groupSize > 0 && */--cnt == 0) {
307                             // Every groupSize digits insert the separator
308                             *pNewEnd-- = separator;
309                             cnt = groupSize;
310                             Debug.Assert(pNewEnd >= result, "Separator cannot be the first character");
311                         }
312                     }
313                 }
314                 return new string(result, 0, newLen);
315             }
316         #else
317             // Safe version is about 20% slower after NGEN
318             char[] result = new char[newLen];
319             char separator = (groupSeparator.Length > 0) ? groupSeparator[0] : ' ';
320
321             int oldEnd = oldLen - 1;
322             int newEnd = newLen - 1;
323             int cnt = groupSize;
324
325             while (true) {
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;
328                 if (newEnd < 0) {
329                     break;
330                 }
331                 if (/*groupSize > 0 && */--cnt == 0) {
332                     // Every groupSize digits insert the separator
333                     result[newEnd--] = separator;
334                     cnt = groupSize;
335                     Debug.Assert(newEnd >= 0, "Separator cannot be the first character");
336                 }
337             }
338             return new string(result, 0, newLen);
339         #endif
340         }
341     }
342 }