Add numerical checks for conditional tag.

- numbers are compared with their value
- strings are compared by checking the length
- Collections are evaluated on cheking their size
This commit is contained in:
Jo-Be-Co
2026-03-21 01:06:21 +01:00
parent cd9a070784
commit a8621699c1
4 changed files with 61 additions and 13 deletions

View File

@@ -160,20 +160,38 @@ public partial class ConditionalTagCollection<TClass>(bool caseSensitive = true)
var match = CheckRegex().Match(checkString);
var valStr = match.Groups["val"].Value;
var ival = -1;
var isNumop = match.Groups["numop"].Success && int.TryParse(valStr, out ival);
var checkItem = match.Groups["op"].ValueSpan switch
{
"=" or "" => (v, culture) => VComparedToStr(v, culture, valStr) == 0,
"!=" or "!" => (v, culture) => VComparedToStr(v, culture, valStr) != 0,
"~" => GetRegExpCheck(valStr),
"#=" => (v, _) => VAsInt(v) == ival,
"#!=" => (v, _) => VAsInt(v) != ival,
"#>=" or ">=" => (v, _) => VAsInt(v) >= ival,
"#>" or ">" => (v, _) => VAsInt(v) > ival,
"#<=" or "<=" => (v, _) => VAsInt(v) <= ival,
"#<" or "<" => (v, _) => VAsInt(v) < ival,
_ => DefaultPredicate,
};
return (v, culture) => v switch
{
null => false,
IEnumerable<object> e => e.Any(o => checkItem(o, culture)),
_ => checkItem(v, culture)
};
return isNumop
? (v, culture) => v switch
{
null => false,
IEnumerable<object> e => checkItem(e.Count(), culture),
string s => checkItem(s.Length, culture),
_ => checkItem(v, culture)
}
: (v, culture) => v switch
{
null => false,
IEnumerable<object> e => e.Any(o => checkItem(o, culture)),
_ => checkItem(v, culture)
};
int? VAsInt(object v) => v is int iv ? iv : int.TryParse(v.ToString(), out var parsed) ? parsed : null;
}
private static int VComparedToStr(object? v, CultureInfo? culture, string valStr)
@@ -239,10 +257,12 @@ public partial class ConditionalTagCollection<TClass>(bool caseSensitive = true)
(?x) # option x: ignore all unescaped whitespace in pattern and allow comments starting with #
^\s* # anchor at start of line trimming leading whitespace
(?<op> # capture operator in <op> and <numop>
~|!=?|=? # - string comparison operators including ~ for regexp. No operator is like =
(?<numop>\#=|\#!=|\#?>=|\#?>|\#?<=|\#?<) # - numerical operators start with a # and might be omitted if unique
| ~|!=?|=? # - string comparison operators including ~ for regexp. No operator is like =
) \s* # ignore space between operator and value
(?<val> # capture value in <val>
.*? # - string for comparison. May be empty. Non-greedy capture resulting in no whitespace at the end
(?<val>(?(numop) # capture value in <val>
\d+ # - numerical operators have to be followed by a number
| .*? ) # - string for comparison. May be empty. Non-greedy capture resulting in no whitespace at the end
)\s*$ # trimming up to the end
""")]
private static partial Regex CheckRegex();

View File

@@ -22,7 +22,7 @@ public class PropertyTagCollection<TClass> : TagCollection
if (formatter.Method.ReturnType != typeof(string)
|| parameters.Length != 4
|| parameters[0].ParameterType != typeof(ITemplateTag)
|| parameters[0].ParameterType != typeof(ITemplateTag)
|| parameters[2].ParameterType != typeof(string)
|| !typeof(CultureInfo).IsAssignableFrom(parameters[3].ParameterType))
throw new ArgumentException(
@@ -183,7 +183,7 @@ public class PropertyTagCollection<TClass> : TagCollection
{
public override Regex NameMatcher { get; }
private Func<Expression, string?, Expression> CreateToStringExpression { get; }
private Func<Expression, string?, Expression> CreateToObjectExpression { get; }
private Func<Expression, string?, Expression> CreateToObjectExpression { get; } = (expVal, _) => expVal;
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PF.PropertyFormatter<TPropertyValue, TPreFormatted> preFormatter,
PF.PropertyFinalizer<TPreFormatted> finalizer, PF.PropertyFinalizer<TPropertyValue> formatter)
@@ -204,6 +204,8 @@ public class PropertyTagCollection<TClass> : TagCollection
"""
, options);
// if no format is specified, we can directly use the expVal from the property-getter as object value,
// otherwise we need to call the preFormatter with the format string and culture info to get the formatted value as object.
CreateToObjectExpression = (expVal, format) =>
format is null
? expVal
@@ -215,6 +217,9 @@ public class PropertyTagCollection<TClass> : TagCollection
Expression.Constant(format),
CultureParameter);
// if no format is specified, we can use the specific formatter to format the value to string directly,
// otherwise we need to call the preFormatter with the format string and culture info to get the formatted value as object,
// and then call the finalizer to get the final string value.
CreateToStringExpression = (expVal, format) =>
format is null
? Expression.Call(
@@ -241,7 +246,6 @@ public class PropertyTagCollection<TClass> : TagCollection
: base(templateTag, propertyGetter)
{
NameMatcher = new Regex(@$"^<{TagNameForRegex()}>", options);
CreateToObjectExpression = (expVal, _) => expVal;
CreateToStringExpression = (expVal, _) =>
Expression.Call(

View File

@@ -131,7 +131,7 @@ namespace TemplatesTests
[DataRow("4<bitrate> - <bitrate> 4", "", "", "1 8 - 1 8")]
[DataRow("<channels><channels><samplerate><channels><channels>", "", "", "100")]
[DataRow(" <channels> <channels> <samplerate> <channels> <channels>", "", "", "100")]
[DataRow(" <channels> - <channels> <samplerate> <channels> - <channels>", "", "", "- 100 -")]
[DataRow(" <channels> - <channels> <samplerate> <channels> - <channels>", "", "", "- 100 -")]
public void Tests_removeSpaces(string template, string dirFullPath, string extension, string expected)
{
@@ -400,26 +400,34 @@ namespace TemplatesTests
[DataRow("<has libation version->empty-string<-has>", "")]
[DataRow("<is libation version[=foobar]->empty-string<-has>", "")]
[DataRow("<is libation version[=]->empty-string<-has>", "empty-string")]
[DataRow("<is libation version[#=0]->empty-string<-has>", "empty-string")]
[DataRow("<is libation version[]->empty-string<-has>", "empty-string")]
[DataRow("<has file version->null-string<-has>", "")]
[DataRow("<has file version[=foobar]->null-string<-has>", "")]
[DataRow("<has file version[=]->null-string<-has>", "")]
[DataRow("<is file version[#=0]->null-string<-has>", "")]
[DataRow("<has file version[]->null-string<-has>", "")]
[DataRow("<has year->null-int<-has>", "")]
[DataRow("<is year[=]->null-int<-has>", "")]
[DataRow("<is year[#=0]->null-int<-has>", "")]
[DataRow("<is year[0]->null-int<-has>", "")]
[DataRow("<is year[]->null-int<-has>", "")]
[DataRow("<has FAKE->unknown-tag<-has>", "")]
[DataRow("<is FAKE[=]->unknown-tag<-has>", "")]
[DataRow("<is FAKE[=foobar]->unknown-tag<-has>", "")]
[DataRow("<is FAKE[#=0]->unknown-tag<-has>", "")]
[DataRow("<is FAKE[]->unknown-tag<-has>", "")]
[DataRow("<has narrator->empty-list<-has>", "")]
[DataRow("<is narrator[=foobar]->empty-list<-has>", "")]
[DataRow("<is narrator[=]->empty-list<-has>", "")]
[DataRow("<is narrator[~.*]->empty-list<-has>", "")]
[DataRow("<is narrator[<1]->empty-list<-has>", "empty-list")]
[DataRow("<is narrator[#=0]->empty-list<-has>", "empty-list")]
[DataRow("<is narrator[]->empty-list<-has>", "")]
[DataRow("<is first narrator->no-first<-has>", "")]
[DataRow("<is first narrator[=foobar]->no-first<-has>", "")]
[DataRow("<is first narrator[=]->no-first<-has>", "")]
[DataRow("<is first narrator[#=0]->no-first<-has>", "")]
[DataRow("<is first narrator[]->no-first<-has>", "")]
public void HasValue_on_empty_test(string template, string expected)
{
@@ -468,13 +476,21 @@ namespace TemplatesTests
[DataRow("<has ch# 0->true<-has>", "true")]
[DataRow("<is title[=A Study in Scarlet: An Audible Original Drama]->true<-has>", "true")]
[DataRow("<is title[U][=A STUDY IN SCARLET: AN AUDIBLE ORIGINAL DRAMA]->true<-has>", "true")]
[DataRow("<is title[#=45]->true<-has>", "true")]
[DataRow("<is title[!=foo]->true<-has>", "true")]
[DataRow("<is title[~A Study.*]->true<-has>", "true")]
[DataRow("<is title[foo]->true<-has>", "")]
[DataRow("<is ch count[>=1]->true<-has>", "true")]
[DataRow("<is ch count[>1]->true<-has>", "true")]
[DataRow("<is ch count[<=100]->true<-has>", "true")]
[DataRow("<is ch count[<100]->true<-has>", "true")]
[DataRow("<is ch count[=2]->true<-has>", "true")]
[DataRow("<is author[>=2]->true<-has>", "true")]
[DataRow("<is author[#=2]->true<-has>", "true")]
[DataRow("<is author[=Arthur Conan Doyle]->true<-has>", "true")]
[DataRow("<is author[format({L})][=Doyle]->true<-has>", "true")]
[DataRow("<is author[format({L})separator(:)][=Doyle:Fry]->true<-has>", "true")]
[DataRow("<is author[>=3]->true<-has>", "")]
[DataRow("<is author[=Sherlock]->true<-has>", "")]
public void HasValue_test(string template, string expected)
{

View File

@@ -185,9 +185,17 @@ You can use custom formatters to construct customized DateTime string. For more
| =STRING **†** | Matches if one item is equal to STRING (case ignored) | <has tag[=Tag1]-> |
| !=STRING **†** | Matches if one item is not equal to STRING (case ignored) | <has first author[!=Arthur]-> |
| ~STRING **†** | Matches if one items is matched by the regular expression STRING (case ignored) | <has title[~(\[XYZ\]).*\\1]-> |
| #=NUMBER **‡** | Matches if the number value is equal to NUMBER | <has channels[#=2]-> |
| #!=NUMBER **‡** | Matches if the number value is not equal to NUMBER | <has author[#!=1]-> |
| #>=NUMBER **‡** | Matches if the number value is greater than or equal to NUMBER | <has bitrate[#>=128]-> |
| #>NUMBER **‡** | Matches if the number value is greater than NUMBER | <has title[#>30]-> |
| #<=NUMBER **‡** | Matches if the number value is less than or equal to NUMBER | <has first narrator[format({F})][#<=1]-> |
| #<NUMBER **‡** | Matches if the number value is less than NUMBER | <has author[#<3]-> |
**†** STRING maybe escaped with a backslash. So even square brackets could be used. If a single backslash should be part of the string, it must be doubled.
**‡** NUMBER checks on lists are checking the size of the list. If the value to check is a string, its length is used.
#### More complex examples
This example will truncate the title to 4 characters and check its (trimmed) value to be "the" in any case: