1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 09:27:29 +08:00

Merge pull request #23129 from Elvendir/add-last-played-filter

Add last played search filter in song select
This commit is contained in:
Bartłomiej Dach 2024-02-14 18:05:31 +01:00 committed by GitHub
commit 692ff8ea11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 215 additions and 0 deletions

View File

@ -454,5 +454,111 @@ namespace osu.Game.Tests.NonVisual.Filtering
return false; 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);
}
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
@ -64,6 +65,7 @@ namespace osu.Game.Screens.Select.Carousel
match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(BeatmapInfo.Difficulty.CircleSize); match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(BeatmapInfo.Difficulty.CircleSize);
match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(BeatmapInfo.Difficulty.OverallDifficulty); match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(BeatmapInfo.Difficulty.OverallDifficulty);
match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(BeatmapInfo.Length); 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.BPM.HasFilter || criteria.BPM.IsInRange(BeatmapInfo.BPM);
match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor); match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor);

View File

@ -35,6 +35,7 @@ namespace osu.Game.Screens.Select
public OptionalRange<double> BPM; public OptionalRange<double> BPM;
public OptionalRange<int> BeatDivisor; public OptionalRange<int> BeatDivisor;
public OptionalRange<BeatmapOnlineStatus> OnlineStatus; public OptionalRange<BeatmapOnlineStatus> OnlineStatus;
public OptionalRange<DateTimeOffset> LastPlayed;
public OptionalTextFilter Creator; public OptionalTextFilter Creator;
public OptionalTextFilter Artist; public OptionalTextFilter Artist;
public OptionalTextFilter Title; public OptionalTextFilter Title;

View File

@ -61,6 +61,10 @@ namespace osu.Game.Screens.Select
case "length": case "length":
return tryUpdateLengthRange(criteria, op, value); return tryUpdateLengthRange(criteria, op, value);
case "played":
case "lastplayed":
return tryUpdateDateAgoRange(ref criteria.LastPlayed, op, value);
case "divisor": case "divisor":
return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt); return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt);
@ -376,5 +380,107 @@ namespace osu.Game.Screens.Select
return tryUpdateCriteriaRange(ref criteria.Length, op, totalLength, minScale / 2.0); 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);
}
} }
} }