From f7e68847207ed7bdd6d915a49807f3173c392596 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 25 Feb 2023 23:00:15 +0200 Subject: [PATCH] cardigann: invariant date string parsing (#14074) --- .../Extensions/StringExtensions.cs | 13 ++ .../Indexers/CardigannIndexer.cs | 31 ++--- src/Jackett.Common/Utils/DateTimeUtil.cs | 115 ++++++++++-------- .../Common/Utils/DateTimeUtilTests.cs | 28 +++++ 4 files changed, 115 insertions(+), 72 deletions(-) diff --git a/src/Jackett.Common/Extensions/StringExtensions.cs b/src/Jackett.Common/Extensions/StringExtensions.cs index f02e376e6..fc933d3f6 100644 --- a/src/Jackett.Common/Extensions/StringExtensions.cs +++ b/src/Jackett.Common/Extensions/StringExtensions.cs @@ -10,5 +10,18 @@ namespace Jackett.Common.Extensions public static bool ContainsIgnoreCase(this string source, string contains) => source != null && contains != null && CultureInfo.InvariantCulture.CompareInfo.IndexOf(source, contains, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0; public static bool ContainsIgnoreCase(this IEnumerable source, string value) => source.Contains(value, StringComparer.InvariantCultureIgnoreCase); + + public static bool IsAllDigits(this string input) + { + foreach (var c in input) + { + if (c < '0' || c > '9') + { + return false; + } + } + + return true; + } } } diff --git a/src/Jackett.Common/Indexers/CardigannIndexer.cs b/src/Jackett.Common/Indexers/CardigannIndexer.cs index 6616d14ce..e633f8aaf 100644 --- a/src/Jackett.Common/Indexers/CardigannIndexer.cs +++ b/src/Jackett.Common/Indexers/CardigannIndexer.cs @@ -1013,19 +1013,14 @@ namespace Jackett.Common.Indexers case "dateparse": var layout = (string)Filter.Args; - if (layout.Contains("yy") && DateTime.TryParseExact(Data, layout, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedDate)) - Data = parsedDate.ToString(DateTimeUtil.Rfc1123ZPattern); - else + try { - try - { - var datetime = DateTimeUtil.ParseDateTimeGoLang(Data, layout); - Data = datetime.ToString(DateTimeUtil.Rfc1123ZPattern); - } - catch (FormatException ex) - { - logger.Debug(ex.Message); - } + var datetime = DateTimeUtil.ParseDateTimeGoLang(Data, layout); + Data = datetime.ToString(DateTimeUtil.Rfc1123ZPattern, CultureInfo.InvariantCulture); + } + catch (FormatException ex) + { + logger.Debug(ex.Message); } break; case "regexp": @@ -1093,10 +1088,10 @@ namespace Jackett.Common.Indexers break; case "timeago": case "reltime": - Data = DateTimeUtil.FromTimeAgo(Data).ToString(DateTimeUtil.Rfc1123ZPattern); + Data = DateTimeUtil.FromTimeAgo(Data).ToString(DateTimeUtil.Rfc1123ZPattern, CultureInfo.InvariantCulture); break; case "fuzzytime": - Data = DateTimeUtil.FromUnknown(Data).ToString(DateTimeUtil.Rfc1123ZPattern); + Data = DateTimeUtil.FromUnknown(Data).ToString(DateTimeUtil.Rfc1123ZPattern, CultureInfo.InvariantCulture); break; case "validfilename": Data = StringUtil.MakeValidFileName(Data, '_', false); @@ -1509,7 +1504,7 @@ namespace Jackett.Common.Indexers variables[variablesKey] = null; continue; } - throw new Exception(string.Format("Error while parsing field={0}, selector={1}, value={2}: {3}", Field.Key, Field.Value.Selector, (value == null ? "" : value), ex.Message)); + throw new Exception($"Error while parsing field={Field.Key}, selector={Field.Value.Selector}, value={value ?? ""}: {ex.Message}", ex); } } @@ -1646,7 +1641,7 @@ namespace Jackett.Common.Indexers variables[variablesKey] = null; continue; } - throw new Exception(string.Format("Error while parsing field={0}, selector={1}, value={2}: {3}", Field.Key, Field.Value.Selector, (value == null ? "" : value), ex.Message)); + throw new Exception($"Error while parsing field={Field.Key}, selector={Field.Value.Selector}, value={value ?? ""}: {ex.Message}", ex); } } @@ -2040,8 +2035,8 @@ namespace Jackett.Common.Indexers value = release.Seeders.ToString(); break; case "date": - release.PublishDate = DateTimeUtil.FromUnknown(value); - value = release.PublishDate.ToString(DateTimeUtil.Rfc1123ZPattern); + release.PublishDate = DateTime.TryParseExact(value, DateTimeUtil.Rfc1123ZPattern, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedDate) ? parsedDate : DateTimeUtil.FromUnknown(value); + value = release.PublishDate.ToString(DateTimeUtil.Rfc1123ZPattern, CultureInfo.InvariantCulture); break; case "files": release.Files = ParseUtil.CoerceLong(value); diff --git a/src/Jackett.Common/Utils/DateTimeUtil.cs b/src/Jackett.Common/Utils/DateTimeUtil.cs index 789bc1ccf..e471c79ba 100644 --- a/src/Jackett.Common/Utils/DateTimeUtil.cs +++ b/src/Jackett.Common/Utils/DateTimeUtil.cs @@ -2,6 +2,7 @@ using System; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; +using Jackett.Common.Extensions; namespace Jackett.Common.Utils { @@ -117,7 +118,7 @@ namespace Jackett.Common.Utils var now = relativeFrom ?? DateTime.Now; // try parsing the str as an unix timestamp - if (str.All(char.IsDigit) && long.TryParse(str, out var unixTimeStamp)) + if (str.IsAllDigits() && long.TryParse(str, out var unixTimeStamp)) return UnixTimestampToDateTime(unixTimeStamp); if (str.ToLower().Contains("now")) @@ -227,81 +228,87 @@ namespace Jackett.Common.Utils var now = relativeFrom ?? DateTime.Now; date = ParseUtil.NormalizeSpace(date); - var pattern = layout; - // year - pattern = pattern.Replace("2006", "yyyy"); - pattern = pattern.Replace("06", "yy"); + var commonStandardFormats = new[] { "y", "h", "d" }; - // month - pattern = pattern.Replace("January", "MMMM"); - pattern = pattern.Replace("Jan", "MMM"); - pattern = pattern.Replace("01", "MM"); + if (commonStandardFormats.Any(layout.ContainsIgnoreCase) && DateTime.TryParseExact(date, layout, CultureInfo.InvariantCulture, DateTimeStyles.None, out var parsedDate)) + return parsedDate; - // day - pattern = pattern.Replace("Monday", "dddd"); - pattern = pattern.Replace("Mon", "ddd"); - pattern = pattern.Replace("02", "dd"); - //pattern = pattern.Replace("_2", ""); // space padding not supported nativly by C#? - pattern = pattern.Replace("2", "d"); + var format = layout - // hours/minutes/seconds - pattern = pattern.Replace("05", "ss"); + // year + .Replace("2006", "yyyy") + .Replace("06", "yy") - pattern = pattern.Replace("15", "HH"); - pattern = pattern.Replace("03", "hh"); - pattern = pattern.Replace("3", "h"); + // month + .Replace("January", "MMMM") + .Replace("Jan", "MMM") + .Replace("01", "MM") - pattern = pattern.Replace("04", "mm"); - pattern = pattern.Replace("4", "m"); + // day + .Replace("Monday", "dddd") + .Replace("Mon", "ddd") + .Replace("02", "dd") + //pattern = pattern.Replace("_2", "") // space padding not supported nativly by C#? + .Replace("2", "d") - pattern = pattern.Replace("5", "s"); + // hours/minutes/seconds + .Replace("05", "ss") - // month again - pattern = pattern.Replace("1", "M"); + .Replace("15", "HH") + .Replace("03", "hh") + .Replace("3", "h") - // fractional seconds - pattern = pattern.Replace(".0000", "ffff"); - pattern = pattern.Replace(".000", "fff"); - pattern = pattern.Replace(".00", "ff"); - pattern = pattern.Replace(".0", "f"); + .Replace("04", "mm") + .Replace("4", "m") - pattern = pattern.Replace(".9999", "FFFF"); - pattern = pattern.Replace(".999", "FFF"); - pattern = pattern.Replace(".99", "FF"); - pattern = pattern.Replace(".9", "F"); + .Replace("5", "s") - // AM/PM - pattern = pattern.Replace("PM", "tt"); - pattern = pattern.Replace("pm", "tt"); // not sure if this works + // month again + .Replace("1", "M") - // timezones - // these might need further tuning - //pattern = pattern.Replace("MST", ""); - //pattern = pattern.Replace("Z07:00:00", ""); - pattern = pattern.Replace("Z07:00", "'Z'zzz"); - pattern = pattern.Replace("Z07", "'Z'zz"); - //pattern = pattern.Replace("Z070000", ""); - //pattern = pattern.Replace("Z0700", ""); - pattern = pattern.Replace("Z07:00", "'Z'zzz"); - pattern = pattern.Replace("Z07", "'Z'zz"); - //pattern = pattern.Replace("-07:00:00", ""); - pattern = pattern.Replace("-07:00", "zzz"); - //pattern = pattern.Replace("-0700", "zz"); - pattern = pattern.Replace("-07", "zz"); + // fractional seconds + .Replace(".0000", "ffff") + .Replace(".000", "fff") + .Replace(".00", "ff") + .Replace(".0", "f") + + .Replace(".9999", "FFFF") + .Replace(".999", "FFF") + .Replace(".99", "FF") + .Replace(".9", "F") + + // AM/PM + .Replace("PM", "tt") + .Replace("pm", "tt") // not sure if this works + + // timezones + // these might need further tuning + //pattern = pattern.Replace("MST", "") + //pattern = pattern.Replace("Z07:00:00", "") + .Replace("Z07:00", "'Z'zzz") + .Replace("Z07", "'Z'zz") + //pattern = pattern.Replace("Z070000", "") + //pattern = pattern.Replace("Z0700", "") + .Replace("Z07:00", "'Z'zzz") + .Replace("Z07", "'Z'zz") + //pattern = pattern.Replace("-07:00:00", "") + .Replace("-07:00", "zzz") + //pattern = pattern.Replace("-0700", "zz") + .Replace("-07", "zz"); try { - var dateTime = DateTime.ParseExact(date, pattern, CultureInfo.InvariantCulture); + var dateTime = DateTime.ParseExact(date, format, CultureInfo.InvariantCulture); - if (!pattern.Contains("yy") && dateTime > now) + if (!format.Contains("yy") && dateTime > now) dateTime = dateTime.AddYears(-1); return dateTime; } catch (FormatException ex) { - throw new FormatException($"Error while parsing DateTime \"{date}\", using layout \"{layout}\" ({pattern}): {ex.Message}", ex); + throw new FormatException($"Error while parsing DateTime \"{date}\", using layout \"{layout}\" ({format}): {ex.Message}", ex); } } diff --git a/src/Jackett.Test/Common/Utils/DateTimeUtilTests.cs b/src/Jackett.Test/Common/Utils/DateTimeUtilTests.cs index 55969a1a0..5d3157152 100644 --- a/src/Jackett.Test/Common/Utils/DateTimeUtilTests.cs +++ b/src/Jackett.Test/Common/Utils/DateTimeUtilTests.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.Threading; using Jackett.Common.Utils; using NUnit.Framework; @@ -178,5 +180,31 @@ namespace Jackett.Test.Common.Utils var diff = Math.Abs((dt1 - dt2).TotalSeconds); Assert.True(diff < delta, $"Dates are not similar. Expected: {dt1} But was: {dt2}"); } + + [TestCase("pt-BR")] + [TestCase("en-US")] + public void AssertFormattingDatesInvariant(string culture) + { + Thread.CurrentThread.CurrentCulture = new CultureInfo(culture); + + var dateNow = DateTime.Now; + + Assert.AreEqual( + dateNow.ToString("ddd, dd MMM yyyy HH':'mm':'ss z", CultureInfo.InvariantCulture), + DateTimeUtil.FromUnknown(dateNow.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)).ToString(DateTimeUtil.Rfc1123ZPattern, CultureInfo.InvariantCulture)); + } + + [TestCase("2022-08-08 02:07:39 -02:00", "2006-01-02 15:04:05 -07:00", "yyyy-MM-dd HH:mm:ss zzz", "2022-08-08 04:07:39 +00:00")] + [TestCase("2022-08-08 02:07:39 -02:00", "yyyy-MM-dd HH:mm:ss zzz", "yyyy-MM-dd HH:mm:ss zzz", "2022-08-08 04:07:39 +00:00")] + [TestCase("2022-08-08 -02:00", "2006-01-02 -07:00", "yyyy-MM-dd zzz", "2022-08-08 +00:00")] + [TestCase("2022-08-08 -02:00", "yyyy-MM-dd zzz", "yyyy-MM-dd zzz", "2022-08-08 +00:00")] + [TestCase("02:07:39 -02:00", "15:04:05 -07:00", "HH:mm:ss zzz", "04:07:39 +00:00")] + [TestCase("02:07:39 -02:00", "HH:mm:ss zzz", "HH:mm:ss zzz", "04:07:39 +00:00")] + [TestCase("-02:00", "zzz", "zzz", "+00:00")] + [TestCase("-02:00", "-07:00", "zzz", "+00:00")] + public void AssertParsingDateTimeGolang(string dateInput, string format, string standardFormat, string expectedDate) + { + Assert.AreEqual(expectedDate, DateTimeUtil.ParseDateTimeGoLang(dateInput, format).ToUniversalTime().ToString(standardFormat, CultureInfo.InvariantCulture)); + } } }