1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-06 09:07:25 +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:
Dean Herbert 2021-03-08 23:23:24 +09:00 committed by GitHub
commit 9b5d11f2a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 350 additions and 74 deletions

View File

@ -4,8 +4,10 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Filter;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Filter;
namespace osu.Game.Tests.NonVisual.Filtering namespace osu.Game.Tests.NonVisual.Filtering
{ {
@ -214,5 +216,31 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual(filtered, carouselItem.Filtered.Value); 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;
}
} }
} }

View File

@ -4,7 +4,9 @@
using System; using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Filter;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
namespace osu.Game.Tests.NonVisual.Filtering namespace osu.Game.Tests.NonVisual.Filtering
{ {
@ -194,5 +196,63 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual(1, filterCriteria.SearchTerms.Length); Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
Assert.AreEqual("double\"quote", filterCriteria.Artist.SearchTerm); 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;
}
}
} }
} }

View 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);
}
}

View File

@ -26,6 +26,7 @@ using JetBrains.Annotations;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets.Filter;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets namespace osu.Game.Rulesets
@ -306,5 +307,11 @@ namespace osu.Game.Rulesets
/// <param name="result">The result type to get the name for.</param> /// <param name="result">The result type to get the name for.</param>
/// <returns>The display name.</returns> /// <returns>The display name.</returns>
public virtual string GetDisplayNameForHitResult(HitResult result) => result.GetDescription(); 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;
} }
} }

View File

@ -73,6 +73,9 @@ namespace osu.Game.Screens.Select.Carousel
if (match) if (match)
match &= criteria.Collection?.Beatmaps.Contains(Beatmap) ?? true; match &= criteria.Collection?.Beatmaps.Contains(Beatmap) ?? true;
if (match && criteria.RulesetCriteria != null)
match &= criteria.RulesetCriteria.Matches(Beatmap);
Filtered.Value = !match; Filtered.Value = !match;
} }

View 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
}
}

View File

@ -2,6 +2,7 @@
// 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;
using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -36,6 +37,8 @@ namespace osu.Game.Screens.Select
public FilterCriteria CreateCriteria() public FilterCriteria CreateCriteria()
{ {
Debug.Assert(ruleset.Value.ID != null);
var query = searchTextBox.Text; var query = searchTextBox.Text;
var criteria = new FilterCriteria var criteria = new FilterCriteria
@ -53,6 +56,8 @@ namespace osu.Game.Screens.Select
if (!maximumStars.IsDefault) if (!maximumStars.IsDefault)
criteria.UserStarDifficulty.Max = maximumStars.Value; criteria.UserStarDifficulty.Max = maximumStars.Value;
criteria.RulesetCriteria = ruleset.Value.CreateInstance().CreateRulesetFilterCriteria();
FilterQueryParser.ApplyQueries(criteria, query); FilterQueryParser.ApplyQueries(criteria, query);
return criteria; return criteria;
} }

View File

@ -8,6 +8,7 @@ using JetBrains.Annotations;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Collections; using osu.Game.Collections;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Filter;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
namespace osu.Game.Screens.Select namespace osu.Game.Screens.Select
@ -69,6 +70,9 @@ namespace osu.Game.Screens.Select
[CanBeNull] [CanBeNull]
public BeatmapCollection Collection; public BeatmapCollection Collection;
[CanBeNull]
public IRulesetFilterCriteria RulesetCriteria { get; set; }
public struct OptionalRange<T> : IEquatable<OptionalRange<T>> public struct OptionalRange<T> : IEquatable<OptionalRange<T>>
where T : struct where T : struct
{ {

View File

@ -5,13 +5,17 @@ using System;
using System.Globalization; using System.Globalization;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Screens.Select.Filter;
namespace osu.Game.Screens.Select 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( 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); RegexOptions.Compiled | RegexOptions.IgnoreCase);
internal static void ApplyQueries(FilterCriteria criteria, string query) 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)) foreach (Match match in query_syntax_regex.Matches(query))
{ {
var key = match.Groups["key"].Value.ToLower(); 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; var value = match.Groups["value"].Value;
parseKeywordCriteria(criteria, key, value, op); if (tryParseKeywordCriteria(criteria, key, value, op))
query = query.Replace(match.ToString(), "");
query = query.Replace(match.ToString(), "");
} }
criteria.SearchText = query; 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) switch (key)
{ {
case "stars" when parseFloatWithPoint(value, out var stars): case "stars":
updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2); return TryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2);
break;
case "ar" when parseFloatWithPoint(value, out var ar): case "ar":
updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2); return TryUpdateCriteriaRange(ref criteria.ApproachRate, op, value);
break;
case "dr" when parseFloatWithPoint(value, out var dr): case "dr":
case "hp" when parseFloatWithPoint(value, out dr): case "hp":
updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2); return TryUpdateCriteriaRange(ref criteria.DrainRate, op, value);
break;
case "cs" when parseFloatWithPoint(value, out var cs): case "cs":
updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2); return TryUpdateCriteriaRange(ref criteria.CircleSize, op, value);
break;
case "bpm" when parseDoubleWithPoint(value, out var bpm): case "bpm":
updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2); return TryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.01d / 2);
break;
case "length" when parseDoubleWithPoint(value.TrimEnd('m', 's', 'h'), out var length): case "length":
var scale = getLengthScale(value); return tryUpdateLengthRange(criteria, op, value);
updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
break;
case "divisor" when parseInt(value, out var divisor): case "divisor":
updateCriteriaRange(ref criteria.BeatDivisor, op, divisor); return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt);
break;
case "status" when Enum.TryParse<BeatmapSetOnlineStatus>(value, true, out var statusValue): case "status":
updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue); return TryUpdateCriteriaRange(ref criteria.OnlineStatus, op, value,
break; (string s, out BeatmapSetOnlineStatus val) => Enum.TryParse(value, true, out val));
case "creator": case "creator":
updateCriteriaText(ref criteria.Creator, op, value); return TryUpdateCriteriaText(ref criteria.Creator, op, value);
break;
case "artist": case "artist":
updateCriteriaText(ref criteria.Artist, op, value); return TryUpdateCriteriaText(ref criteria.Artist, op, value);
break;
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('m') ? 60000 :
value.EndsWith('h') ? 3600000 : 1000; 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); 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); 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); 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) switch (op)
{ {
case "=": case Operator.Equal:
case ":":
textFilter.SearchTerm = value.Trim('"'); 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) switch (op)
{ {
default: default:
return; return false;
case "=": case Operator.Equal:
case ":":
range.Min = value - tolerance; range.Min = value - tolerance;
range.Max = value + tolerance; range.Max = value + tolerance;
break; break;
case ">": case Operator.Greater:
range.Min = value + tolerance; range.Min = value + tolerance;
break; break;
case ">=": case Operator.GreaterOrEqual:
case ">:":
range.Min = value - tolerance; range.Min = value - tolerance;
break; break;
case "<": case Operator.Less:
range.Max = value - tolerance; range.Max = value - tolerance;
break; break;
case "<=": case Operator.LessOrEqual:
case "<:":
range.Max = value + tolerance; range.Max = value + tolerance;
break; 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) switch (op)
{ {
default: default:
return; return false;
case "=": case Operator.Equal:
case ":":
range.Min = value - tolerance; range.Min = value - tolerance;
range.Max = value + tolerance; range.Max = value + tolerance;
break; break;
case ">": case Operator.Greater:
range.Min = value + tolerance; range.Min = value + tolerance;
break; break;
case ">=": case Operator.GreaterOrEqual:
case ">:":
range.Min = value - tolerance; range.Min = value - tolerance;
break; break;
case "<": case Operator.Less:
range.Max = value - tolerance; range.Max = value - tolerance;
break; break;
case "<=": case Operator.LessOrEqual:
case "<:":
range.Max = value + tolerance; range.Max = value + tolerance;
break; 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 where T : struct
{ {
switch (op) switch (op)
{ {
default: default:
return; return false;
case "=": case Operator.Equal:
case ":":
range.IsLowerInclusive = range.IsUpperInclusive = true; range.IsLowerInclusive = range.IsUpperInclusive = true;
range.Min = value; range.Min = value;
range.Max = value; range.Max = value;
break; break;
case ">": case Operator.Greater:
range.IsLowerInclusive = false; range.IsLowerInclusive = false;
range.Min = value; range.Min = value;
break; break;
case ">=": case Operator.GreaterOrEqual:
case ">:":
range.IsLowerInclusive = true; range.IsLowerInclusive = true;
range.Min = value; range.Min = value;
break; break;
case "<": case Operator.Less:
range.IsUpperInclusive = false; range.IsUpperInclusive = false;
range.Max = value; range.Max = value;
break; break;
case "<=": case Operator.LessOrEqual:
case "<:":
range.IsUpperInclusive = true; range.IsUpperInclusive = true;
range.Max = value; range.Max = value;
break; 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);
} }
} }
} }