From f8e43fd8d36ad6501b1617013824d1ee87a33b89 Mon Sep 17 00:00:00 2001 From: WitherFlower <52672543+WitherFlower@users.noreply.github.com> Date: Tue, 8 Oct 2024 13:46:21 +0200 Subject: [PATCH 1/6] Add tests for ranked and submitted date filtering --- .../Filtering/FilterQueryParserTest.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 9ecfa72947..852b1e1441 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -627,6 +627,88 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual(DateTimeOffset.MinValue.AddMilliseconds(1), filterCriteria.LastPlayed.Min); } + private static DateTimeOffset dateTimeOffsetFromDateOnly(int year, int month, int day) => + new DateTimeOffset(year, month, day, 0, 0, 0, TimeSpan.Zero); + + private static readonly object[] ranked_date_valid_test_cases = + { + new object[] { "ranked<2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<2012-03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max }, + + new object[] { "ranked<=2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<=2012-03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max }, + + new object[] { "ranked>2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>2012-03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min }, + + new object[] { "ranked>=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>=2012-03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min }, + + new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked=2012-03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked=2012-03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max }, + }; + + [Test] + [TestCaseSource(nameof(ranked_date_valid_test_cases))] + public void TestValidRankedDateQueries(string query, DateTimeOffset expected, Func f) + { + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual(true, filterCriteria.DateRanked.HasFilter); + Assert.AreEqual(expected, f(filterCriteria)); + } + + private static readonly object[] ranked_date_invalid_test_cases = + { + new object[] { "ranked<0" }, + new object[] { "ranked=99999" }, + new object[] { "ranked>=2012-03-05-04" }, + new object[] { "ranked>2007.5" }, + }; + + [Test] + [TestCaseSource(nameof(ranked_date_invalid_test_cases))] + public void TestInvalidRankedDateQueries(string query) + { + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual(false, filterCriteria.DateRanked.HasFilter); + } + + private static readonly object[] submitted_date_test_cases = + { + new object[] { "submitted<2012", true }, + new object[] { "submitted<2012-03", true }, + new object[] { "submitted<2012-03-05", true }, + new object[] { "submitted<2012-3-5", true }, + + new object[] { "submitted<0", false }, + new object[] { "submitted=99999", false }, + new object[] { "submitted>=2012-03-05-04", false }, + new object[] { "submitted>2007.5", false }, + }; + + [Test] + [TestCaseSource(nameof(submitted_date_test_cases))] + public void TestInvalidRankedDateQueries(string query, bool expected) + { + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual(expected, filterCriteria.DateSubmitted.HasFilter); + } + private static readonly object[] played_query_tests = { new object[] { "0", DateTimeOffset.MinValue, true }, From 2369e98cfc34ecb9c75ac66e89403eed10807e09 Mon Sep 17 00:00:00 2001 From: WitherFlower <52672543+WitherFlower@users.noreply.github.com> Date: Tue, 8 Oct 2024 13:46:58 +0200 Subject: [PATCH 2/6] Implement ranked and submitted date filtering --- .../Select/Carousel/CarouselBeatmap.cs | 2 + osu.Game/Screens/Select/FilterCriteria.cs | 2 + osu.Game/Screens/Select/FilterQueryParser.cs | 185 ++++++++++++++++++ 3 files changed, 189 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 8f38ae710c..aa064f97c9 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -66,6 +66,8 @@ namespace osu.Game.Screens.Select.Carousel match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(BeatmapInfo.Difficulty.OverallDifficulty); match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(BeatmapInfo.Length); match &= !criteria.LastPlayed.HasFilter || criteria.LastPlayed.IsInRange(BeatmapInfo.LastPlayed ?? DateTimeOffset.MinValue); + match &= !criteria.DateRanked.HasFilter || criteria.DateRanked.IsInRange(BeatmapInfo.BeatmapSet?.DateRanked ?? DateTimeOffset.MinValue); + match &= !criteria.DateSubmitted.HasFilter || criteria.DateSubmitted.IsInRange(BeatmapInfo.BeatmapSet?.DateSubmitted ?? DateTimeOffset.MinValue); match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(BeatmapInfo.BPM); match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor); diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 190efd0fb0..77d7ff0e9f 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -37,6 +37,8 @@ namespace osu.Game.Screens.Select public OptionalRange BeatDivisor; public OptionalSet OnlineStatus = new OptionalSet(); public OptionalRange LastPlayed; + public OptionalRange DateRanked; + public OptionalRange DateSubmitted; public OptionalTextFilter Creator; public OptionalTextFilter Artist; public OptionalTextFilter Title; diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 6c9a95a250..17fa6598c5 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -65,6 +65,12 @@ namespace osu.Game.Screens.Select case "lastplayed": return tryUpdateDateAgoRange(ref criteria.LastPlayed, op, value); + case "ranked": + return tryUpdateRankedDateRange(ref criteria.DateRanked, op, value); + + case "submitted": + return tryUpdateRankedDateRange(ref criteria.DateSubmitted, op, value); + case "played": if (!tryParseBool(value, out bool played)) return false; @@ -592,5 +598,184 @@ namespace osu.Game.Screens.Select return tryUpdateCriteriaRange(ref dateRange, op, dateTimeOffset.Value); } + + /// + /// Helper function for building a UTC date from only the year, month and day. + /// UTC is used to keep consistent search results with osu!web. + /// + private static DateTimeOffset dateTimeOffsetFromDateOnly(int year, int month, int day) => + new DateTimeOffset(year, month, day, 0, 0, 0, TimeSpan.Zero); + + /// + /// Parses a string containing a ranked or submitted date filter. + /// Returns a boolean depending on whether parsing was successful or not. + /// Accepted dates are in the formats `yyyy`, `yyyy-mm` and `yyyy-mm-dd`. Leading zeros are accepted. + /// + /// The to store the parsed data into, if successful. + /// The operator of the filtering query + /// The string value to attempt parsing for. + private static bool tryUpdateRankedDateRange(ref FilterCriteria.OptionalRange dateRange, Operator op, string val) + { + GroupCollection? match = null; + + match ??= tryMatchRegex(val, @"^(?\d+)(-(?\d+)(-(?\d+))?)?$"); + + if (match == null) + return false; + + int? year = null; + int? month = null; + int? day = null; + + List keys = new List { @"year", @"month", @"day" }; + + foreach (string key in keys) + { + if (!match.TryGetValue(key, out var group) || !group.Success) + continue; + + if (group.Success) + { + if (!tryParseDoubleWithPoint(group.Value, out double value)) + return false; + + switch (key) + { + + case @"year": + year = (int)value; + break; + + case @"month": + month = (int)value; + break; + + case @"day": + day = (int)value; + break; + } + } + } + + if (year == null) + { + return false; + } + + try + { + DateTimeOffset dateTimeOffset; + + switch (op) + { + case Operator.Less: + if (month == null) + { + month = 1; + day = 1; + } + + if (day == null) + day = 1; + + dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value); + return tryUpdateCriteriaRange(ref dateRange, op, dateTimeOffset); + + case Operator.LessOrEqual: + if (month == null) + { + month = 1; + day = 1; + dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddYears(1); + return tryUpdateCriteriaRange(ref dateRange, Operator.Less, dateTimeOffset); + } + if (day == null) + { + day = 1; + dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddMonths(1); + return tryUpdateCriteriaRange(ref dateRange, Operator.Less, dateTimeOffset); + } + + dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddDays(1); + return tryUpdateCriteriaRange(ref dateRange, Operator.Less, dateTimeOffset); + + case Operator.GreaterOrEqual: + if (month == null) + { + month = 1; + day = 1; + } + + if (day == null) + day = 1; + + dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value); + return tryUpdateCriteriaRange(ref dateRange, op, dateTimeOffset); + + case Operator.Greater: + if (month == null) + { + month = 1; + day = 1; + dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddYears(1); + return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, dateTimeOffset); + } + if (day == null) + { + day = 1; + dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddMonths(1); + return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, dateTimeOffset); + } + + dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddDays(1); + return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, dateTimeOffset); + + case Operator.Equal: + + DateTimeOffset minDateTimeOffset; + DateTimeOffset maxDateTimeOffset; + + if (month == null) + { + month = 1; + day = 1; + minDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value); + maxDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddYears(1); + return ( + tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, minDateTimeOffset) + && + tryUpdateCriteriaRange(ref dateRange, Operator.Less, maxDateTimeOffset) + ); + } + + if (day == null) + { + day = 1; + minDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value); + maxDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddMonths(1); + return ( + tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, minDateTimeOffset) + && + tryUpdateCriteriaRange(ref dateRange, Operator.Less, maxDateTimeOffset) + ); + } + + minDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value); + maxDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddDays(1); + + return ( + tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, minDateTimeOffset) + && + tryUpdateCriteriaRange(ref dateRange, Operator.Less, maxDateTimeOffset) + ); + default: + return false; + } + } + catch (ArgumentOutOfRangeException) + { + return false; + } + } } } From b58576f31b05aeb18af742e6145c5a66f375ba20 Mon Sep 17 00:00:00 2001 From: WitherFlower <52672543+WitherFlower@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:13:27 +0200 Subject: [PATCH 3/6] Add slash and dot as valid separators in dates. --- .../Filtering/FilterQueryParserTest.cs | 23 +++++++++---------- osu.Game/Screens/Select/FilterQueryParser.cs | 5 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 852b1e1441..ad3be17c15 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -633,23 +633,23 @@ namespace osu.Game.Tests.NonVisual.Filtering private static readonly object[] ranked_date_valid_test_cases = { new object[] { "ranked<2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Max }, - new object[] { "ranked<2012-03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Max }, - new object[] { "ranked<2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<2012.03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max }, new object[] { "ranked<2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max }, new object[] { "ranked<=2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Max }, - new object[] { "ranked<=2012-03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max }, - new object[] { "ranked<=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<=2012.03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<=2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max }, new object[] { "ranked<=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max }, new object[] { "ranked>2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, - new object[] { "ranked>2012-03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Min }, - new object[] { "ranked>2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>2012.03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min }, new object[] { "ranked>2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min }, new object[] { "ranked>=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, - new object[] { "ranked>=2012-03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min }, - new object[] { "ranked>=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>=2012.03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>=2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min }, new object[] { "ranked>=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min }, new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, @@ -675,7 +675,6 @@ namespace osu.Game.Tests.NonVisual.Filtering new object[] { "ranked<0" }, new object[] { "ranked=99999" }, new object[] { "ranked>=2012-03-05-04" }, - new object[] { "ranked>2007.5" }, }; [Test] @@ -690,14 +689,14 @@ namespace osu.Game.Tests.NonVisual.Filtering private static readonly object[] submitted_date_test_cases = { new object[] { "submitted<2012", true }, - new object[] { "submitted<2012-03", true }, - new object[] { "submitted<2012-03-05", true }, + new object[] { "submitted<2012.03", true }, + new object[] { "submitted<2012/03/05", true }, new object[] { "submitted<2012-3-5", true }, new object[] { "submitted<0", false }, new object[] { "submitted=99999", false }, new object[] { "submitted>=2012-03-05-04", false }, - new object[] { "submitted>2007.5", false }, + new object[] { "submitted>=2012/03.05-04", false }, }; [Test] diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 17fa6598c5..d6f46c0fbd 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -609,7 +609,8 @@ namespace osu.Game.Screens.Select /// /// Parses a string containing a ranked or submitted date filter. /// Returns a boolean depending on whether parsing was successful or not. - /// Accepted dates are in the formats `yyyy`, `yyyy-mm` and `yyyy-mm-dd`. Leading zeros are accepted. + /// Accepted dates are in the formats `yyyy`, `yyyy-mm` and `yyyy-mm-dd`. + /// Leading zeros are accepted. Numbers can be separated by `-`, `/`, or `.` /// /// The to store the parsed data into, if successful. /// The operator of the filtering query @@ -618,7 +619,7 @@ namespace osu.Game.Screens.Select { GroupCollection? match = null; - match ??= tryMatchRegex(val, @"^(?\d+)(-(?\d+)(-(?\d+))?)?$"); + match ??= tryMatchRegex(val, @"^(?\d+)((-|/|\.)(?\d+)((-|/|\.)(?\d+))?)?$"); if (match == null) return false; From 6b532824b1b07605f45e314900186a410649ea9a Mon Sep 17 00:00:00 2001 From: WitherFlower <52672543+WitherFlower@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:29:03 +0200 Subject: [PATCH 4/6] Fix code quality and formatting issues --- .../Filtering/FilterQueryParserTest.cs | 52 +++++++++---------- osu.Game/Screens/Select/FilterQueryParser.cs | 44 +++++----------- 2 files changed, 38 insertions(+), 58 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index ad3be17c15..c8f063719d 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -632,32 +632,32 @@ namespace osu.Game.Tests.NonVisual.Filtering private static readonly object[] ranked_date_valid_test_cases = { - new object[] { "ranked<2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Max }, - new object[] { "ranked<2012.03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Max }, - new object[] { "ranked<2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max }, - new object[] { "ranked<2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<2012.03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Max }, - new object[] { "ranked<=2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Max }, - new object[] { "ranked<=2012.03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<=2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<=2012.03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max }, new object[] { "ranked<=2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max }, - new object[] { "ranked<=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked<=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max }, - new object[] { "ranked>2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, - new object[] { "ranked>2012.03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Min }, - new object[] { "ranked>2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min }, - new object[] { "ranked>2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>2012", dateTimeOffsetFromDateOnly(2013, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>2012.03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Min }, - new object[] { "ranked>=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, - new object[] { "ranked>=2012.03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>=2012.03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min }, new object[] { "ranked>=2012/03/05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min }, - new object[] { "ranked>=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked>=2012-3-5", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min }, - new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, - new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, - new object[] { "ranked=2012-03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min }, - new object[] { "ranked=2012-03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max }, - new object[] { "ranked=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min }, - new object[] { "ranked=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked=2012", dateTimeOffsetFromDateOnly(2012, 1, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked=2012-03", dateTimeOffsetFromDateOnly(2012, 3, 1), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked=2012-03", dateTimeOffsetFromDateOnly(2012, 4, 1), (FilterCriteria x) => x.DateRanked.Max }, + new object[] { "ranked=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 5), (FilterCriteria x) => x.DateRanked.Min }, + new object[] { "ranked=2012-03-05", dateTimeOffsetFromDateOnly(2012, 3, 6), (FilterCriteria x) => x.DateRanked.Max }, }; [Test] @@ -688,13 +688,13 @@ namespace osu.Game.Tests.NonVisual.Filtering private static readonly object[] submitted_date_test_cases = { - new object[] { "submitted<2012", true }, - new object[] { "submitted<2012.03", true }, - new object[] { "submitted<2012/03/05", true }, - new object[] { "submitted<2012-3-5", true }, + new object[] { "submitted<2012", true }, + new object[] { "submitted<2012.03", true }, + new object[] { "submitted<2012/03/05", true }, + new object[] { "submitted<2012-3-5", true }, - new object[] { "submitted<0", false }, - new object[] { "submitted=99999", false }, + new object[] { "submitted<0", false }, + new object[] { "submitted=99999", false }, new object[] { "submitted>=2012-03-05-04", false }, new object[] { "submitted>=2012/03.05-04", false }, }; diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index d6f46c0fbd..9f29459012 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -642,7 +642,6 @@ namespace osu.Game.Screens.Select switch (key) { - case @"year": year = (int)value; break; @@ -670,14 +669,8 @@ namespace osu.Game.Screens.Select switch (op) { case Operator.Less: - if (month == null) - { - month = 1; - day = 1; - } - - if (day == null) - day = 1; + month ??= 1; + day ??= 1; dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value); return tryUpdateCriteriaRange(ref dateRange, op, dateTimeOffset); @@ -690,6 +683,7 @@ namespace osu.Game.Screens.Select dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddYears(1); return tryUpdateCriteriaRange(ref dateRange, Operator.Less, dateTimeOffset); } + if (day == null) { day = 1; @@ -701,14 +695,8 @@ namespace osu.Game.Screens.Select return tryUpdateCriteriaRange(ref dateRange, Operator.Less, dateTimeOffset); case Operator.GreaterOrEqual: - if (month == null) - { - month = 1; - day = 1; - } - - if (day == null) - day = 1; + month ??= 1; + day ??= 1; dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value); return tryUpdateCriteriaRange(ref dateRange, op, dateTimeOffset); @@ -721,6 +709,7 @@ namespace osu.Game.Screens.Select dateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddYears(1); return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, dateTimeOffset); } + if (day == null) { day = 1; @@ -742,11 +731,8 @@ namespace osu.Game.Screens.Select day = 1; minDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value); maxDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddYears(1); - return ( - tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, minDateTimeOffset) - && - tryUpdateCriteriaRange(ref dateRange, Operator.Less, maxDateTimeOffset) - ); + return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, minDateTimeOffset) + && tryUpdateCriteriaRange(ref dateRange, Operator.Less, maxDateTimeOffset); } if (day == null) @@ -754,21 +740,15 @@ namespace osu.Game.Screens.Select day = 1; minDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value); maxDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddMonths(1); - return ( - tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, minDateTimeOffset) - && - tryUpdateCriteriaRange(ref dateRange, Operator.Less, maxDateTimeOffset) - ); + return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, minDateTimeOffset) + && tryUpdateCriteriaRange(ref dateRange, Operator.Less, maxDateTimeOffset); } minDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value); maxDateTimeOffset = dateTimeOffsetFromDateOnly(year.Value, month.Value, day.Value).AddDays(1); + return tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, minDateTimeOffset) + && tryUpdateCriteriaRange(ref dateRange, Operator.Less, maxDateTimeOffset); - return ( - tryUpdateCriteriaRange(ref dateRange, Operator.GreaterOrEqual, minDateTimeOffset) - && - tryUpdateCriteriaRange(ref dateRange, Operator.Less, maxDateTimeOffset) - ); default: return false; } From eb2f1d09f87d6b6dd296ac85e2c46f576304c5af Mon Sep 17 00:00:00 2001 From: WitherFlower <52672543+WitherFlower@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:42:43 +0200 Subject: [PATCH 5/6] Improve regex readability by using character set --- osu.Game/Screens/Select/FilterQueryParser.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 9f29459012..ccffd34dc2 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -617,9 +617,7 @@ namespace osu.Game.Screens.Select /// The string value to attempt parsing for. private static bool tryUpdateRankedDateRange(ref FilterCriteria.OptionalRange dateRange, Operator op, string val) { - GroupCollection? match = null; - - match ??= tryMatchRegex(val, @"^(?\d+)((-|/|\.)(?\d+)((-|/|\.)(?\d+))?)?$"); + GroupCollection? match = tryMatchRegex(val, @"^(?\d+)([-/.](?\d+)([-/.](?\d+))?)?$"); if (match == null) return false; From da376c534b6c5e2e7f3f8cb380f6bddd9a7496f6 Mon Sep 17 00:00:00 2001 From: WitherFlower <52672543+WitherFlower@users.noreply.github.com> Date: Fri, 11 Oct 2024 09:49:47 +0200 Subject: [PATCH 6/6] Filter out unranked/unsubmitted beatmaps when using the ranked/submitted search filters --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index aa064f97c9..3947cefb91 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -66,8 +66,8 @@ namespace osu.Game.Screens.Select.Carousel match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(BeatmapInfo.Difficulty.OverallDifficulty); match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(BeatmapInfo.Length); match &= !criteria.LastPlayed.HasFilter || criteria.LastPlayed.IsInRange(BeatmapInfo.LastPlayed ?? DateTimeOffset.MinValue); - match &= !criteria.DateRanked.HasFilter || criteria.DateRanked.IsInRange(BeatmapInfo.BeatmapSet?.DateRanked ?? DateTimeOffset.MinValue); - match &= !criteria.DateSubmitted.HasFilter || criteria.DateSubmitted.IsInRange(BeatmapInfo.BeatmapSet?.DateSubmitted ?? DateTimeOffset.MinValue); + match &= !criteria.DateRanked.HasFilter || (BeatmapInfo.BeatmapSet?.DateRanked != null && criteria.DateRanked.IsInRange(BeatmapInfo.BeatmapSet.DateRanked.Value)); + match &= !criteria.DateSubmitted.HasFilter || (BeatmapInfo.BeatmapSet?.DateSubmitted != null && criteria.DateSubmitted.IsInRange(BeatmapInfo.BeatmapSet.DateSubmitted.Value)); match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(BeatmapInfo.BPM); match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor);