Fix IDE0025 (use expression body for properties)
[mono-project.git] / netcore / System.Private.CoreLib / shared / System / TimeZoneInfo.StringSerializer.cs
blob218cb0effce93b7b23b8fca3c0f66a7fb658dd13
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;
8 using System.Text;
10 namespace System
12 public sealed partial class TimeZoneInfo
14 /// <summary>
15 /// Used to serialize and deserialize TimeZoneInfo objects based on the custom string serialization format.
16 /// </summary>
17 private struct StringSerializer
19 private enum State
21 Escaped = 0,
22 NotEscaped = 1,
23 StartOfToken = 2,
24 EndOfLine = 3
27 private readonly string _serializedText;
28 private int _currentTokenStartIndex;
29 private State _state;
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";
40 /// <summary>
41 /// Creates the custom serialized string representation of a TimeZoneInfo instance.
42 /// </summary>
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);
95 /// <summary>
96 /// Instantiates a TimeZoneInfo from a custom serialized string.
97 /// </summary>
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;
130 /// <summary>
131 /// Appends the String to the StringBuilder with all of the reserved chars escaped.
133 /// ";" -> "\;"
134 /// "[" -> "\["
135 /// "]" -> "\]"
136 /// "\" -> "\\"
137 /// </summary>
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);
150 /// <summary>
151 /// Helper method to serialize a TimeZoneInfo.TransitionTime object.
152 /// </summary>
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);
167 else
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);
177 /// <summary>
178 /// Helper function to determine if the passed in string token is allowed to be preceded by an escape sequence token.
179 /// </summary>
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));
188 /// <summary>
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).
192 /// </summary>
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])
213 case Esc:
214 tokenState = State.Escaped;
215 break;
217 case Lhs:
218 depth++;
219 break;
220 case Rhs:
221 depth--;
222 if (depth == 0)
224 _currentTokenStartIndex = i + 1;
225 if (_currentTokenStartIndex >= _serializedText.Length)
227 _state = State.EndOfLine;
229 else
231 _state = State.StartOfToken;
233 return;
235 break;
237 case '\0':
238 // invalid character
239 throw new SerializationException(SR.Serialization_InvalidData);
241 default:
242 break;
247 throw new SerializationException(SR.Serialization_InvalidData);
250 /// <summary>
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.
255 /// </summary>
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])
283 case Esc:
284 tokenState = State.Escaped;
285 break;
287 case Lhs:
288 // '[' is an unexpected character
289 throw new SerializationException(SR.Serialization_InvalidData);
291 case Rhs:
292 // ']' is an unexpected character
293 throw new SerializationException(SR.Serialization_InvalidData);
295 case Sep:
296 _currentTokenStartIndex = i + 1;
297 if (_currentTokenStartIndex >= _serializedText.Length)
299 _state = State.EndOfLine;
301 else
303 _state = State.StartOfToken;
305 return StringBuilderCache.GetStringAndRelease(token);
307 case '\0':
308 // invalid character
309 throw new SerializationException(SR.Serialization_InvalidData);
311 default:
312 token.Append(_serializedText[i]);
313 break;
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);
329 /// <summary>
330 /// Helper function to read a DateTime token.
331 /// </summary>
332 private DateTime GetNextDateTimeValue(string format)
334 string token = GetNextStringValue();
335 DateTime time;
336 if (!DateTime.TryParseExact(token, format, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out time))
338 throw new SerializationException(SR.Serialization_InvalidData);
340 return time;
343 /// <summary>
344 /// Helper function to read a TimeSpan token.
345 /// </summary>
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);
359 /// <summary>
360 /// Helper function to read an Int32 token.
361 /// </summary>
362 private int GetNextInt32Value()
364 string token = GetNextStringValue();
365 int value;
366 if (!int.TryParse(token, NumberStyles.AllowLeadingSign /* "[sign]digits" */, CultureInfo.InvariantCulture, out value))
368 throw new SerializationException(SR.Serialization_InvalidData);
370 return value;
373 /// <summary>
374 /// Helper function to read an AdjustmentRule[] token.
375 /// </summary>
376 private AdjustmentRule[]? GetNextAdjustmentRuleArrayValue()
378 List<AdjustmentRule> rules = new List<AdjustmentRule>(1);
379 int count = 0;
381 // individual AdjustmentRule array elements do not require semicolons
382 AdjustmentRule? rule = GetNextAdjustmentRuleValue();
383 while (rule != null)
385 rules.Add(rule);
386 count++;
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;
404 /// <summary>
405 /// Helper function to read an AdjustmentRule token.
406 /// </summary>
407 private AdjustmentRule? GetNextAdjustmentRuleValue()
409 // first verify the internal state of the object
410 if (_state == State.EndOfLine)
412 return null;
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)
423 return null;
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);
475 else
477 _currentTokenStartIndex++;
480 // create the AdjustmentRule from the deserialized fields ...
482 AdjustmentRule rule;
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;
497 else
499 _state = State.StartOfToken;
501 return rule;
504 /// <summary>
505 /// Helper function to read a TransitionTime token.
506 /// </summary>
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);
560 else
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);
591 else
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++;
603 sepFound = true;
606 if (!sepFound)
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;
617 else
619 _state = State.StartOfToken;
621 return transition;