From b05d8c930dddb41ad1ed8efe6bb93c17f216fdb7 Mon Sep 17 00:00:00 2001 From: Qstick Date: Tue, 7 Dec 2021 20:19:05 -0600 Subject: [PATCH] Date Routines Test Cases --- .../ParserTests/DateTimeRoutinesFixture.cs | 54 +++++ src/NzbDrone.Core/Parser/DateTimeRoutines.cs | 200 ++++++++++++++---- src/NzbDrone.Core/Parser/DateTimeUtil.cs | 4 +- 3 files changed, 213 insertions(+), 45 deletions(-) create mode 100644 src/NzbDrone.Core.Test/ParserTests/DateTimeRoutinesFixture.cs diff --git a/src/NzbDrone.Core.Test/ParserTests/DateTimeRoutinesFixture.cs b/src/NzbDrone.Core.Test/ParserTests/DateTimeRoutinesFixture.cs new file mode 100644 index 000000000..7de34424e --- /dev/null +++ b/src/NzbDrone.Core.Test/ParserTests/DateTimeRoutinesFixture.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections; +using NUnit.Framework; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.ParserTests +{ + [TestFixture] + public class DateTimeRoutinesFixture : CoreTest + { + public static IEnumerable DateTimeTestCases + { + get + { + yield return new TestCaseData(@"Member since: 10-Feb-2008").Returns(new DateTime(2008, 2, 10, 0, 0, 0)); + yield return new TestCaseData(@"Last Update: 18:16 11 Feb '08 ").Returns(new DateTime(2008, 2, 11, 18, 16, 0)); + yield return new TestCaseData(@"date Tue, Feb 10, 2008 at 11:06 AM").Returns(new DateTime(2008, 2, 10, 11, 06, 0)); + yield return new TestCaseData(@"see at 12/31/2007 14:16:32").Returns(new DateTime(2007, 12, 31, 14, 16, 32)); + yield return new TestCaseData(@"sack finish 14:16:32 November 15 2008, 1-144 app").Returns(new DateTime(2008, 11, 15, 14, 16, 32)); + yield return new TestCaseData(@"Genesis Message - Wed 04 Feb 08 - 19:40").Returns(new DateTime(2008, 2, 4, 19, 40, 0)); + yield return new TestCaseData(@"The day 07/31/07 14:16:32 is ").Returns(new DateTime(2007, 7, 31, 14, 16, 32)); + yield return new TestCaseData(@"Shipping is on us until December 24, 2008 within the U.S. ").Returns(new DateTime(2008, 12, 24, 0, 0, 0)); + yield return new TestCaseData(@" 2008 within the U.S. at 14:16:32").Returns(new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, 14, 16, 32)); + yield return new TestCaseData(@"5th November, 1994, 8:15:30 pm").Returns(new DateTime(1994, 11, 5, 20, 15, 30)); + yield return new TestCaseData(@"7 boxes January 31 , 14:16:32.").Returns(new DateTime(DateTime.Now.Year, 1, 31, 14, 16, 32)); + yield return new TestCaseData(@"the blue sky of Sept 30th 2008 14:16:32").Returns(new DateTime(2008, 9, 30, 14, 16, 32)); + yield return new TestCaseData(@" e.g. 1997-07-16T19:20:30+01:00").Returns(new DateTime(1997, 7, 16, 19, 20, 30)); + yield return new TestCaseData(@"Apr 1st, 2008 14:16:32 tufa 6767").Returns(new DateTime(2008, 4, 1, 14, 16, 32)); + yield return new TestCaseData(@"wait for 07/31/07 14:16:32").Returns(new DateTime(2007, 7, 31, 14, 16, 32)); + yield return new TestCaseData(@"later 12.31.08 and before 1.01.09").Returns(new DateTime(2008, 12, 31, 0, 0, 0)); + yield return new TestCaseData(@"Expires: Sept 30th 2008 14:16:32").Returns(new DateTime(2008, 9, 30, 14, 16, 32)); + yield return new TestCaseData(@"Offer expires Apr 1st, 2007, 14:16:32").Returns(new DateTime(2007, 4, 1, 14, 16, 32)); + yield return new TestCaseData(@"Expires 14:16:32 January 31.").Returns(new DateTime(DateTime.Now.Year, 1, 31, 14, 16, 32)); + yield return new TestCaseData(@"Expires 14:16:32 January 31-st.").Returns(new DateTime(DateTime.Now.Year, 1, 31, 14, 16, 32)); + yield return new TestCaseData(@"Expires 23rd January 2010.").Returns(new DateTime(2010, 1, 23, 0, 0, 0)); + yield return new TestCaseData(@"Expires January 22nd, 2010.").Returns(new DateTime(2010, 1, 22, 0, 0, 0)); + yield return new TestCaseData(@"Expires DEC 22, 2010.").Returns(new DateTime(2010, 12, 22, 0, 0, 0)); + yield return new TestCaseData(@"Version: 1.0.0.692 6/1/2010 2:28:04 AM ").Returns(new DateTime(2010, 6, 1, 2, 28, 4)); + yield return new TestCaseData(@"Version: 1.0.0.692 04/21/11 12:30am ").Returns(new DateTime(2011, 4, 21, 00, 30, 00)); + yield return new TestCaseData(@"Version: 1.0.0.692 04/21/11 12:30pm ").Returns(new DateTime(2011, 4, 21, 12, 30, 00)); + yield return new TestCaseData(@"Version: Thu Aug 06 22:32:15 MDT 2009 ").Returns(new DateTime(2009, 8, 6, 22, 32, 15)); + } + } + + [TestCaseSource("DateTimeTestCases")] + public DateTime should_parse_date(string date) + { + DateTimeRoutines.TryParseDateOrTime(date, DateTimeRoutines.DateTimeFormat.USDate, out var parsedDateTime); + + return parsedDateTime.DateTime; + } + } +} diff --git a/src/NzbDrone.Core/Parser/DateTimeRoutines.cs b/src/NzbDrone.Core/Parser/DateTimeRoutines.cs index 27cdcc17b..0592ed222 100644 --- a/src/NzbDrone.Core/Parser/DateTimeRoutines.cs +++ b/src/NzbDrone.Core/Parser/DateTimeRoutines.cs @@ -21,6 +21,9 @@ namespace NzbDrone.Core.Parser public readonly DateTime DateTime; public readonly bool IsDateFound; public readonly bool IsTimeFound; + public readonly TimeSpan UtcOffset; + public readonly bool IsUtcOffsetFound; + public DateTime UtcDateTime; internal ParsedDateTime(int index_of_date, int length_of_date, int index_of_time, int length_of_time, DateTime date_time) { @@ -31,6 +34,46 @@ namespace NzbDrone.Core.Parser DateTime = date_time; IsDateFound = index_of_date > -1; IsTimeFound = index_of_time > -1; + UtcOffset = new TimeSpan(25, 0, 0); + IsUtcOffsetFound = false; + UtcDateTime = new DateTime(1, 1, 1); + } + + internal ParsedDateTime(int index_of_date, int length_of_date, int index_of_time, int length_of_time, DateTime date_time, TimeSpan utc_offset) + { + IndexOfDate = index_of_date; + LengthOfDate = length_of_date; + IndexOfTime = index_of_time; + LengthOfTime = length_of_time; + DateTime = date_time; + IsDateFound = index_of_date > -1; + IsTimeFound = index_of_time > -1; + UtcOffset = utc_offset; + IsUtcOffsetFound = Math.Abs(utc_offset.TotalHours) < 12; + if (!IsUtcOffsetFound) + { + UtcDateTime = new DateTime(1, 1, 1); + } + else + { + if (index_of_date < 0) + { + //to avoid negative date exception when date is undefined + var ts = date_time.TimeOfDay + utc_offset; + if (ts < new TimeSpan(0)) + { + UtcDateTime = new DateTime(1, 1, 2) + ts; + } + else + { + UtcDateTime = new DateTime(1, 1, 1) + ts; + } + } + else + { + UtcDateTime = date_time + utc_offset; + } + } } } @@ -78,7 +121,7 @@ namespace NzbDrone.Core.Parser } var date_time = new DateTime(DefaultDate.Year, DefaultDate.Month, DefaultDate.Day, parsed_time.DateTime.Hour, parsed_time.DateTime.Minute, parsed_time.DateTime.Second); - parsed_date_time = new ParsedDateTime(-1, -1, parsed_time.IndexOfTime, parsed_time.LengthOfTime, date_time); + parsed_date_time = new ParsedDateTime(-1, -1, parsed_time.IndexOfTime, parsed_time.LengthOfTime, date_time, parsed_time.UtcOffset); } else { @@ -90,7 +133,7 @@ namespace NzbDrone.Core.Parser else { var date_time = new DateTime(parsed_date.DateTime.Year, parsed_date.DateTime.Month, parsed_date.DateTime.Day, parsed_time.DateTime.Hour, parsed_time.DateTime.Minute, parsed_time.DateTime.Second); - parsed_date_time = new ParsedDateTime(parsed_date.IndexOfDate, parsed_date.LengthOfDate, parsed_time.IndexOfTime, parsed_time.LengthOfTime, date_time); + parsed_date_time = new ParsedDateTime(parsed_date.IndexOfDate, parsed_date.LengthOfDate, parsed_time.IndexOfTime, parsed_time.LengthOfTime, date_time, parsed_time.UtcOffset); } } @@ -101,72 +144,137 @@ namespace NzbDrone.Core.Parser { parsed_time = null; + string time_zone_r; + if (default_format == DateTimeFormat.USDate) + { + time_zone_r = @"(?:\s*(?'time_zone'UTC|GMT|CST|EST))?"; + } + else + { + time_zone_r = @"(?:\s*(?'time_zone'UTC|GMT))?"; + } + Match m; if (parsed_date != null && parsed_date.IndexOfDate > -1) { //look around the found date - //look for [h]h:mm[:ss] [PM/AM] - m = Regex.Match(str.Substring(parsed_date.IndexOfDate + parsed_date.LengthOfDate), @"(?<=^\s*,?\s+|^\s*at\s*|^\s*[T\-]\s*)(?'hour'\d{1,2})\s*:\s*(?'minute'\d{2})\s*(?::\s*(?'second'\d{2}))?(?:\s*(?'ampm'AM|am|PM|pm))?(?=$|[^\d\w])", RegexOptions.Compiled); + //look for hh:mm:ss + m = Regex.Match(str.Substring(parsed_date.IndexOfDate + parsed_date.LengthOfDate), @"(?<=^\s*,?\s+|^\s*at\s*|^\s*[T\-]\s*)(?'hour'\d{2})\s*:\s*(?'minute'\d{2})\s*:\s*(?'second'\d{2})\s+(?'offset_sign'[\+\-])(?'offset_hh'\d{2}):?(?'offset_mm'\d{2})(?=$|[^\d\w])", RegexOptions.Compiled); + if (!m.Success) + { + //look for [h]h:mm[:ss] [PM/AM] [UTC/GMT] + m = Regex.Match(str.Substring(parsed_date.IndexOfDate + parsed_date.LengthOfDate), @"(?<=^\s*,?\s+|^\s*at\s*|^\s*[T\-]\s*)(?'hour'\d{1,2})\s*:\s*(?'minute'\d{2})\s*(?::\s*(?'second'\d{2}))?(?:\s*(?'ampm'AM|am|PM|pm))?" + time_zone_r + @"(?=$|[^\d\w])", RegexOptions.Compiled); + } + + if (!m.Success) + { + //look for [h]h:mm:ss [PM/AM] [UTC/GMT] + m = Regex.Match(str.Substring(0, parsed_date.IndexOfDate), @"(?<=^|[^\d])(?'hour'\d{1,2})\s*:\s*(?'minute'\d{2})\s*(?::\s*(?'second'\d{2}))?(?:\s*(?'ampm'AM|am|PM|pm))?" + time_zone_r + @"(?=$|[\s,]+)", RegexOptions.Compiled); + } + if (!m.Success) { - //look for [h]h:mm:ss - m = Regex.Match(str.Substring(0, parsed_date.IndexOfDate), @"(?<=^|[^\d])(?'hour'\d{1,2})\s*:\s*(?'minute'\d{2})\s*(?::\s*(?'second'\d{2}))?(?:\s*(?'ampm'AM|am|PM|pm))?(?=$|[\s,]+)", RegexOptions.Compiled); + //look for [h]h:mm:ss [PM/AM] [UTC/GMT] within + m = Regex.Match(str.Substring(parsed_date.IndexOfDate, parsed_date.LengthOfDate), @"(?<=^|[^\d])(?'hour'\d{1,2})\s*:\s*(?'minute'\d{2})\s*(?::\s*(?'second'\d{2}))?(?:\s*(?'ampm'AM|am|PM|pm))?" + time_zone_r + @"(?=$|[\s,]+)", RegexOptions.Compiled); } } else { - //look anywere within string - //look for [h]h:mm[:ss] [PM/AM] - m = Regex.Match(str, @"(?<=^|\s+|\s*T\s*)(?'hour'\d{1,2})\s*:\s*(?'minute'\d{2})\s*(?::\s*(?'second'\d{2}))?(?:\s*(?'ampm'AM|am|PM|pm))?(?=$|[^\d\w])", RegexOptions.Compiled); + //look anywhere within string + //look for hh:mm:ss + m = Regex.Match(str, @"(?<=^|\s+|\s*T\s*)(?'hour'\d{2})\s*:\s*(?'minute'\d{2})\s*:\s*(?'second'\d{2})\s+(?'offset_sign'[\+\-])(?'offset_hh'\d{2}):?(?'offset_mm'\d{2})?(?=$|[^\d\w])", RegexOptions.Compiled); + if (!m.Success) + { + //look for [h]h:mm[:ss] [PM/AM] [UTC/GMT] + m = Regex.Match(str, @"(?<=^|\s+|\s*T\s*)(?'hour'\d{1,2})\s*:\s*(?'minute'\d{2})\s*(?::\s*(?'second'\d{2}))?(?:\s*(?'ampm'AM|am|PM|pm))?" + time_zone_r + @"(?=$|[^\d\w])", RegexOptions.Compiled); + } } - if (m.Success) + if (!m.Success) { - try + return false; + } + + //try + //{ + var hour = int.Parse(m.Groups["hour"].Value); + if (hour < 0 || hour > 23) + { + return false; + } + + var minute = int.Parse(m.Groups["minute"].Value); + if (minute < 0 || minute > 59) + { + return false; + } + + var second = 0; + if (!string.IsNullOrEmpty(m.Groups["second"].Value)) + { + second = int.Parse(m.Groups["second"].Value); + if (second < 0 || second > 59) { - var hour = int.Parse(m.Groups["hour"].Value); - if (hour < 0 || hour > 23) - { - return false; - } + return false; + } + } - var minute = int.Parse(m.Groups["minute"].Value); - if (minute < 0 || minute > 59) - { - return false; - } + if (string.Compare(m.Groups["ampm"].Value, "PM", true) == 0 && hour < 12) + { + hour += 12; + } + else if (string.Compare(m.Groups["ampm"].Value, "AM", true) == 0 && hour == 12) + { + hour -= 12; + } - var second = 0; - if (!string.IsNullOrEmpty(m.Groups["second"].Value)) - { - second = int.Parse(m.Groups["second"].Value); - if (second < 0 || second > 59) - { - return false; - } - } + var date_time = new DateTime(1, 1, 1, hour, minute, second); - if (string.Compare(m.Groups["ampm"].Value, "PM", true) == 0 && hour < 12) - { - hour += 12; - } - else if (string.Compare(m.Groups["ampm"].Value, "AM", true) == 0 && hour == 12) - { - hour -= 12; - } + if (m.Groups["offset_hh"].Success) + { + var offset_hh = int.Parse(m.Groups["offset_hh"].Value); + var offset_mm = 0; + if (m.Groups["offset_mm"].Success) + { + offset_mm = int.Parse(m.Groups["offset_mm"].Value); + } - var date_time = new DateTime(1, 1, 1, hour, minute, second); - parsed_time = new ParsedDateTime(-1, -1, m.Index, m.Length, date_time); + var utc_offset = new TimeSpan(offset_hh, offset_mm, 0); + if (m.Groups["offset_sign"].Value == "-") + { + utc_offset = -utc_offset; } - catch + + parsed_time = new ParsedDateTime(-1, -1, m.Index, m.Length, date_time, utc_offset); + return true; + } + + if (m.Groups["time_zone"].Success) + { + TimeSpan utc_offset; + switch (m.Groups["time_zone"].Value) { - return false; + case "UTC": + case "GMT": + utc_offset = new TimeSpan(0, 0, 0); + break; + case "CST": + utc_offset = new TimeSpan(-6, 0, 0); + break; + case "EST": + utc_offset = new TimeSpan(-5, 0, 0); + break; + default: + throw new Exception("Time zone: " + m.Groups["time_zone"].Value + " is not defined."); } + parsed_time = new ParsedDateTime(-1, -1, m.Index, m.Length, date_time, utc_offset); return true; } - return false; + parsed_time = new ParsedDateTime(-1, -1, m.Index, m.Length, date_time); + + return true; } public static bool TryParseDate(this string str, DateTimeFormat default_format, out ParsedDateTime parsed_date) @@ -230,6 +338,12 @@ namespace NzbDrone.Core.Parser m = Regex.Match(str, @"(?:^|[^\d\w])(?'year'\d{4})\s+(?'month'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[uarychilestmbro]*\s+(?'day'\d{1,2})(?:-?st|-?th|-?rd|-?nd)?(?=$|[^\d\w])", RegexOptions.Compiled | RegexOptions.IgnoreCase); } + if (!m.Success) + { + //look for month dd hh:mm:ss MDT|UTC yyyy + m = Regex.Match(str, @"(?:^|[^\d\w])(?'month'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[uarychilestmbro]*\s+(?'day'\d{1,2})\s+\d{2}\:\d{2}\:\d{2}\s+(?:MDT|UTC)\s+(?'year'\d{4})(?=$|[^\d\w])", RegexOptions.Compiled | RegexOptions.IgnoreCase); + } + if (!m.Success) { //look for month dd [yyyy] diff --git a/src/NzbDrone.Core/Parser/DateTimeUtil.cs b/src/NzbDrone.Core/Parser/DateTimeUtil.cs index 2f6596350..3e05f60a7 100644 --- a/src/NzbDrone.Core/Parser/DateTimeUtil.cs +++ b/src/NzbDrone.Core/Parser/DateTimeUtil.cs @@ -104,10 +104,10 @@ namespace NzbDrone.Core.Parser { var dtFormat = format == "UK" ? DateTimeRoutines.DateTimeFormat.UKDate : - DateTimeRoutines.DateTimeFormat.UKDate; + DateTimeRoutines.DateTimeFormat.USDate; if (DateTimeRoutines.TryParseDateOrTime( - str, dtFormat, out var dt)) + str, dtFormat, out DateTimeRoutines.ParsedDateTime dt)) { return dt.DateTime; }