From a21bb8174daa740a225bda2cea81bd1ed9c03642 Mon Sep 17 00:00:00 2001 From: Jo-Be-Co Date: Thu, 14 May 2026 02:04:26 +0200 Subject: [PATCH 1/4] #1762 introduce CommonFormatters.TryGetLiteral --- .../NamingTemplate/CommonFormatters.cs | 30 ++++++++++++++++++- .../Templates/Templates.cs | 22 +++----------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/Source/FileManager/NamingTemplate/CommonFormatters.cs b/Source/FileManager/NamingTemplate/CommonFormatters.cs index 1469ba93..85f80c2f 100644 --- a/Source/FileManager/NamingTemplate/CommonFormatters.cs +++ b/Source/FileManager/NamingTemplate/CommonFormatters.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; using System.Text.RegularExpressions; @@ -66,7 +67,7 @@ public static partial class CommonFormatters { if (string.IsNullOrWhiteSpace(templateString)) return ""; - // is this function is called from toString implementation of the IFormattable interface, we only get a IFormatProvider + // if this function is called from toString implementation of the IFormattable interface, we only get a IFormatProvider var culture = GetCultureInfo(provider); var oldUiCulture = Thread.CurrentThread.CurrentUICulture; var result = CollapseSpacesAndTrimRegex().Replace(TagFormatRegex().ReplaceWithGaps(templateString, GetValueForMatchingTag, Unescape), string.Empty); @@ -211,6 +212,33 @@ public static partial class CommonFormatters return StringFormatter(templateTag, language, "3u", culture); } + public static bool TryGetLiteral(string? value, [NotNullWhen(true)] out object? literal) + { + // check if value is quoted + if (StringValueRegex().TryMatch(value, out var stringValue)) + { + // inside the quotes, doubled quotes are used to represent literal quotes. So replace them back to single quotes if there are any. + // this match helps to determine which quote type is being used so that the correct one can be replaced. + var doubleQuote = stringValue.Groups["double"]; + literal = doubleQuote.Success + ? stringValue.Groups["value"].Value.Replace(doubleQuote.Value, stringValue.Groups["quote"].Value) + : stringValue.Groups["value"].Value; + return true; + } + + if (int.TryParse(value, out var intValue)) + { + literal = intValue; + return true; + } + + literal = null; + return false; + } + + [GeneratedRegex("""^\s*(?['"])(?(?:(?\k{2})|.)*)\k\s*$""")] + private static partial Regex StringValueRegex(); + public static string Unescape(string valueSpan) { return Unescape(valueSpan, ['\'', '"']); diff --git a/Source/LibationFileManager/Templates/Templates.cs b/Source/LibationFileManager/Templates/Templates.cs index c5be97b4..bcbc0aff 100644 --- a/Source/LibationFileManager/Templates/Templates.cs +++ b/Source/LibationFileManager/Templates/Templates.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using AaxDecrypter; using Dinah.Core; using FileManager; @@ -20,7 +19,7 @@ public interface ITemplate static abstract IEnumerable TagCollections { get; } } -public abstract partial class Templates +public abstract class Templates { public const string ErrorFullPathIsInvalid = @"No colons or full paths allowed. Eg: should not start with C:\"; public const string WarningNoChapterNumberTag = "Should include chapter number tag in template used for naming files which are split by chapter. Ie: or "; @@ -353,20 +352,10 @@ public abstract partial class Templates private static object? TryGetValue(ITemplateTag _, CombinedDto dtos, string property, CultureInfo? culture) { - // check for string literal first - if (StringValueRegex().TryMatch(property, out var stringValue)) + // check for literal (string or int) + if (CommonFormatters.TryGetLiteral(property, out var stringValue)) { - // inside the quotes, doubled quotes are used to represent literal quotes. So replace them back to single quotes if there are any. - // this match helps to determine which quote type is being used so that the correct one can be replaced. - var doubleQuote = stringValue.Groups["double"]; - return doubleQuote.Success - ? stringValue.Groups["value"].Value.Replace(doubleQuote.Value, stringValue.Groups["quote"].Value) - : stringValue.Groups["value"].Value; - } - // then check for int literal - if (int.TryParse(property, out var intVal)) - { - return intVal; + return stringValue; } // then check for property tags and retrieve their value @@ -388,9 +377,6 @@ public abstract partial class Templates return null; } - [GeneratedRegex(@"^\s*(?['""])(?(?:(?\k{2})|.)*)\k\s*$")] - private static partial Regex StringValueRegex(); - private static bool HasValue(object? value, object? _, CultureInfo? culture) { bool CheckItem(object o, CultureInfo? _) => !string.IsNullOrWhiteSpace(o.ToString()); From f1940321397c82448daabe5d674ea18232a60e5b Mon Sep 17 00:00:00 2001 From: Jo-Be-Co Date: Thu, 14 May 2026 02:10:38 +0200 Subject: [PATCH 2/4] #1762 add filter() to list properties --- .../Templates/IListFormat[TList].cs | 49 ++++++++++++- .../CommonFormattersTests.cs | 28 +++++++- .../TemplatesTests.cs | 7 ++ docs/features/naming-templates.md | 71 ++++++++++--------- 4 files changed, 115 insertions(+), 40 deletions(-) diff --git a/Source/LibationFileManager/Templates/IListFormat[TList].cs b/Source/LibationFileManager/Templates/IListFormat[TList].cs index ff3c70d5..3aef7d6e 100644 --- a/Source/LibationFileManager/Templates/IListFormat[TList].cs +++ b/Source/LibationFileManager/Templates/IListFormat[TList].cs @@ -11,13 +11,31 @@ internal partial interface IListFormat where TList : IListFormat { static IEnumerable FilteredList(string formatString, IEnumerable items, CultureInfo? culture) where T : IFormattable { - return Max(formatString, Slice(formatString, Unique(formatString, items, culture))); + return Max(formatString, Slice(formatString, Unique(formatString, Filter(formatString, items, culture), culture))); static StringComparer GetStringComparer(CultureInfo? culture) { return StringComparer.Create(culture ?? CultureInfo.CurrentCulture, ignoreCase: true); } + static IEnumerable Filter(string formatString, IEnumerable items, CultureInfo? culture) + { + if (!FilterRegex().TryMatch(formatString, out var filterMatch)) + return items; + + // read the format to apply on each item + var format = filterMatch.ResolveValue("format"); + // use the operator to get a predicate function that compares the formatted item to the value specified in the filter + var predicate = CompareCondition.GetPredicate(filterMatch.Value, filterMatch.ResolveValue("op")); + // the value to compare the formatted item to. Might be a number or a quoted string. + CommonFormatters.TryGetLiteral(filterMatch.ResolveValue("value"), out var value); + + // return only the items that match the predicate + return items.Where(FilterPredicate); + + bool FilterPredicate(T n) => predicate(n.ToString(format, culture), value, culture); + } + static IEnumerable Unique(string formatString, IEnumerable items, CultureInfo? culture) { return UniqueRegex().TryMatch(formatString, out var uniqueMatch) @@ -120,11 +138,36 @@ internal partial interface IListFormat where TList : IListFormat [GeneratedRegex("""[Ss]eparator\((?(?:\\.|'[^']*'|"[^"]*"|[^\\'"])*?)\)""")] private static partial Regex SeparatorRegex(); - /// Count will substitute all list members with a single number equal to there count + /// Count will substitute all list members with a single number equal to their count [GeneratedRegex("""[Cc]ount\((?(?:\\.|'[^']*'|"[^"]*"|[^\\'"])*?)\)""")] private static partial Regex CountRegex(); /// Unique will shrink the list to unique members after applying format to them [GeneratedRegex("""[Uu]nique\((?(?:\\.|'[^']*'|"[^"]*"|[^\\'"])*?)\)""")] private static partial Regex UniqueRegex(); -} + + /// The filter will reduce the list, keeping only the items that match the specified criteria. + [GeneratedRegex(""" + (?x) # option x: ignore all unescaped whitespace in pattern and allow comments starting with # + [Ff]ilter # name of the command 'filter' or 'Filter' + \( # details are enclosed in brackets + (?(?: # the first part captured as specifies how to format items before comparison + \\. # - '\' escapes always the next character. + | '[^']*' # - allow 'string' to be included in the format, with '' being an escaped ' character + | "[^"]*" # - allow "string" to be included in the format, with "" being an escaped " character + | [^\\'"] # - match any other character. This will not catch the operator at first. Because ... + ) *? ) # With *? the pattern above tries not to consume the operator. + \s* # Separate the following operator with whitespace + (? # capture operator in + [\#!≡=≠~<>≤≥&∉∌∈∌⋂⊆⊇⊂⊃-]+ # allow a wide range of operators, all non alphanumeric so that no operator is confused as value + | :[a-z_]+: # allow :named: operators for readability, e.g. :contains: + ) \s* # ignore space between operator and second property + (? # the second operand is captured as and is a quoted string encapsulated in either single or double quotes + '(?:[^']|'')*' # - allow 'string' to be included in the format, with '' being an escaped ' character + | "(?:[^"]|"")*" # - allow "string" to be included in the format, with "" being an escaped " character + | \d+ # - allow a number + ) # + \s* \) # end the filter details with optional whitespace and a closing bracket + """)] + private static partial Regex FilterRegex(); +} \ No newline at end of file diff --git a/Source/_Tests/FileManager.Tests/CommonFormattersTests.cs b/Source/_Tests/FileManager.Tests/CommonFormattersTests.cs index bbfa9642..fb559da6 100644 --- a/Source/_Tests/FileManager.Tests/CommonFormattersTests.cs +++ b/Source/_Tests/FileManager.Tests/CommonFormattersTests.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using AssertionHelper; using FileManager.NamingTemplate; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -322,10 +321,33 @@ public class CommonFormattersTests Assert.AreEqual(expected, unescaped); } + [TestMethod] + [DataRow(null, false, null, "null")] + [DataRow("", false, null, "emptystring")] + [DataRow("42", true, 42, "number")] + [DataRow("\"only\" at start", false, null, "partly quoted")] + [DataRow("\"mismatched'", false, null, "wrong pair of quotes")] + [DataRow("\"simple string\"", true, "simple string", "simple quoted with double quotes")] + [DataRow("'simple string'", true, "simple string", "simple quoted with single quotes")] + [DataRow("\"string with \"\"escaped\"\" quotes\"", true, "string with \"escaped\" quotes", "quoted with embedded doubled quotes")] + [DataRow("'string with ''escaped'' quotes'", true, "string with 'escaped' quotes", "quoted with embedded doubled single quotes")] + [DataRow("\"string with 'single' quotes\"", true, "string with 'single' quotes", "quoted with embedded other quote type")] + [DataRow("\"string with ''doubled single'' and \\\"escaped double\\\" quotes\"", true, "string with ''doubled single'' and \\\"escaped double\\\" quotes", "quoted with embedded doubling")] + [DataRow(" \"string with whitespace\" ", true, "string with whitespace", "quoted with whitespace")] + [DataRow("\"\"", true, "", "empty quoted string")] + public void TryQuotedString_Various(string? value, bool expectedSuccess, object? expectedValue, string testDescription) + { + // WHEN + var result = CommonFormatters.TryGetLiteral(value, out var unQuotedValue); + + // THEN + Assert.AreEqual(expectedSuccess, result, $"Failed for: {testDescription}"); + Assert.AreEqual(expectedValue, unQuotedValue, $"Failed for: {testDescription}"); + } private class TestClass { - public string? Author { get; set; } - public string? Title { get; set; } + public string? Author { get; init; } + public string? Title { get; init; } } } \ No newline at end of file diff --git a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs index dbcaa794..0b661dc6 100644 --- a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs +++ b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs @@ -400,6 +400,12 @@ namespace TemplatesTests [DataRow("", "Charles E. Gannon, Emma Gannon")] [DataRow("", "Emma Gannon, Charles E. Gannon")] [DataRow("", "Browne, Gannon, Fetherolf, Montgomery, Van Doren")] + [DataRow("", "2")] + [DataRow("", "Browne, Bon Jovi")] // match correct position of operator + [DataRow(@"", "Browne, Bon Jovi")] // allow quoted quotes + [DataRow("", "")] // strings with numerical operators are substituted by their length + [DataRow("", "6")] + [DataRow("", "Fetherolf, Montgomery")] [DataRow("", "7")] [DataRow("", "7")] [DataRow("", "2")] @@ -873,6 +879,7 @@ namespace TemplatesTests [DataRow("", "03")] [DataRow("", "Tag3")] [DataRow("", "1")] + [DataRow("", "Tag2, Tag3")] [DataRow("", "Tag1")] [DataRow("", "Tag2, Tag3")] [DataRow("", "Tag3, Tag2, Tag1")] diff --git a/docs/features/naming-templates.md b/docs/features/naming-templates.md index 726e0f35..2fdd53ab 100644 --- a/docs/features/naming-templates.md +++ b/docs/features/naming-templates.md @@ -131,19 +131,20 @@ Text formatting can change length and case of the text. Use \<#\>, \<#\>\ ### Text List Formatters -| Formatter | Description | Example Usage | Example Result | -|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------|----------------------------------------------| -| separator() | Specify the text used to join
multiple entries.

Default is ", " | `` | Tag1_Tag2_Tag3_Tag4_Tag5 | -| format(\{S\}) **†** | Formats the entries by placing their values into the specified template.
Use \{S:[Text formatters](#text-formatters)\} to place the entry and optionally apply a format. | ``separator(;)]>` | Tag=tag1;Tag=tag2;Tag=tag3;Tag=tag4;Tag=tag5 | -| unique(FMT) **†** | Reduce list members to a unique set. Entries are compared to each other after applying the given format. Duplicate entries (after format is applied) are removed, keeping the first occurrence. | ``
``separator(;)]>` | Tag1, Tag2, Tag3
tag1 | -| sort(S) | Sorts the elements by their value.

*Sorting direction:*
uppercase = ascending
lowercase = descending

Default is unsorted | ``separator(;)]>` | Tag5;Tag4;Tag3;Tag2;Tag1 | -| max(#) | Only use the first # of entries | `` | Tag1 | -| slice(#) | Only use the nth entry of the list | `` | Tag2 | -| slice(#..) | Only use entries of the list starting from # | `` | Tag2, Tag3, Tag4, Tag5 | -| slice(..#) | Like max(#). Only use the first # of entries | `` | Tag1 | -| slice(#..#) | Only use entries of the list starting from # and ending at # (inclusive) | `` | Tag2, Tag3, Tag4 | -| slice(-#..-#) | Numbers may be specified negative. In that case positions ar counted from the end with -1 pointing at the last member | `` | Tag3, Tag4 | -| count(FMT) **‡** | Instead of returning some or all members of the list, print out the number of entries using the specified [format](#number-formatters). | ``
`` | 5
05 | +| Formatter | Description | Example Usage | Example Result | +|--------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------|----------------------------------------------| +| separator() | Specify the text used to join
multiple entries.

Default is ", " | `` | Tag1_Tag2_Tag3_Tag4_Tag5 | +| format(\{S\}) **†** | Formats the entries by placing their values into the specified template.
Use \{S:[Text formatters](#text-formatters)\} to place the entry and optionally apply a format. | ``separator(;)]>` | Tag=tag1;Tag=tag2;Tag=tag3;Tag=tag4;Tag=tag5 | +| unique(FMT) **†** | Reduce list members to a unique set. Entries are compared to each other after applying the given format. Duplicate entries (after format is applied) are removed, keeping the first occurrence. | ``
``separator(;)]>` | Tag1, Tag2, Tag3
tag1 | +| sort(S) | Sorts the elements by their value.

*Sorting direction:*
uppercase = ascending
lowercase = descending

Default is unsorted | ``separator(;)]>` | Tag5;Tag4;Tag3;Tag2;Tag1 | +| max(#) | Only use the first # of entries | `` | Tag1 | +| slice(#) | Only use the nth entry of the list | `` | Tag2 | +| slice(#..) | Only use entries of the list starting from # | `` | Tag2, Tag3, Tag4, Tag5 | +| slice(..#) | Like max(#). Only use the first # of entries | `` | Tag1 | +| slice(#..#) | Only use entries of the list starting from # and ending at # (inclusive) | `` | Tag2, Tag3, Tag4 | +| slice(-#..-#) | Numbers may be specified negative. In that case positions ar counted from the end with -1 pointing at the last member | `` | Tag3, Tag4 | +| count(FMT) **‡** | Instead of returning some or all members of the list, print out the number of entries using the specified [format](#number-formatters). | ``
`` | 5
05 | +| filter(FMT [CHECK](#checks) VALUE) **†** | Filter list entries based on a condition. Each item is first formatted using the specified text format (or the default format if FMT is omitted), then compared against VALUE using the specified [CHECK](#checks). Only matching entries are included in the output.

**Syntax:** `filter(FORMAT CHECK VALUE)` or `filter(CHECK VALUE)`
- `FORMAT`: Optional text format to apply to each entry (e.g., `{S}`, `{S:L}`, `{S:3}`); defaults to `{S}`
- `CHECK`: Comparison operator (e.g., `=`, `!=`, `~`)
- `VALUE`: The value to compare against | ``
``
`Tag1, Tag2, Tag4, Tag5
TagA, TagB, TagC | **†** For further information on format templates, please refer to the [Format templates](#format-templates) section. @@ -159,15 +160,16 @@ Text formatting can change length and case of the text. Use \<#\>, \<#\>\ ### Series List Formatters -| Formatter | Description | Example Usage | Example Result | -|---------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |-------------------------------------------------------------------------------------------| ------------------------------------------------------------------------------------------------------------------- | -| separator() | Specify the text used to join
multiple series names.

Default is ", " | `` | Sherlock Holmes; Some Other Series | -| format(\{N \| # \| ID\}) **†** | Formats the series properties
using the name series tags.
See [Series Formatter Usage](#series-formatters) above. | ``separator(; )]>`
`` | Sherlock Holmes, 1-6; Book Collection, 1
B08376S3R2-Sherlock Holmes, 01.0-06.0, B000000000-Book Collection, 01.0 | -| unique(FMT) **†** | Reduce list members to a unique set. Entries are compared to each other after applying the given format. Duplicate entries (after format is applied) are removed, keeping the first occurrence. | ``
``separator(; )]>` | Sherlock Holmes; Some Other Series
sherlock holmes; some other series | -| sort(N \| # \| ID) | Sorts the series by name, number or ID.

These terms define the primary, secondary, tertiary, … sorting order.
You may combine multiple terms in sequence to specify multi‑level sorting.

*Sorting direction:*
uppercase = ascending
lowercase = descending

Default is unsorted | ``separator(; )]>` | Book Collection, 1; Sherlock Holmes, 1-6 | -| max(#) | Only use the first # of series | `` | Sherlock Holmes | -| slice(#..#) | Only use entries of the series list starting from # and ending at # (inclusive)

See [Text List Formatter Usage](#Text-List-Formatters) above for details on all the variants of `slice()` | `` | Sherlock Holmes | -| count(FMT) **‡** | Instead of returning some or all members of the list, print out the number of series using the specified [format](#number-formatters). | ``
`` | 2
02 | +| Formatter | Description | Example Usage | Example Result | +|-------------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |-------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------| +| separator() | Specify the text used to join
multiple series names.

Default is ", " | `` | Sherlock Holmes; Some Other Series | +| format(\{N \| # \| ID\}) **†** | Formats the series properties
using the name series tags.
See [Series Formatter Usage](#series-formatters) above. | ``separator(; )]>`
`` | Sherlock Holmes, 1-6; Book Collection, 1
B08376S3R2-Sherlock Holmes, 01.0-06.0, B000000000-Book Collection, 01.0 | +| unique(FMT) **†** | Reduce list members to a unique set. Entries are compared to each other after applying the given format. Duplicate entries (after format is applied) are removed, keeping the first occurrence. | ``
``separator(; )]>` | Sherlock Holmes; Some Other Series
sherlock holmes; some other series | +| sort(N \| # \| ID) | Sorts the series by name, number or ID.

These terms define the primary, secondary, tertiary, … sorting order.
You may combine multiple terms in sequence to specify multi‑level sorting.

*Sorting direction:*
uppercase = ascending
lowercase = descending

Default is unsorted | ``separator(; )]>` | Book Collection, 1; Sherlock Holmes, 1-6 | +| max(#) | Only use the first # of series | `` | Sherlock Holmes | +| slice(#..#) | Only use entries of the series list starting from # and ending at # (inclusive)

See [Text List Formatter Usage](#Text-List-Formatters) above for details on all the variants of `slice()` | `` | Sherlock Holmes | +| count(FMT) **‡** | Instead of returning some or all members of the list, print out the number of series using the specified [format](#number-formatters). | ``
`` | 2
02 | +| filter(FMT [CHECK](#checks) VALUE) **†** | Filter list entries based on a condition. Each series is first formatted using the specified [Series Format](#series-formatters) (or the default format if FMT is omitted), then compared against VALUE using the specified [CHECK](#checks). Only matching entries are included in the output.

**Syntax:** `filter(FORMAT CHECK VALUE)` or `filter(CHECK VALUE)`
- `FORMAT`: Optional series format to apply to each entry (e.g., `{N}`, `{N:L}`, `{#}`); defaults to `{N}`
- `CHECK`: Comparison operator (e.g., `=`, `!=`, `~`)
- `VALUE`: The value to compare against | ``
`Sherlock Holmes | **†** For further information on format templates, please refer to the [Format templates](#format-templates) section. @@ -183,15 +185,16 @@ Text formatting can change length and case of the text. Use \<#\>, \<#\>\ ### Name List Formatters -| Formatter | Description | Example Usage | Example Result | -|------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| -| separator() | Specify the text used to join
multiple people's names.

Default is ", " | `` | Arthur Conan Doyle; Stephen Fry | -| format(\{T \| F \| M \| L \| S \| ID\}) **†** | Formats the human name using
the name part tags.
See [Name Formatter Usage](#name-formatters) above. | ``separator(; )]>`
``_{ID}_) separator(; )]>` | DOYLE, Arthur; FRY, Stephen
Doyle, A. \_B000AQ43GQ\_;
Fry, S. \_B000APAGVS\_ | -| unique(FMT) **†** | Reduce list members to a unique set. Entries are compared to each other after applying the given format. Duplicate entries (after format is applied) are removed, keeping the first occurrence. | ``
``separator(; )]>` | Arthur Conan Doyle, Stephen Fry
doyle; fry | -| sort(T \| F \| M \| L \| S \| ID) | Sorts the names by title,
first, middle, or last name,
suffix or Audible Contributor ID

These terms define the primary, secondary, tertiary, … sorting order.
You may combine multiple terms in sequence to specify multi‑level sorting.

*Sorting direction:*
uppercase = ascending
lowercase = descending

Default is unsorted | ``
``
`` | Stephen Fry, Arthur Conan Doyle
Stephen King, Stephen Fry
John P. Smith \_B000TTTBBB\_, John P. Smith \_B000TTTCCC\_, John S. Smith \_B000HHHVVV\_ | -| max(#) | Only use the first # of names

Default is all names | `` | Arthur Conan Doyle | -| slice(#..#) | Only use entries of the names list starting from # and ending at # (inclusive)

See [Text List Formatter Usage](#Text-List-Formatters) above for details on all the variants of `slice()` | `` | Arthur Conan Doyle | -| count(FMT) **‡** | Instead of returning some or all members of the list, print out the number of names using the specified [format](#number-formatters). | ``
`` | 2
02 | +| Formatter | Description | Example Usage | Example Result | +|-----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| +| separator() | Specify the text used to join
multiple people's names.

Default is ", " | `` | Arthur Conan Doyle; Stephen Fry | +| format(\{T \| F \| M \| L \| S \| ID\}) **†** | Formats the human name using
the name part tags.
See [Name Formatter Usage](#name-formatters) above. | ``separator(; )]>`
``_{ID}_) separator(; )]>` | DOYLE, Arthur; FRY, Stephen
Doyle, A. \_B000AQ43GQ\_;
Fry, S. \_B000APAGVS\_ | +| unique(FMT) **†** | Reduce list members to a unique set. Entries are compared to each other after applying the given format. Duplicate entries (after format is applied) are removed, keeping the first occurrence. | ``
``separator(; )]>` | Arthur Conan Doyle, Stephen Fry
doyle; fry | +| sort(T \| F \| M \| L \| S \| ID) | Sorts the names by title,
first, middle, or last name,
suffix or Audible Contributor ID

These terms define the primary, secondary, tertiary, … sorting order.
You may combine multiple terms in sequence to specify multi‑level sorting.

*Sorting direction:*
uppercase = ascending
lowercase = descending

Default is unsorted | ``
``
`` | Stephen Fry, Arthur Conan Doyle
Stephen King, Stephen Fry
John P. Smith \_B000TTTBBB\_, John P. Smith \_B000TTTCCC\_, John S. Smith \_B000HHHVVV\_ | +| max(#) | Only use the first # of names

Default is all names | `` | Arthur Conan Doyle | +| slice(#..#) | Only use entries of the names list starting from # and ending at # (inclusive)

See [Text List Formatter Usage](#Text-List-Formatters) above for details on all the variants of `slice()` | `` | Arthur Conan Doyle | +| count(FMT) **‡** | Instead of returning some or all members of the list, print out the number of names using the specified [format](#number-formatters). | ``
`` | 2
02 | +| filter(FMT [CHECK](#checks) VALUE) **†** | Filter list entries based on a condition. Each person is first formatted using the specified name format (or the default format if FMT is omitted), then compared against VALUE using the specified [CHECK](#checks). Only matching entries are included in the output.

**Syntax:** `filter(FORMAT CHECK VALUE)` or `filter(CHECK VALUE)`
- `FORMAT`: Optional name format to apply to each entry (e.g., `{L}`, `{M}`, `{L}, {F}`); defaults to `{T} {F} {M} {L} {S}`
- `CHECK`: Comparison operator (e.g., `=`, `!=`, `~`)
- `VALUE`: The value to compare against | ``
``
`Stephen Fry
Stephen Fry | **†** For further information on format templates, please refer to the [Format templates](#format-templates) section. @@ -314,9 +317,9 @@ string literal `O'Reilly`, you can use either `'O''Reilly'` or `"O'Reilly"`. | String Checks | Unicode Operator | Description | Examples | |---------------|------------------|--------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------| -| = | | Matches if values are equal (case-insensitive) | \ | -| != | | Matches if values are not equal (case-insensitive) | \
\ | -| ~ | | Matches if the first parameter matches the regular expression specified by the second parameter (case-insensitive) | \ | +| = | | Matches if values are equal (case-insensitive) | \
\ | +| != | | Matches if values are not equal (case-insensitive) | \
\
\ | +| ~ | | Matches if the first parameter matches the regular expression specified by the second parameter (case-insensitive) | \
\
\ | | Number Checks | Unicode Operator | Description | Examples | |---------------|------------------|----------------------------------------------------------------|-----------------------------------------------------------| From c2c7b04acd42029c75ada0b43db12c6aca75b324 Mon Sep 17 00:00:00 2001 From: Jo-Be-Co Date: Thu, 14 May 2026 02:12:25 +0200 Subject: [PATCH 3/4] minor fixes and comment corrections --- .../NamingTemplate/CompareCondition.cs | 2 +- .../ConditionalTagCollection[TClass].cs | 9 ++++----- .../Templates/Templates.cs | 6 +++--- docs/features/naming-templates.md | 20 +++++++++---------- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Source/FileManager/NamingTemplate/CompareCondition.cs b/Source/FileManager/NamingTemplate/CompareCondition.cs index 306b1d47..648c9655 100644 --- a/Source/FileManager/NamingTemplate/CompareCondition.cs +++ b/Source/FileManager/NamingTemplate/CompareCondition.cs @@ -264,7 +264,7 @@ public static partial class CompareCondition [GeneratedRegex(""" (?x) # option x: ignore all unescaped whitespace in pattern and allow comments starting with # - ^(?>(?(? # anchor at start of line. Capture operator in , and with every char escapable + ^(?>(?(? # anchor at start of line. Capture operator in , and ≡ | == | :equals: # - list operators: ≡ for checking if two lists contain the same items regardless of order | ∌ | !>> | ∌ | :not_contains: # - list operators: ∌ for checking if the first list does not contain any item of the second list | ∋ | >> | :contains: # - list operators: ∋ for checking if the first list contains all items of the second list diff --git a/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs index d4731613..33a2afec 100644 --- a/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using System.Linq.Expressions; using System.Text.RegularExpressions; @@ -28,7 +27,7 @@ public delegate object? ValueProvider(ITemplateTag templateTag, T value, s public delegate bool ConditionEvaluator(object? value1, object? value2, CultureInfo? culture); -public partial class ConditionalTagCollection(bool caseSensitive = true) : TagCollection(typeof(TClass), caseSensitive) +public class ConditionalTagCollection(bool caseSensitive = true) : TagCollection(typeof(TClass), caseSensitive) { /// /// Register a conditional tag. @@ -74,7 +73,7 @@ public partial class ConditionalTagCollection(bool caseSensitive = true) AddPropertyTag(new ConditionalTag(templateTag, Options, Parameter, valueProvider1, valueProvider2)); } - private partial class ConditionalTag : TagBase, IClosingPropertyTag + private class ConditionalTag : TagBase, IClosingPropertyTag { public override Regex NameMatcher { get; } public Regex NameCloseMatcher { get; } @@ -162,8 +161,8 @@ public partial class ConditionalTagCollection(bool caseSensitive = true) | . )+? # - match any character to form the property name. Capture non greedy so it won't match the operator part. (? end with a whitepace. Otherwise "" would be matchable. \s+ # Separate the following operand with whitespace - (? # capture operator in and with every char escapable - [\#!≡=≠~<>≤≥&∉∌∈∌⋂⊆⊇⊂⊃-]+ # allow a wide range of operators, all non alphanumeric + (? # capture operator in + [\#!≡=≠~<>≤≥&∉∌∈∌⋂⊆⊇⊂⊃-]+ # allow a wide range of operators, all non alphanumeric so that no operator is confused as property | :[a-z_]+: # allow :named: operators for readability, e.g. :contains: ) \s+ # ignore space between operator and second property (?.+? # - capture the non greedy so it won't end on whitespace diff --git a/Source/LibationFileManager/Templates/Templates.cs b/Source/LibationFileManager/Templates/Templates.cs index bcbc0aff..d6c57085 100644 --- a/Source/LibationFileManager/Templates/Templates.cs +++ b/Source/LibationFileManager/Templates/Templates.cs @@ -379,12 +379,12 @@ public abstract class Templates private static bool HasValue(object? value, object? _, CultureInfo? culture) { - bool CheckItem(object o, CultureInfo? _) => !string.IsNullOrWhiteSpace(o.ToString()); + bool CheckItem(object o) => !string.IsNullOrWhiteSpace(o.ToString()); return value switch { null => false, - IEnumerable e => e.Any(o => CheckItem(o, culture)), - _ => CheckItem(value, culture) + IEnumerable e => e.Any(CheckItem), + _ => CheckItem(value) }; } diff --git a/docs/features/naming-templates.md b/docs/features/naming-templates.md index 2fdd53ab..9bab038c 100644 --- a/docs/features/naming-templates.md +++ b/docs/features/naming-templates.md @@ -76,7 +76,7 @@ Anything between the opening tag (``) and closing tag (`<-tagname>`) w | \...\<-is\> | Only include if the PROPERTY or a single value of a list PROPERTY satisfies the CHECK | Conditional | | \...\<-is\> | Only include if the formatted PROPERTY or a single value of a list PROPERTY satisfies the CHECK | Conditional | | \...\<-is\> | Only include if the joined form of all formatted values of a list PROPERTY satisfies the CHECK | Conditional | -| \...\<-cmp\> | Only include if two given PROPERTIES satisfy the CHECK | Conditional | +| \...\<-cmp\> | Only include if two given PROPERTIES satisfy the CHECK | Conditional | **†** Only affects the podcast series folder naming if "Save all podcast episodes to the series parent folder" option is checked. @@ -84,15 +84,15 @@ For example, `<-if podcast>` will evaluate to the podcast's You can invert the condition (instead of displaying the text when the condition is true, display the text when it is false) by playing a `!` symbol before the opening tag name. -| Inverted Tag | Description | Type | -|--------------------------------------------------------|--------------------------------------------------------------------------------------------------| ----------- | -| \...\<-if series\> | Only include if _not_ part of a book series or podcast | Conditional | -| \...\<-if podcast\> | Only include if _not_ part of a podcast | Conditional | -| \...\<-if bookseries\> | Only include if _not_ part of a book series | Conditional | -| \...\<-if podcastparent\> **†** | Only include if item is _not_ a podcast series parent | Conditional | -| \...\<-has\> | Only include if the PROPERTY _does not_ have a value (i.e. is null or empty) | Conditional | -| \...\<-is\> | Only include if neither the whole PROPERTY nor the values of a list PROPERTY satisfies the CHECK | Conditional | -| \...\<-cmp\> | Only include if two given PROPERTIES _do not_ satisfy the CHECK | Conditional | +| Inverted Tag | Description | Type | +|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------| ----------- | +| \...\<-if series\> | Only include if _not_ part of a book series or podcast | Conditional | +| \...\<-if podcast\> | Only include if _not_ part of a podcast | Conditional | +| \...\<-if bookseries\> | Only include if _not_ part of a book series | Conditional | +| \...\<-if podcastparent\> **†** | Only include if item is _not_ a podcast series parent | Conditional | +| \...\<-has\> | Only include if the PROPERTY _does not_ have a value (i.e. is null or empty) | Conditional | +| \...\<-is\> | Only include if neither the whole PROPERTY nor the values of a list PROPERTY satisfies the CHECK | Conditional | +| \...\<-cmp\> | Only include if two given PROPERTIES _do not_ satisfy the CHECK | Conditional | **†** Only affects the podcast series folder naming if "Save all podcast episodes to the series parent folder" option is checked. From 4be0361e42610bb5429d775dfe59c71b0706bdb1 Mon Sep 17 00:00:00 2001 From: Jo-Be-Co Date: Thu, 14 May 2026 16:51:51 +0200 Subject: [PATCH 4/4] fixing typos and naming of test method --- .../Templates/IListFormat[TList].cs | 2 +- .../CommonFormattersTests.cs | 4 +- .../ConditionalTagCollectionTests.cs | 2 +- docs/features/naming-templates.md | 48 +++++++++---------- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Source/LibationFileManager/Templates/IListFormat[TList].cs b/Source/LibationFileManager/Templates/IListFormat[TList].cs index 3aef7d6e..933cb3bc 100644 --- a/Source/LibationFileManager/Templates/IListFormat[TList].cs +++ b/Source/LibationFileManager/Templates/IListFormat[TList].cs @@ -170,4 +170,4 @@ internal partial interface IListFormat where TList : IListFormat \s* \) # end the filter details with optional whitespace and a closing bracket """)] private static partial Regex FilterRegex(); -} \ No newline at end of file +} diff --git a/Source/_Tests/FileManager.Tests/CommonFormattersTests.cs b/Source/_Tests/FileManager.Tests/CommonFormattersTests.cs index fb559da6..7ae6269f 100644 --- a/Source/_Tests/FileManager.Tests/CommonFormattersTests.cs +++ b/Source/_Tests/FileManager.Tests/CommonFormattersTests.cs @@ -335,7 +335,7 @@ public class CommonFormattersTests [DataRow("\"string with ''doubled single'' and \\\"escaped double\\\" quotes\"", true, "string with ''doubled single'' and \\\"escaped double\\\" quotes", "quoted with embedded doubling")] [DataRow(" \"string with whitespace\" ", true, "string with whitespace", "quoted with whitespace")] [DataRow("\"\"", true, "", "empty quoted string")] - public void TryQuotedString_Various(string? value, bool expectedSuccess, object? expectedValue, string testDescription) + public void TryGetLiteral_Various(string? value, bool expectedSuccess, object? expectedValue, string testDescription) { // WHEN var result = CommonFormatters.TryGetLiteral(value, out var unQuotedValue); @@ -350,4 +350,4 @@ public class CommonFormattersTests public string? Author { get; init; } public string? Title { get; init; } } -} \ No newline at end of file +} diff --git a/Source/_Tests/FileManager.Tests/ConditionalTagCollectionTests.cs b/Source/_Tests/FileManager.Tests/ConditionalTagCollectionTests.cs index 2ede9eea..8c8e2dd5 100644 --- a/Source/_Tests/FileManager.Tests/ConditionalTagCollectionTests.cs +++ b/Source/_Tests/FileManager.Tests/ConditionalTagCollectionTests.cs @@ -126,4 +126,4 @@ public class ConditionalTagCollectionTests // Assert: Should parse successfully without exceptions Assert.IsNotNull(namingTemplate); } -} \ No newline at end of file +} diff --git a/docs/features/naming-templates.md b/docs/features/naming-templates.md index 9bab038c..cec86d0e 100644 --- a/docs/features/naming-templates.md +++ b/docs/features/naming-templates.md @@ -131,20 +131,20 @@ Text formatting can change length and case of the text. Use \<#\>, \<#\>\ ### Text List Formatters -| Formatter | Description | Example Usage | Example Result | -|--------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------|----------------------------------------------| -| separator() | Specify the text used to join
multiple entries.

Default is ", " | `` | Tag1_Tag2_Tag3_Tag4_Tag5 | -| format(\{S\}) **†** | Formats the entries by placing their values into the specified template.
Use \{S:[Text formatters](#text-formatters)\} to place the entry and optionally apply a format. | ``separator(;)]>` | Tag=tag1;Tag=tag2;Tag=tag3;Tag=tag4;Tag=tag5 | -| unique(FMT) **†** | Reduce list members to a unique set. Entries are compared to each other after applying the given format. Duplicate entries (after format is applied) are removed, keeping the first occurrence. | ``
``separator(;)]>` | Tag1, Tag2, Tag3
tag1 | -| sort(S) | Sorts the elements by their value.

*Sorting direction:*
uppercase = ascending
lowercase = descending

Default is unsorted | ``separator(;)]>` | Tag5;Tag4;Tag3;Tag2;Tag1 | -| max(#) | Only use the first # of entries | `` | Tag1 | -| slice(#) | Only use the nth entry of the list | `` | Tag2 | -| slice(#..) | Only use entries of the list starting from # | `` | Tag2, Tag3, Tag4, Tag5 | -| slice(..#) | Like max(#). Only use the first # of entries | `` | Tag1 | -| slice(#..#) | Only use entries of the list starting from # and ending at # (inclusive) | `` | Tag2, Tag3, Tag4 | -| slice(-#..-#) | Numbers may be specified negative. In that case positions ar counted from the end with -1 pointing at the last member | `` | Tag3, Tag4 | -| count(FMT) **‡** | Instead of returning some or all members of the list, print out the number of entries using the specified [format](#number-formatters). | ``
`` | 5
05 | -| filter(FMT [CHECK](#checks) VALUE) **†** | Filter list entries based on a condition. Each item is first formatted using the specified text format (or the default format if FMT is omitted), then compared against VALUE using the specified [CHECK](#checks). Only matching entries are included in the output.

**Syntax:** `filter(FORMAT CHECK VALUE)` or `filter(CHECK VALUE)`
- `FORMAT`: Optional text format to apply to each entry (e.g., `{S}`, `{S:L}`, `{S:3}`); defaults to `{S}`
- `CHECK`: Comparison operator (e.g., `=`, `!=`, `~`)
- `VALUE`: The value to compare against | ``
``
`Tag1, Tag2, Tag4, Tag5
TagA, TagB, TagC | +| Formatter | Description | Example Usage | Example Result | +|--------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|--------------------------------------------------| +| separator() | Specify the text used to join
multiple entries.

Default is ", " | `` | Tag1_Tag2_Tag3_Tag4_Tag5 | +| format(\{S\}) **†** | Formats the entries by placing their values into the specified template.
Use \{S:[Text formatters](#text-formatters)\} to place the entry and optionally apply a format. | ``separator(;)]>` | Tag=tag1;Tag=tag2;Tag=tag3;Tag=tag4;Tag=tag5 | +| unique(FMT) **†** | Reduce list members to a unique set. Entries are compared to each other after applying the given format. Duplicate entries (after format is applied) are removed, keeping the first occurrence. | ``
``separator(;)]>` | Tag1, Tag2, Tag3
tag1 | +| sort(S) | Sorts the elements by their value.

*Sorting direction:*
uppercase = ascending
lowercase = descending

Default is unsorted | ``separator(;)]>` | Tag5;Tag4;Tag3;Tag2;Tag1 | +| max(#) | Only use the first # of entries | `` | Tag1 | +| slice(#) | Only use the nth entry of the list | `` | Tag2 | +| slice(#..) | Only use entries of the list starting from # | `` | Tag2, Tag3, Tag4, Tag5 | +| slice(..#) | Like max(#). Only use the first # of entries | `` | Tag1 | +| slice(#..#) | Only use entries of the list starting from # and ending at # (inclusive) | `` | Tag2, Tag3, Tag4 | +| slice(-#..-#) | Numbers may be specified negative. In that case positions ar counted from the end with -1 pointing at the last member | `` | Tag3, Tag4 | +| count(FMT) **‡** | Instead of returning some or all members of the list, print out the number of entries using the specified [format](#number-formatters). | ``
`` | 5
05 | +| filter(FMT [CHECK](#checks) VALUE) **†** | Filter list entries based on a condition. Each item is first formatted using the specified text format (or the default format if FMT is omitted), then compared against VALUE using the specified [CHECK](#checks). Only matching entries are included in the output.

**Syntax:** `filter(FORMAT CHECK VALUE)` or `filter(CHECK VALUE)`
- `FORMAT`: Optional text format to apply to each entry (e.g., `{S}`, `{S:L}`, `{S:3}`); defaults to `{S}`
- `CHECK`: Comparison operator (e.g., `=`, `!=`, `~`)
- `VALUE`: The value to compare against | ``
`` | Tag3, Tag4, Tag5
Tag1, Tag2, Tag3, Tag4, Tag5 | **†** For further information on format templates, please refer to the [Format templates](#format-templates) section. @@ -160,16 +160,16 @@ Text formatting can change length and case of the text. Use \<#\>, \<#\>\ ### Series List Formatters -| Formatter | Description | Example Usage | Example Result | -|-------------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |-------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------| -| separator() | Specify the text used to join
multiple series names.

Default is ", " | `` | Sherlock Holmes; Some Other Series | +| Formatter | Description | Example Usage | Example Result | +|-------------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------| +| separator() | Specify the text used to join
multiple series names.

Default is ", " | `` | Sherlock Holmes; Some Other Series | | format(\{N \| # \| ID\}) **†** | Formats the series properties
using the name series tags.
See [Series Formatter Usage](#series-formatters) above. | ``separator(; )]>`
`` | Sherlock Holmes, 1-6; Book Collection, 1
B08376S3R2-Sherlock Holmes, 01.0-06.0, B000000000-Book Collection, 01.0 | -| unique(FMT) **†** | Reduce list members to a unique set. Entries are compared to each other after applying the given format. Duplicate entries (after format is applied) are removed, keeping the first occurrence. | ``
``separator(; )]>` | Sherlock Holmes; Some Other Series
sherlock holmes; some other series | -| sort(N \| # \| ID) | Sorts the series by name, number or ID.

These terms define the primary, secondary, tertiary, … sorting order.
You may combine multiple terms in sequence to specify multi‑level sorting.

*Sorting direction:*
uppercase = ascending
lowercase = descending

Default is unsorted | ``separator(; )]>` | Book Collection, 1; Sherlock Holmes, 1-6 | -| max(#) | Only use the first # of series | `` | Sherlock Holmes | -| slice(#..#) | Only use entries of the series list starting from # and ending at # (inclusive)

See [Text List Formatter Usage](#Text-List-Formatters) above for details on all the variants of `slice()` | `` | Sherlock Holmes | -| count(FMT) **‡** | Instead of returning some or all members of the list, print out the number of series using the specified [format](#number-formatters). | ``
`` | 2
02 | -| filter(FMT [CHECK](#checks) VALUE) **†** | Filter list entries based on a condition. Each series is first formatted using the specified [Series Format](#series-formatters) (or the default format if FMT is omitted), then compared against VALUE using the specified [CHECK](#checks). Only matching entries are included in the output.

**Syntax:** `filter(FORMAT CHECK VALUE)` or `filter(CHECK VALUE)`
- `FORMAT`: Optional series format to apply to each entry (e.g., `{N}`, `{N:L}`, `{#}`); defaults to `{N}`
- `CHECK`: Comparison operator (e.g., `=`, `!=`, `~`)
- `VALUE`: The value to compare against | ``
`Sherlock Holmes | +| unique(FMT) **†** | Reduce list members to a unique set. Entries are compared to each other after applying the given format. Duplicate entries (after format is applied) are removed, keeping the first occurrence. | ``
``separator(; )]>` | Sherlock Holmes; Some Other Series
sherlock holmes; some other series | +| sort(N \| # \| ID) | Sorts the series by name, number or ID.

These terms define the primary, secondary, tertiary, … sorting order.
You may combine multiple terms in sequence to specify multi‑level sorting.

*Sorting direction:*
uppercase = ascending
lowercase = descending

Default is unsorted | ``separator(; )]>` | Book Collection, 1; Sherlock Holmes, 1-6 | +| max(#) | Only use the first # of series | `` | Sherlock Holmes | +| slice(#..#) | Only use entries of the series list starting from # and ending at # (inclusive)

See [Text List Formatter Usage](#Text-List-Formatters) above for details on all the variants of `slice()` | `` | Sherlock Holmes | +| count(FMT) **‡** | Instead of returning some or all members of the list, print out the number of series using the specified [format](#number-formatters). | ``
`` | 2
02 | +| filter(FMT [CHECK](#checks) VALUE) **†** | Filter list entries based on a condition. Each series is first formatted using the specified [Series Format](#series-formatters) (or the default format if FMT is omitted), then compared against VALUE using the specified [CHECK](#checks). Only matching entries are included in the output.

**Syntax:** `filter(FORMAT CHECK VALUE)` or `filter(CHECK VALUE)`
- `FORMAT`: Optional series format to apply to each entry (e.g., `{N}`, `{N:L}`, `{#}`); defaults to `{N}`
- `CHECK`: Comparison operator (e.g., `=`, `!=`, `~`)
- `VALUE`: The value to compare against | ``
`` | Sherlock Holmes
Sherlock Holmes | **†** For further information on format templates, please refer to the [Format templates](#format-templates) section. @@ -194,7 +194,7 @@ Text formatting can change length and case of the text. Use \<#\>, \<#\>\ | max(#) | Only use the first # of names

Default is all names | `` | Arthur Conan Doyle | | slice(#..#) | Only use entries of the names list starting from # and ending at # (inclusive)

See [Text List Formatter Usage](#Text-List-Formatters) above for details on all the variants of `slice()` | `` | Arthur Conan Doyle | | count(FMT) **‡** | Instead of returning some or all members of the list, print out the number of names using the specified [format](#number-formatters). | ``
`` | 2
02 | -| filter(FMT [CHECK](#checks) VALUE) **†** | Filter list entries based on a condition. Each person is first formatted using the specified name format (or the default format if FMT is omitted), then compared against VALUE using the specified [CHECK](#checks). Only matching entries are included in the output.

**Syntax:** `filter(FORMAT CHECK VALUE)` or `filter(CHECK VALUE)`
- `FORMAT`: Optional name format to apply to each entry (e.g., `{L}`, `{M}`, `{L}, {F}`); defaults to `{T} {F} {M} {L} {S}`
- `CHECK`: Comparison operator (e.g., `=`, `!=`, `~`)
- `VALUE`: The value to compare against | ``
``
`Stephen Fry
Stephen Fry | +| filter(FMT [CHECK](#checks) VALUE) **†** | Filter list entries based on a condition. Each person is first formatted using the specified name format (or the default format if FMT is omitted), then compared against VALUE using the specified [CHECK](#checks). Only matching entries are included in the output.

**Syntax:** `filter(FORMAT CHECK VALUE)` or `filter(CHECK VALUE)`
- `FORMAT`: Optional name format to apply to each entry (e.g., `{L}`, `{M}`, `{L}, {F}`); defaults to `{T} {F} {M} {L} {S}`
- `CHECK`: Comparison operator (e.g., `=`, `!=`, `~`)
- `VALUE`: The value to compare against | ``
``
`` | Conan Doyle
Stephen Fry
Stephen Fry | **†** For further information on format templates, please refer to the [Format templates](#format-templates) section.