1 //------------------------------------------------------------------------------
2 // <copyright file="NumberAction.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 // <owner current="true" primary="true">Microsoft</owner>
6 //------------------------------------------------------------------------------
8 namespace System.Xml.Xsl.XsltOld {
9 using Res = System.Xml.Utils.Res;
10 using System.Diagnostics;
12 using System.Globalization;
13 using System.Collections;
14 using System.Collections.Generic;
15 using System.Xml.XPath;
16 using System.Xml.Xsl.Runtime;
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
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)
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
32 public FormatInfo(bool isSeparator, string formatString) {
33 this.isSeparator = isSeparator;
34 this.formatString = formatString;
37 public FormatInfo() {}
40 static FormatInfo DefaultFormat = new FormatInfo(false, "0");
41 static FormatInfo DefaultSeparator = new FormatInfo(true , ".");
43 class NumberingFormat : NumberFormatterBase {
44 NumberingSequence seq;
49 internal NumberingFormat() {}
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; }
57 internal void setGroupingSize(int sizeGroup) {
58 if (0 <= sizeGroup && sizeGroup <= 9) {
59 this.sizeGroup = sizeGroup;
63 internal String FormatItem(object value) {
69 dblVal = XmlConvert.ToXPathDouble(value);
71 if (0.5 <= dblVal && !double.IsPositiveInfinity(dblVal)) {
72 dblVal = XmlConvert.XPathRound(dblVal);
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);
81 Debug.Assert(dblVal >= 1);
84 case NumberingSequence.Arabic :
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);
94 case NumberingSequence.UCRoman :
95 case NumberingSequence.LCRoman:
96 if (dblVal <= MaxRomanValue) {
97 StringBuilder sb = new StringBuilder();
98 ConvertToRoman(sb, dblVal, seq == NumberingSequence.UCRoman);
104 return ConvertToArabic(dblVal, cMinLen, sizeGroup, separator);
107 static string ConvertToArabic(double val, int minLength, int groupSize, string groupSeparator) {
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;
117 str = val.ToString("N", NumberFormat);
120 str = Convert.ToString(val, CultureInfo.InvariantCulture);
123 if (str.Length >= minLength) {
126 StringBuilder sb = new StringBuilder(minLength);
127 sb.Append('0', minLength - str.Length);
129 return sb.ToString();
135 private const int OutputNumber = 2;
137 private String level;
138 private String countPattern;
139 private int countKey = Compiler.InvalidQueryKey;
141 private int fromKey = Compiler.InvalidQueryKey;
142 private String value;
143 private int valueKey = Compiler.InvalidQueryKey;
144 private Avt formatAvt;
146 private Avt letterAvt;
147 private Avt groupingSepAvt;
148 private Avt groupingSizeAvt;
149 // Compile time precalculated AVTs
150 private List<FormatInfo> formatTokens;
152 private String letter;
153 private String groupingSep;
154 private String groupingSize;
155 private bool forwardCompatibility;
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);
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);
170 else if (Ref.Equal(name, compiler.Atoms.From)) {
172 this.fromKey = compiler.AddQuery(value, /*allowVars:*/true, /*allowKey:*/true, /*pattern*/true);
174 else if (Ref.Equal(name, compiler.Atoms.Value)) {
176 this.valueKey = compiler.AddQuery(value);
178 else if (Ref.Equal(name, compiler.Atoms.Format)) {
179 this.formatAvt = Avt.CompileAvt(compiler, value);
181 else if (Ref.Equal(name, compiler.Atoms.Lang)) {
182 this.langAvt = Avt.CompileAvt(compiler, value);
184 else if (Ref.Equal(name, compiler.Atoms.LetterValue)) {
185 this.letterAvt = Avt.CompileAvt(compiler, value);
187 else if (Ref.Equal(name, compiler.Atoms.GroupingSeparator)) {
188 this.groupingSepAvt = Avt.CompileAvt(compiler, value);
190 else if (Ref.Equal(name, compiler.Atoms.GroupingSize)) {
191 this.groupingSizeAvt = Avt.CompileAvt(compiler, value);
199 internal override void Compile(Compiler compiler) {
200 CompileAttributes(compiler);
201 CheckEmpty(compiler);
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");
211 this.groupingSize = PrecalculateAvt(ref this.groupingSizeAvt);
214 private int numberAny(Processor processor, ActionFrame frame) {
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();
222 XPathNavigator startNode = endNode.Clone();
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
228 if(processor.Matches(startNode, this.fromKey)) {
232 }while(startNode.MoveToParent());
235 processor.Matches(startNode, this.fromKey) || // we hit 'from' or
236 startNode.NodeType == XPathNodeType.Root // we are at root
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)) {
247 else if(MatchCountKey(processor, frame.Node, sel.Current)) {
250 if(sel.Current.IsSamePosition(endNode)) {
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)) {
267 if (sel.Current.IsSamePosition(endNode)) {
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) {
282 if (processor.Matches(nav, this.fromKey)) {
285 }while (nav.MoveToParent());
289 private bool moveToCount(XPathNavigator nav, Processor processor, XPathNavigator contextNode) {
291 if (this.fromKey != Compiler.InvalidQueryKey && processor.Matches(nav, this.fromKey)) {
294 if (MatchCountKey(processor, contextNode, nav)) {
297 }while (nav.MoveToParent());
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();
306 if (runner.MoveToParent()) {
307 runner.MoveToFirstChild();
308 while (! runner.IsSamePosition(nav)) {
309 if (MatchCountKey(processor, contextNode, runner)) {
312 if (! runner.MoveToNext()) {
313 Debug.Fail("We implementing preceding-sibling::node() and some how miss context node 'nav'");
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;
335 XPathNavigator nav = value as XPathNavigator;
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) {
348 Debug.Assert(frame != null);
349 Debug.Assert(frame.NodeSet != null);
351 if (this.valueKey != Compiler.InvalidQueryKey) {
352 list.Add(SimplifyValue(processor.Evaluate(frame, this.valueKey)));
354 else if (this.level == "any") {
355 int number = numberAny(processor, frame);
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();
367 while (moveToCount(countNode, processor, contextNode)) {
368 list.Insert(0, numberCount(countNode, processor, contextNode));
369 if(! multiple || ! countNode.MoveToParent()) {
373 if(! checkFrom(processor, countNode)) {
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)
386 goto case OutputNumber;
388 Debug.Assert(frame.StoredOutput != null);
389 if (! processor.TextEvent(frame.StoredOutput)) {
390 frame.State = OutputNumber;
396 Debug.Fail("Invalid Number Action execution state");
401 private bool MatchCountKey(Processor processor, XPathNavigator contextNode, XPathNavigator nav){
402 if (this.countKey != Compiler.InvalidQueryKey) {
403 return processor.Matches(nav, this.countKey);
405 if (contextNode.Name == nav.Name && BasicNodeType(contextNode.NodeType) == BasicNodeType(nav.NodeType)) {
411 private XPathNodeType BasicNodeType(XPathNodeType type) {
412 if(type == XPathNodeType.SignificantWhitespace || type == XPathNodeType.Whitespace) {
413 return XPathNodeType.Text;
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)
425 private static string Format(ArrayList numberlist, List<FormatInfo> tokens, string lang, string letter, string groupingSep, string groupingSize) {
426 StringBuilder result = new StringBuilder();
428 if (tokens != null) {
429 cFormats = tokens.Count;
432 NumberingFormat numberingFormat = new NumberingFormat();
433 if (groupingSize != null) {
435 numberingFormat.setGroupingSize(Convert.ToInt32(groupingSize, CultureInfo.InvariantCulture));
437 catch (System.FormatException) {}
438 catch (System.OverflowException) {}
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");
445 numberingFormat.setGroupingSeparator(groupingSep);
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];
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);
460 int numberlistCount = numberlist.Count;
461 for(int i = 0; i < numberlistCount; i++ ) {
462 int formatIndex = i * 2;
463 bool haveFormat = formatIndex < cFormats;
465 FormatInfo thisSeparator = haveFormat ? tokens[formatIndex + 0] : periodicSeparator;
466 Debug.Assert(thisSeparator.isSeparator);
467 result.Append(thisSeparator.formatString);
470 FormatInfo thisFormat = haveFormat ? tokens[formatIndex + 1] : periodicFormat;
471 Debug.Assert(!thisFormat.isSeparator);
473 //numberingFormat.setletter(this.letter);
474 //numberingFormat.setLang(this.lang);
476 numberingFormat.setNumberingType(thisFormat.numSequence);
477 numberingFormat.setMinLen(thisFormat.length);
478 result.Append(numberingFormat.FormatItem(numberlist[i]));
482 result.Append(sufix.formatString);
486 numberingFormat.setNumberingType(NumberingSequence.Arabic);
487 for (int i = 0; i < numberlist.Count; i++) {
491 result.Append(numberingFormat.FormatItem(numberlist[i]));
494 return result.ToString();
498 ----------------------------------------------------------------------------
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
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 ----------------------------------------------------------------------------
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;
513 seq = NumberingSequence.Nil;
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)
522 // Leading zeros request padding. Track how much.
524 } while ((--tokLen > 0) && (wch == wsToken[++startLen]));
526 if (wsToken[startLen] != (char)(wch + 1)) {
527 // If next character isn't "one", then use Arabic
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;
565 if (tokLen > 1 && wsToken[startLen + 1] == 0x5b50) {
566 // 60-based Zodiak numbering begins with two characters
567 seq = NumberingSequence.Zodiac3;
572 // 10-based Zodiak numbering begins with one character
573 seq = NumberingSequence.Zodiac1;
577 seq = NumberingSequence.Arabic;
582 //if (tokLen != 1 || 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;
593 ----------------------------------------------------------------------------
596 Parse format string into format tokens (alphanumeric) and separators
600 private static List<FormatInfo> ParseFormat(string formatString) {
601 if (formatString == null || formatString.Length == 0) {
605 bool lastAlphaNumeric = CharUtil.IsAlphaNumeric(formatString[length]);
606 List<FormatInfo> tokens = new List<FormatInfo>();
609 if (lastAlphaNumeric) {
610 // If the first one is alpha num add empty separator as a prefix.
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);
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);
630 // Begin parsing the next format token or separator
632 tokens.Add(formatInfo);
633 // Flip flag from format token to separator (or vice-versa)
634 lastAlphaNumeric = currentchar;
644 private string ParseLetter(string letter) {
645 if (letter == null || letter == "traditional" || letter == "alphabetic") {
648 if (! this.forwardCompatibility) {
649 throw XsltException.Create(Res.Xslt_InvalidAttrValue, "letter-value", letter);