Updates referencesource to .NET 4.7
[mono.git] / mcs / class / referencesource / System.Xml / System / Xml / Schema / XsdDuration.cs
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>
50         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>
74         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>
81         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>
129         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>
136         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>
229         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>
272         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>
280         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
290         internal Exception TryToTimeSpan(out TimeSpan result) {
291             return TryToTimeSpan(DurationType.Duration, out result);
292         }
293 #endif
294
295         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>
355         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>
363         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
443         internal static Exception TryParse(string s, out XsdDuration result) {
444             return TryParse(s, DurationType.Duration, out result);
445         }
446 #endif
447
448         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
622         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 }