Format details on fields specified in format. Introduce by colon.

Use dictionaries for field access and sorting.

Added sorting to series lists.
This commit is contained in:
Jo-Be-Co
2026-03-12 04:15:52 +01:00
parent d161bdfaeb
commit 812e0c3b60
9 changed files with 184 additions and 87 deletions

View File

@@ -12,18 +12,65 @@ public static partial class CommonFormatters
public delegate string PropertyFormatter<in T>(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*(?<left>\d+)?\s*(?<case>[uUlLtT])?\s*$")]
private static partial Regex StringFormatRegex();
public static string TemplateStringFormatter<T>(T toFormat, string? templateString, IFormatProvider? provider, Dictionary<string, Func<T, object?>> 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(@"\{(?<tag>[[A-Z]+|#)(?::(?<format>.*?))?\}", 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);
}
}

View File

@@ -31,7 +31,7 @@ public class NamingTemplate
/// </summary>
/// <param name="culture"></param>
/// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param>
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)

View File

@@ -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<string, Func<ContributorDto, object?>> 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)

View File

@@ -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<NameListFormat>
public static string Formatter(ITemplateTag _, IEnumerable<ContributorDto>? names, string formatString, CultureInfo? culture)
=> names is null
? string.Empty
: IListFormat<NameListFormat>.Join(formatString, Sort(names, formatString), culture);
: IListFormat<NameListFormat>.Join(formatString, Sort(names, formatString, ContributorDto.FormatReplacements), culture);
private static IEnumerable<ContributorDto> Sort(IEnumerable<ContributorDto> names, string formatString)
private static IEnumerable<T> Sort<T>(IEnumerable<T> entries, string formatString, Dictionary<string, Func<T, object?>> 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<T>? 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;
}
/// <summary> Sort must have exactly one of the characters F, M, or L </summary>
[GeneratedRegex(@"[Ss]ort\(\s*?([FML])\s*?\)")]
private const string Token = @"(?:[TFMLS]|ID)";
/// <summary> 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.</summary>
[GeneratedRegex($@"[Ss]ort\(\s*(?i:(?<pattern>(?:{Token}\s*?)+))\s*\)")]
private static partial Regex SortRegex();
[GeneratedRegex($@"\G(?<token>{Token})\s*", RegexOptions.IgnoreCase)]
private static partial Regex SortTokenizer();
/// <summary> Format must have at least one of the string {T}, {F}, {M}, {L}, {S}, or {ID} </summary>
[GeneratedRegex(@"[Ff]ormat\((.*?(?:{[TFMLS]}|{ID})+.*?)\)")]
[GeneratedRegex($@"[Ff]ormat\((.*?\{{{Token}(?::.*?)?\}}.*?)\)")]
public static partial Regex FormatRegex();
}

View File

@@ -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<string, Func<SeriesDto, object?>> 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) ?? "";
/// <summary> Format must have at least one of the string {N}, {#}, {ID} </summary>
[GeneratedRegex(@"{#(?:\:(.*?))?}")]
private static partial Regex FormatRegex();
return CommonFormatters.TemplateStringFormatter(this, format, provider, FormatReplacements).Trim();
}
}

View File

@@ -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<SeriesListFormat>
public static string Formatter(ITemplateTag _, IEnumerable<SeriesDto>? series, string formatString, CultureInfo? culture)
=> series is null
? string.Empty
: IListFormat<SeriesListFormat>.Join(formatString, series, culture);
: IListFormat<SeriesListFormat>.Join(formatString, Sort(series, formatString, SeriesDto.FormatReplacements), culture);
private static IEnumerable<T> Sort<T>(IEnumerable<T> entries, string formatString, Dictionary<string, Func<T, object?>> formatReplacements)
{
var sortMatch = SortRegex().Match(formatString);
if (!sortMatch.Success) return entries;
IOrderedEnumerable<T>? 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)";
/// <summary> 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.</summary>
[GeneratedRegex($@"[Ss]ort\(\s*(?i:(?<pattern>(?:{Token}\s*?)+))\s*\)")]
private static partial Regex SortRegex();
[GeneratedRegex($@"\G(?<token>{Token})\s*", RegexOptions.IgnoreCase)]
private static partial Regex SortTokenizer();
/// <summary> Format must have at least one of the string {N}, {#}, {ID} </summary>
[GeneratedRegex(@"[Ff]ormat\((.*?(?:{#(?:\:.*?)?}|{N}|{ID})+.*?)\)")]
[GeneratedRegex($@"[Ff]ormat\((.*?\{{{Token}(?::.*?)?\}}.*?)\)")]
public static partial Regex FormatRegex();
}

View File

@@ -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();

View File

@@ -160,10 +160,12 @@ namespace TemplatesTests
[TestMethod]
[DataRow("<bitrate>Kbps <samplerate>Hz", "128Kbps 44100Hz")]
[DataRow("<bitrate>Kbps <samplerate[6]>Hz", "128Kbps 044100Hz")]
[DataRow("<bitrate[4]>Kbps <samplerate>Hz", "0128Kbps 44100Hz")]
[DataRow("<bitrate[4]>Kbps <titleshort[u]>", "0128Kbps A STUDY IN SCARLET")]
[DataRow("<bitrate[1]>Kbps <samplerate>Hz", "128Kbps 44100Hz")]
[DataRow("<bitrate[2]>Kbps <titleshort[u]>", "128Kbps A STUDY IN SCARLET")]
[DataRow("<bitrate[3]>Kbps <titleshort[t]>", "128Kbps A Study In Scarlet")]
[DataRow("<bitrate[4]>Kbps <titleshort[l]>", "0128Kbps a study in scarlet")]
[DataRow("<bitrate[4]>Kbps <samplerate[6]>Hz", "0128Kbps 044100Hz")]
[DataRow("<codec[t]> <samplerate[6]>Hz", "Aac-Lc 044100Hz")]
[DataRow("<codec[3T]> <titleshort[ 5 U ]>", "AAC A STU")]
[DataRow("<bitrate [ 4 ] >Kbps <samplerate [ 6 ] >Hz", "0128Kbps 044100Hz")]
public void FormatTags(string template, string expected)
{
@@ -333,11 +335,11 @@ namespace TemplatesTests
[DataRow("<author>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")]
[DataRow("<author[]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")]
[DataRow("<author[sort(F)]>", "Charles E. Gannon, Christopher John Fetherolf, Jill Conner Browne, Jon Bon Jovi, Lucy Maud Montgomery, Paul Van Doren")]
[DataRow("<author[sort(L)]>", "Jon Bon Jovi, Jill Conner Browne, Christopher John Fetherolf, Charles E. Gannon, Lucy Maud Montgomery, Paul Van Doren")]
[DataRow("<author[sort(M)]>", "Jon Bon Jovi, Paul Van Doren, Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery")]
[DataRow("<author[sort(f)]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")]
[DataRow("<author[sort(m)]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")]
[DataRow("<author[sort(l)]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery, Jon Bon Jovi, Paul Van Doren")]
[DataRow("<author[sort(L)]>", "Jon Bon Jovi, Jill Conner Browne, Christopher John Fetherolf, Charles E. Gannon, Lucy Maud Montgomery, Paul Van Doren")]
[DataRow("<author[sort(f)]>", "Charles E. Gannon, Christopher John Fetherolf, Jill Conner Browne, Jon Bon Jovi, Lucy Maud Montgomery, Paul Van Doren")]
[DataRow("<author[sort(m)]>", "Jon Bon Jovi, Paul Van Doren, Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf, Lucy Maud Montgomery")]
[DataRow("<author[sort(l)]>", "Jon Bon Jovi, Jill Conner Browne, Christopher John Fetherolf, Charles E. Gannon, Lucy Maud Montgomery, Paul Van Doren")]
[DataRow("<author [ max( 1 ) ]>", "Jill Conner Browne")]
[DataRow("<author[max(2)]>", "Jill Conner Browne, Charles E. Gannon")]
[DataRow("<author[max(3)]>", "Jill Conner Browne, Charles E. Gannon, Christopher John Fetherolf")]

View File

@@ -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 | \<title[L]\> | a study in scarlet a sherlock holmes novel |
| U | Converts text to uppercase | \<title short[U]\> | 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 multilevel 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 multilevel 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