1 //------------------------------------------------------------------------------
2 // <copyright file="XsdDuration.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.Schema {
9     using System;
10     using System.Diagnostics;
11     using System.Text;
12 
13     /// <summary>
14     /// This structure holds components of an Xsd Duration.  It is used internally to support Xsd durations without loss
15     /// of fidelity.  XsdDuration structures are immutable once they've been created.
16     /// </summary>
17 #if SILVERLIGHT
18     [System.Runtime.CompilerServices.FriendAccessAllowed] // used by System.Runtime.Serialization.dll
19 #endif
20     internal struct XsdDuration {
21         private int years;
22         private int months;
23         private int days;
24         private int hours;
25         private int minutes;
26         private int seconds;
27         private uint nanoseconds;       // High bit is used to indicate whether duration is negative
28 
29         private const uint NegativeBit = 0x80000000;
30 
31         private enum Parts {
32             HasNone = 0,
33             HasYears = 1,
34             HasMonths = 2,
35             HasDays = 4,
36             HasHours = 8,
37             HasMinutes = 16,
38             HasSeconds = 32,
39         }
40 
41         public enum DurationType {
42             Duration,
43             YearMonthDuration,
44             DayTimeDuration,
45         };
46 
47         /// <summary>
48         /// Construct an XsdDuration from component parts.
49         /// </summary>
XsdDurationSystem.Xml.Schema.XsdDuration50         public XsdDuration(bool isNegative, int years, int months, int days, int hours, int minutes, int seconds, int nanoseconds) {
51             if (years < 0) throw new ArgumentOutOfRangeException("years");
52             if (months < 0) throw new ArgumentOutOfRangeException("months");
53             if (days < 0) throw new ArgumentOutOfRangeException("days");
54             if (hours < 0) throw new ArgumentOutOfRangeException("hours");
55             if (minutes < 0) throw new ArgumentOutOfRangeException("minutes");
56             if (seconds < 0) throw new ArgumentOutOfRangeException("seconds");
57             if (nanoseconds < 0 || nanoseconds > 999999999) throw new ArgumentOutOfRangeException("nanoseconds");
58 
59             this.years = years;
60             this.months = months;
61             this.days = days;
62             this.hours = hours;
63             this.minutes = minutes;
64             this.seconds = seconds;
65             this.nanoseconds = (uint) nanoseconds;
66 
67             if (isNegative)
68                 this.nanoseconds |= NegativeBit;
69         }
70 
71         /// <summary>
72         /// Construct an XsdDuration from a TimeSpan value.
73         /// </summary>
XsdDurationSystem.Xml.Schema.XsdDuration74         public XsdDuration(TimeSpan timeSpan) : this(timeSpan, DurationType.Duration) {
75         }
76 
77         /// <summary>
78         /// Construct an XsdDuration from a TimeSpan value that represents an xsd:duration, an xdt:dayTimeDuration, or
79         /// an xdt:yearMonthDuration.
80         /// </summary>
XsdDurationSystem.Xml.Schema.XsdDuration81         public XsdDuration(TimeSpan timeSpan, DurationType durationType) {
82             long ticks = timeSpan.Ticks;
83             ulong ticksPos;
84             bool isNegative;
85 
86             if (ticks < 0) {
87                 // Note that (ulong) -Int64.MinValue = Int64.MaxValue + 1, which is what we want for that special case
88                 isNegative = true;
89                 ticksPos = (ulong) -ticks;
90             }
91             else {
92                 isNegative = false;
93                 ticksPos = (ulong) ticks;
94             }
95 
96             if (durationType == DurationType.YearMonthDuration) {
97                 int years = (int) (ticksPos / ((ulong) TimeSpan.TicksPerDay * 365));
98                 int months = (int) ((ticksPos % ((ulong) TimeSpan.TicksPerDay * 365)) / ((ulong) TimeSpan.TicksPerDay * 30));
99 
100                 if (months == 12) {
101                     // If remaining days >= 360 and < 365, then round off to year
102                     years++;
103                     months = 0;
104                 }
105 
106                 this = new XsdDuration(isNegative, years, months, 0, 0, 0, 0, 0);
107             }
108             else {
109                 Debug.Assert(durationType == DurationType.Duration || durationType == DurationType.DayTimeDuration);
110 
111                 // Tick count is expressed in 100 nanosecond intervals
112                 this.nanoseconds = (uint) (ticksPos % 10000000) * 100;
113                 if (isNegative)
114                     this.nanoseconds |= NegativeBit;
115 
116                 this.years = 0;
117                 this.months = 0;
118                 this.days = (int) (ticksPos / (ulong) TimeSpan.TicksPerDay);
119                 this.hours = (int) ((ticksPos / (ulong) TimeSpan.TicksPerHour) % 24);
120                 this.minutes = (int) ((ticksPos / (ulong) TimeSpan.TicksPerMinute) % 60);
121                 this.seconds = (int) ((ticksPos / (ulong) TimeSpan.TicksPerSecond) % 60);
122             }
123         }
124 
125         /// <summary>
126         /// Constructs an XsdDuration from a string in the xsd:duration format.  Components are stored with loss
127         /// of fidelity (except in the case of overflow).
128         /// </summary>
XsdDurationSystem.Xml.Schema.XsdDuration129         public XsdDuration(string s) : this(s, DurationType.Duration) {
130         }
131 
132         /// <summary>
133         /// Constructs an XsdDuration from a string in the xsd:duration format.  Components are stored without loss
134         /// of fidelity (except in the case of overflow).
135         /// </summary>
XsdDurationSystem.Xml.Schema.XsdDuration136         public XsdDuration(string s, DurationType durationType) {
137             XsdDuration result;
138             Exception exception = TryParse(s, durationType, out result);
139             if (exception != null) {
140                 throw exception;
141             }
142             this.years = result.Years;
143             this.months = result.Months;
144             this.days = result.Days;
145             this.hours = result.Hours;
146             this.minutes = result.Minutes;
147             this.seconds = result.Seconds;
148             this.nanoseconds = (uint)result.Nanoseconds;
149             if (result.IsNegative) {
150                 this.nanoseconds |= NegativeBit;
151             }
152             return;
153         }
154 
155         /// <summary>
156         /// Return true if this duration is negative.
157         /// </summary>
158         public bool IsNegative {
159             get { return (this.nanoseconds & NegativeBit) != 0; }
160         }
161 
162         /// <summary>
163         /// Return number of years in this duration (stored in 31 bits).
164         /// </summary>
165         public int Years {
166             get { return this.years; }
167         }
168 
169         /// <summary>
170         /// Return number of months in this duration (stored in 31 bits).
171         /// </summary>
172         public int Months {
173             get { return this.months; }
174         }
175 
176         /// <summary>
177         /// Return number of days in this duration (stored in 31 bits).
178         /// </summary>
179         public int Days {
180             get { return this.days; }
181         }
182 
183         /// <summary>
184         /// Return number of hours in this duration (stored in 31 bits).
185         /// </summary>
186         public int Hours {
187             get { return this.hours; }
188         }
189 
190         /// <summary>
191         /// Return number of minutes in this duration (stored in 31 bits).
192         /// </summary>
193         public int Minutes {
194             get { return this.minutes; }
195         }
196 
197         /// <summary>
198         /// Return number of seconds in this duration (stored in 31 bits).
199         /// </summary>
200         public int Seconds {
201             get { return this.seconds; }
202         }
203 
204         /// <summary>
205         /// Return number of nanoseconds in this duration.
206         /// </summary>
207         public int Nanoseconds {
208             get { return (int) (this.nanoseconds & ~NegativeBit); }
209         }
210 
211 #if !SILVERLIGHT
212         /// <summary>
213         /// Return number of microseconds in this duration.
214         /// </summary>
215         public int Microseconds {
216             get { return Nanoseconds / 1000; }
217         }
218 
219         /// <summary>
220         /// Return number of milliseconds in this duration.
221         /// </summary>
222         public int Milliseconds {
223             get { return Nanoseconds / 1000000; }
224         }
225 
226         /// <summary>
227         /// Normalize year-month part and day-time part so that month < 12, hour < 24, minute < 60, and second < 60.
228         /// </summary>
NormalizeSystem.Xml.Schema.XsdDuration229         public XsdDuration Normalize() {
230             int years = Years;
231             int months = Months;
232             int days = Days;
233             int hours = Hours;
234             int minutes = Minutes;
235             int seconds = Seconds;
236 
237             try {
238                 checked {
239                     if (months >= 12) {
240                         years += months / 12;
241                         months %= 12;
242                     }
243 
244                     if (seconds >= 60) {
245                         minutes += seconds / 60;
246                         seconds %= 60;
247                     }
248 
249                     if (minutes >= 60) {
250                         hours += minutes / 60;
251                         minutes %= 60;
252                     }
253 
254                     if (hours >= 24) {
255                         days += hours / 24;
256                         hours %= 24;
257                     }
258                 }
259             }
260             catch (OverflowException) {
261                 throw new OverflowException(Res.GetString(Res.XmlConvert_Overflow, ToString(), "Duration"));
262             }
263 
264             return new XsdDuration(IsNegative, years, months, days, hours, minutes, seconds, Nanoseconds);
265         }
266 #endif
267 
268         /// <summary>
269         /// Internal helper method that converts an Xsd duration to a TimeSpan value.  This code uses the estimate
270         /// that there are 365 days in the year and 30 days in a month.
271         /// </summary>
ToTimeSpanSystem.Xml.Schema.XsdDuration272         public TimeSpan ToTimeSpan() {
273             return ToTimeSpan(DurationType.Duration);
274         }
275 
276         /// <summary>
277         /// Internal helper method that converts an Xsd duration to a TimeSpan value.  This code uses the estimate
278         /// that there are 365 days in the year and 30 days in a month.
279         /// </summary>
ToTimeSpanSystem.Xml.Schema.XsdDuration280         public TimeSpan ToTimeSpan(DurationType durationType) {
281             TimeSpan result;
282             Exception exception = TryToTimeSpan(durationType, out result);
283             if (exception != null) {
284                 throw exception;
285             }
286             return result;
287         }
288 
289 #if !SILVERLIGHT
TryToTimeSpanSystem.Xml.Schema.XsdDuration290         internal Exception TryToTimeSpan(out TimeSpan result) {
291             return TryToTimeSpan(DurationType.Duration, out result);
292         }
293 #endif
294 
TryToTimeSpanSystem.Xml.Schema.XsdDuration295         internal Exception TryToTimeSpan(DurationType durationType, out TimeSpan result) {
296             Exception exception = null;
297             ulong ticks = 0;
298 
299             // Throw error if result cannot fit into a long
300             try {
301                 checked {
302                     // Discard year and month parts if constructing TimeSpan for DayTimeDuration
303                     if (durationType != DurationType.DayTimeDuration) {
304                         ticks += ((ulong) this.years + (ulong) this.months / 12) * 365;
305                         ticks += ((ulong) this.months % 12) * 30;
306                     }
307 
308                     // Discard day and time parts if constructing TimeSpan for YearMonthDuration
309                     if (durationType != DurationType.YearMonthDuration) {
310                         ticks += (ulong) this.days;
311 
312                         ticks *= 24;
313                         ticks += (ulong) this.hours;
314 
315                         ticks *= 60;
316                         ticks += (ulong) this.minutes;
317 
318                         ticks *= 60;
319                         ticks += (ulong) this.seconds;
320 
321                         // Tick count interval is in 100 nanosecond intervals (7 digits)
322                         ticks *= (ulong) TimeSpan.TicksPerSecond;
323                         ticks += (ulong) Nanoseconds / 100;
324                     }
325                     else {
326                         // Multiply YearMonth duration by number of ticks per day
327                         ticks *= (ulong) TimeSpan.TicksPerDay;
328                     }
329 
330                     if (IsNegative) {
331                         // Handle special case of Int64.MaxValue + 1 before negation, since it would otherwise overflow
332                         if (ticks == (ulong) Int64.MaxValue + 1) {
333                             result = new TimeSpan(Int64.MinValue);
334                         }
335                         else {
336                             result = new TimeSpan(-((long) ticks));
337                         }
338                     }
339                     else {
340                         result = new TimeSpan((long) ticks);
341                     }
342                     return null;
343                 }
344             }
345             catch (OverflowException) {
346                 result = TimeSpan.MinValue;
347                 exception = new OverflowException(Res.GetString(Res.XmlConvert_Overflow, durationType, "TimeSpan"));
348             }
349             return exception;
350         }
351 
352         /// <summary>
353         /// Return the string representation of this Xsd duration.
354         /// </summary>
ToStringSystem.Xml.Schema.XsdDuration355         public override string ToString() {
356             return ToString(DurationType.Duration);
357         }
358 
359         /// <summary>
360         /// Return the string representation according to xsd:duration rules, xdt:dayTimeDuration rules, or
361         /// xdt:yearMonthDuration rules.
362         /// </summary>
ToStringSystem.Xml.Schema.XsdDuration363         internal string ToString(DurationType durationType) {
364             StringBuilder sb = new StringBuilder(20);
365             int nanoseconds, digit, zeroIdx, len;
366 
367             if (IsNegative)
368                 sb.Append('-');
369 
370             sb.Append('P');
371 
372             if (durationType != DurationType.DayTimeDuration) {
373 
374                 if (this.years != 0) {
375                     sb.Append(XmlConvert.ToString(this.years));
376                     sb.Append('Y');
377                 }
378 
379                 if (this.months != 0) {
380                     sb.Append(XmlConvert.ToString(this.months));
381                     sb.Append('M');
382                 }
383             }
384 
385             if (durationType != DurationType.YearMonthDuration) {
386                 if (this.days != 0) {
387                     sb.Append(XmlConvert.ToString(this.days));
388                     sb.Append('D');
389                 }
390 
391                 if (this.hours != 0 || this.minutes != 0 || this.seconds != 0 || Nanoseconds != 0) {
392                     sb.Append('T');
393                     if (this.hours != 0) {
394                         sb.Append(XmlConvert.ToString(this.hours));
395                         sb.Append('H');
396                     }
397 
398                     if (this.minutes != 0) {
399                         sb.Append(XmlConvert.ToString(this.minutes));
400                         sb.Append('M');
401                     }
402 
403                     nanoseconds = Nanoseconds;
404                     if (this.seconds != 0 || nanoseconds != 0) {
405                         sb.Append(XmlConvert.ToString(this.seconds));
406                         if (nanoseconds != 0) {
407                             sb.Append('.');
408 
409                             len = sb.Length;
410                             sb.Length += 9;
411                             zeroIdx = sb.Length - 1;
412 
413                             for (int idx = zeroIdx; idx >= len; idx--) {
414                                 digit = nanoseconds % 10;
415                                 sb[idx] = (char) (digit + '0');
416 
417                                 if (zeroIdx == idx && digit == 0)
418                                     zeroIdx--;
419 
420                                 nanoseconds /= 10;
421                             }
422 
423                             sb.Length = zeroIdx + 1;
424                         }
425                         sb.Append('S');
426                     }
427                 }
428 
429                 // Zero is represented as "PT0S"
430                 if (sb[sb.Length - 1] == 'P')
431                     sb.Append("T0S");
432             }
433             else {
434                 // Zero is represented as "T0M"
435                 if (sb[sb.Length - 1] == 'P')
436                     sb.Append("0M");
437             }
438 
439             return sb.ToString();
440         }
441 
442 #if !SILVERLIGHT
TryParseSystem.Xml.Schema.XsdDuration443         internal static Exception TryParse(string s, out XsdDuration result) {
444             return TryParse(s, DurationType.Duration, out result);
445         }
446 #endif
447 
TryParseSystem.Xml.Schema.XsdDuration448         internal static Exception TryParse(string s, DurationType durationType, out XsdDuration result) {
449             string errorCode;
450             int length;
451             int value, pos, numDigits;
452             Parts parts = Parts.HasNone;
453 
454             result = new XsdDuration();
455 
456             s = s.Trim();
457             length = s.Length;
458 
459             pos = 0;
460             numDigits = 0;
461 
462             if (pos >= length) goto InvalidFormat;
463 
464             if (s[pos] == '-') {
465                 pos++;
466                 result.nanoseconds = NegativeBit;
467             }
468             else {
469                 result.nanoseconds = 0;
470             }
471 
472             if (pos >= length) goto InvalidFormat;
473 
474             if (s[pos++] != 'P') goto InvalidFormat;
475 
476             errorCode = TryParseDigits(s, ref pos, false, out value, out numDigits);
477             if (errorCode != null) goto Error;
478 
479             if (pos >= length) goto InvalidFormat;
480 
481             if (s[pos] == 'Y') {
482                 if (numDigits == 0) goto InvalidFormat;
483 
484                 parts |= Parts.HasYears;
485                 result.years = value;
486                 if (++pos == length) goto Done;
487 
488                 errorCode = TryParseDigits(s, ref pos, false, out value, out numDigits);
489                 if (errorCode != null) goto Error;
490 
491                 if (pos >= length) goto InvalidFormat;
492             }
493 
494             if (s[pos] == 'M') {
495                 if (numDigits == 0) goto InvalidFormat;
496 
497                 parts |= Parts.HasMonths;
498                 result.months = value;
499                 if (++pos == length) goto Done;
500 
501                 errorCode = TryParseDigits(s, ref pos, false, out value, out numDigits);
502                 if (errorCode != null) goto Error;
503 
504                 if (pos >= length) goto InvalidFormat;
505             }
506 
507             if (s[pos] == 'D') {
508                 if (numDigits == 0) goto InvalidFormat;
509 
510                 parts |= Parts.HasDays;
511                 result.days = value;
512                 if (++pos == length) goto Done;
513 
514                 errorCode = TryParseDigits(s, ref pos, false, out value, out numDigits);
515                 if (errorCode != null) goto Error;
516 
517                 if (pos >= length) goto InvalidFormat;
518             }
519 
520             if (s[pos] == 'T') {
521                 if (numDigits != 0) goto InvalidFormat;
522 
523                 pos++;
524                 errorCode = TryParseDigits(s, ref pos, false, out value, out numDigits);
525                 if (errorCode != null) goto Error;
526 
527                 if (pos >= length) goto InvalidFormat;
528 
529                 if (s[pos] == 'H') {
530                     if (numDigits == 0) goto InvalidFormat;
531 
532                     parts |= Parts.HasHours;
533                     result.hours = value;
534                     if (++pos == length) goto Done;
535 
536                     errorCode = TryParseDigits(s, ref pos, false, out value, out numDigits);
537                     if (errorCode != null) goto Error;
538 
539                     if (pos >= length) goto InvalidFormat;
540                 }
541 
542                 if (s[pos] == 'M') {
543                     if (numDigits == 0) goto InvalidFormat;
544 
545                     parts |= Parts.HasMinutes;
546                     result.minutes = value;
547                     if (++pos == length) goto Done;
548 
549                     errorCode = TryParseDigits(s, ref pos, false, out value, out numDigits);
550                     if (errorCode != null) goto Error;
551 
552                     if (pos >= length) goto InvalidFormat;
553                 }
554 
555                 if (s[pos] == '.') {
556                     pos++;
557 
558                     parts |= Parts.HasSeconds;
559                     result.seconds = value;
560 
561                     errorCode = TryParseDigits(s, ref pos, true, out value, out numDigits);
562                     if (errorCode != null) goto Error;
563 
564                     if (numDigits == 0) { //If there are no digits after the decimal point, assume 0
565                         value = 0;
566                     }
567                     // Normalize to nanosecond intervals
568                     for (; numDigits > 9; numDigits--)
569                         value /= 10;
570 
571                     for (; numDigits < 9; numDigits++)
572                         value *= 10;
573 
574                     result.nanoseconds |= (uint) value;
575 
576                     if (pos >= length) goto InvalidFormat;
577 
578                     if (s[pos] != 'S') goto InvalidFormat;
579                     if (++pos == length) goto Done;
580                 }
581                 else if (s[pos] == 'S') {
582                     if (numDigits == 0) goto InvalidFormat;
583 
584                     parts |= Parts.HasSeconds;
585                     result.seconds = value;
586                     if (++pos == length) goto Done;
587                 }
588             }
589 
590             // Duration cannot end with digits
591             if (numDigits != 0) goto InvalidFormat;
592 
593             // No further characters are allowed
594             if (pos != length) goto InvalidFormat;
595 
596         Done:
597             // At least one part must be defined
598             if (parts == Parts.HasNone) goto InvalidFormat;
599 
600             if (durationType == DurationType.DayTimeDuration) {
601                 if ((parts & (Parts.HasYears | Parts.HasMonths)) != 0)
602                     goto InvalidFormat;
603             }
604             else if (durationType == DurationType.YearMonthDuration) {
605                 if ((parts & ~(XsdDuration.Parts.HasYears | XsdDuration.Parts.HasMonths)) != 0)
606                     goto InvalidFormat;
607             }
608             return null;
609 
610         InvalidFormat:
611             return new FormatException(Res.GetString(Res.XmlConvert_BadFormat, s, durationType));
612 
613         Error:
614             return new OverflowException(Res.GetString(Res.XmlConvert_Overflow, s, durationType));
615         }
616 
617         /// Helper method that constructs an integer from leading digits starting at s[offset].  "offset" is
618         /// updated to contain an offset just beyond the last digit.  The number of digits consumed is returned in
619         /// cntDigits.  The integer is returned (0 if no digits).  If the digits cannot fit into an Int32:
620         ///   1. If eatDigits is true, then additional digits will be silently discarded (don't count towards numDigits)
621         ///   2. If eatDigits is false, an overflow exception is thrown
TryParseDigitsSystem.Xml.Schema.XsdDuration622         private static string TryParseDigits(string s, ref int offset, bool eatDigits, out int result, out int numDigits) {
623             int offsetStart = offset;
624             int offsetEnd = s.Length;
625             int digit;
626 
627             result = 0;
628             numDigits = 0;
629 
630             while (offset < offsetEnd && s[offset] >= '0' && s[offset] <= '9') {
631                 digit = s[offset] - '0';
632 
633                 if (result > (Int32.MaxValue - digit) / 10) {
634                     if (!eatDigits) {
635                         return Res.XmlConvert_Overflow;
636                     }
637 
638                     // Skip past any remaining digits
639                     numDigits = offset - offsetStart;
640 
641                     while (offset < offsetEnd && s[offset] >= '0' && s[offset] <= '9') {
642                         offset++;
643                     }
644 
645                     return null;
646                 }
647 
648                 result = result * 10 + digit;
649                 offset++;
650             }
651 
652             numDigits = offset - offsetStart;
653             return null;
654         }
655     }
656 }
657