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
);