From a8621699c1f66edc846ac03ef4e26a767ce2287a Mon Sep 17 00:00:00 2001 From: Jo-Be-Co Date: Sat, 21 Mar 2026 01:06:21 +0100 Subject: [PATCH] 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 --- .../ConditionalTagCollection[TClass].cs | 38 ++++++++++++++----- .../PropertyTagCollection[TClass].cs | 10 +++-- .../TemplatesTests.cs | 18 ++++++++- docs/features/naming-templates.md | 8 ++++ 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs index db64723f..9756732e 100644 --- a/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs @@ -160,20 +160,38 @@ public partial class ConditionalTagCollection(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 e => e.Any(o => checkItem(o, culture)), - _ => checkItem(v, culture) - }; + return isNumop + ? (v, culture) => v switch + { + null => false, + IEnumerable e => checkItem(e.Count(), culture), + string s => checkItem(s.Length, culture), + _ => checkItem(v, culture) + } + : (v, culture) => v switch + { + null => false, + IEnumerable 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(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 (? # capture operator in and - ~|!=?|=? # - string comparison operators including ~ for regexp. No operator is like = + (?\#=|\#!=|\#?>=|\#?>|\#?<=|\#?<) # - 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 - (? # capture value in - .*? # - string for comparison. May be empty. Non-greedy capture resulting in no whitespace at the end + (?(?(numop) # capture value in + \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(); diff --git a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs index 43ba013f..a600d9b1 100644 --- a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs @@ -22,7 +22,7 @@ public class PropertyTagCollection : 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 : TagCollection { public override Regex NameMatcher { get; } private Func CreateToStringExpression { get; } - private Func CreateToObjectExpression { get; } + private Func CreateToObjectExpression { get; } = (expVal, _) => expVal; public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PF.PropertyFormatter preFormatter, PF.PropertyFinalizer finalizer, PF.PropertyFinalizer formatter) @@ -204,6 +204,8 @@ public class PropertyTagCollection : 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 : 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 : TagCollection : base(templateTag, propertyGetter) { NameMatcher = new Regex(@$"^<{TagNameForRegex()}>", options); - CreateToObjectExpression = (expVal, _) => expVal; CreateToStringExpression = (expVal, _) => Expression.Call( diff --git a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs index 99476a9d..c37da967 100644 --- a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs +++ b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs @@ -131,7 +131,7 @@ namespace TemplatesTests [DataRow("4 - 4", "", "", "1 8 - 1 8")] [DataRow("", "", "", "100")] [DataRow(" ", "", "", "100")] - [DataRow(" - - ", "", "", "- 100 -")] + [DataRow(" - - ", "", "", "- 100 -")] public void Tests_removeSpaces(string template, string dirFullPath, string extension, string expected) { @@ -400,26 +400,34 @@ namespace TemplatesTests [DataRow("empty-string<-has>", "")] [DataRow("empty-string<-has>", "")] [DataRow("empty-string<-has>", "empty-string")] + [DataRow("empty-string<-has>", "empty-string")] [DataRow("empty-string<-has>", "empty-string")] [DataRow("null-string<-has>", "")] [DataRow("null-string<-has>", "")] [DataRow("null-string<-has>", "")] + [DataRow("null-string<-has>", "")] [DataRow("null-string<-has>", "")] [DataRow("null-int<-has>", "")] [DataRow("null-int<-has>", "")] + [DataRow("null-int<-has>", "")] + [DataRow("null-int<-has>", "")] [DataRow("null-int<-has>", "")] [DataRow("unknown-tag<-has>", "")] [DataRow("unknown-tag<-has>", "")] [DataRow("unknown-tag<-has>", "")] + [DataRow("unknown-tag<-has>", "")] [DataRow("unknown-tag<-has>", "")] [DataRow("empty-list<-has>", "")] [DataRow("empty-list<-has>", "")] [DataRow("empty-list<-has>", "")] [DataRow("empty-list<-has>", "")] + [DataRow("empty-list<-has>", "empty-list")] + [DataRow("empty-list<-has>", "empty-list")] [DataRow("empty-list<-has>", "")] [DataRow("no-first<-has>", "")] [DataRow("no-first<-has>", "")] [DataRow("no-first<-has>", "")] + [DataRow("no-first<-has>", "")] [DataRow("no-first<-has>", "")] public void HasValue_on_empty_test(string template, string expected) { @@ -468,13 +476,21 @@ namespace TemplatesTests [DataRow("true<-has>", "true")] [DataRow("true<-has>", "true")] [DataRow("true<-has>", "true")] + [DataRow("true<-has>", "true")] [DataRow("true<-has>", "true")] [DataRow("true<-has>", "true")] [DataRow("true<-has>", "")] + [DataRow("=1]->true<-has>", "true")] + [DataRow("1]->true<-has>", "true")] + [DataRow("true<-has>", "true")] + [DataRow("true<-has>", "true")] [DataRow("true<-has>", "true")] + [DataRow("=2]->true<-has>", "true")] + [DataRow("true<-has>", "true")] [DataRow("true<-has>", "true")] [DataRow("true<-has>", "true")] [DataRow("true<-has>", "true")] + [DataRow("=3]->true<-has>", "")] [DataRow("true<-has>", "")] public void HasValue_test(string template, string expected) { diff --git a/docs/features/naming-templates.md b/docs/features/naming-templates.md index 7b61958a..d86c450e 100644 --- a/docs/features/naming-templates.md +++ b/docs/features/naming-templates.md @@ -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) | | | !=STRING **†** | Matches if one item is not equal to STRING (case ignored) | | | ~STRING **†** | Matches if one items is matched by the regular expression STRING (case ignored) | | +| #=NUMBER **‡** | Matches if the number value is equal to NUMBER | | +| #!=NUMBER **‡** | Matches if the number value is not equal to NUMBER | | +| #>=NUMBER **‡** | Matches if the number value is greater than or equal to NUMBER | =128]-> | +| #>NUMBER **‡** | Matches if the number value is greater than NUMBER | 30]-> | +| #<=NUMBER **‡** | Matches if the number value is less than or equal to NUMBER | | +| # | **†** 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: