static void Main(string[] args) { try { foreach (var testCase in s_testCases) { Console.WriteLine(testCase.ToString()); testCase.PerformTest(); } Console.WriteLine(); Console.WriteLine("Parse failure cases:"); foreach (var testCase in s_parseFailureCases) { Console.WriteLine(testCase); TimeZoneTag tag; if (TimeZoneTag.TryParse(testCase, out tag)) { throw new ApplicationException("Failed parse failure test"); } } Console.WriteLine(); Console.WriteLine("All tests passed."); } catch (Exception err) { Console.WriteLine(err.ToString()); } Console.WriteLine(); Console.WriteLine("Press any key to exit."); Console.ReadKey(); }
public bool Equals(TimeZoneTag other) { if (other == null) { return(false); } return(Kind == other.Kind && m_offset == other.m_offset); }
/// <summary> /// Constructs a DateTag from constituent values /// </summary> /// <param name="date">A <see cref="DateTime"/> value.</param> /// <param name="timeZone">A <see cref="TimeZoneTag"/> value or null if unknown.</param> /// <param name="precision">Precision in terms of significant digits. If zero /// then set to maximum (<see cref="PrecisionMax"/>).</param> /// <remarks> /// <para>If timeZone is null, the timezone will be set to <see cref="TimeZoneTag.ForceLocal"/> /// if the <paramref name="date"/> <see cref="DateTime.Kind"/> is <see cref="DateTimeKind.Local"/> /// or <see cref="TimeZoneTag.Unknown"/>, and to <see cref="TimeZoneTag.ForceUtc"/> if /// <see cref="DateTime.Kind"/> is <see cref="DateTimeKind.Utc"/>. /// </para> /// <para>If precision is zero, the precision is detected by the number of trailing zeros /// after the seconds decimal point. The lowest precision detected is <see cref="PrecisionSecond"/>. /// See <see cref="DetectPrecision(DateTime)"/>. /// </para> /// </remarks> public DateTag(DateTime date, TimeZoneTag timeZone = null, int precision = 0) { // Default the timezone value if needed. if (timeZone == null) { switch (date.Kind) { case DateTimeKind.Local: timeZone = TimeZoneTag.ForceLocal; break; case DateTimeKind.Utc: timeZone = TimeZoneTag.ForceUtc; break; default: timeZone = TimeZoneTag.Zero; break; } } // Change date to a local timezone if needed if (date.Kind == DateTimeKind.Utc) { date = timeZone.ToLocal(date); } // Limit precision to compatible range if (precision > PrecisionMax) { precision = PrecisionMax; } if (precision < PrecisionMin) { precision = DetectPrecision(date); } m_dateTicks = date.Ticks; TimeZone = timeZone; Precision = precision; }
// Throws an exception if any test fails. // Exceptions are convenient indicators because they can include a message and the location of the error. public void PerformTest() { // Parse test TimeZoneTag tag; if (!TimeZoneTag.TryParse(m_srcTag, out tag)) { throw new ApplicationException("Failed TryParse test."); } if (!tag.ToString().Equals(m_normalizedTag, StringComparison.Ordinal)) { throw new ApplicationException("Failed ToString test."); } if (tag.Kind != m_kind) { throw new ApplicationException("Failed Kind test."); } int utcOffset = (tag.Kind == TimeZoneKind.Normal) ? m_utcOffset : 0; if (tag.UtcOffset != new TimeSpan(0, utcOffset, 0)) { throw new ApplicationException("Failed UtcOffset test."); } if (tag.UtcOffsetMinutes != utcOffset) { throw new ApplicationException("Failed UtcOffsetMinutes test."); } if (tag.UtcOffsetTicks != utcOffset * c_ticksPerMinute) { throw new ApplicationException("Failed UtcOffsetTicks test."); } TimeZoneTag tag2 = new TimeZoneTag(m_utcOffset, m_kind); if (tag2.GetHashCode() != tag.GetHashCode()) { throw new ApplicationException("Failed GetHashCode test."); } if (!tag2.Equals(tag)) { throw new ApplicationException("Failed Equals test."); } tag2 = new TimeZoneTag(new TimeSpan(0, utcOffset, 0), m_kind); if (!tag2.Equals(tag)) { throw new ApplicationException("Failed TimeSpan Constructor test"); } tag2 = new TimeZoneTag(utcOffset * c_ticksPerMinute, m_kind); if (!tag2.Equals(tag)) { throw new ApplicationException("Failed Ticks Constructor test"); } if (m_kind == TimeZoneKind.Normal) { tag2 = new TimeZoneTag(m_utcOffset + 1, m_kind); if (tag2.GetHashCode() == tag.GetHashCode()) { throw new ApplicationException("Failed GetHashCode no match test"); } if (tag2.Equals(tag)) { throw new ApplicationException("Failed Not Equals test"); } tag2 = new TimeZoneTag(m_utcOffset, TimeZoneKind.ForceLocal); if (tag2.Equals(tag)) { throw new ApplicationException("Failed ForceLocal test"); } tag2 = new TimeZoneTag(m_utcOffset, TimeZoneKind.ForceLocal); if (tag2.Equals(tag)) { throw new ApplicationException("Failed ForceUtc test"); } if (utcOffset == 0 && !tag.Equals(TimeZoneTag.Zero)) { throw new ApplicationException("Failed Zero test"); } } else if (m_kind == TimeZoneKind.ForceLocal) { if (!tag.Equals(TimeZoneTag.ForceLocal)) { throw new ApplicationException("Failed ForceLocal test"); } if (tag.Equals(TimeZoneTag.ForceUtc)) { throw new ApplicationException("Failed ForceUtc test"); } } else // m_kind == TimeZoneKind.ForceUtc { if (!tag.Equals(TimeZoneTag.ForceUtc)) { throw new ApplicationException("Failed ForceUtc test"); } if (tag.Equals(TimeZoneTag.ForceLocal)) { throw new ApplicationException("Failed ForceLocal test"); } } tag2 = TimeZoneTag.Parse(m_srcTag); if (!tag2.Equals(tag)) { throw new ApplicationException("Failed Parse test"); } tag2 = TimeZoneTag.Parse(m_normalizedTag); if (!tag2.Equals(tag)) { throw new ApplicationException("Failed Parse Normalized test"); } DateTime dtLocal = new DateTime(1968, 7, 23, 8, 24, 46, 22, DateTimeKind.Local); DateTime dtUtc = new DateTime(dtLocal.Ticks - (utcOffset * c_ticksPerMinute), DateTimeKind.Utc); DateTimeOffset dto = new DateTimeOffset(dtLocal.Ticks, TimeSpan.FromMinutes(utcOffset)); if (!tag.ToLocal(dtUtc).Equals(dtLocal)) { throw new ApplicationException("Failed ToLocal test"); } if (!tag.ToUtc(dtLocal).Equals(dtUtc)) { throw new ApplicationException("Failed ToUtc test"); } if (!tag.ToLocal(dtLocal).Equals(dtLocal)) { throw new ApplicationException("Failed ToLocal already local test"); } if (!tag.ToUtc(dtUtc).Equals(dtUtc)) { throw new ApplicationException("Failed ToUtc already utc test"); } if (!tag.ToLocal(DateTime.SpecifyKind(dtUtc, DateTimeKind.Unspecified)).Equals(dtLocal)) { throw new ApplicationException("Failed ToLocal Unspecified test"); } if (!tag.ToUtc(DateTime.SpecifyKind(dtLocal, DateTimeKind.Unspecified)).Equals(dtUtc)) { throw new ApplicationException("Failed ToUtc Unspecified test"); } if (!tag.ToDateTimeOffset(dtUtc).Equals(dto)) { throw new ApplicationException("Failed ToDateTimeOffset UTC test"); } if (!tag.ToDateTimeOffset(dtLocal).Equals(dto)) { throw new ApplicationException("Failed ToDateTimeOffset Local test"); } }
/// <summary> /// Parses a metadata date tag into a <see cref="DateTag"/> including <see cref="DateTime"/>, <see cref="TimeZoneTag"/>, and significant digits. /// </summary> /// <param name="dateTag">The value to be parsed in <see cref="https://www.w3.org/TR/NOTE-datetime">W3CDTF</see> format.</param> /// <param name="result">The result of the parsing.</param> /// <returns>True if successful, else false.</returns> /// <remarks> /// <para>The <see cref="https://www.w3.org/TR/NOTE-datetime">W3CDTF</see> format has date and timezone portions. /// This method parses both.</para> /// <para>If the timezone portion is not included in the input string then the resulting <paramref name="timezone"/> /// will have <see cref="Kind"/> set to <see cref="TimeZoneKind.ForceLocal"/>. /// </para> /// <para>If the timezone portion is set to "Z" indicating UTC, then the resulting <paramref name="timezone"/> /// will have <see cref="Kind"/> set to <see cref="TimeZoneKind.ForceUtc"/> and the <see cref="UtcOffset"/> /// will be zero. /// </para> /// <para>The W2CDTF format permits partial date-time values. For example "2018" is just a year with no /// other information. The <paramref name="precision"/> value indicates how much detail is included /// as follows: 4 = year, 6 = month, 8 = day, 10 = hour, 12 = minute, 14 = second, 17 = millisecond, 20 = microsecond, /// 21 = tick (100 nanoseconds). /// </para> /// </remarks> public static bool TryParse(string dateTag, out DateTag result) { // Init values for failure case result = new DateTag(ZeroDate, TimeZoneTag.Zero, 0); // Init parts int year = 0; int month = 1; int day = 1; int hour = 12; // Noon int minute = 0; int second = 0; long ticks = 0; // Track position int pos = 0; if (dateTag.Length < 4) { return(false); } if (!int.TryParse(dateTag.Substring(0, 4), out year) || year < 1 || year > 9999) { return(false); } int precision = PrecisionYear; pos = 4; if (dateTag.Length > 5 && dateTag[4] == '-') { if (!int.TryParse(dateTag.Substring(5, 2), out month) || month < 1 || month > 12) { return(false); } precision = PrecisionMonth; pos = 7; if (dateTag.Length > 8 && dateTag[7] == '-') { if (!int.TryParse(dateTag.Substring(8, 2), out day) || day < 1 || day > DateTime.DaysInMonth(year, month)) { return(false); } precision = PrecisionDay; pos = 10; if (dateTag.Length > 11 && (dateTag[10] == 'T' || dateTag[10] == ' ')) // Even though W3CDTF and ISO 8601 specify 'T' separating date and time, tolerate a space as an alternative. { if (!int.TryParse(dateTag.Substring(11, 2), out hour) || hour < 0 || hour > 23) { return(false); } precision = PrecisionHour; pos = 13; if (dateTag.Length > 14 && dateTag[13] == ':') { if (!int.TryParse(dateTag.Substring(14, 2), out minute) || minute < 0 || minute > 59) { return(false); } precision = PrecisionMinute; pos = 16; if (dateTag.Length > 17 && dateTag[16] == ':') { if (!int.TryParse(dateTag.Substring(17, 2), out second) || second < 0 || second > 59) { return(false); } precision = PrecisionSecond; pos = 19; if (dateTag.Length > 20 && dateTag[19] == '.') { ++pos; int anchor = pos; while (pos < dateTag.Length && char.IsDigit(dateTag[pos])) { ++pos; } precision = PrecisionSecond + (pos - anchor); if (precision > PrecisionMax) { precision = PrecisionMax; } double d; if (!double.TryParse(dateTag.Substring(anchor, pos - anchor), out d)) { return(false); } ticks = (long)(d * Math.Pow(10.0, 7.0 - (pos - anchor))); } } } } } } // Attempt to parse the timezone TimeZoneTag timezone; DateTimeKind dtk = DateTimeKind.Unspecified; if (pos < dateTag.Length) { if (!TimeZoneTag.TryParse(dateTag.Substring(pos), out timezone)) { return(false); } dtk = (timezone.Kind == TimeZoneKind.ForceUtc) ? DateTimeKind.Utc : DateTimeKind.Local; } else { timezone = TimeZoneTag.ForceLocal; dtk = DateTimeKind.Local; } result = new DateTag(new DateTime(year, month, day, hour, minute, second, dtk).AddTicks(ticks), timezone, precision); return(true); }
/// <summary> /// Parses a timezone string into a TimeZoneTag instance. /// </summary> /// <param name="s">The timezone string to parse.</param> /// <param name="result">The parsed timezone.</param> /// <returns>True if successful, else false.</returns> /// <remarks> /// <para>See <see cref="TimeZoneTag"/> for details about valid values. /// </para> /// <para>Example timezone values:</para> /// <para> "-05:00" (UTC minus 5 hours)</para> /// <para> "+06:00" (UTC plus 6 hours)</para> /// <para> "+09:30" (UTC plus 9 1/2 hours)</para> /// <para> "Z" (UTC. Offset to local is unknown.)</para> /// <para> "0" (Local. Offset to UTC is unknwon.)</para> /// <para>Tolerable timezone values:</para> /// <para> "-5" (UTC minus 5 hours)</para> /// <para> "+6 (UTC plus 6 hours)</para> /// </remarks> public static bool TryParse(string timezoneTag, out TimeZoneTag result) { if (string.IsNullOrEmpty(timezoneTag)) { result = Zero; return(false); } if (timezoneTag.Equals(c_local, StringComparison.Ordinal)) { result = ForceLocal; return(true); } if (timezoneTag.Equals(c_utc, StringComparison.Ordinal)) { result = ForceUtc; return(true); } result = Zero; if (timezoneTag.Length < 2) { return(false); } bool negative; if (timezoneTag[0] == '+') { negative = false; } else if (timezoneTag[0] == '-') { negative = true; } else { return(false); } var parts = timezoneTag.Substring(1).Split(':'); if (parts.Length < 1 || parts.Length > 2) { return(false); } int hours; if (!int.TryParse(parts[0], out hours)) { return(false); } if (hours < 0) { return(false); } int minutes = 0; if (parts.Length > 1) { if (!int.TryParse(parts[1], out minutes)) { return(false); } if (minutes < 0 || minutes > 59) { return(false); } } int totalMinutes = hours * 60 + minutes; if (negative) { totalMinutes = -totalMinutes; } if (totalMinutes < c_minOffset || totalMinutes > c_maxOffset) { return(false); } result = new TimeZoneTag(totalMinutes, TimeZoneKind.Normal); return(true); }