Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add last played search filter in song select #23129

Merged
merged 21 commits into from Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
102576c
adddede LastPlayed as filter option in beatmap carousel
Elvendir Mar 18, 2023
216a88e
limited max Date comparison
Elvendir Mar 18, 2023
6dead81
Generalized tryUpdateLastPlayedRange to tryUpdateDateRange and Added…
Elvendir Mar 19, 2023
4b053b4
changed regex match to be inline with standard
Elvendir Apr 1, 2023
52adb99
Update osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
Elvendir Apr 4, 2023
d6c6507
Update osu.Game/Screens/Select/FilterQueryParser.cs
Elvendir Apr 4, 2023
0c1d6eb
- rewrote upper and lower bound tests
Elvendir Apr 5, 2023
df17051
-renamed function inverse() to reverseInequalityOperator() for clarity
Elvendir Apr 5, 2023
c2f225f
Made Operator.Equal not parse for date filter and added corresponding…
Elvendir Apr 5, 2023
928145c
Enforce integer value before y and M
Elvendir Apr 5, 2023
8e156fd
Enforce integer through regex match instead
Elvendir Apr 6, 2023
8ba9677
Merge branch 'master' into add-last-played-filter
peppy Jun 6, 2023
1113355
Merge branch 'master' into add-last-played-filter
bdach Feb 14, 2024
f0f37df
Revert unnecessary change
bdach Feb 14, 2024
d7dfc8b
Add failing test coverage for empty date filter not parsing
bdach Feb 14, 2024
c24328d
Abandon date filter if no meaningful time bound found during parsing
bdach Feb 14, 2024
a8ae0a0
Simplify parsing
bdach Feb 14, 2024
f1d69ab
Rename test
bdach Feb 14, 2024
414066f
Inline problematic function (and rename things to make more sense)
bdach Feb 14, 2024
f7bea00
Improve test coverage
bdach Feb 14, 2024
ae9a266
Sprinkle some raw string prefixes
bdach Feb 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
106 changes: 106 additions & 0 deletions osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
Expand Up @@ -454,5 +454,111 @@ public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
return false;
}
}

private static readonly object[] correct_date_query_examples =
{
new object[] { "600" },
new object[] { "0.5s" },
new object[] { "120m" },
new object[] { "48h120s" },
new object[] { "10y24M" },
new object[] { "10y60d120s" },
new object[] { "0y0M2d" },
new object[] { "1y1M2d" }
};

[Test]
[TestCaseSource(nameof(correct_date_query_examples))]
public void TestValidDateQueries(string dateQuery)
{
string query = $"played<{dateQuery} time";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual(true, filterCriteria.LastPlayed.HasFilter);
}

private static readonly object[] incorrect_date_query_examples =
{
new object[] { ".5s" },
new object[] { "7m27" },
new object[] { "7m7m7m" },
new object[] { "5s6m" },
new object[] { "7d7y" },
new object[] { "0:3:6" },
new object[] { "0:3:" },
new object[] { "\"three days\"" },
new object[] { "0.1y0.1M2d" },
new object[] { "0.99y0.99M2d" },
new object[] { string.Empty }
};

[Test]
[TestCaseSource(nameof(incorrect_date_query_examples))]
public void TestInvalidDateQueries(string dateQuery)
{
string query = $"played<{dateQuery} time";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual(false, filterCriteria.LastPlayed.HasFilter);
}

[Test]
public void TestGreaterDateQuery()
{
const string query = "played>50";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.That(filterCriteria.LastPlayed.Max, Is.Not.Null);
Assert.That(filterCriteria.LastPlayed.Min, Is.Null);
// the parser internally references `DateTimeOffset.Now`, so to not make things too annoying for tests, just assume some tolerance
// (irrelevant in proportion to the actual filter proscribed).
Assert.That(filterCriteria.LastPlayed.Max, Is.EqualTo(DateTimeOffset.Now.AddDays(-50)).Within(TimeSpan.FromSeconds(5)));
}

[Test]
public void TestLowerDateQuery()
{
const string query = "played<50";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.That(filterCriteria.LastPlayed.Max, Is.Null);
Assert.That(filterCriteria.LastPlayed.Min, Is.Not.Null);
// the parser internally references `DateTimeOffset.Now`, so to not make things too annoying for tests, just assume some tolerance
// (irrelevant in proportion to the actual filter proscribed).
Assert.That(filterCriteria.LastPlayed.Min, Is.EqualTo(DateTimeOffset.Now.AddDays(-50)).Within(TimeSpan.FromSeconds(5)));
}

[Test]
public void TestBothSidesDateQuery()
{
const string query = "played>3M played<1y6M";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.That(filterCriteria.LastPlayed.Min, Is.Not.Null);
Assert.That(filterCriteria.LastPlayed.Max, Is.Not.Null);
// the parser internally references `DateTimeOffset.Now`, so to not make things too annoying for tests, just assume some tolerance
// (irrelevant in proportion to the actual filter proscribed).
Assert.That(filterCriteria.LastPlayed.Min, Is.EqualTo(DateTimeOffset.Now.AddYears(-1).AddMonths(-6)).Within(TimeSpan.FromSeconds(5)));
Assert.That(filterCriteria.LastPlayed.Max, Is.EqualTo(DateTimeOffset.Now.AddMonths(-3)).Within(TimeSpan.FromSeconds(5)));
}

[Test]
public void TestEqualDateQuery()
{
const string query = "played=50";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual(false, filterCriteria.LastPlayed.HasFilter);
}

[Test]
public void TestOutOfRangeDateQuery()
{
const string query = "played<10000y";
var filterCriteria = new FilterCriteria();
FilterQueryParser.ApplyQueries(filterCriteria, query);
Assert.AreEqual(true, filterCriteria.LastPlayed.HasFilter);
Assert.AreEqual(DateTimeOffset.MinValue.AddMilliseconds(1), filterCriteria.LastPlayed.Min);
}
}
}
2 changes: 2 additions & 0 deletions osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select.Filter;
Expand Down Expand Up @@ -64,6 +65,7 @@ private bool checkMatch(FilterCriteria criteria)
match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(BeatmapInfo.Difficulty.CircleSize);
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.BPM.HasFilter || criteria.BPM.IsInRange(BeatmapInfo.BPM);

match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor);
Expand Down
1 change: 1 addition & 0 deletions osu.Game/Screens/Select/FilterCriteria.cs
Expand Up @@ -35,6 +35,7 @@ public class FilterCriteria
public OptionalRange<double> BPM;
public OptionalRange<int> BeatDivisor;
public OptionalRange<BeatmapOnlineStatus> OnlineStatus;
public OptionalRange<DateTimeOffset> LastPlayed;
public OptionalTextFilter Creator;
public OptionalTextFilter Artist;
public OptionalTextFilter Title;
Expand Down
106 changes: 106 additions & 0 deletions osu.Game/Screens/Select/FilterQueryParser.cs
Expand Up @@ -61,6 +61,10 @@ private static bool tryParseKeywordCriteria(FilterCriteria criteria, string key,
case "length":
return tryUpdateLengthRange(criteria, op, value);

case "played":
case "lastplayed":
return tryUpdateDateAgoRange(ref criteria.LastPlayed, op, value);

case "divisor":
return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt);

Expand Down Expand Up @@ -376,5 +380,107 @@ private static bool tryUpdateLengthRange(FilterCriteria criteria, Operator op, s

return tryUpdateCriteriaRange(ref criteria.Length, op, totalLength, minScale / 2.0);
}

/// <summary>
/// This function is intended for parsing "days / months / years ago" type filters.
/// </summary>
private static bool tryUpdateDateAgoRange(ref FilterCriteria.OptionalRange<DateTimeOffset> dateRange, Operator op, string val)
{
switch (op)
{
case Operator.Equal:
// an equality filter is difficult to define for support here.
// if "3 months 2 days ago" means a single concrete time instant, such a filter is basically useless.
// if it means a range of 24 hours, then that is annoying to write and also comes with its own implications
// (does it mean "time instant 3 months 2 days ago, within 12 hours of tolerance either direction"?
// does it mean "the full calendar day, from midnight to midnight, 3 months 2 days ago"?)
// as such, for simplicity, just refuse to support this.
return false;

// for the remaining operators, since the value provided to this function is an "ago" type value
// (as in, referring to some amount of time back),
// we'll want to flip the operator, such that `>5d` means "more than five days ago", as in "*before* five days ago",
// as intended by the user.
case Operator.Less:
op = Operator.Greater;
break;

case Operator.LessOrEqual:
op = Operator.GreaterOrEqual;
break;

case Operator.Greater:
op = Operator.Less;
break;

case Operator.GreaterOrEqual:
op = Operator.LessOrEqual;
break;
}

GroupCollection? match = null;

match ??= tryMatchRegex(val, @"^((?<years>\d+)y)?((?<months>\d+)M)?((?<days>\d+(\.\d+)?)d)?((?<hours>\d+(\.\d+)?)h)?((?<minutes>\d+(\.\d+)?)m)?((?<seconds>\d+(\.\d+)?)s)?$");
match ??= tryMatchRegex(val, @"^(?<days>\d+(\.\d+)?)$");

if (match == null)
return false;

DateTimeOffset? dateTimeOffset = null;
DateTimeOffset now = DateTimeOffset.Now;

try
{
List<string> keys = new List<string> { @"seconds", @"minutes", @"hours", @"days", @"months", @"years" };

foreach (string key in keys)
{
if (!match.TryGetValue(key, out var group) || !group.Success)
continue;

if (group.Success)
{
if (!tryParseDoubleWithPoint(group.Value, out double length))
return false;

switch (key)
{
case @"seconds":
dateTimeOffset = (dateTimeOffset ?? now).AddSeconds(-length);
break;

case @"minutes":
dateTimeOffset = (dateTimeOffset ?? now).AddMinutes(-length);
break;

case @"hours":
dateTimeOffset = (dateTimeOffset ?? now).AddHours(-length);
break;

case @"days":
dateTimeOffset = (dateTimeOffset ?? now).AddDays(-length);
break;

case @"months":
dateTimeOffset = (dateTimeOffset ?? now).AddMonths(-(int)length);
break;

case @"years":
dateTimeOffset = (dateTimeOffset ?? now).AddYears(-(int)length);
break;
}
}
}
}
catch (ArgumentOutOfRangeException)
{
dateTimeOffset = DateTimeOffset.MinValue.AddMilliseconds(1);
}

if (!dateTimeOffset.HasValue)
return false;

return tryUpdateCriteriaRange(ref dateRange, op, dateTimeOffset.Value);
}
}
}