mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-05-24 16:37:19 -04:00
Merge pull request #1803 from Jo-Be-Co/1762_filter
1762 add filter() to list properties
This commit is contained in:
@@ -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*(?<quote>['"])(?<value>(?:(?<double>\k<quote>{2})|.)*)\k<quote>\s*$""")]
|
||||
private static partial Regex StringValueRegex();
|
||||
|
||||
public static string Unescape(string valueSpan)
|
||||
{
|
||||
return Unescape(valueSpan, ['\'', '"']);
|
||||
|
||||
@@ -264,7 +264,7 @@ public static partial class CompareCondition
|
||||
|
||||
[GeneratedRegex("""
|
||||
(?x) # option x: ignore all unescaped whitespace in pattern and allow comments starting with #
|
||||
^(?>(?<op>(?<list_op> # anchor at start of line. Capture operator in <op>, <list_op> and <num_op> with every char escapable
|
||||
^(?>(?<op>(?<list_op> # anchor at start of line. Capture operator in <op>, <list_op> and <num_op>
|
||||
≡ | == | :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
|
||||
|
||||
@@ -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<in T>(ITemplateTag templateTag, T value, s
|
||||
|
||||
public delegate bool ConditionEvaluator(object? value1, object? value2, CultureInfo? culture);
|
||||
|
||||
public partial class ConditionalTagCollection<TClass>(bool caseSensitive = true) : TagCollection(typeof(TClass), caseSensitive)
|
||||
public class ConditionalTagCollection<TClass>(bool caseSensitive = true) : TagCollection(typeof(TClass), caseSensitive)
|
||||
{
|
||||
/// <summary>
|
||||
/// Register a conditional tag.
|
||||
@@ -74,7 +73,7 @@ public partial class ConditionalTagCollection<TClass>(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<TClass>(bool caseSensitive = true)
|
||||
| . )+? # - match any character to form the property name. Capture non greedy so it won't match the operator part.
|
||||
(?<!\s)) # - don't let <property> end with a whitepace. Otherwise "<tagname = tag2->" would be matchable.
|
||||
\s+ # Separate the following operand with whitespace
|
||||
(?<check_or_op> # capture operator in <op> and <num_op> with every char escapable
|
||||
[\#!≡=≠~<>≤≥&∉∌∈∌⋂⊆⊇⊂⊃-]+ # allow a wide range of operators, all non alphanumeric
|
||||
(?<check_or_op> # capture operator in <check_or_op>
|
||||
[\#!≡=≠~<>≤≥&∉∌∈∌⋂⊆⊇⊂⊃-]+ # 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
|
||||
(?<second_property>.+? # - capture the <second_property> non greedy so it won't end on whitespace
|
||||
|
||||
@@ -11,13 +11,31 @@ internal partial interface IListFormat<TList> where TList : IListFormat<TList>
|
||||
{
|
||||
static IEnumerable<T> FilteredList<T>(string formatString, IEnumerable<T> 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<T> Filter(string formatString, IEnumerable<T> 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<T> Unique(string formatString, IEnumerable<T> items, CultureInfo? culture)
|
||||
{
|
||||
return UniqueRegex().TryMatch(formatString, out var uniqueMatch)
|
||||
@@ -120,11 +138,36 @@ internal partial interface IListFormat<TList> where TList : IListFormat<TList>
|
||||
[GeneratedRegex("""[Ss]eparator\((?<separator>(?:\\.|'[^']*'|"[^"]*"|[^\\'"])*?)\)""")]
|
||||
private static partial Regex SeparatorRegex();
|
||||
|
||||
/// <summary> Count will substitute all list members with a single number equal to there count </summary>
|
||||
/// <summary> Count will substitute all list members with a single number equal to their count </summary>
|
||||
[GeneratedRegex("""[Cc]ount\((?<format>(?:\\.|'[^']*'|"[^"]*"|[^\\'"])*?)\)""")]
|
||||
private static partial Regex CountRegex();
|
||||
|
||||
/// <summary> Unique will shrink the list to unique members after applying format to them </summary>
|
||||
[GeneratedRegex("""[Uu]nique\((?<format>(?:\\.|'[^']*'|"[^"]*"|[^\\'"])*?)\)""")]
|
||||
private static partial Regex UniqueRegex();
|
||||
|
||||
/// <summary> The filter will reduce the list, keeping only the items that match the specified criteria. </summary>
|
||||
[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
|
||||
(?<format>(?: # the first part captured as <format> 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
|
||||
(?<op> # capture operator in <op>
|
||||
[\#!≡=≠~<>≤≥&∉∌∈∌⋂⊆⊇⊂⊃-]+ # 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
|
||||
(?<value> # the second operand is captured as <value> 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();
|
||||
}
|
||||
|
||||
@@ -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<TagCollection> 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: <ch#> or <ch# 0>";
|
||||
@@ -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,17 +377,14 @@ public abstract partial class Templates
|
||||
return null;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"^\s*(?<quote>['""])(?<value>(?:(?<double>\k<quote>{2})|.)*)\k<quote>\s*$")]
|
||||
private static partial Regex StringValueRegex();
|
||||
|
||||
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<object> e => e.Any(o => CheckItem(o, culture)),
|
||||
_ => CheckItem(value, culture)
|
||||
IEnumerable<object> e => e.Any(CheckItem),
|
||||
_ => CheckItem(value)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 TryGetLiteral_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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,4 +126,4 @@ public class ConditionalTagCollectionTests
|
||||
// Assert: Should parse successfully without exceptions
|
||||
Assert.IsNotNull(namingTemplate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,6 +400,12 @@ namespace TemplatesTests
|
||||
[DataRow("<author[sort(LF) slice(4..5)]>", "Charles E. Gannon, Emma Gannon")]
|
||||
[DataRow("<author[sort(Lf) slice(4..5)]>", "Emma Gannon, Charles E. Gannon")]
|
||||
[DataRow("<author[unique({L:1}) format({L})]>", "Browne, Gannon, Fetherolf, Montgomery, Van Doren")]
|
||||
[DataRow("<author[filter({L:1} = 'B') count()]>", "2")]
|
||||
[DataRow("<author[filter({F:1}~{L}~'J~B') format({L})]>", "Browne, Bon Jovi")] // match correct position of operator
|
||||
[DataRow(@"<author[filter({F:1}\'{L:1} = 'J''B') format({L})]>", "Browne, Bon Jovi")] // allow quoted quotes
|
||||
[DataRow("<author[filter(<'99') count()]>", "")] // strings with numerical operators are substituted by their length
|
||||
[DataRow("<author[filter(<26) count()]>", "6")]
|
||||
[DataRow("<author[filter({L:1} != 'B') format({L}) slice(2..3)]>", "Fetherolf, Montgomery")]
|
||||
[DataRow("<author[count()]>", "7")]
|
||||
[DataRow("<author[max(42) count()]>", "7")]
|
||||
[DataRow("<author[max(2) count()]>", "2")]
|
||||
@@ -873,6 +879,7 @@ namespace TemplatesTests
|
||||
[DataRow("<tag[count(00)]>", "03")]
|
||||
[DataRow("<tag[unique({S:3}) sort(s)]>", "Tag3")]
|
||||
[DataRow("<tag[unique({S:3}) count()]>", "1")]
|
||||
[DataRow("<tag[filter(~ '[2-5]')]>", "Tag2, Tag3")]
|
||||
[DataRow("<tag [max(1)]>", "Tag1")]
|
||||
[DataRow("<tag [slice(2..)]>", "Tag2, Tag3")]
|
||||
[DataRow("<tag[sort(s)]>", "Tag3, Tag2, Tag1")]
|
||||
|
||||
@@ -76,7 +76,7 @@ Anything between the opening tag (`<tagname->`) and closing tag (`<-tagname>`) w
|
||||
| \<is PROPERTY[[CHECK](#checks)]-\>...\<-is\> | Only include if the PROPERTY or a single value of a list PROPERTY satisfies the CHECK | Conditional |
|
||||
| \<is PROPERTY[FORMAT][[CHECK](#checks)]-\>...\<-is\> | Only include if the formatted PROPERTY or a single value of a list PROPERTY satisfies the CHECK | Conditional |
|
||||
| \<is PROPERTY[...separator(...)...][[CHECK](#checks)]-\>...\<-is\> | Only include if the joined form of all formatted values of a list PROPERTY satisfies the CHECK | Conditional |
|
||||
| \<cmp 1st-PROPERTY [[CHECK](#checks)] 2nd-PROPERTY-\>...\<-cmp\> | Only include if two given PROPERTIES satisfy the CHECK | Conditional |
|
||||
| \<cmp 1st-PROPERTY [CHECK](#checks) 2nd-PROPERTY-\>...\<-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-><series><-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-\>...\<-if series\> | Only include if _not_ part of a book series or podcast | Conditional |
|
||||
| \<!if podcast-\>...\<-if podcast\> | Only include if _not_ part of a podcast | Conditional |
|
||||
| \<!if bookseries-\>...\<-if bookseries\> | Only include if _not_ part of a book series | Conditional |
|
||||
| \<!if podcastparent-\>...\<-if podcastparent\> **†** | Only include if item is _not_ a podcast series parent | Conditional |
|
||||
| \<!has PROPERTY-\>...\<-has\> | Only include if the PROPERTY _does not_ have a value (i.e. is null or empty) | Conditional |
|
||||
| \<!is PROPERTY[[CHECK](#checks)]-\>...\<-is\> | Only include if neither the whole PROPERTY nor the values of a list PROPERTY satisfies the CHECK | Conditional |
|
||||
| \<!cmp 1st-PROPERTY [CHECK] 2nd-PROPERTY-\>...\<-cmp\> | Only include if two given PROPERTIES _do not_ satisfy the CHECK | Conditional |
|
||||
| Inverted Tag | Description | Type |
|
||||
|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------| ----------- |
|
||||
| \<!if series-\>...\<-if series\> | Only include if _not_ part of a book series or podcast | Conditional |
|
||||
| \<!if podcast-\>...\<-if podcast\> | Only include if _not_ part of a podcast | Conditional |
|
||||
| \<!if bookseries-\>...\<-if bookseries\> | Only include if _not_ part of a book series | Conditional |
|
||||
| \<!if podcastparent-\>...\<-if podcastparent\> **†** | Only include if item is _not_ a podcast series parent | Conditional |
|
||||
| \<!has PROPERTY-\>...\<-has\> | Only include if the PROPERTY _does not_ have a value (i.e. is null or empty) | Conditional |
|
||||
| \<!is PROPERTY[[CHECK](#checks)]-\>...\<-is\> | Only include if neither the whole PROPERTY nor the values of a list PROPERTY satisfies the CHECK | Conditional |
|
||||
| \<!cmp 1st-PROPERTY [CHECK](#checks) 2nd-PROPERTY-\>...\<-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.
|
||||
|
||||
@@ -131,19 +131,20 @@ Text formatting can change length and case of the text. Use \<#\>, \<#\>\<case\>
|
||||
|
||||
### Text List Formatters
|
||||
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------|----------------------------------------------|
|
||||
| separator() | Specify the text used to join<br>multiple entries.<br><br>Default is ", " | `<tag[separator(_)]>` | Tag1_Tag2_Tag3_Tag4_Tag5 |
|
||||
| format(\{S\}) **†** | Formats the entries by placing their values into the specified template.<br>Use \{S:[Text formatters](#text-formatters)\} to place the entry and optionally apply a format. | `<tag[format(Tag={S:l})`<br>`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. | `<tag[unique()]>`<hr>`<tag[unique({S:1L})`<br>`separator(;)]>` | Tag1, Tag2, Tag3<hr>tag1 |
|
||||
| sort(S) | Sorts the elements by their value.<br><br>*Sorting direction:*<br>uppercase = ascending<br>lowercase = descending<br><br>Default is unsorted | `<tag[sort(s)`<br>`separator(;)]>` | Tag5;Tag4;Tag3;Tag2;Tag1 |
|
||||
| max(#) | Only use the first # of entries | `<tag[max(1)]>` | Tag1 |
|
||||
| slice(#) | Only use the nth entry of the list | `<tag[slice(2)]>` | Tag2 |
|
||||
| slice(#..) | Only use entries of the list starting from # | `<tag[slice(2..)]>` | Tag2, Tag3, Tag4, Tag5 |
|
||||
| slice(..#) | Like max(#). Only use the first # of entries | `<tag[slice(..1)]>` | Tag1 |
|
||||
| slice(#..#) | Only use entries of the list starting from # and ending at # (inclusive) | `<tag[slice(2..4)]>` | 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 | `<tag[slice(-3..-2)]>` | 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). | `<tag[count()]>`<hr>`<tag[count(00)]>` | 5<hr>05 |
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
|--------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|--------------------------------------------------|
|
||||
| separator() | Specify the text used to join<br>multiple entries.<br><br>Default is ", " | `<tag[separator(_)]>` | Tag1_Tag2_Tag3_Tag4_Tag5 |
|
||||
| format(\{S\}) **†** | Formats the entries by placing their values into the specified template.<br>Use \{S:[Text formatters](#text-formatters)\} to place the entry and optionally apply a format. | `<tag[format(Tag={S:l})`<br>`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. | `<tag[unique()]>`<hr>`<tag[unique({S:1L})`<br>`separator(;)]>` | Tag1, Tag2, Tag3<hr>tag1 |
|
||||
| sort(S) | Sorts the elements by their value.<br><br>*Sorting direction:*<br>uppercase = ascending<br>lowercase = descending<br><br>Default is unsorted | `<tag[sort(s)`<br>`separator(;)]>` | Tag5;Tag4;Tag3;Tag2;Tag1 |
|
||||
| max(#) | Only use the first # of entries | `<tag[max(1)]>` | Tag1 |
|
||||
| slice(#) | Only use the nth entry of the list | `<tag[slice(2)]>` | Tag2 |
|
||||
| slice(#..) | Only use entries of the list starting from # | `<tag[slice(2..)]>` | Tag2, Tag3, Tag4, Tag5 |
|
||||
| slice(..#) | Like max(#). Only use the first # of entries | `<tag[slice(..1)]>` | Tag1 |
|
||||
| slice(#..#) | Only use entries of the list starting from # and ending at # (inclusive) | `<tag[slice(2..4)]>` | 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 | `<tag[slice(-3..-2)]>` | 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). | `<tag[count()]>`<hr>`<tag[count(00)]>` | 5<hr>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.<br><br>**Syntax:** `filter(FORMAT CHECK VALUE)` or `filter(CHECK VALUE)`<br>- `FORMAT`: Optional text format to apply to each entry (e.g., `{S}`, `{S:L}`, `{S:3}`); defaults to `{S}`<br>- `CHECK`: Comparison operator (e.g., `=`, `!=`, `~`)<br>- `VALUE`: The value to compare against | `<tag[filter({S} ~ '[3-9]')]>`<hr>`<tag[filter({S} != 'Ignore')]>` | Tag3, Tag4, Tag5<hr>Tag1, Tag2, Tag3, Tag4, Tag5 |
|
||||
|
||||
**†** 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 \<#\>, \<#\>\<case\>
|
||||
|
||||
### Series List Formatters
|
||||
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
|---------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |-------------------------------------------------------------------------------------------| ------------------------------------------------------------------------------------------------------------------- |
|
||||
| separator() | Specify the text used to join<br>multiple series names.<br><br>Default is ", " | `<series[separator(; )]>` | Sherlock Holmes; Some Other Series |
|
||||
| format(\{N \| # \| ID\}) **†** | Formats the series properties<br>using the name series tags.<br>See [Series Formatter Usage](#series-formatters) above. | `<series[format({N}, {#})`<br>`separator(; )]>`<hr>`<series[format({ID}-{N}, {#:00.0})]>` | Sherlock Holmes, 1-6; Book Collection, 1<hr>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. | `<series[unique()]>`<hr>`<series[unique({N:L})`<br>`separator(; )]>` | Sherlock Holmes; Some Other Series<hr>sherlock holmes; some other series |
|
||||
| sort(N \| # \| ID) | Sorts the series by name, number or ID.<br><br>These terms define the primary, secondary, tertiary, … sorting order.<br>You may combine multiple terms in sequence to specify multi‑level sorting.<br><br>*Sorting direction:*<br>uppercase = ascending<br>lowercase = descending<br><br>Default is unsorted | `<series[sort(N)`<br>`separator(; )]>` | Book Collection, 1; Sherlock Holmes, 1-6 |
|
||||
| max(#) | Only use the first # of series | `<series[max(1)]>` | Sherlock Holmes |
|
||||
| slice(#..#) | Only use entries of the series list starting from # and ending at # (inclusive)<br><br>See [Text List Formatter Usage](#Text-List-Formatters) above for details on all the variants of `slice()` | `<series[slice(..-2)]>` | 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). | `<series[count()]>`<hr>`<series[count(00)]>` | 2<hr>02 |
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
|-------------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|
|
||||
| separator() | Specify the text used to join<br>multiple series names.<br><br>Default is ", " | `<series[separator(; )]>` | Sherlock Holmes; Some Other Series |
|
||||
| format(\{N \| # \| ID\}) **†** | Formats the series properties<br>using the name series tags.<br>See [Series Formatter Usage](#series-formatters) above. | `<series[format({N}, {#})`<br>`separator(; )]>`<hr>`<series[format({ID}-{N}, {#:00.0})]>` | Sherlock Holmes, 1-6; Book Collection, 1<hr>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. | `<series[unique()]>`<hr>`<series[unique({N:L})`<br>`separator(; )]>` | Sherlock Holmes; Some Other Series<hr>sherlock holmes; some other series |
|
||||
| sort(N \| # \| ID) | Sorts the series by name, number or ID.<br><br>These terms define the primary, secondary, tertiary, … sorting order.<br>You may combine multiple terms in sequence to specify multi‑level sorting.<br><br>*Sorting direction:*<br>uppercase = ascending<br>lowercase = descending<br><br>Default is unsorted | `<series[sort(N)`<br>`separator(; )]>` | Book Collection, 1; Sherlock Holmes, 1-6 |
|
||||
| max(#) | Only use the first # of series | `<series[max(1)]>` | Sherlock Holmes |
|
||||
| slice(#..#) | Only use entries of the series list starting from # and ending at # (inclusive)<br><br>See [Text List Formatter Usage](#Text-List-Formatters) above for details on all the variants of `slice()` | `<series[slice(..-2)]>` | 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). | `<series[count()]>`<hr>`<series[count(00)]>` | 2<hr>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.<br><br>**Syntax:** `filter(FORMAT CHECK VALUE)` or `filter(CHECK VALUE)`<br>- `FORMAT`: Optional series format to apply to each entry (e.g., `{N}`, `{N:L}`, `{#}`); defaults to `{N}`<br>- `CHECK`: Comparison operator (e.g., `=`, `!=`, `~`)<br>- `VALUE`: The value to compare against | `<series[filter({N} = 'Holmes')]>`<hr>`<series[filter(~ "Sherlock")]>` | Sherlock Holmes<hr>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 \<#\>, \<#\>\<case\>
|
||||
|
||||
### Name List Formatters
|
||||
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
|------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| separator() | Specify the text used to join<br>multiple people's names.<br><br>Default is ", " | `<author[separator(; )]>` | Arthur Conan Doyle; Stephen Fry |
|
||||
| format(\{T \| F \| M \| L \| S \| ID\}) **†** | Formats the human name using<br>the name part tags.<br>See [Name Formatter Usage](#name-formatters) above. | `<author[format({L:u}, {F})`<br>`separator(; )]>`<hr>`<author[format({L}, {F:1}.`<br>`_{ID}_) separator(; )]>` | DOYLE, Arthur; FRY, Stephen<hr>Doyle, A. \_B000AQ43GQ\_;<br>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. | `<author[unique()]>`<hr>`<author[unique({L:L})`<br>`separator(; )]>` | Arthur Conan Doyle, Stephen Fry<hr>doyle; fry |
|
||||
| sort(T \| F \| M \| L \| S \| ID) | Sorts the names by title,<br> first, middle, or last name,<br>suffix or Audible Contributor ID<br><br>These terms define the primary, secondary, tertiary, … sorting order.<br>You may combine multiple terms in sequence to specify multi‑level sorting.<br><br>*Sorting direction:*<br>uppercase = ascending<br>lowercase = descending<br><br>Default is unsorted | `<author[sort(M)]>`<hr>`<author[sort(Fl)]>`<hr>`<author[sort(L FM ID)]>` | Stephen Fry, Arthur Conan Doyle<hr>Stephen King, Stephen Fry<hr>John P. Smith \_B000TTTBBB\_, John P. Smith \_B000TTTCCC\_, John S. Smith \_B000HHHVVV\_ |
|
||||
| max(#) | Only use the first # of names<br><br>Default is all names | `<author[max(1)]>` | Arthur Conan Doyle |
|
||||
| slice(#..#) | Only use entries of the names list starting from # and ending at # (inclusive)<br><br>See [Text List Formatter Usage](#Text-List-Formatters) above for details on all the variants of `slice()` | `<author[slice(..-2)]>` | 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). | `<author[count()]>`<hr>`<author[count(00)]>` | 2<hr>02 |
|
||||
| Formatter | Description | Example Usage | Example Result |
|
||||
|-----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| separator() | Specify the text used to join<br>multiple people's names.<br><br>Default is ", " | `<author[separator(; )]>` | Arthur Conan Doyle; Stephen Fry |
|
||||
| format(\{T \| F \| M \| L \| S \| ID\}) **†** | Formats the human name using<br>the name part tags.<br>See [Name Formatter Usage](#name-formatters) above. | `<author[format({L:u}, {F})`<br>`separator(; )]>`<hr>`<author[format({L}, {F:1}.`<br>`_{ID}_) separator(; )]>` | DOYLE, Arthur; FRY, Stephen<hr>Doyle, A. \_B000AQ43GQ\_;<br>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. | `<author[unique()]>`<hr>`<author[unique({L:L})`<br>`separator(; )]>` | Arthur Conan Doyle, Stephen Fry<hr>doyle; fry |
|
||||
| sort(T \| F \| M \| L \| S \| ID) | Sorts the names by title,<br> first, middle, or last name,<br>suffix or Audible Contributor ID<br><br>These terms define the primary, secondary, tertiary, … sorting order.<br>You may combine multiple terms in sequence to specify multi‑level sorting.<br><br>*Sorting direction:*<br>uppercase = ascending<br>lowercase = descending<br><br>Default is unsorted | `<author[sort(M)]>`<hr>`<author[sort(Fl)]>`<hr>`<author[sort(L FM ID)]>` | Stephen Fry, Arthur Conan Doyle<hr>Stephen King, Stephen Fry<hr>John P. Smith \_B000TTTBBB\_, John P. Smith \_B000TTTCCC\_, John S. Smith \_B000HHHVVV\_ |
|
||||
| max(#) | Only use the first # of names<br><br>Default is all names | `<author[max(1)]>` | Arthur Conan Doyle |
|
||||
| slice(#..#) | Only use entries of the names list starting from # and ending at # (inclusive)<br><br>See [Text List Formatter Usage](#Text-List-Formatters) above for details on all the variants of `slice()` | `<author[slice(..-2)]>` | 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). | `<author[count()]>`<hr>`<author[count(00)]>` | 2<hr>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.<br><br>**Syntax:** `filter(FORMAT CHECK VALUE)` or `filter(CHECK VALUE)`<br>- `FORMAT`: Optional name format to apply to each entry (e.g., `{L}`, `{M}`, `{L}, {F}`); defaults to `{T} {F} {M} {L} {S}`<br>- `CHECK`: Comparison operator (e.g., `=`, `!=`, `~`)<br>- `VALUE`: The value to compare against | `<author[filter({L} = 'Doyle')]>`<hr>`<author[filter({F} ~ '^Stephen')]>`<hr>`<narrator[filter(~ "Fry")]>` | Conan Doyle<hr>Stephen Fry<hr>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) | \<is tag[=Tag1]-\> |
|
||||
| != | | Matches if values are not equal (case-insensitive) | \<is first author[!=Arthur]-\><br>\<cmp "foo" != 'bar'-\> |
|
||||
| ~ | | Matches if the first parameter matches the regular expression specified by the second parameter (case-insensitive) | \<is title[~(\[XYZ\]).*\\1]-\> |
|
||||
| = | | Matches if values are equal (case-insensitive) | \<is tag[=Tag1]-\><br>\<author[filter({L} = 'Doyle')]> |
|
||||
| != | | Matches if values are not equal (case-insensitive) | \<is first author[!=Arthur]-\><br>\<cmp "foo" != 'bar'-\><br>\<tag[filter({S} != 'Ignore')]> |
|
||||
| ~ | | Matches if the first parameter matches the regular expression specified by the second parameter (case-insensitive) | \<is title[~(\[XYZ\]).*\\1]-\><br>\<tag[filter({S} ~ '[3-9]')]><br>\<narrator[filter({F} ~ '^Stephen')]> |
|
||||
|
||||
| Number Checks | Unicode Operator | Description | Examples |
|
||||
|---------------|------------------|----------------------------------------------------------------|-----------------------------------------------------------|
|
||||
|
||||
Reference in New Issue
Block a user