Updates referencesource to .NET 4.7
[mono.git] / mcs / class / referencesource / System.Data.SqlXml / System / Xml / Xsl / XsltOld / NumberAction.cs
1 //------------------------------------------------------------------------------
2 // <copyright file="NumberAction.cs" company="Microsoft">
3 //     Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 // <owner current="true" primary="true">Microsoft</owner>
6 //------------------------------------------------------------------------------
7
8 namespace System.Xml.Xsl.XsltOld {
9     using Res = System.Xml.Utils.Res;
10     using System.Diagnostics;
11     using System.Text;
12     using System.Globalization;
13     using System.Collections;
14     using System.Collections.Generic;
15     using System.Xml.XPath;
16     using System.Xml.Xsl.Runtime;
17
18     internal class NumberAction : ContainerAction {
19         const long msofnfcNil  =            0x00000000;     // no flags
20         const long msofnfcTraditional =     0x00000001;     // use traditional numbering
21         const long msofnfcAlwaysFormat =    0x00000002;     // if requested format is not supported, use Arabic (Western) style
22
23         const int cchMaxFormat        = 63 ;     // max size of formatted result
24         const int cchMaxFormatDecimal = 11  ;    // max size of formatted decimal result (doesn't handle the case of a very large pwszSeparator or minlen)
25
26         internal class FormatInfo {
27             public bool               isSeparator;      // False for alphanumeric strings of chars
28             public NumberingSequence  numSequence;      // Specifies numbering sequence
29             public int                length;           // Minimum length of decimal numbers (if necessary, pad to left with zeros)
30             public string             formatString;     // Format string for separator token
31
32             public FormatInfo(bool isSeparator, string formatString) {
33                 this.isSeparator  = isSeparator;
34                 this.formatString = formatString;
35             }
36
37             public FormatInfo() {}
38         }
39
40         static FormatInfo DefaultFormat    = new FormatInfo(false, "0");
41         static FormatInfo DefaultSeparator = new FormatInfo(true , ".");
42
43         class NumberingFormat : NumberFormatterBase {
44             NumberingSequence seq;
45             int     cMinLen;
46             string  separator;
47             int     sizeGroup;
48
49             internal NumberingFormat() {}
50
51             internal void setNumberingType(NumberingSequence seq) { this.seq = seq; }
52             //void setLangID(LID langid) {_langid = langid;}
53             //internal void setTraditional(bool fTraditional) {_grfnfc = fTraditional ? msofnfcTraditional : 0;}
54             internal void setMinLen(int cMinLen) { this.cMinLen = cMinLen; }
55             internal void setGroupingSeparator(string separator) { this.separator = separator; }
56
57             internal void setGroupingSize(int sizeGroup) {
58                 if (0 <= sizeGroup && sizeGroup <= 9) {
59                     this.sizeGroup = sizeGroup;
60                 }
61             }
62
63             internal String FormatItem(object value) {
64                 double dblVal;
65
66                 if (value is int) {
67                     dblVal = (int)value;
68                 } else {
69                     dblVal = XmlConvert.ToXPathDouble(value);
70
71                     if (0.5 <= dblVal && !double.IsPositiveInfinity(dblVal)) {
72                         dblVal = XmlConvert.XPathRound(dblVal);
73                     } else {
74                         // It is an error if the number is NaN, infinite or less than 0.5; an XSLT processor may signal the error;
75                         // if it does not signal the error, it must recover by converting the number to a string as if by a call
76                         // to the string function and inserting the resulting string into the result tree.
77                         return XmlConvert.ToXPathString(value);
78                     }
79                 }
80
81                 Debug.Assert(dblVal >= 1);
82
83                 switch (seq) {
84                 case NumberingSequence.Arabic :
85                     break;
86                 case NumberingSequence.UCLetter :
87                 case NumberingSequence.LCLetter :
88                     if (dblVal <= MaxAlphabeticValue) {
89                         StringBuilder sb = new StringBuilder();
90                         ConvertToAlphabetic(sb, dblVal, seq == NumberingSequence.UCLetter ? 'A' : 'a', 26);
91                         return sb.ToString();
92                     }
93                     break;
94                 case NumberingSequence.UCRoman :
95                 case NumberingSequence.LCRoman:
96                     if (dblVal <= MaxRomanValue) {
97                         StringBuilder sb = new StringBuilder();
98                         ConvertToRoman(sb, dblVal, seq == NumberingSequence.UCRoman);
99                         return sb.ToString();
100                     }
101                     break;
102                 }
103
104                 return ConvertToArabic(dblVal, cMinLen, sizeGroup, separator);
105             }
106
107             static string ConvertToArabic(double val, int minLength, int groupSize, string groupSeparator) {
108                 String str;
109
110                 if (groupSize != 0 && groupSeparator != null ) {
111                     NumberFormatInfo NumberFormat = new NumberFormatInfo();
112                     NumberFormat.NumberGroupSizes = new int[] { groupSize };
113                     NumberFormat.NumberGroupSeparator = groupSeparator;
114                     if (Math.Floor(val) == val) {
115                         NumberFormat.NumberDecimalDigits = 0;
116                     }
117                     str = val.ToString("N", NumberFormat);
118                 }
119                 else {
120                     str = Convert.ToString(val, CultureInfo.InvariantCulture);
121                 }
122
123                 if (str.Length >= minLength) {
124                     return str;
125                 } else {
126                     StringBuilder sb = new StringBuilder(minLength);
127                     sb.Append('0', minLength - str.Length);
128                     sb.Append(str);
129                     return sb.ToString();
130                 }
131             }
132         }
133
134     // States:
135         private const int OutputNumber = 2;
136
137         private String    level;
138         private String    countPattern;
139         private int       countKey = Compiler.InvalidQueryKey;
140         private String    from;
141         private int       fromKey = Compiler.InvalidQueryKey;
142         private String    value;
143         private int       valueKey = Compiler.InvalidQueryKey;
144         private Avt       formatAvt;
145         private Avt       langAvt;
146         private Avt       letterAvt;
147         private Avt       groupingSepAvt;
148         private Avt       groupingSizeAvt;
149         // Compile time precalculated AVTs
150         private List<FormatInfo> formatTokens;
151         private String    lang;
152         private String    letter;
153         private String    groupingSep;
154         private String    groupingSize;
155         private bool      forwardCompatibility;
156
157         internal override bool CompileAttribute(Compiler compiler) {
158             string name   = compiler.Input.LocalName;
159             string value  = compiler.Input.Value;
160             if (Ref.Equal(name, compiler.Atoms.Level)) {
161                 if (value != "any" && value != "multiple" && value != "single") {
162                     throw XsltException.Create(Res.Xslt_InvalidAttrValue, "level", value);
163                 }
164                 this.level = value;
165             }
166             else if (Ref.Equal(name, compiler.Atoms.Count)) {
167                 this.countPattern = value;
168                 this.countKey = compiler.AddQuery(value, /*allowVars:*/true, /*allowKey:*/true, /*pattern*/true);
169             }
170             else if (Ref.Equal(name, compiler.Atoms.From)) {
171                 this.from = value;
172                 this.fromKey = compiler.AddQuery(value, /*allowVars:*/true, /*allowKey:*/true, /*pattern*/true);
173             }
174             else if (Ref.Equal(name, compiler.Atoms.Value)) {
175                 this.value = value;
176                 this.valueKey = compiler.AddQuery(value);
177             }
178             else if (Ref.Equal(name, compiler.Atoms.Format)) {
179                 this.formatAvt = Avt.CompileAvt(compiler, value);
180             }
181             else if (Ref.Equal(name, compiler.Atoms.Lang)) {
182                 this.langAvt = Avt.CompileAvt(compiler, value);
183             }
184             else if (Ref.Equal(name, compiler.Atoms.LetterValue)) {
185                 this.letterAvt = Avt.CompileAvt(compiler, value);
186             }
187             else if (Ref.Equal(name, compiler.Atoms.GroupingSeparator)) {
188                 this.groupingSepAvt = Avt.CompileAvt(compiler, value);
189             }
190             else if (Ref.Equal(name, compiler.Atoms.GroupingSize)) {
191                 this.groupingSizeAvt = Avt.CompileAvt(compiler, value);
192             }
193             else {
194                return false;
195             }
196             return true;
197         }
198
199         internal override void Compile(Compiler compiler) {
200             CompileAttributes(compiler);
201             CheckEmpty(compiler);
202
203             this.forwardCompatibility = compiler.ForwardCompatibility;
204             this.formatTokens  = ParseFormat(PrecalculateAvt(ref this.formatAvt));
205             this.letter        = ParseLetter(PrecalculateAvt(ref this.letterAvt));
206             this.lang          = PrecalculateAvt(ref this.langAvt);
207             this.groupingSep   = PrecalculateAvt(ref this.groupingSepAvt);
208             if (this.groupingSep != null && this.groupingSep.Length > 1) {
209                 throw XsltException.Create(Res.Xslt_CharAttribute, "grouping-separator");
210             }
211             this.groupingSize  = PrecalculateAvt(ref this.groupingSizeAvt);
212         }
213
214         private int numberAny(Processor processor, ActionFrame frame) {
215             int result = 0;
216             // Our current point will be our end point in this search
217             XPathNavigator endNode = frame.Node;
218             if(endNode.NodeType == XPathNodeType.Attribute || endNode.NodeType == XPathNodeType.Namespace) {
219                 endNode = endNode.Clone();
220                 endNode.MoveToParent();
221             }
222             XPathNavigator startNode = endNode.Clone();
223
224             if(this.fromKey != Compiler.InvalidQueryKey) {
225                 bool hitFrom = false;
226                 // First try to find start by traversing up. This gives the best candidate or we hit root
227                 do{
228                     if(processor.Matches(startNode, this.fromKey)) {
229                         hitFrom = true;
230                         break;
231                     }
232                 }while(startNode.MoveToParent());
233
234                 Debug.Assert(
235                     processor.Matches(startNode, this.fromKey) ||   // we hit 'from' or
236                     startNode.NodeType == XPathNodeType.Root        // we are at root
237                 );
238
239                 // from this point (matched parent | root) create descendent quiery:
240                 // we have to reset 'result' on each 'from' node, because this point can' be not last from point;
241                 XPathNodeIterator  sel = startNode.SelectDescendants(XPathNodeType.All, /*matchSelf:*/ true);
242                 while (sel.MoveNext()) {
243                     if(processor.Matches(sel.Current, this.fromKey)) {
244                         hitFrom = true;
245                         result = 0;
246                     }
247                     else if(MatchCountKey(processor, frame.Node, sel.Current)) {
248                         result ++;
249                     }
250                     if(sel.Current.IsSamePosition(endNode)) {
251                         break;
252                     }
253                 }
254                 if(! hitFrom) {
255                     result = 0;
256                 }
257             }
258             else {
259                 // without 'from' we startting from the root
260                 startNode.MoveToRoot();
261                 XPathNodeIterator  sel = startNode.SelectDescendants(XPathNodeType.All, /*matchSelf:*/ true);
262                 // and count root node by itself
263                 while (sel.MoveNext()) {
264                     if (MatchCountKey(processor, frame.Node, sel.Current)) {
265                         result ++;
266                     }
267                     if (sel.Current.IsSamePosition(endNode)) {
268                         break;
269                     }
270                 }
271             }
272             return result;
273         }
274
275         // check 'from' condition:
276         // if 'from' exist it has to be ancestor-or-self for the nav
277         private bool checkFrom(Processor processor, XPathNavigator nav) {
278             if(this.fromKey == Compiler.InvalidQueryKey) {
279                 return true;
280             }
281             do {
282                 if (processor.Matches(nav, this.fromKey)) {
283                     return true;
284                 }
285             }while (nav.MoveToParent());
286             return false;
287         }
288
289         private bool moveToCount(XPathNavigator nav, Processor processor, XPathNavigator contextNode) {
290             do {
291                 if (this.fromKey != Compiler.InvalidQueryKey && processor.Matches(nav, this.fromKey)) {
292                     return false;
293                 }
294                 if (MatchCountKey(processor, contextNode, nav)) {
295                     return true;
296                 }
297             }while (nav.MoveToParent());
298             return false;
299         }
300
301         private int numberCount(XPathNavigator nav, Processor processor, XPathNavigator contextNode) {
302             Debug.Assert(nav.NodeType != XPathNodeType.Attribute && nav.NodeType != XPathNodeType.Namespace);
303             Debug.Assert(MatchCountKey(processor, contextNode, nav));
304             XPathNavigator runner = nav.Clone();
305             int number = 1;
306             if (runner.MoveToParent()) {
307                 runner.MoveToFirstChild();
308                 while (! runner.IsSamePosition(nav)) {
309                     if (MatchCountKey(processor, contextNode, runner)) {
310                         number++;
311                     }
312                     if (! runner.MoveToNext()) {
313                         Debug.Fail("We implementing preceding-sibling::node() and some how miss context node 'nav'");
314                         break;
315                     }
316                 }
317             }
318             return number;
319         }
320
321         private static object SimplifyValue(object value) {
322             // If result of xsl:number is not in correct range it should be returned as is.
323             // so we need intermidiate string value.
324             // If it's already a double we would like to keep it as double.
325             // So this function converts to string only if if result is nodeset or RTF
326             Debug.Assert(!(value is int));
327             if (Type.GetTypeCode(value.GetType()) == TypeCode.Object) {
328                 XPathNodeIterator nodeset = value as XPathNodeIterator;
329                 if (nodeset != null) {
330                     if (nodeset.MoveNext()) {
331                         return nodeset.Current.Value;
332                     }
333                     return string.Empty;
334                 }
335                 XPathNavigator nav = value as XPathNavigator;
336                 if (nav != null) {
337                     return nav.Value;
338                 }
339             }
340             return value;
341         }
342
343         internal override void Execute(Processor processor, ActionFrame frame) {
344             Debug.Assert(processor != null && frame != null);
345             ArrayList list = processor.NumberList;
346             switch (frame.State) {
347             case Initialized:
348                 Debug.Assert(frame != null);
349                 Debug.Assert(frame.NodeSet != null);
350                 list.Clear();
351                 if (this.valueKey != Compiler.InvalidQueryKey) {
352                     list.Add(SimplifyValue(processor.Evaluate(frame, this.valueKey)));
353                 }
354                 else if (this.level == "any") {
355                     int number = numberAny(processor, frame);
356                     if (number != 0) {
357                         list.Add(number);
358                     }
359                 }
360                 else {
361                     bool multiple = (this.level == "multiple");
362                     XPathNavigator contextNode = frame.Node;         // context of xsl:number element. We using this node in MatchCountKey()
363                     XPathNavigator countNode   = frame.Node.Clone(); // node we count for
364                     if (countNode.NodeType == XPathNodeType.Attribute || countNode.NodeType == XPathNodeType.Namespace) {
365                         countNode.MoveToParent();
366                     }
367                     while (moveToCount(countNode, processor, contextNode)) {
368                         list.Insert(0, numberCount(countNode, processor, contextNode));
369                         if(! multiple || ! countNode.MoveToParent()) {
370                             break;
371                         }
372                     }
373                     if(! checkFrom(processor, countNode)) {
374                         list.Clear();
375                     }
376                 }
377
378                 /*CalculatingFormat:*/
379                 frame.StoredOutput = Format(list,
380                     this.formatAvt       == null ? this.formatTokens : ParseFormat(this.formatAvt.Evaluate(processor, frame)),
381                     this.langAvt         == null ? this.lang         : this.langAvt        .Evaluate(processor, frame),
382                     this.letterAvt       == null ? this.letter       : ParseLetter(this.letterAvt.Evaluate(processor, frame)),
383                     this.groupingSepAvt  == null ? this.groupingSep  : this.groupingSepAvt .Evaluate(processor, frame),
384                     this.groupingSizeAvt == null ? this.groupingSize : this.groupingSizeAvt.Evaluate(processor, frame)
385                 );
386                 goto case OutputNumber;
387             case OutputNumber :
388                 Debug.Assert(frame.StoredOutput != null);
389                 if (! processor.TextEvent(frame.StoredOutput)) {
390                     frame.State        = OutputNumber;
391                     break;
392                 }
393                 frame.Finished();
394                 break;
395             default:
396                 Debug.Fail("Invalid Number Action execution state");
397                 break;
398             }
399         }
400
401         private bool MatchCountKey(Processor processor, XPathNavigator contextNode, XPathNavigator nav){
402             if (this.countKey != Compiler.InvalidQueryKey) {
403                 return processor.Matches(nav, this.countKey);
404             }
405             if (contextNode.Name == nav.Name && BasicNodeType(contextNode.NodeType) == BasicNodeType(nav.NodeType)) {
406                 return true;
407             }
408             return false;
409         }
410
411         private XPathNodeType BasicNodeType(XPathNodeType type) {
412             if(type == XPathNodeType.SignificantWhitespace || type == XPathNodeType.Whitespace) {
413                 return XPathNodeType.Text;
414             }
415             else {
416                 return type;
417             }
418         }
419
420         // Microsoft: perf.
421         // for each call to xsl:number Format() will build new NumberingFormat object.
422         // in case of no AVTs we can build this object at compile time and reuse it on execution time.
423         // even partial step in this d---- will be usefull (when cFormats == 0)
424
425         private static string Format(ArrayList numberlist, List<FormatInfo> tokens, string lang, string letter, string groupingSep, string groupingSize) {
426             StringBuilder result = new StringBuilder();
427             int cFormats = 0;
428             if (tokens != null) {
429                 cFormats = tokens.Count;
430             }
431
432             NumberingFormat numberingFormat = new NumberingFormat();
433             if (groupingSize != null) {
434                 try {
435                     numberingFormat.setGroupingSize(Convert.ToInt32(groupingSize, CultureInfo.InvariantCulture));
436                 }
437                 catch (System.FormatException) {}
438                 catch (System.OverflowException) {}
439             }
440             if (groupingSep != null) {
441                 if (groupingSep.Length > 1) {
442                     // It is a breaking change to throw an exception, SQLBUDT 324367
443                     //throw XsltException.Create(Res.Xslt_CharAttribute, "grouping-separator");
444                 }
445                 numberingFormat.setGroupingSeparator(groupingSep);
446             }
447             if (0 < cFormats) {
448                 FormatInfo prefix = tokens[0];
449                 Debug.Assert(prefix == null || prefix.isSeparator);
450                 FormatInfo sufix = null;
451                 if (cFormats % 2 == 1) {
452                     sufix = tokens[cFormats - 1];
453                     cFormats --;
454                 }
455                 FormatInfo periodicSeparator = 2 < cFormats ? tokens[cFormats - 2] : DefaultSeparator;
456                 FormatInfo periodicFormat    = 0 < cFormats ? tokens[cFormats - 1] : DefaultFormat   ;
457                 if (prefix != null) {
458                     result.Append(prefix.formatString);
459                 }
460                 int numberlistCount = numberlist.Count;
461                 for(int i = 0; i < numberlistCount; i++ ) {
462                     int formatIndex   = i * 2;
463                     bool haveFormat = formatIndex < cFormats;
464                     if (0 < i) {
465                         FormatInfo thisSeparator = haveFormat ? tokens[formatIndex + 0] : periodicSeparator;
466                         Debug.Assert(thisSeparator.isSeparator);
467                         result.Append(thisSeparator.formatString);
468                     }
469
470                     FormatInfo thisFormat = haveFormat ? tokens[formatIndex + 1] : periodicFormat;
471                     Debug.Assert(!thisFormat.isSeparator);
472
473                     //numberingFormat.setletter(this.letter);
474                     //numberingFormat.setLang(this.lang);
475
476                     numberingFormat.setNumberingType(thisFormat.numSequence);
477                     numberingFormat.setMinLen(thisFormat.length);
478                     result.Append(numberingFormat.FormatItem(numberlist[i]));
479                 }
480
481                 if (sufix != null) {
482                     result.Append(sufix.formatString);
483                 }
484             }
485             else {
486                 numberingFormat.setNumberingType(NumberingSequence.Arabic);
487                 for (int i = 0; i < numberlist.Count; i++) {
488                     if (i != 0) {
489                         result.Append(".");
490                     }
491                     result.Append(numberingFormat.FormatItem(numberlist[i]));
492                 }
493             }
494             return result.ToString();
495         }
496
497         /*
498         ----------------------------------------------------------------------------
499             mapFormatToken()
500
501             Maps a token of alphanumeric characters to a numbering format ID and a
502             minimum length bound.  Tokens specify the character(s) that begins a
503             Unicode
504             numbering sequence.  For example, "i" specifies lower case roman numeral
505             numbering.  Leading "zeros" specify a minimum length to be maintained by
506             padding, if necessary.
507         ----------------------------------------------------------------------------
508         */
509         private static void mapFormatToken(String wsToken, int startLen, int tokLen, out NumberingSequence seq, out int pminlen) {
510             char wch = wsToken[startLen];
511             bool UseArabic = false;
512             pminlen = 1;
513             seq = NumberingSequence.Nil;
514
515             switch ((int)wch) {
516             case 0x0030:    // Digit zero
517             case 0x0966:    // Hindi digit zero
518             case 0x0e50:    // Thai digit zero
519             case 0xc77b:    // Korean digit zero
520             case 0xff10:    // Digit zero (double-byte)
521                 do {
522                     // Leading zeros request padding.  Track how much.
523                     pminlen++;
524                 } while ((--tokLen > 0) && (wch == wsToken[++startLen]));
525
526                 if (wsToken[startLen] != (char)(wch + 1)) {
527                     // If next character isn't "one", then use Arabic
528                     UseArabic = true;
529                 }
530                 break;
531             }
532
533             if (!UseArabic) {
534                 // Map characters of token to number format ID
535                 switch ((int)wsToken[startLen]) {
536                 case 0x0031: seq = NumberingSequence.Arabic; break;
537                 case 0x0041: seq = NumberingSequence.UCLetter; break;
538                 case 0x0049: seq = NumberingSequence.UCRoman; break;
539                 case 0x0061: seq = NumberingSequence.LCLetter; break;
540                 case 0x0069: seq = NumberingSequence.LCRoman; break;
541                 case 0x0410: seq = NumberingSequence.UCRus; break;
542                 case 0x0430: seq = NumberingSequence.LCRus; break;
543                 case 0x05d0: seq = NumberingSequence.Hebrew; break;
544                 case 0x0623: seq = NumberingSequence.ArabicScript; break;
545                 case 0x0905: seq = NumberingSequence.Hindi2; break;
546                 case 0x0915: seq = NumberingSequence.Hindi1; break;
547                 case 0x0967: seq = NumberingSequence.Hindi3; break;
548                 case 0x0e01: seq = NumberingSequence.Thai1; break;
549                 case 0x0e51: seq = NumberingSequence.Thai2; break;
550                 case 0x30a2: seq = NumberingSequence.DAiueo; break;
551                 case 0x30a4: seq = NumberingSequence.DIroha; break;
552                 case 0x3131: seq = NumberingSequence.DChosung; break;
553                 case 0x4e00: seq = NumberingSequence.FEDecimal; break;
554                 case 0x58f1: seq = NumberingSequence.DbNum3; break;
555                 case 0x58f9: seq = NumberingSequence.ChnCmplx; break;
556                 case 0x5b50: seq = NumberingSequence.Zodiac2; break;
557                 case 0xac00: seq = NumberingSequence.Ganada; break;
558                 case 0xc77c: seq = NumberingSequence.KorDbNum1; break;
559                 case 0xd558: seq = NumberingSequence.KorDbNum3; break;
560                 case 0xff11: seq = NumberingSequence.DArabic; break;
561                 case 0xff71: seq = NumberingSequence.Aiueo; break;
562                 case 0xff72: seq = NumberingSequence.Iroha; break;
563
564                 case 0x7532:
565                     if (tokLen > 1 && wsToken[startLen + 1] == 0x5b50) {
566                         // 60-based Zodiak numbering begins with two characters
567                         seq = NumberingSequence.Zodiac3;
568                         tokLen--;
569                         startLen++;
570                     }
571                     else {
572                         // 10-based Zodiak numbering begins with one character
573                         seq = NumberingSequence.Zodiac1;
574                     }
575                     break;
576                 default:
577                     seq = NumberingSequence.Arabic;
578                     break;
579                 }
580             }
581
582             //if (tokLen != 1 || UseArabic) {
583             if (UseArabic) {
584                 // If remaining token length is not 1, then don't recognize
585                 // sequence and default to Arabic with no zero padding.
586                 seq = NumberingSequence.Arabic;
587                 pminlen = 0;
588             }
589         }
590
591
592         /*
593         ----------------------------------------------------------------------------
594             parseFormat()
595
596             Parse format string into format tokens (alphanumeric) and separators
597             (non-alphanumeric).
598
599         */
600         private static List<FormatInfo> ParseFormat(string formatString) {
601             if (formatString == null || formatString.Length == 0) {
602                 return null;
603             }
604             int length = 0;
605             bool lastAlphaNumeric = CharUtil.IsAlphaNumeric(formatString[length]);
606             List<FormatInfo> tokens = new List<FormatInfo>();
607             int count = 0;
608
609             if (lastAlphaNumeric) {
610                 // If the first one is alpha num add empty separator as a prefix.
611                 tokens.Add(null);
612             }
613
614             while (length <= formatString.Length) {
615                 // Loop until a switch from format token to separator is detected (or vice-versa)
616                 bool currentchar = length < formatString.Length ? CharUtil.IsAlphaNumeric(formatString[length]) : !lastAlphaNumeric;
617                 if (lastAlphaNumeric != currentchar) {
618                     FormatInfo formatInfo = new FormatInfo();
619                     if (lastAlphaNumeric) {
620                         // We just finished a format token.  Map it to a numbering format ID and a min-length bound.
621                         mapFormatToken(formatString, count, length - count, out formatInfo.numSequence, out formatInfo.length);
622                     }
623                     else {
624                         formatInfo.isSeparator = true;
625                         // We just finished a separator.  Save its length and a pointer to it.
626                         formatInfo.formatString = formatString.Substring(count, length - count);
627                     }
628                     count = length;
629                     length++;
630                     // Begin parsing the next format token or separator
631
632                     tokens.Add(formatInfo);
633                     // Flip flag from format token to separator (or vice-versa)
634                     lastAlphaNumeric = currentchar;
635                 }
636                 else {
637                     length++;
638                 }
639             }
640
641             return tokens;
642         }
643
644         private string ParseLetter(string letter) {
645             if (letter == null || letter == "traditional" || letter == "alphabetic") {
646                 return letter;
647             }
648             if (! this.forwardCompatibility) {
649                 throw XsltException.Create(Res.Xslt_InvalidAttrValue, "letter-value", letter);
650             }
651             return null;
652         }
653     }
654 }