/// <summary> /// Returns the next string from the token stream. /// </summary> /// <param name="tokens">The tokens to parse from.</param> /// <param name="name">The name of the expected value, for use in the exception if no value is available.</param> private string NextString(Tokens tokens, string name) { if (!tokens.HasNextToken) { throw new InvalidDataException($"Missing zone info token: {name}"); } return tokens.NextToken(name); }
private static void AssertTokensEqual(IList<string> expectedTokens, Tokens tokens) { for (int i = 0; i < expectedTokens.Count; i++) { Assert.True(tokens.HasNextToken, "Not enough items in enumeration"); var actual = tokens.NextToken(i.ToString()); if (actual == null) { Assert.Fail("The enumeration item at index [" + i + "] is null"); } Assert.AreEqual(expectedTokens[i], actual, "The enumeration item at index [" + i + "] is not correct"); } Assert.False(tokens.HasNextToken, "Too many items in enumeration"); }
/// <summary> /// Returns the next token, which is optional, converting "-" to null. /// </summary> /// <param name="tokens">The tokens.</param> /// <param name="name">The name of the expected value, for use in the exception if no value is available.</param> private string NextOptional(Tokens tokens, string name) => ParserHelper.ParseOptional(NextString(tokens, name));
/// <summary> /// Parses the next token as an offset. /// </summary> /// <param name="tokens">The tokens.</param> /// <param name="name">The name of the expected value, for use in the exception if no value is available.</param> private Offset NextOffset(Tokens tokens, string name) => ParserHelper.ParseOffset(NextString(tokens, name));
/// <summary> /// Parses the next token as a month number (1-12). /// </summary> /// <param name="tokens">The tokens.</param> /// <param name="name">The name of the expected value, for use in the exception if no value is available.</param> private int NextMonth(Tokens tokens, string name) { var value = NextString(tokens, name); return ParseMonth(value); }
/// <summary> /// Parses a time zone definition and returns the Zone object. /// </summary> /// <remarks> /// # GMTOFF RULES FORMAT [ UntilYear [ UntilMonth [ UntilDay [ UntilTime [ ZoneCharacter ] ] ] ] ] /// </remarks> /// <param name="name">The name of the zone being parsed.</param> /// <param name="tokens">The tokens to parse.</param> /// <returns>The Zone object.</returns> internal ZoneLine ParseZone(string name, Tokens tokens) { var offset = NextOffset(tokens, "Gmt Offset"); var rules = NextOptional(tokens, "Rules"); var format = NextString(tokens, "Format"); int year = NextYear(tokens, Int32.MaxValue); if (tokens.HasNextToken) { var until = ParseDateTimeOfYear(tokens, false); return new ZoneLine(name, offset, rules, format, year, until); } return new ZoneLine(name, offset, rules, format, year, ZoneYearOffset.StartOfYear); }
/// <summary> /// Parses a daylight savings rule and returns the Rule object. /// </summary> /// <remarks> /// # Rule NAME FROM TO TYPE IN ON AT SAVE LETTER/S /// </remarks> /// <param name="tokens">The tokens to parse.</param> /// <returns>The Rule object.</returns> internal RuleLine ParseRule(Tokens tokens) { var name = NextString(tokens, "GetName"); int fromYear = NextYear(tokens, 0); // This basically doesn't happen these days, but if we have any recurrent rules // which start at the dawn of time, make them effective from 1900. This matches // zic behaviour in the only cases of this that we've seen, e.g. the systemv rules // prior to 2001a. if (fromYear == int.MinValue) { fromYear = 1900; } int toYear = NextYear(tokens, fromYear); if (toYear < fromYear) { throw new ArgumentException($"To year cannot be before the from year in a Rule: {toYear} < {fromYear}"); } var type = NextOptional(tokens, "Type"); var yearOffset = ParseDateTimeOfYear(tokens, true); var savings = NextOffset(tokens, "SaveMillis"); var daylightSavingsIndicator = NextOptional(tokens, "LetterS"); // The name of the zone recurrence is currently the name of the rule. Later (in ZoneRule.GetRecurrences) // it will be replaced with the formatted name. It's not ideal, but it avoids a lot of duplication. var recurrence = new ZoneRecurrence(name, savings, yearOffset, fromYear, toYear); return new RuleLine(recurrence, daylightSavingsIndicator, type); }
/// <summary> /// Parses an alias link and returns the ZoneAlias object. /// </summary> /// <param name="tokens">The tokens to parse.</param> /// <returns>The ZoneAlias object.</returns> internal Tuple<string, string> ParseLink(Tokens tokens) { var existing = NextString(tokens, "Existing"); var alias = NextString(tokens, "Alias"); return Tuple.Create(existing, alias); }
/// <summary> /// Nexts the month. /// </summary> /// <param name="tokens">The tokens.</param> /// <param name="name">The name.</param> private int NextMonth(Tokens tokens, string name) { var value = NextString(tokens, name); int result = ParseMonth(value); return result == 0 ? 1 : result; }
/// <summary> /// Returns the next token, which is optional, converting "-" to null. /// </summary> /// <param name="tokens">The tokens.</param> /// <param name="name">The name of the expected value, for use in the exception if no value is available.</param> private string?NextOptional(Tokens tokens, string name) => ParserHelper.ParseOptional(NextString(tokens, name));
/// <summary> /// Parses the next token as a month number (1-12). /// </summary> /// <param name="tokens">The tokens.</param> /// <param name="name">The name of the expected value, for use in the exception if no value is available.</param> private int NextMonth(Tokens tokens, string name) { var value = NextString(tokens, name); return(ParseMonth(value)); }
/// <summary> /// Parses the ZoneYearOffset for a rule or zone. This is something like "3rd Sunday of October at 2am". /// </summary> /// <remarks> /// IN ON AT /// </remarks> /// <param name="tokens">The tokens to parse.</param> /// <param name="forRule">True if this is for a Rule line, in which case ON/AT are mandatory; /// false for a Zone line, in which case it's part of "until" and they're optional</param> /// <returns>The ZoneYearOffset object.</returns> internal ZoneYearOffset ParseDateTimeOfYear(Tokens tokens, bool forRule) { var mode = ZoneYearOffset.StartOfYear.Mode; var timeOfDay = ZoneYearOffset.StartOfYear.TimeOfDay; int monthOfYear = NextMonth(tokens, "MonthOfYear"); int dayOfMonth = 1; int dayOfWeek = 0; bool advanceDayOfWeek = false; bool addDay = false; if (tokens.HasNextToken || forRule) { var on = NextString(tokens, "On"); if (on.StartsWith("last", StringComparison.Ordinal)) { dayOfMonth = -1; dayOfWeek = ParseDayOfWeek(on.Substring(4)); } else { int index = on.IndexOf(">=", StringComparison.Ordinal); if (index > 0) { dayOfMonth = Int32.Parse(on.Substring(index + 2), CultureInfo.InvariantCulture); dayOfWeek = ParseDayOfWeek(on.Substring(0, index)); advanceDayOfWeek = true; } else { index = on.IndexOf("<=", StringComparison.Ordinal); if (index > 0) { dayOfMonth = Int32.Parse(on.Substring(index + 2), CultureInfo.InvariantCulture); dayOfWeek = ParseDayOfWeek(on.Substring(0, index)); } else { try { dayOfMonth = Int32.Parse(on, CultureInfo.InvariantCulture); dayOfWeek = 0; } catch (FormatException e) { throw new ArgumentException($"Unparsable ON token: {on}", e); } } } } if (tokens.HasNextToken || forRule) { var atTime = NextString(tokens, "AT"); if (!string.IsNullOrEmpty(atTime)) { if (Char.IsLetter(atTime[atTime.Length - 1])) { char zoneCharacter = atTime[atTime.Length - 1]; mode = ConvertModeCharacter(zoneCharacter); atTime = atTime.Substring(0, atTime.Length - 1); } if (atTime == "24:00") { timeOfDay = LocalTime.Midnight; addDay = true; } // As of TZDB 2018f, Japan's fallback transitions occur at 25:00. We can't // represent this entirely accurately, but this is as close as we can approximate it. else if (atTime == "25:00") { timeOfDay = new LocalTime(1, 0); addDay = true; } else { timeOfDay = ParserHelper.ParseTime(atTime); } } } } return(new ZoneYearOffset(mode, monthOfYear, dayOfMonth, dayOfWeek, advanceDayOfWeek, timeOfDay, addDay)); }
/// <summary> /// Parses an alias link and returns the ZoneAlias object. /// </summary> /// <param name="tokens">The tokens to parse.</param> /// <returns>The ZoneAlias object.</returns> internal ZoneAlias ParseLink(Tokens tokens) { var existing = NextString(tokens, "Existing"); var alias = NextString(tokens, "Alias"); return new ZoneAlias(existing, alias); }
/// <summary> /// Parses the ZoneYearOffset for a rule or zone. This is something like "3rd Sunday of October at 2am". /// </summary> /// <remarks> /// IN ON AT /// </remarks> /// <param name="tokens">The tokens to parse.</param> /// <param name="forRule">True if this is for a Rule line, in which case ON/AT are mandatory; /// false for a Zone line, in which case it's part of "until" and they're optional</param> /// <returns>The ZoneYearOffset object.</returns> internal ZoneYearOffset ParseDateTimeOfYear(Tokens tokens, bool forRule) { var mode = ZoneYearOffset.StartOfYear.Mode; var timeOfDay = ZoneYearOffset.StartOfYear.TimeOfDay; int monthOfYear = NextMonth(tokens, "MonthOfYear"); int dayOfMonth = 1; int dayOfWeek = 0; bool advanceDayOfWeek = false; bool addDay = false; if (tokens.HasNextToken || forRule) { var on = NextString(tokens, "On"); if (on.StartsWith("last", StringComparison.Ordinal)) { dayOfMonth = -1; dayOfWeek = ParseDayOfWeek(on.Substring(4)); } else { int index = on.IndexOf(">=", StringComparison.Ordinal); if (index > 0) { dayOfMonth = Int32.Parse(on.Substring(index + 2), CultureInfo.InvariantCulture); dayOfWeek = ParseDayOfWeek(on.Substring(0, index)); advanceDayOfWeek = true; } else { index = on.IndexOf("<=", StringComparison.Ordinal); if (index > 0) { dayOfMonth = Int32.Parse(on.Substring(index + 2), CultureInfo.InvariantCulture); dayOfWeek = ParseDayOfWeek(on.Substring(0, index)); } else { try { dayOfMonth = Int32.Parse(on, CultureInfo.InvariantCulture); dayOfWeek = 0; } catch (FormatException e) { throw new ArgumentException($"Unparsable ON token: {on}", e); } } } } if (tokens.HasNextToken || forRule) { var atTime = NextString(tokens, "AT"); if (!string.IsNullOrEmpty(atTime)) { if (Char.IsLetter(atTime[atTime.Length - 1])) { char zoneCharacter = atTime[atTime.Length - 1]; mode = ConvertModeCharacter(zoneCharacter); atTime = atTime.Substring(0, atTime.Length - 1); } if (atTime == "24:00") { timeOfDay = LocalTime.Midnight; addDay = true; } else { timeOfDay = ParserHelper.ParseTime(atTime); } } } } return new ZoneYearOffset(mode, monthOfYear, dayOfMonth, dayOfWeek, advanceDayOfWeek, timeOfDay, addDay); }
/// <summary> /// Parses the next string from the token stream as a year. /// </summary> /// <param name="tokens">The tokens.</param> /// <param name="defaultValue">The default value to return if the year isn't specified.</param> private static int NextYear(Tokens tokens, int defaultValue) { int result = defaultValue; string text; if (tokens.TryNextToken(out text)) { result = ParserHelper.ParseYear(text, defaultValue); } return result; }
/// <summary> /// Parses a daylight savings rule and returns the Rule object. /// </summary> /// <remarks> /// # Rule NAME FROM TO TYPE IN ON AT SAVE LETTER/S /// </remarks> /// <param name="tokens">The tokens to parse.</param> /// <returns>The Rule object.</returns> internal ZoneRule ParseRule(Tokens tokens) { var name = NextString(tokens, "GetName"); int fromYear = NextYear(tokens, "FromYear", 0); int toYear = NextYear(tokens, "ToYear", fromYear); if (toYear < fromYear) { throw new ArgumentException("To year cannot be before the from year in a Rule: " + toYear + " < " + fromYear); } /* string type = */ NextOptional(tokens, "Type"); var yearOffset = ParseDateTimeOfYear(tokens, true); var savings = NextOffset(tokens, "SaveMillis"); var letterS = NextOptional(tokens, "LetterS"); var recurrence = new ZoneRecurrence(name, savings, yearOffset, fromYear, toYear); return new ZoneRule(recurrence, letterS); }