mirror of
https://github.com/ppy/osu.git
synced 2024-11-06 06:17:23 +08:00
Merge pull request #11958 from bdach/ruleset-filter-v3
Allow rulesets to specify custom song select filtering criteria
This commit is contained in:
commit
9b5d11f2a5
@ -4,8 +4,10 @@
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Tests.NonVisual.Filtering
|
||||
{
|
||||
@ -214,5 +216,31 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
|
||||
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomRulesetCriteria([Values(null, true, false)] bool? matchCustomCriteria)
|
||||
{
|
||||
var beatmap = getExampleBeatmap();
|
||||
|
||||
var customCriteria = matchCustomCriteria is bool match ? new CustomCriteria(match) : null;
|
||||
var criteria = new FilterCriteria { RulesetCriteria = customCriteria };
|
||||
var carouselItem = new CarouselBeatmap(beatmap);
|
||||
carouselItem.Filter(criteria);
|
||||
|
||||
Assert.AreEqual(matchCustomCriteria == false, carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
private class CustomCriteria : IRulesetFilterCriteria
|
||||
{
|
||||
private readonly bool match;
|
||||
|
||||
public CustomCriteria(bool shouldMatch)
|
||||
{
|
||||
match = shouldMatch;
|
||||
}
|
||||
|
||||
public bool Matches(BeatmapInfo beatmap) => match;
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) => false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,9 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Tests.NonVisual.Filtering
|
||||
{
|
||||
@ -194,5 +196,63 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
|
||||
Assert.AreEqual("double\"quote", filterCriteria.Artist.SearchTerm);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOperatorParsing()
|
||||
{
|
||||
const string query = "artist=><something";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("><something", filterCriteria.Artist.SearchTerm);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUnrecognisedKeywordIsIgnored()
|
||||
{
|
||||
const string query = "unrecognised=keyword";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("unrecognised=keyword", filterCriteria.SearchText);
|
||||
}
|
||||
|
||||
[TestCase("cs=nope")]
|
||||
[TestCase("bpm>=bad")]
|
||||
[TestCase("divisor<nah")]
|
||||
[TestCase("status=noidea")]
|
||||
public void TestInvalidKeywordValueIsIgnored(string query)
|
||||
{
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual(query, filterCriteria.SearchText);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomKeywordIsParsed()
|
||||
{
|
||||
var customCriteria = new CustomFilterCriteria();
|
||||
const string query = "custom=readme unrecognised=keyword";
|
||||
var filterCriteria = new FilterCriteria { RulesetCriteria = customCriteria };
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("readme", customCriteria.CustomValue);
|
||||
Assert.AreEqual("unrecognised=keyword", filterCriteria.SearchText.Trim());
|
||||
}
|
||||
|
||||
private class CustomFilterCriteria : IRulesetFilterCriteria
|
||||
{
|
||||
public string CustomValue { get; set; }
|
||||
|
||||
public bool Matches(BeatmapInfo beatmap) => true;
|
||||
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
|
||||
{
|
||||
if (key == "custom" && op == Operator.Equal)
|
||||
{
|
||||
CustomValue = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
55
osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs
Normal file
55
osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs
Normal file
@ -0,0 +1,55 @@
|
||||
// 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 osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Rulesets.Filter
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows for extending the beatmap filtering capabilities of song select (as implemented in <see cref="FilterCriteria"/>)
|
||||
/// with ruleset-specific criteria.
|
||||
/// </summary>
|
||||
public interface IRulesetFilterCriteria
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether the supplied <paramref name="beatmap"/> satisfies ruleset-specific custom criteria,
|
||||
/// in addition to the ones mandated by song select.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap to test the criteria against.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the beatmap matches the ruleset-specific custom filtering criteria,
|
||||
/// <c>false</c> otherwise.
|
||||
/// </returns>
|
||||
bool Matches(BeatmapInfo beatmap);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a single custom keyword criterion, given by the user via the song select search box.
|
||||
/// The format of the criterion is:
|
||||
/// <code>
|
||||
/// {key}{op}{value}
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// For adding optional string criteria, <see cref="FilterCriteria.OptionalTextFilter"/> can be used for matching,
|
||||
/// along with <see cref="FilterQueryParser.TryUpdateCriteriaText"/> for parsing.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For adding numerical-type range criteria, <see cref="FilterCriteria.OptionalRange{T}"/> can be used for matching,
|
||||
/// along with <see cref="FilterQueryParser.TryUpdateCriteriaRange{T}(ref osu.Game.Screens.Select.FilterCriteria.OptionalRange{T},osu.Game.Screens.Select.Filter.Operator,string,FilterQueryParser.TryParseFunction{T})"/>
|
||||
/// and <see cref="float"/>- and <see cref="double"/>-typed overloads for parsing.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="key">The key (name) of the criterion.</param>
|
||||
/// <param name="op">The operator in the criterion.</param>
|
||||
/// <param name="value">The value of the criterion.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the keyword criterion is valid, <c>false</c> if it has been ignored.
|
||||
/// Valid criteria are stripped from <see cref="FilterCriteria.SearchText"/>,
|
||||
/// while ignored criteria are included in <see cref="FilterCriteria.SearchText"/>.
|
||||
/// </returns>
|
||||
bool TryParseCustomKeywordCriteria(string key, Operator op, string value);
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ using JetBrains.Annotations;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
|
||||
namespace osu.Game.Rulesets
|
||||
@ -306,5 +307,11 @@ namespace osu.Game.Rulesets
|
||||
/// <param name="result">The result type to get the name for.</param>
|
||||
/// <returns>The display name.</returns>
|
||||
public virtual string GetDisplayNameForHitResult(HitResult result) => result.GetDescription();
|
||||
|
||||
/// <summary>
|
||||
/// Creates ruleset-specific beatmap filter criteria to be used on the song select screen.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public virtual IRulesetFilterCriteria CreateRulesetFilterCriteria() => null;
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +73,9 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
if (match)
|
||||
match &= criteria.Collection?.Beatmaps.Contains(Beatmap) ?? true;
|
||||
|
||||
if (match && criteria.RulesetCriteria != null)
|
||||
match &= criteria.RulesetCriteria.Matches(Beatmap);
|
||||
|
||||
Filtered.Value = !match;
|
||||
}
|
||||
|
||||
|
17
osu.Game/Screens/Select/Filter/Operator.cs
Normal file
17
osu.Game/Screens/Select/Filter/Operator.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Screens.Select.Filter
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines logical operators that can be used in the song select search box keyword filters.
|
||||
/// </summary>
|
||||
public enum Operator
|
||||
{
|
||||
Less,
|
||||
LessOrEqual,
|
||||
Equal,
|
||||
GreaterOrEqual,
|
||||
Greater
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -36,6 +37,8 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
public FilterCriteria CreateCriteria()
|
||||
{
|
||||
Debug.Assert(ruleset.Value.ID != null);
|
||||
|
||||
var query = searchTextBox.Text;
|
||||
|
||||
var criteria = new FilterCriteria
|
||||
@ -53,6 +56,8 @@ namespace osu.Game.Screens.Select
|
||||
if (!maximumStars.IsDefault)
|
||||
criteria.UserStarDifficulty.Max = maximumStars.Value;
|
||||
|
||||
criteria.RulesetCriteria = ruleset.Value.CreateInstance().CreateRulesetFilterCriteria();
|
||||
|
||||
FilterQueryParser.ApplyQueries(criteria, query);
|
||||
return criteria;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using JetBrains.Annotations;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
@ -69,6 +70,9 @@ namespace osu.Game.Screens.Select
|
||||
[CanBeNull]
|
||||
public BeatmapCollection Collection;
|
||||
|
||||
[CanBeNull]
|
||||
public IRulesetFilterCriteria RulesetCriteria { get; set; }
|
||||
|
||||
public struct OptionalRange<T> : IEquatable<OptionalRange<T>>
|
||||
where T : struct
|
||||
{
|
||||
|
@ -5,13 +5,17 @@ using System;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
internal static class FilterQueryParser
|
||||
/// <summary>
|
||||
/// Utility class used for parsing song select filter queries entered via the search box.
|
||||
/// </summary>
|
||||
public static class FilterQueryParser
|
||||
{
|
||||
private static readonly Regex query_syntax_regex = new Regex(
|
||||
@"\b(?<key>stars|ar|dr|hp|cs|divisor|length|objects|bpm|status|creator|artist)(?<op>[=:><]+)(?<value>("".*"")|(\S*))",
|
||||
@"\b(?<key>\w+)(?<op>(:|=|(>|<)(:|=)?))(?<value>("".*"")|(\S*))",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
internal static void ApplyQueries(FilterCriteria criteria, string query)
|
||||
@ -19,62 +23,81 @@ namespace osu.Game.Screens.Select
|
||||
foreach (Match match in query_syntax_regex.Matches(query))
|
||||
{
|
||||
var key = match.Groups["key"].Value.ToLower();
|
||||
var op = match.Groups["op"].Value;
|
||||
var op = parseOperator(match.Groups["op"].Value);
|
||||
var value = match.Groups["value"].Value;
|
||||
|
||||
parseKeywordCriteria(criteria, key, value, op);
|
||||
|
||||
query = query.Replace(match.ToString(), "");
|
||||
if (tryParseKeywordCriteria(criteria, key, value, op))
|
||||
query = query.Replace(match.ToString(), "");
|
||||
}
|
||||
|
||||
criteria.SearchText = query;
|
||||
}
|
||||
|
||||
private static void parseKeywordCriteria(FilterCriteria criteria, string key, string value, string op)
|
||||
private static bool tryParseKeywordCriteria(FilterCriteria criteria, string key, string value, Operator op)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "stars" when parseFloatWithPoint(value, out var stars):
|
||||
updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2);
|
||||
break;
|
||||
case "stars":
|
||||
return TryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2);
|
||||
|
||||
case "ar" when parseFloatWithPoint(value, out var ar):
|
||||
updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2);
|
||||
break;
|
||||
case "ar":
|
||||
return TryUpdateCriteriaRange(ref criteria.ApproachRate, op, value);
|
||||
|
||||
case "dr" when parseFloatWithPoint(value, out var dr):
|
||||
case "hp" when parseFloatWithPoint(value, out dr):
|
||||
updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2);
|
||||
break;
|
||||
case "dr":
|
||||
case "hp":
|
||||
return TryUpdateCriteriaRange(ref criteria.DrainRate, op, value);
|
||||
|
||||
case "cs" when parseFloatWithPoint(value, out var cs):
|
||||
updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2);
|
||||
break;
|
||||
case "cs":
|
||||
return TryUpdateCriteriaRange(ref criteria.CircleSize, op, value);
|
||||
|
||||
case "bpm" when parseDoubleWithPoint(value, out var bpm):
|
||||
updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2);
|
||||
break;
|
||||
case "bpm":
|
||||
return TryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.01d / 2);
|
||||
|
||||
case "length" when parseDoubleWithPoint(value.TrimEnd('m', 's', 'h'), out var length):
|
||||
var scale = getLengthScale(value);
|
||||
updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
|
||||
break;
|
||||
case "length":
|
||||
return tryUpdateLengthRange(criteria, op, value);
|
||||
|
||||
case "divisor" when parseInt(value, out var divisor):
|
||||
updateCriteriaRange(ref criteria.BeatDivisor, op, divisor);
|
||||
break;
|
||||
case "divisor":
|
||||
return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt);
|
||||
|
||||
case "status" when Enum.TryParse<BeatmapSetOnlineStatus>(value, true, out var statusValue):
|
||||
updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue);
|
||||
break;
|
||||
case "status":
|
||||
return TryUpdateCriteriaRange(ref criteria.OnlineStatus, op, value,
|
||||
(string s, out BeatmapSetOnlineStatus val) => Enum.TryParse(value, true, out val));
|
||||
|
||||
case "creator":
|
||||
updateCriteriaText(ref criteria.Creator, op, value);
|
||||
break;
|
||||
return TryUpdateCriteriaText(ref criteria.Creator, op, value);
|
||||
|
||||
case "artist":
|
||||
updateCriteriaText(ref criteria.Artist, op, value);
|
||||
break;
|
||||
return TryUpdateCriteriaText(ref criteria.Artist, op, value);
|
||||
|
||||
default:
|
||||
return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Operator parseOperator(string value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case "=":
|
||||
case ":":
|
||||
return Operator.Equal;
|
||||
|
||||
case "<":
|
||||
return Operator.Less;
|
||||
|
||||
case "<=":
|
||||
case "<:":
|
||||
return Operator.LessOrEqual;
|
||||
|
||||
case ">":
|
||||
return Operator.Greater;
|
||||
|
||||
case ">=":
|
||||
case ">:":
|
||||
return Operator.GreaterOrEqual;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported operator {value}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,129 +107,203 @@ namespace osu.Game.Screens.Select
|
||||
value.EndsWith('m') ? 60000 :
|
||||
value.EndsWith('h') ? 3600000 : 1000;
|
||||
|
||||
private static bool parseFloatWithPoint(string value, out float result) =>
|
||||
private static bool tryParseFloatWithPoint(string value, out float result) =>
|
||||
float.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static bool parseDoubleWithPoint(string value, out double result) =>
|
||||
private static bool tryParseDoubleWithPoint(string value, out double result) =>
|
||||
double.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static bool parseInt(string value, out int result) =>
|
||||
private static bool tryParseInt(string value, out int result) =>
|
||||
int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static void updateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, string op, string value)
|
||||
/// <summary>
|
||||
/// Attempts to parse a keyword filter with the specified <paramref name="op"/> and textual <paramref name="value"/>.
|
||||
/// If the value indicates a valid textual filter, the function returns <c>true</c> and the resulting data is stored into
|
||||
/// <paramref name="textFilter"/>.
|
||||
/// </summary>
|
||||
/// <param name="textFilter">The <see cref="FilterCriteria.OptionalTextFilter"/> to store the parsed data into, if successful.</param>
|
||||
/// <param name="op">
|
||||
/// The operator for the keyword filter.
|
||||
/// Only <see cref="Operator.Equal"/> is valid for textual filters.
|
||||
/// </param>
|
||||
/// <param name="value">The value of the keyword filter.</param>
|
||||
public static bool TryUpdateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, Operator op, string value)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
case "=":
|
||||
case ":":
|
||||
case Operator.Equal:
|
||||
textFilter.SearchTerm = value.Trim('"');
|
||||
break;
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, string op, float value, float tolerance = 0.05f)
|
||||
/// <summary>
|
||||
/// Attempts to parse a keyword filter of type <see cref="float"/>
|
||||
/// from the specified <paramref name="op"/> and <paramref name="val"/>.
|
||||
/// If <paramref name="val"/> can be parsed as a <see cref="float"/>, the function returns <c>true</c>
|
||||
/// and the resulting range constraint is stored into <paramref name="range"/>.
|
||||
/// </summary>
|
||||
/// <param name="range">
|
||||
/// The <see cref="float"/>-typed <see cref="FilterCriteria.OptionalRange{T}"/>
|
||||
/// to store the parsed data into, if successful.
|
||||
/// </param>
|
||||
/// <param name="op">The operator for the keyword filter.</param>
|
||||
/// <param name="val">The value of the keyword filter.</param>
|
||||
/// <param name="tolerance">Allowed tolerance of the parsed range boundary value.</param>
|
||||
public static bool TryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, Operator op, string val, float tolerance = 0.05f)
|
||||
=> tryParseFloatWithPoint(val, out float value) && tryUpdateCriteriaRange(ref range, op, value, tolerance);
|
||||
|
||||
private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, Operator op, float value, float tolerance = 0.05f)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
default:
|
||||
return;
|
||||
return false;
|
||||
|
||||
case "=":
|
||||
case ":":
|
||||
case Operator.Equal:
|
||||
range.Min = value - tolerance;
|
||||
range.Max = value + tolerance;
|
||||
break;
|
||||
|
||||
case ">":
|
||||
case Operator.Greater:
|
||||
range.Min = value + tolerance;
|
||||
break;
|
||||
|
||||
case ">=":
|
||||
case ">:":
|
||||
case Operator.GreaterOrEqual:
|
||||
range.Min = value - tolerance;
|
||||
break;
|
||||
|
||||
case "<":
|
||||
case Operator.Less:
|
||||
range.Max = value - tolerance;
|
||||
break;
|
||||
|
||||
case "<=":
|
||||
case "<:":
|
||||
case Operator.LessOrEqual:
|
||||
range.Max = value + tolerance;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, string op, double value, double tolerance = 0.05)
|
||||
/// <summary>
|
||||
/// Attempts to parse a keyword filter of type <see cref="double"/>
|
||||
/// from the specified <paramref name="op"/> and <paramref name="val"/>.
|
||||
/// If <paramref name="val"/> can be parsed as a <see cref="double"/>, the function returns <c>true</c>
|
||||
/// and the resulting range constraint is stored into <paramref name="range"/>.
|
||||
/// </summary>
|
||||
/// <param name="range">
|
||||
/// The <see cref="double"/>-typed <see cref="FilterCriteria.OptionalRange{T}"/>
|
||||
/// to store the parsed data into, if successful.
|
||||
/// </param>
|
||||
/// <param name="op">The operator for the keyword filter.</param>
|
||||
/// <param name="val">The value of the keyword filter.</param>
|
||||
/// <param name="tolerance">Allowed tolerance of the parsed range boundary value.</param>
|
||||
public static bool TryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, Operator op, string val, double tolerance = 0.05)
|
||||
=> tryParseDoubleWithPoint(val, out double value) && tryUpdateCriteriaRange(ref range, op, value, tolerance);
|
||||
|
||||
private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, Operator op, double value, double tolerance = 0.05)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
default:
|
||||
return;
|
||||
return false;
|
||||
|
||||
case "=":
|
||||
case ":":
|
||||
case Operator.Equal:
|
||||
range.Min = value - tolerance;
|
||||
range.Max = value + tolerance;
|
||||
break;
|
||||
|
||||
case ">":
|
||||
case Operator.Greater:
|
||||
range.Min = value + tolerance;
|
||||
break;
|
||||
|
||||
case ">=":
|
||||
case ">:":
|
||||
case Operator.GreaterOrEqual:
|
||||
range.Min = value - tolerance;
|
||||
break;
|
||||
|
||||
case "<":
|
||||
case Operator.Less:
|
||||
range.Max = value - tolerance;
|
||||
break;
|
||||
|
||||
case "<=":
|
||||
case "<:":
|
||||
case Operator.LessOrEqual:
|
||||
range.Max = value + tolerance;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void updateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, string op, T value)
|
||||
/// <summary>
|
||||
/// Used to determine whether the string value <paramref name="val"/> can be converted to type <typeparamref name="T"/>.
|
||||
/// If conversion can be performed, the delegate returns <c>true</c>
|
||||
/// and the conversion result is returned in the <c>out</c> parameter <paramref name="parsed"/>.
|
||||
/// </summary>
|
||||
/// <param name="val">The string value to attempt parsing for.</param>
|
||||
/// <param name="parsed">The parsed value, if conversion is possible.</param>
|
||||
public delegate bool TryParseFunction<T>(string val, out T parsed);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a keyword filter of type <typeparamref name="T"/>,
|
||||
/// from the specified <paramref name="op"/> and <paramref name="val"/>.
|
||||
/// If <paramref name="val"/> can be parsed into <typeparamref name="T"/> using <paramref name="parseFunction"/>, the function returns <c>true</c>
|
||||
/// and the resulting range constraint is stored into <paramref name="range"/>.
|
||||
/// </summary>
|
||||
/// <param name="range">The <see cref="FilterCriteria.OptionalRange{T}"/> to store the parsed data into, if successful.</param>
|
||||
/// <param name="op">The operator for the keyword filter.</param>
|
||||
/// <param name="val">The value of the keyword filter.</param>
|
||||
/// <param name="parseFunction">Function used to determine if <paramref name="val"/> can be converted to type <typeparamref name="T"/>.</param>
|
||||
public static bool TryUpdateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, Operator op, string val, TryParseFunction<T> parseFunction)
|
||||
where T : struct
|
||||
=> parseFunction.Invoke(val, out var converted) && tryUpdateCriteriaRange(ref range, op, converted);
|
||||
|
||||
private static bool tryUpdateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, Operator op, T value)
|
||||
where T : struct
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
default:
|
||||
return;
|
||||
return false;
|
||||
|
||||
case "=":
|
||||
case ":":
|
||||
case Operator.Equal:
|
||||
range.IsLowerInclusive = range.IsUpperInclusive = true;
|
||||
range.Min = value;
|
||||
range.Max = value;
|
||||
break;
|
||||
|
||||
case ">":
|
||||
case Operator.Greater:
|
||||
range.IsLowerInclusive = false;
|
||||
range.Min = value;
|
||||
break;
|
||||
|
||||
case ">=":
|
||||
case ">:":
|
||||
case Operator.GreaterOrEqual:
|
||||
range.IsLowerInclusive = true;
|
||||
range.Min = value;
|
||||
break;
|
||||
|
||||
case "<":
|
||||
case Operator.Less:
|
||||
range.IsUpperInclusive = false;
|
||||
range.Max = value;
|
||||
break;
|
||||
|
||||
case "<=":
|
||||
case "<:":
|
||||
case Operator.LessOrEqual:
|
||||
range.IsUpperInclusive = true;
|
||||
range.Max = value;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool tryUpdateLengthRange(FilterCriteria criteria, Operator op, string val)
|
||||
{
|
||||
if (!tryParseDoubleWithPoint(val.TrimEnd('m', 's', 'h'), out var length))
|
||||
return false;
|
||||
|
||||
var scale = getLengthScale(val);
|
||||
return tryUpdateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user