From 812e0c3b6088fead5ca4df70e097653ef3d91657 Mon Sep 17 00:00:00 2001 From: Jo-Be-Co Date: Thu, 12 Mar 2026 04:15:52 +0100 Subject: [PATCH] Format details on fields specified in format. Introduce by colon. Use dictionaries for field access and sorting. Added sorting to series lists. --- .../NamingTemplate/CommonFormatters.cs | 59 ++++++++++++++++--- .../NamingTemplate/NamingTemplate.cs | 14 +---- .../Templates/ContributorDto.cs | 31 +++++----- .../Templates/NameListFormat.cs | 42 +++++++++---- .../Templates/SeriesDto.cs | 29 +++++---- .../Templates/SeriesListFormat.cs | 38 +++++++++++- .../Templates/SeriesOrder.cs | 1 + .../TemplatesTests.cs | 16 ++--- docs/features/naming-templates.md | 41 +++++++------ 9 files changed, 184 insertions(+), 87 deletions(-) diff --git a/Source/FileManager/NamingTemplate/CommonFormatters.cs b/Source/FileManager/NamingTemplate/CommonFormatters.cs index 6168a295..04ad3454 100644 --- a/Source/FileManager/NamingTemplate/CommonFormatters.cs +++ b/Source/FileManager/NamingTemplate/CommonFormatters.cs @@ -12,18 +12,65 @@ public static partial class CommonFormatters public delegate string PropertyFormatter(ITemplateTag templateTag, T? value, string formatString, CultureInfo? culture); public static string StringFormatter(ITemplateTag _, string? value, string formatString, CultureInfo? culture) + => _StringFormatter(value, formatString, culture); + + private static string _StringFormatter(string? value, string formatString, CultureInfo? culture) { - if (value is null) return ""; + if (string.IsNullOrEmpty(value)) return string.Empty; + if (string.IsNullOrEmpty(formatString)) return value; + + var match = StringFormatRegex().Match(formatString); + if (!match.Success) return value; + + // first shorten the string if a number is specified in the format string + if (int.TryParse(match.Groups["left"].ValueSpan, out var length) && length < value.Length) + value = value[..length]; + culture ??= CultureInfo.CurrentCulture; - return formatString switch + return match.Groups["case"].ValueSpan switch { "u" or "U" => value.ToUpper(culture), "l" or "L" => value.ToLower(culture), + "T" => culture.TextInfo.ToTitleCase(value), + "t" => culture.TextInfo.ToTitleCase(value.ToLower(culture)), _ => value, }; } + [GeneratedRegex(@"^\s*(?\d+)?\s*(?[uUlLtT])?\s*$")] + private static partial Regex StringFormatRegex(); + + public static string TemplateStringFormatter(T toFormat, string? templateString, IFormatProvider? provider, Dictionary> replacements) + { + if (string.IsNullOrEmpty(templateString)) return ""; + + // is this function is called from toString implementation of the IFormattable interface, we only get a IFormatProvider + var culture = provider as CultureInfo ?? (provider?.GetFormat(typeof(CultureInfo)) as CultureInfo); + return TagFormatRegex().Replace(templateString, GetValueForMatchingTag); + + string GetValueForMatchingTag(Match m) + { + var tag = m.Groups["tag"].Value; + if (!replacements.TryGetValue(tag, out var getter)) return m.Value; + + var value = getter(toFormat); + var format = m.Groups["format"].Value; + return value switch + { + IFormattable formattable => formattable.ToString(format, provider), + _ => _StringFormatter(value?.ToString(), format, culture), + }; + } + } + + // The templateString is scanned for contained braces with an enclosed tagname. + // The tagname may be followed by an optional format specifier separated by a colon. + // All other parts of the template string are left untouched as well as the braces where the tagname is unknown. + // TemplateStringFormatter will use a dictionary to lookup the tagname and the corresponding value getter. + [GeneratedRegex(@"\{(?[[A-Z]+|#)(?::(?.*?))?\}", RegexOptions.IgnoreCase)] + private static partial Regex TagFormatRegex(); + public static string FormattableFormatter(ITemplateTag _, IFormattable? value, string formatString, CultureInfo? culture) => value?.ToString(formatString, culture) ?? ""; @@ -52,12 +99,6 @@ public static partial class CommonFormatters public static string LanguageShortFormatter(ITemplateTag templateTag, string? language, string formatString, CultureInfo? culture) { - if (language is null) - return ""; - - language = language.Trim(); - if (language.Length > 3) language = language[..3]; - - return StringFormatter(templateTag, language, formatString, culture); + return StringFormatter(templateTag, language?.Trim(), "3u", culture); } } \ No newline at end of file diff --git a/Source/FileManager/NamingTemplate/NamingTemplate.cs b/Source/FileManager/NamingTemplate/NamingTemplate.cs index 7538b9f8..1423f50e 100644 --- a/Source/FileManager/NamingTemplate/NamingTemplate.cs +++ b/Source/FileManager/NamingTemplate/NamingTemplate.cs @@ -31,7 +31,7 @@ public class NamingTemplate /// /// /// Instances of the TClass used in and - public TemplatePart Evaluate(params object?[] propertyClasses) //CultureInfo? culture, + public TemplatePart Evaluate(params object?[] propertyClasses) { if (_templateToString is null) throw new InvalidOperationException(); @@ -39,8 +39,7 @@ public class NamingTemplate // Match propertyClasses to the arguments required by templateToString.DynamicInvoke(). // First parameter is "this", so ignore it. var parameters = _templateToString.Method.GetParameters(); - int skip = _templateToString.Target == null ? 0 : 1; - var delegateArgTypes = parameters.Skip(skip).ToList(); + var delegateArgTypes = parameters.Skip(1).ToList(); object?[] args = new object?[delegateArgTypes.Count]; // args = delegateArgTypes.Join(propertyClasses, dat => dat.ParameterType, pc => pc?.GetType(), (_, i) => i, @@ -48,14 +47,7 @@ public class NamingTemplate for (int i = 0; i < delegateArgTypes.Count; i++) { var p = delegateArgTypes[i]; - if (typeof(CultureInfo).IsAssignableFrom(p.ParameterType) && false) - { - args[i] = null;//culture; - } - else - { - args[i] = propertyClasses.FirstOrDefault(pc => pc != null && p.ParameterType.IsInstanceOfType(pc)); - } + args[i] = propertyClasses.FirstOrDefault(pc => pc != null && p.ParameterType.IsInstanceOfType(pc)); } if (args.Length != delegateArgTypes.Count) diff --git a/Source/LibationFileManager/Templates/ContributorDto.cs b/Source/LibationFileManager/Templates/ContributorDto.cs index b578a66b..4be259a1 100644 --- a/Source/LibationFileManager/Templates/ContributorDto.cs +++ b/Source/LibationFileManager/Templates/ContributorDto.cs @@ -1,13 +1,28 @@ using NameParser; using System; +using System.Collections.Generic; +using FileManager.NamingTemplate; namespace LibationFileManager.Templates; public class ContributorDto(string name, string? audibleContributorId) : IFormattable { - public HumanName HumanName { get; } = new(RemoveSuffix(name), Prefer.FirstOverPrefix); + private HumanName HumanName { get; } = new(RemoveSuffix(name), Prefer.FirstOverPrefix); private string? AudibleContributorId { get; } = audibleContributorId; + public static readonly Dictionary> FormatReplacements = new(StringComparer.OrdinalIgnoreCase) + { + // Single-word names parse as first names. Use it as last name. + { "L", dto => string.IsNullOrWhiteSpace(dto.HumanName.Last) ? dto.HumanName.First : dto.HumanName.Last }, + // Because of the above, if we have only a first name, then we'd double the name as "FirstName FirstName", so clear the first name in that situation. + { "F", dto => string.IsNullOrWhiteSpace(dto.HumanName.Last) ? dto.HumanName.Last : dto.HumanName.First }, + + { "T", dto => dto.HumanName.Title }, + { "M", dto => dto.HumanName.Middle }, + { "S", dto => dto.HumanName.Suffix }, + { "ID", dto => dto.AudibleContributorId }, + }; + public override string ToString() => ToString("{T} {F} {M} {L} {S}", null); @@ -16,19 +31,7 @@ public class ContributorDto(string name, string? audibleContributorId) : IFormat if (string.IsNullOrWhiteSpace(format)) return ToString(); - //Single-word names parse as first names. Use it as last name. - var lastName = string.IsNullOrWhiteSpace(HumanName.Last) ? HumanName.First : HumanName.Last; - //Because of the above, if the have only a first name, then we'd double the name as "FirstName FirstName", so clear the first name in that situation. - var firstName = string.IsNullOrWhiteSpace(HumanName.Last) ? HumanName.Last : HumanName.First; - - return format - .Replace("{T}", HumanName.Title) - .Replace("{F}", firstName) - .Replace("{M}", HumanName.Middle) - .Replace("{L}", lastName) - .Replace("{S}", HumanName.Suffix) - .Replace("{ID}", AudibleContributorId) - .Trim(); + return CommonFormatters.TemplateStringFormatter(this, format, provider, FormatReplacements).Trim(); } private static string RemoveSuffix(string namesString) diff --git a/Source/LibationFileManager/Templates/NameListFormat.cs b/Source/LibationFileManager/Templates/NameListFormat.cs index 4f6fa938..203981fb 100644 --- a/Source/LibationFileManager/Templates/NameListFormat.cs +++ b/Source/LibationFileManager/Templates/NameListFormat.cs @@ -1,4 +1,5 @@ -using FileManager.NamingTemplate; +using System; +using FileManager.NamingTemplate; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -11,24 +12,39 @@ internal partial class NameListFormat : IListFormat public static string Formatter(ITemplateTag _, IEnumerable? names, string formatString, CultureInfo? culture) => names is null ? string.Empty - : IListFormat.Join(formatString, Sort(names, formatString), culture); + : IListFormat.Join(formatString, Sort(names, formatString, ContributorDto.FormatReplacements), culture); - private static IEnumerable Sort(IEnumerable names, string formatString) + private static IEnumerable Sort(IEnumerable entries, string formatString, Dictionary> formatReplacements) { var sortMatch = SortRegex().Match(formatString); - return - sortMatch.Success - ? sortMatch.Groups[1].Value == "F" ? names.OrderBy(n => n.HumanName.First) - : sortMatch.Groups[1].Value == "M" ? names.OrderBy(n => n.HumanName.Middle) - : sortMatch.Groups[1].Value == "L" ? names.OrderBy(n => n.HumanName.Last) - : names - : names; + if (!sortMatch.Success) return entries; + + IOrderedEnumerable? ordered = null; + foreach (Match m in SortTokenizer().Matches(sortMatch.Groups["pattern"].Value)) + { + // Dictionary is case-insensitive, no ToUpper needed + if (!formatReplacements.TryGetValue(m.Groups["token"].Value, out var selector)) + continue; + + ordered = ordered is null + // ReSharper disable once PossibleMultipleEnumeration + ? entries.OrderBy(selector) + : ordered.ThenBy(selector); + } + + return ordered ?? entries; } - /// Sort must have exactly one of the characters F, M, or L - [GeneratedRegex(@"[Ss]ort\(\s*?([FML])\s*?\)")] + private const string Token = @"(?:[TFMLS]|ID)"; + + /// Sort must have at least one of the token labels T, F, M, L, S or ID.Add multiple tokens to sort by multiple fields. Spaces may be used to separate tokens. + [GeneratedRegex($@"[Ss]ort\(\s*(?i:(?(?:{Token}\s*?)+))\s*\)")] private static partial Regex SortRegex(); + + [GeneratedRegex($@"\G(?{Token})\s*", RegexOptions.IgnoreCase)] + private static partial Regex SortTokenizer(); + /// Format must have at least one of the string {T}, {F}, {M}, {L}, {S}, or {ID} - [GeneratedRegex(@"[Ff]ormat\((.*?(?:{[TFMLS]}|{ID})+.*?)\)")] + [GeneratedRegex($@"[Ff]ormat\((.*?\{{{Token}(?::.*?)?\}}.*?)\)")] public static partial Regex FormatRegex(); } diff --git a/Source/LibationFileManager/Templates/SeriesDto.cs b/Source/LibationFileManager/Templates/SeriesDto.cs index 22b653d7..913878a5 100644 --- a/Source/LibationFileManager/Templates/SeriesDto.cs +++ b/Source/LibationFileManager/Templates/SeriesDto.cs @@ -1,24 +1,27 @@ using System; -using System.Text.RegularExpressions; +using System.Collections.Generic; +using FileManager.NamingTemplate; namespace LibationFileManager.Templates; -public partial record SeriesDto(string? Name, string? Number, string AudibleSeriesId) : IFormattable +public record SeriesDto(string? Name, string? Number, string AudibleSeriesId) : IFormattable { public SeriesOrder Order { get; } = SeriesOrder.Parse(Number); + public static readonly Dictionary> FormatReplacements = new(StringComparer.OrdinalIgnoreCase) + { + { "#", dto => dto.Order }, + { "N", dto => dto.Name }, + { "ID", dto => dto.AudibleSeriesId } + }; + public override string? ToString() => Name?.Trim(); + public string ToString(string? format, IFormatProvider? provider) - => string.IsNullOrWhiteSpace(format) ? ToString() ?? string.Empty - : FormatRegex().Replace(format, MatchEvaluator) - .Replace("{N}", Name) - .Replace("{ID}", AudibleSeriesId) - .Trim(); + { + if (string.IsNullOrWhiteSpace(format)) + return ToString() ?? string.Empty; - private string MatchEvaluator(Match match) - => Order?.ToString(match.Groups[1].Value, null) ?? ""; - - /// Format must have at least one of the string {N}, {#}, {ID} - [GeneratedRegex(@"{#(?:\:(.*?))?}")] - private static partial Regex FormatRegex(); + return CommonFormatters.TemplateStringFormatter(this, format, provider, FormatReplacements).Trim(); + } } diff --git a/Source/LibationFileManager/Templates/SeriesListFormat.cs b/Source/LibationFileManager/Templates/SeriesListFormat.cs index a844b04a..1ddbe66e 100644 --- a/Source/LibationFileManager/Templates/SeriesListFormat.cs +++ b/Source/LibationFileManager/Templates/SeriesListFormat.cs @@ -1,6 +1,8 @@ -using FileManager.NamingTemplate; +using System; +using FileManager.NamingTemplate; using System.Collections.Generic; using System.Globalization; +using System.Linq; using System.Text.RegularExpressions; namespace LibationFileManager.Templates; @@ -10,9 +12,39 @@ internal partial class SeriesListFormat : IListFormat public static string Formatter(ITemplateTag _, IEnumerable? series, string formatString, CultureInfo? culture) => series is null ? string.Empty - : IListFormat.Join(formatString, series, culture); + : IListFormat.Join(formatString, Sort(series, formatString, SeriesDto.FormatReplacements), culture); + + private static IEnumerable Sort(IEnumerable entries, string formatString, Dictionary> formatReplacements) + { + var sortMatch = SortRegex().Match(formatString); + if (!sortMatch.Success) return entries; + + IOrderedEnumerable? ordered = null; + foreach (Match m in SortTokenizer().Matches(sortMatch.Groups["pattern"].Value)) + { + // Dictionary is case-insensitive, no ToUpper needed + if (!formatReplacements.TryGetValue(m.Groups["token"].Value, out var selector)) + continue; + + ordered = ordered is null + // ReSharper disable once PossibleMultipleEnumeration + ? entries.OrderBy(selector) + : ordered.ThenBy(selector); + } + + return ordered ?? entries; + } + + private const string Token = @"(?:[#N]|ID)"; + + /// Sort must have at least one of the token labels T, F, M, L, S or ID. Use lower case for descending direction and add multiple tokens to sort by multiple fields. Spaces may be used to separate tokens. + [GeneratedRegex($@"[Ss]ort\(\s*(?i:(?(?:{Token}\s*?)+))\s*\)")] + private static partial Regex SortRegex(); + + [GeneratedRegex($@"\G(?{Token})\s*", RegexOptions.IgnoreCase)] + private static partial Regex SortTokenizer(); /// Format must have at least one of the string {N}, {#}, {ID} - [GeneratedRegex(@"[Ff]ormat\((.*?(?:{#(?:\:.*?)?}|{N}|{ID})+.*?)\)")] + [GeneratedRegex($@"[Ff]ormat\((.*?\{{{Token}(?::.*?)?\}}.*?)\)")] public static partial Regex FormatRegex(); } diff --git a/Source/LibationFileManager/Templates/SeriesOrder.cs b/Source/LibationFileManager/Templates/SeriesOrder.cs index ed896a0b..f952b883 100644 --- a/Source/LibationFileManager/Templates/SeriesOrder.cs +++ b/Source/LibationFileManager/Templates/SeriesOrder.cs @@ -23,6 +23,7 @@ public class SeriesOrder : IFormattable => string.Concat(OrderParts.Select(p => p switch { float f => f.ToString(format, formatProvider ?? CultureInfo.InvariantCulture), + IFormattable f => f.ToString(format, formatProvider), _ => p.ToString(), })).Trim(); diff --git a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs index 2cf36e72..78c8893b 100644 --- a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs +++ b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs @@ -160,10 +160,12 @@ namespace TemplatesTests [TestMethod] [DataRow("Kbps Hz", "128Kbps 44100Hz")] [DataRow("Kbps Hz", "128Kbps 044100Hz")] - [DataRow("Kbps Hz", "0128Kbps 44100Hz")] - [DataRow("Kbps ", "0128Kbps A STUDY IN SCARLET")] + [DataRow("Kbps Hz", "128Kbps 44100Hz")] + [DataRow("Kbps ", "128Kbps A STUDY IN SCARLET")] + [DataRow("Kbps ", "128Kbps A Study In Scarlet")] [DataRow("Kbps ", "0128Kbps a study in scarlet")] - [DataRow("Kbps Hz", "0128Kbps 044100Hz")] + [DataRow(" Hz", "Aac-Lc 044100Hz")] + [DataRow(" ", "AAC A STU")] [DataRow("Kbps Hz", "0128Kbps 044100Hz")] public void FormatTags(string template, string expected) { @@ -333,11 +335,11 @@ namespace TemplatesTests [DataRow("", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")] [DataRow("", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")] [DataRow("", "Charles E. Gannon, Christopher John Fetherolf, Jill Conner Browne, Jon Bon Jovi, Lucy Maud Montgomery, Paul Van Doren")] - [DataRow("", "Jon Bon Jovi, Jill Conner Browne, Christopher John Fetherolf, Charles E. Gannon, Lucy Maud Montgomery, Paul Van Doren")] [DataRow("", "Jon Bon Jovi, Paul Van Doren, Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery")] - [DataRow("", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")] - [DataRow("", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")] - [DataRow("", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")] + [DataRow("", "Jon Bon Jovi, Jill Conner Browne, Christopher John Fetherolf, Charles E. Gannon, Lucy Maud Montgomery, Paul Van Doren")] + [DataRow("", "Charles E. Gannon, Christopher John Fetherolf, Jill Conner Browne, Jon Bon Jovi, Lucy Maud Montgomery, Paul Van Doren")] + [DataRow("", "Jon Bon Jovi, Paul Van Doren, Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery")] + [DataRow("", "Jon Bon Jovi, Jill Conner Browne, Christopher John Fetherolf, Charles E. Gannon, Lucy Maud Montgomery, Paul Van Doren")] [DataRow("", "Jill Conner Browne")] [DataRow("", "Jill Conner Browne, Charles E. Gannon")] [DataRow("", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf")] diff --git a/docs/features/naming-templates.md b/docs/features/naming-templates.md index 5da07008..5bdd1989 100644 --- a/docs/features/naming-templates.md +++ b/docs/features/naming-templates.md @@ -102,10 +102,16 @@ And this example will customize the title based on whether the book has a subtit ### Text Formatters -| Formatter | Description | Example Usage | Example Result | -| --------- | -------------------------- | ------------------ | ------------------------------------------- | -| L | Converts text to lowercase | \ | a study in scarlet꞉ a sherlock holmes novel | -| U | Converts text to uppercase | \ | A STUDY IN SCARLET | +Text formatting can change length and case of the text. Use <#>, <#><case> or <case> to specify one or both of these. + +| Formatter | Description | Example Usage | Example Result | +| --------- | --------------------------------------------------------------- | ------------------ | ------------------------------------------- | +| # | Cuts down the text to the specified number of characters | \<title[14]\> | A Study in Scar | +| L | Converts text to lowercase | \<title[L]\> | a study in scarlet꞉ a sherlock holmes novel | +| U | Converts text to uppercase | \<title short[U]\> | A STUDY IN SCARLET | +| t | Converts text to title case | \<title[t]\> | The Abc Murders | +| T | Converts text to title case where uppercase words are preserved | \<title[T]\> | The ABC Murders | +| | | \<title[6T]\> | The AB | ### Series Formatters @@ -115,25 +121,26 @@ And this example will customize the title based on whether the book has a subtit ### Series List Formatters -| Formatter | Description | Example Usage | Example Result | -| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| separator() | Speficy 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 | -| max(#) | Only use the first # of series<br><br>Default is all series | `<series[max(1)]>` | Sherlock Holmes | +| Formatter | Description | Example Usage | Example Result | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | +| separator() | Speficy 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 | +| 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>Default is unsorted | `<series[sort(N)`<br>`separator(; )]>` | Book Collection, 1; Sherlock Holmes, 1-6 | +| max(#) | Only use the first # of series<br><br>Default is all series | `<series[max(1)]>` | Sherlock Holmes | ### Name Formatters -| Formatter | Description | Example Usage | Example Result | -| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------- | -| \{T \| F \| M \| L \| S \| ID\} | Formats the human name using<br>the name part tags.<br>\{T\} = Title (e.g. "Dr.")<br>\{F\} = First name<br>\{M\} = Middle name<br>\{L\} = Last Name<br>\{S\} = Suffix (e.g. "PhD")<br>\{ID\} = Audible Contributor ID<br><br>Default is \{T\} \{F\} \{M\} \{L\} \{S\} | `<first narrator[{L}, {F}]>`<hr>`<first author[{L}, {F} _{ID}_]>` | Fry, Stephen<hr>Doyle, Arthur \_B000AQ43GQ\_;<br>Fry, Stephen \_B000APAGVS\_ | +| Formatter | Description | Example Usage | Example Result | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------- | +| \{T \| F \| M \| L \| S \| ID\} | Formats the human name using<br>the name part tags.<br>\{T:[Text_Formatter](#text-formatters)\} = Title (e.g. "Dr.")<br>\{F:[Text_Formatter](#Text-Formatters)\} = First name<br>\{M:[Text_Formatter](#text-formatters)\} = Middle name<br>\{L:[Text_Formatter](#text-formatters)\} = Last Name<br>\{S:[Text_Formatter](#text-formatters)\} = Suffix (e.g. "PhD")<br>\{ID:[Text_Formatter](#text-formatters)\} = Audible Contributor ID<br><br>Formatter parts are optional and introduced by the colon. If specified the string will be used to format the part using the correspoing formatter.<br><br>Default is \{T\} \{F\} \{M\} \{L\} \{S\} | `<first narrator[{L}, {F:1}.]>`<hr>`<first author[{L:u}, {F} _{ID}_]>` | Fry, S.<hr>DOYLE, Arthur \_B000AQ43GQ\_ | ### Name List Formatters -| Formatter | Description | Example Usage | Example Result | -| --------------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| separator() | Speficy 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}, {F})`<br>`separator(; )]>`<hr>`<author[format({L}, {F}`<br>`_{ID}_) separator(; )]>` | Doyle, Arthur; Fry, Stephen<hr>Doyle, Arthur \_B000AQ43GQ\_;<br>Fry, Stephen \_B000APAGVS\_ | -| sort(F \| M \| L) | Sorts the names by first, middle,<br>or last name<br><br>Default is unsorted | `<author[sort(M)]>` | Stephen Fry, Arthur Conan Doyle | +| Formatter | Description | Example Usage | Example Result | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| separator() | Speficy 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\_ | +| 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>Default is unsorted | `<author[sort(M)]>`<hr>`<author[sort(Fl)]>`<hr><author[sort(L FM ID)]> | Stephen Fry, Arthur Conan Doyle<hr>Stephen Fry,Stephen King<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 | ### Number Formatters