1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System
.Collections
.Generic
;
6 using System
.Globalization
;
7 using System
.Runtime
.Serialization
;
12 public sealed partial class TimeZoneInfo
15 /// Used to serialize and deserialize TimeZoneInfo objects based on the custom string serialization format.
17 private struct StringSerializer
27 private readonly string _serializedText
;
28 private int _currentTokenStartIndex
;
31 // the majority of the strings contained in the OS time zones fit in 64 chars
32 private const int InitialCapacityForString
= 64;
33 private const char Esc
= '\\';
34 private const char Sep
= ';';
35 private const char Lhs
= '[';
36 private const char Rhs
= ']';
37 private const string DateTimeFormat
= "MM:dd:yyyy";
38 private const string TimeOfDayFormat
= "HH:mm:ss.FFF";
41 /// Creates the custom serialized string representation of a TimeZoneInfo instance.
43 public static string GetSerializedString(TimeZoneInfo zone
)
45 StringBuilder serializedText
= StringBuilderCache
.Acquire();
48 // <_id>;<_baseUtcOffset>;<_displayName>;<_standardDisplayName>;<_daylightDispayName>
50 SerializeSubstitute(zone
.Id
, serializedText
);
51 serializedText
.Append(Sep
);
52 serializedText
.AppendSpanFormattable(zone
.BaseUtcOffset
.TotalMinutes
, format
: default, CultureInfo
.InvariantCulture
);
53 serializedText
.Append(Sep
);
54 SerializeSubstitute(zone
.DisplayName
, serializedText
);
55 serializedText
.Append(Sep
);
56 SerializeSubstitute(zone
.StandardName
, serializedText
);
57 serializedText
.Append(Sep
);
58 SerializeSubstitute(zone
.DaylightName
, serializedText
);
59 serializedText
.Append(Sep
);
61 AdjustmentRule
[] rules
= zone
.GetAdjustmentRules();
62 foreach (AdjustmentRule rule
in rules
)
64 serializedText
.Append(Lhs
);
65 serializedText
.AppendSpanFormattable(rule
.DateStart
, DateTimeFormat
, DateTimeFormatInfo
.InvariantInfo
);
66 serializedText
.Append(Sep
);
67 serializedText
.AppendSpanFormattable(rule
.DateEnd
, DateTimeFormat
, DateTimeFormatInfo
.InvariantInfo
);
68 serializedText
.Append(Sep
);
69 serializedText
.AppendSpanFormattable(rule
.DaylightDelta
.TotalMinutes
, format
: default, CultureInfo
.InvariantCulture
);
70 serializedText
.Append(Sep
);
71 // serialize the TransitionTime's
72 SerializeTransitionTime(rule
.DaylightTransitionStart
, serializedText
);
73 serializedText
.Append(Sep
);
74 SerializeTransitionTime(rule
.DaylightTransitionEnd
, serializedText
);
75 serializedText
.Append(Sep
);
76 if (rule
.BaseUtcOffsetDelta
!= TimeSpan
.Zero
)
78 // Serialize it only when BaseUtcOffsetDelta has a value to reduce the impact of adding rule.BaseUtcOffsetDelta
79 serializedText
.AppendSpanFormattable(rule
.BaseUtcOffsetDelta
.TotalMinutes
, format
: default, CultureInfo
.InvariantCulture
);
80 serializedText
.Append(Sep
);
82 if (rule
.NoDaylightTransitions
)
84 // Serialize it only when NoDaylightTransitions is true to reduce the impact of adding rule.NoDaylightTransitions
85 serializedText
.Append('1');
86 serializedText
.Append(Sep
);
88 serializedText
.Append(Rhs
);
90 serializedText
.Append(Sep
);
92 return StringBuilderCache
.GetStringAndRelease(serializedText
);
96 /// Instantiates a TimeZoneInfo from a custom serialized string.
98 public static TimeZoneInfo
GetDeserializedTimeZoneInfo(string source
)
100 StringSerializer s
= new StringSerializer(source
);
102 string id
= s
.GetNextStringValue();
103 TimeSpan baseUtcOffset
= s
.GetNextTimeSpanValue();
104 string displayName
= s
.GetNextStringValue();
105 string standardName
= s
.GetNextStringValue();
106 string daylightName
= s
.GetNextStringValue();
107 AdjustmentRule
[]? rules
= s
.GetNextAdjustmentRuleArrayValue();
111 return new TimeZoneInfo(id
, baseUtcOffset
, displayName
, standardName
, daylightName
, rules
, disableDaylightSavingTime
: false);
113 catch (ArgumentException ex
)
115 throw new SerializationException(SR
.Serialization_InvalidData
, ex
);
117 catch (InvalidTimeZoneException ex
)
119 throw new SerializationException(SR
.Serialization_InvalidData
, ex
);
123 private StringSerializer(string str
)
125 _serializedText
= str
;
126 _currentTokenStartIndex
= 0;
127 _state
= State
.StartOfToken
;
131 /// Appends the String to the StringBuilder with all of the reserved chars escaped.
138 private static void SerializeSubstitute(string text
, StringBuilder serializedText
)
140 foreach (char c
in text
)
142 if (c
== Esc
|| c
== Lhs
|| c
== Rhs
|| c
== Sep
)
144 serializedText
.Append('\\');
146 serializedText
.Append(c
);
151 /// Helper method to serialize a TimeZoneInfo.TransitionTime object.
153 private static void SerializeTransitionTime(TransitionTime time
, StringBuilder serializedText
)
155 serializedText
.Append(Lhs
);
156 serializedText
.Append(time
.IsFixedDateRule
? '1' : '0');
157 serializedText
.Append(Sep
);
158 serializedText
.AppendSpanFormattable(time
.TimeOfDay
, TimeOfDayFormat
, DateTimeFormatInfo
.InvariantInfo
);
159 serializedText
.Append(Sep
);
160 serializedText
.AppendSpanFormattable(time
.Month
, format
: default, CultureInfo
.InvariantCulture
);
161 serializedText
.Append(Sep
);
162 if (time
.IsFixedDateRule
)
164 serializedText
.AppendSpanFormattable(time
.Day
, format
: default, CultureInfo
.InvariantCulture
);
165 serializedText
.Append(Sep
);
169 serializedText
.AppendSpanFormattable(time
.Week
, format
: default, CultureInfo
.InvariantCulture
);
170 serializedText
.Append(Sep
);
171 serializedText
.AppendSpanFormattable((int)time
.DayOfWeek
, format
: default, CultureInfo
.InvariantCulture
);
172 serializedText
.Append(Sep
);
174 serializedText
.Append(Rhs
);
178 /// Helper function to determine if the passed in string token is allowed to be preceded by an escape sequence token.
180 private static void VerifyIsEscapableCharacter(char c
)
182 if (c
!= Esc
&& c
!= Sep
&& c
!= Lhs
&& c
!= Rhs
)
184 throw new SerializationException(SR
.Format(SR
.Serialization_InvalidEscapeSequence
, c
));
189 /// Helper function that reads past "v.Next" data fields. Receives a "depth" parameter indicating the
190 /// current relative nested bracket depth that _currentTokenStartIndex is at. The function ends
191 /// successfully when "depth" returns to zero (0).
193 private void SkipVersionNextDataFields(int depth
/* starting depth in the nested brackets ('[', ']')*/)
195 if (_currentTokenStartIndex
< 0 || _currentTokenStartIndex
>= _serializedText
.Length
)
197 throw new SerializationException(SR
.Serialization_InvalidData
);
199 State tokenState
= State
.NotEscaped
;
201 // walk the serialized text, building up the token as we go...
202 for (int i
= _currentTokenStartIndex
; i
< _serializedText
.Length
; i
++)
204 if (tokenState
== State
.Escaped
)
206 VerifyIsEscapableCharacter(_serializedText
[i
]);
207 tokenState
= State
.NotEscaped
;
209 else if (tokenState
== State
.NotEscaped
)
211 switch (_serializedText
[i
])
214 tokenState
= State
.Escaped
;
224 _currentTokenStartIndex
= i
+ 1;
225 if (_currentTokenStartIndex
>= _serializedText
.Length
)
227 _state
= State
.EndOfLine
;
231 _state
= State
.StartOfToken
;
239 throw new SerializationException(SR
.Serialization_InvalidData
);
247 throw new SerializationException(SR
.Serialization_InvalidData
);
251 /// Helper function that reads a string token from the serialized text. The function
252 /// updates <see cref="_currentTokenStartIndex"/> to point to the next token on exit.
253 /// Also <see cref="_state"/> is set to either <see cref="State.StartOfToken"/> or
254 /// <see cref="State.EndOfLine"/> on exit.
256 private string GetNextStringValue()
258 // first verify the internal state of the object
259 if (_state
== State
.EndOfLine
)
261 throw new SerializationException(SR
.Serialization_InvalidData
);
263 if (_currentTokenStartIndex
< 0 || _currentTokenStartIndex
>= _serializedText
.Length
)
265 throw new SerializationException(SR
.Serialization_InvalidData
);
267 State tokenState
= State
.NotEscaped
;
268 StringBuilder token
= StringBuilderCache
.Acquire(InitialCapacityForString
);
270 // walk the serialized text, building up the token as we go...
271 for (int i
= _currentTokenStartIndex
; i
< _serializedText
.Length
; i
++)
273 if (tokenState
== State
.Escaped
)
275 VerifyIsEscapableCharacter(_serializedText
[i
]);
276 token
.Append(_serializedText
[i
]);
277 tokenState
= State
.NotEscaped
;
279 else if (tokenState
== State
.NotEscaped
)
281 switch (_serializedText
[i
])
284 tokenState
= State
.Escaped
;
288 // '[' is an unexpected character
289 throw new SerializationException(SR
.Serialization_InvalidData
);
292 // ']' is an unexpected character
293 throw new SerializationException(SR
.Serialization_InvalidData
);
296 _currentTokenStartIndex
= i
+ 1;
297 if (_currentTokenStartIndex
>= _serializedText
.Length
)
299 _state
= State
.EndOfLine
;
303 _state
= State
.StartOfToken
;
305 return StringBuilderCache
.GetStringAndRelease(token
);
309 throw new SerializationException(SR
.Serialization_InvalidData
);
312 token
.Append(_serializedText
[i
]);
318 // we are at the end of the line
320 if (tokenState
== State
.Escaped
)
322 // we are at the end of the serialized text but we are in an escaped state
323 throw new SerializationException(SR
.Format(SR
.Serialization_InvalidEscapeSequence
, string.Empty
));
326 throw new SerializationException(SR
.Serialization_InvalidData
);
330 /// Helper function to read a DateTime token.
332 private DateTime
GetNextDateTimeValue(string format
)
334 string token
= GetNextStringValue();
336 if (!DateTime
.TryParseExact(token
, format
, DateTimeFormatInfo
.InvariantInfo
, DateTimeStyles
.None
, out time
))
338 throw new SerializationException(SR
.Serialization_InvalidData
);
344 /// Helper function to read a TimeSpan token.
346 private TimeSpan
GetNextTimeSpanValue()
348 int token
= GetNextInt32Value();
351 return new TimeSpan(hours
: 0, minutes
: token
, seconds
: 0);
353 catch (ArgumentOutOfRangeException e
)
355 throw new SerializationException(SR
.Serialization_InvalidData
, e
);
360 /// Helper function to read an Int32 token.
362 private int GetNextInt32Value()
364 string token
= GetNextStringValue();
366 if (!int.TryParse(token
, NumberStyles
.AllowLeadingSign
/* "[sign]digits" */, CultureInfo
.InvariantCulture
, out value))
368 throw new SerializationException(SR
.Serialization_InvalidData
);
374 /// Helper function to read an AdjustmentRule[] token.
376 private AdjustmentRule
[]? GetNextAdjustmentRuleArrayValue()
378 List
<AdjustmentRule
> rules
= new List
<AdjustmentRule
>(1);
381 // individual AdjustmentRule array elements do not require semicolons
382 AdjustmentRule
? rule
= GetNextAdjustmentRuleValue();
388 rule
= GetNextAdjustmentRuleValue();
391 // the AdjustmentRule array must end with a separator
392 if (_state
== State
.EndOfLine
)
394 throw new SerializationException(SR
.Serialization_InvalidData
);
396 if (_currentTokenStartIndex
< 0 || _currentTokenStartIndex
>= _serializedText
.Length
)
398 throw new SerializationException(SR
.Serialization_InvalidData
);
401 return count
!= 0 ? rules
.ToArray() : null;
405 /// Helper function to read an AdjustmentRule token.
407 private AdjustmentRule
? GetNextAdjustmentRuleValue()
409 // first verify the internal state of the object
410 if (_state
== State
.EndOfLine
)
415 if (_currentTokenStartIndex
< 0 || _currentTokenStartIndex
>= _serializedText
.Length
)
417 throw new SerializationException(SR
.Serialization_InvalidData
);
420 // check to see if the very first token we see is the separator
421 if (_serializedText
[_currentTokenStartIndex
] == Sep
)
426 // verify the current token is a left-hand-side marker ("[")
427 if (_serializedText
[_currentTokenStartIndex
] != Lhs
)
429 throw new SerializationException(SR
.Serialization_InvalidData
);
431 _currentTokenStartIndex
++;
433 DateTime dateStart
= GetNextDateTimeValue(DateTimeFormat
);
434 DateTime dateEnd
= GetNextDateTimeValue(DateTimeFormat
);
435 TimeSpan daylightDelta
= GetNextTimeSpanValue();
436 TransitionTime daylightStart
= GetNextTransitionTimeValue();
437 TransitionTime daylightEnd
= GetNextTransitionTimeValue();
438 TimeSpan baseUtcOffsetDelta
= TimeSpan
.Zero
;
439 int noDaylightTransitions
= 0;
441 // verify that the string is now at the right-hand-side marker ("]") ...
443 if (_state
== State
.EndOfLine
|| _currentTokenStartIndex
>= _serializedText
.Length
)
445 throw new SerializationException(SR
.Serialization_InvalidData
);
448 // Check if we have baseUtcOffsetDelta in the serialized string and then deserialize it
449 if ((_serializedText
[_currentTokenStartIndex
] >= '0' && _serializedText
[_currentTokenStartIndex
] <= '9') ||
450 _serializedText
[_currentTokenStartIndex
] == '-' || _serializedText
[_currentTokenStartIndex
] == '+')
452 baseUtcOffsetDelta
= GetNextTimeSpanValue();
455 // Check if we have NoDaylightTransitions in the serialized string and then deserialize it
456 if ((_serializedText
[_currentTokenStartIndex
] >= '0' && _serializedText
[_currentTokenStartIndex
] <= '1'))
458 noDaylightTransitions
= GetNextInt32Value();
461 if (_state
== State
.EndOfLine
|| _currentTokenStartIndex
>= _serializedText
.Length
)
463 throw new SerializationException(SR
.Serialization_InvalidData
);
466 if (_serializedText
[_currentTokenStartIndex
] != Rhs
)
468 // skip ahead of any "v.Next" data at the end of the AdjustmentRule
470 // FUTURE: if the serialization format is extended in the future then this
471 // code section will need to be changed to read the new fields rather
472 // than just skipping the data at the end of the [AdjustmentRule].
473 SkipVersionNextDataFields(1);
477 _currentTokenStartIndex
++;
480 // create the AdjustmentRule from the deserialized fields ...
485 rule
= AdjustmentRule
.CreateAdjustmentRule(dateStart
, dateEnd
, daylightDelta
, daylightStart
, daylightEnd
, baseUtcOffsetDelta
, noDaylightTransitions
> 0);
487 catch (ArgumentException e
)
489 throw new SerializationException(SR
.Serialization_InvalidData
, e
);
492 // finally set the state to either EndOfLine or StartOfToken for the next caller
493 if (_currentTokenStartIndex
>= _serializedText
.Length
)
495 _state
= State
.EndOfLine
;
499 _state
= State
.StartOfToken
;
505 /// Helper function to read a TransitionTime token.
507 private TransitionTime
GetNextTransitionTimeValue()
509 // first verify the internal state of the object
511 if (_state
== State
.EndOfLine
||
512 (_currentTokenStartIndex
< _serializedText
.Length
&& _serializedText
[_currentTokenStartIndex
] == Rhs
))
515 // we are at the end of the line or we are starting at a "]" character
517 throw new SerializationException(SR
.Serialization_InvalidData
);
520 if (_currentTokenStartIndex
< 0 || _currentTokenStartIndex
>= _serializedText
.Length
)
522 throw new SerializationException(SR
.Serialization_InvalidData
);
525 // verify the current token is a left-hand-side marker ("[")
527 if (_serializedText
[_currentTokenStartIndex
] != Lhs
)
529 throw new SerializationException(SR
.Serialization_InvalidData
);
531 _currentTokenStartIndex
++;
533 int isFixedDate
= GetNextInt32Value();
535 if (isFixedDate
!= 0 && isFixedDate
!= 1)
537 throw new SerializationException(SR
.Serialization_InvalidData
);
540 TransitionTime transition
;
542 DateTime timeOfDay
= GetNextDateTimeValue(TimeOfDayFormat
);
543 timeOfDay
= new DateTime(1, 1, 1, timeOfDay
.Hour
, timeOfDay
.Minute
, timeOfDay
.Second
, timeOfDay
.Millisecond
);
545 int month
= GetNextInt32Value();
547 if (isFixedDate
== 1)
549 int day
= GetNextInt32Value();
553 transition
= TransitionTime
.CreateFixedDateRule(timeOfDay
, month
, day
);
555 catch (ArgumentException e
)
557 throw new SerializationException(SR
.Serialization_InvalidData
, e
);
562 int week
= GetNextInt32Value();
563 int dayOfWeek
= GetNextInt32Value();
567 transition
= TransitionTime
.CreateFloatingDateRule(timeOfDay
, month
, week
, (DayOfWeek
)dayOfWeek
);
569 catch (ArgumentException e
)
571 throw new SerializationException(SR
.Serialization_InvalidData
, e
);
575 // verify that the string is now at the right-hand-side marker ("]") ...
577 if (_state
== State
.EndOfLine
|| _currentTokenStartIndex
>= _serializedText
.Length
)
579 throw new SerializationException(SR
.Serialization_InvalidData
);
582 if (_serializedText
[_currentTokenStartIndex
] != Rhs
)
584 // skip ahead of any "v.Next" data at the end of the AdjustmentRule
586 // FUTURE: if the serialization format is extended in the future then this
587 // code section will need to be changed to read the new fields rather
588 // than just skipping the data at the end of the [TransitionTime].
589 SkipVersionNextDataFields(1);
593 _currentTokenStartIndex
++;
596 // check to see if the string is now at the separator (";") ...
597 bool sepFound
= false;
598 if (_currentTokenStartIndex
< _serializedText
.Length
&&
599 _serializedText
[_currentTokenStartIndex
] == Sep
)
601 // handle the case where we ended on a ";"
602 _currentTokenStartIndex
++;
608 // we MUST end on a separator
609 throw new SerializationException(SR
.Serialization_InvalidData
);
612 // finally set the state to either EndOfLine or StartOfToken for the next caller
613 if (_currentTokenStartIndex
>= _serializedText
.Length
)
615 _state
= State
.EndOfLine
;
619 _state
= State
.StartOfToken
;