From a6aa14c7f8f5461f315f0eeee55901983ca64e89 Mon Sep 17 00:00:00 2001 From: Jo-Be-Co Date: Wed, 11 Mar 2026 01:47:02 +0100 Subject: [PATCH] Collect formatters into CommonFormatters --- .../NamingTemplate/CommonFormatters.cs | 63 +++++++++++++++ .../PropertyTagCollection[TClass].cs | 15 ++-- .../Templates/SeriesOrder.cs | 2 +- .../Templates/TemplateTags.cs | 9 ++- .../Templates/Templates.cs | 81 ++++--------------- .../FileNamingTemplateTests.cs | 10 +-- 6 files changed, 92 insertions(+), 88 deletions(-) create mode 100644 Source/FileManager/NamingTemplate/CommonFormatters.cs diff --git a/Source/FileManager/NamingTemplate/CommonFormatters.cs b/Source/FileManager/NamingTemplate/CommonFormatters.cs new file mode 100644 index 00000000..fbf7ba9e --- /dev/null +++ b/Source/FileManager/NamingTemplate/CommonFormatters.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace FileManager.NamingTemplate; + +public static partial class CommonFormatters +{ + public const string DefaultDateFormat = "yyyy-MM-dd"; + + public delegate string PropertyFormatter(ITemplateTag templateTag, T value, string formatString); + + public static string StringFormatter(ITemplateTag _, string? value, string formatString) + { + if (value is null) return ""; + var culture = CultureInfo.CurrentCulture; + + return formatString switch + { + "u" or "U" => value.ToUpper(culture), + "l" or "L" => value.ToLower(culture), + _ => value, + }; + } + + public static string FormattableFormatter(ITemplateTag _, IFormattable? value, string formatString) + => value?.ToString(formatString, null) ?? ""; + + public static string IntegerFormatter(ITemplateTag templateTag, int value, string formatString) + => FloatFormatter(templateTag, value, formatString); + + public static string FloatFormatter(ITemplateTag _, float value, string formatString) + { + var culture = CultureInfo.CurrentCulture; + if (!int.TryParse(formatString, out var numDigits) || numDigits <= 0) return value.ToString(formatString, culture); + //Zero-pad the integer part + var strValue = value.ToString(culture); + var decIndex = culture.CompareInfo.IndexOf(strValue, culture.NumberFormat.NumberDecimalSeparator); + var zeroPad = decIndex == -1 ? int.Max(0, numDigits - strValue.Length) : int.Max(0, numDigits - decIndex); + + return new string('0', zeroPad) + strValue; + } + + public static string DateTimeFormatter(ITemplateTag _, DateTime value, string formatString) + { + var culture = CultureInfo.CurrentCulture; + if (string.IsNullOrEmpty(formatString)) + formatString = CommonFormatters.DefaultDateFormat; + return value.ToString(formatString, culture); + } + + public static string LanguageShortFormatter(string? language) + { + if (language is null) + return ""; + + language = language.Trim(); + if (language.Length <= 3) + return language.ToUpper(); + return language[..3].ToUpper(); + } +} \ No newline at end of file diff --git a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs index 74910eb3..2aa01fb3 100644 --- a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs @@ -5,11 +5,10 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Text.RegularExpressions; +using PF = FileManager.NamingTemplate.CommonFormatters; namespace FileManager.NamingTemplate; -public delegate string PropertyFormatter(ITemplateTag templateTag, T value, string formatString); - public class PropertyTagCollection : TagCollection { private readonly Dictionary _defaultFormatters = new(); @@ -39,7 +38,7 @@ public class PropertyTagCollection : TagCollection /// Optional formatting function that accepts the property /// and a formatting string and returns the value the formatted string. If null, use the default /// formatter if present, or - public void Add(ITemplateTag templateTag, Func propertyGetter, PropertyFormatter? formatter = null) + public void Add(ITemplateTag templateTag, Func propertyGetter, PF.PropertyFormatter? formatter = null) where TProperty : struct => RegisterWithFormatter(templateTag, propertyGetter, formatter); @@ -63,7 +62,7 @@ public class PropertyTagCollection : TagCollection /// Optional formatting function that accepts the property /// and a formatting string and returns the value formatted to string. If null, use the default /// formatter if present, or - public void Add(ITemplateTag templateTag, Func propertyGetter, PropertyFormatter? formatter = null) + public void Add(ITemplateTag templateTag, Func propertyGetter, PF.PropertyFormatter? formatter = null) => RegisterWithFormatter(templateTag, propertyGetter, formatter); /// @@ -77,7 +76,7 @@ public class PropertyTagCollection : TagCollection => RegisterWithToString(templateTag, propertyGetter, toString); private void RegisterWithFormatter - (ITemplateTag templateTag, Func propertyGetter, PropertyFormatter? formatter) + (ITemplateTag templateTag, Func propertyGetter, PF.PropertyFormatter? formatter) { ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag)); ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter)); @@ -102,12 +101,12 @@ public class PropertyTagCollection : TagCollection private static string ToStringFunc(T propertyValue) => propertyValue?.ToString() ?? ""; - private PropertyFormatter? GetDefaultFormatter() + private PF.PropertyFormatter? GetDefaultFormatter() { try { var del = _defaultFormatters.FirstOrDefault(kvp => kvp.Key == typeof(T)).Value; - return del is null ? null : Delegate.CreateDelegate(typeof(PropertyFormatter), del.Target, del.Method) as PropertyFormatter; + return del is null ? null : Delegate.CreateDelegate(typeof(PF.PropertyFormatter), del.Target, del.Method) as PF.PropertyFormatter; } catch { return null; } } @@ -136,7 +135,7 @@ public class PropertyTagCollection : TagCollection public override Regex NameMatcher { get; } private Func CreateToStringExpression { get; } - public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PropertyFormatter formatter) + public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PF.PropertyFormatter formatter) : base(templateTag, propertyGetter) { NameMatcher = new Regex(@$"^<{templateTag.TagName.Replace(" ", "\\s*?")}\s*?(?:\[([^\[\]]*?)\]\s*?)?>", options | RegexOptions.Compiled); diff --git a/Source/LibationFileManager/Templates/SeriesOrder.cs b/Source/LibationFileManager/Templates/SeriesOrder.cs index e739947d..ed896a0b 100644 --- a/Source/LibationFileManager/Templates/SeriesOrder.cs +++ b/Source/LibationFileManager/Templates/SeriesOrder.cs @@ -22,7 +22,7 @@ public class SeriesOrder : IFormattable public string ToString(string? format, IFormatProvider? formatProvider) => string.Concat(OrderParts.Select(p => p switch { - float f => f.ToString(format), + float f => f.ToString(format, formatProvider ?? CultureInfo.InvariantCulture), _ => p.ToString(), })).Trim(); diff --git a/Source/LibationFileManager/Templates/TemplateTags.cs b/Source/LibationFileManager/Templates/TemplateTags.cs index fe4d5774..5cef98c7 100644 --- a/Source/LibationFileManager/Templates/TemplateTags.cs +++ b/Source/LibationFileManager/Templates/TemplateTags.cs @@ -4,7 +4,6 @@ namespace LibationFileManager.Templates; public sealed class TemplateTags : ITemplateTag { - public const string DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; public string TagName { get; } public string DefaultValue { get; } public string Description { get; } @@ -48,9 +47,11 @@ public sealed class TemplateTags : ITemplateTag public static TemplateTags Language { get; } = new("language", "Book's language"); public static TemplateTags LanguageShort { get; } = new("language short", "Book's language abbreviated. Eg: ENG"); - public static TemplateTags FileDate { get; } = new TemplateTags("file date", "File date/time. e.g. yyyy-MM-dd HH-mm", $"", ""); - public static TemplateTags DatePublished { get; } = new TemplateTags("pub date", "Publication date. e.g. yyyy-MM-dd", $"", ""); - public static TemplateTags DateAdded { get; } = new TemplateTags("date added", "Date added to your Audible account. e.g. yyyy-MM-dd", $"", ""); + public static TemplateTags FileDate { get; } = new TemplateTags("file date", "File date/time. e.g. yyyy-MM-dd HH-mm", $"", ""); + public static TemplateTags DatePublished { get; } = new TemplateTags("pub date", "Publication date. e.g. yyyy-MM-dd", $"", ""); + + public static TemplateTags DateAdded { get; } = + new TemplateTags("date added", "Date added to your Audible account. e.g. yyyy-MM-dd", $"", ""); public static TemplateTags IfSeries { get; } = new TemplateTags("if series", "Only include if part of a book series or podcast", "<-if series>", "...<-if series>"); public static TemplateTags IfPodcast { get; } = new TemplateTags("if podcast", "Only include if part of a podcast", "<-if podcast>", "...<-if podcast>"); public static TemplateTags IfPodcastParent { get; } = new TemplateTags("if podcastparent", "Only include if item is a podcast series parent", "<-if podcastparent>", "...<-if podcastparent>"); diff --git a/Source/LibationFileManager/Templates/Templates.cs b/Source/LibationFileManager/Templates/Templates.cs index 2d8265cc..ae2929b2 100644 --- a/Source/LibationFileManager/Templates/Templates.cs +++ b/Source/LibationFileManager/Templates/Templates.cs @@ -252,7 +252,7 @@ public abstract class Templates #region Registered Template Properties private static readonly PropertyTagCollection filePropertyTags = - new(caseSensitive: true, StringFormatter, DateTimeFormatter, IntegerFormatter, FloatFormatter) + new(caseSensitive: true, CommonFormatters.StringFormatter, CommonFormatters.DateTimeFormatter, CommonFormatters.IntegerFormatter, CommonFormatters.FloatFormatter) { //Don't allow formatting of Id { TemplateTags.Id, lb => lb.AudibleProductId, v => v }, @@ -261,15 +261,15 @@ public abstract class Templates { TemplateTags.AudibleTitle, lb => lb.Title }, { TemplateTags.AudibleSubtitle, lb => lb.Subtitle }, { TemplateTags.Author, lb => lb.Authors, NameListFormat.Formatter }, - { TemplateTags.FirstAuthor, lb => lb.FirstAuthor, FormattableFormatter }, + { TemplateTags.FirstAuthor, lb => lb.FirstAuthor, CommonFormatters.FormattableFormatter }, { TemplateTags.Narrator, lb => lb.Narrators, NameListFormat.Formatter }, - { TemplateTags.FirstNarrator, lb => lb.FirstNarrator, FormattableFormatter }, + { TemplateTags.FirstNarrator, lb => lb.FirstNarrator, CommonFormatters.FormattableFormatter }, { TemplateTags.Series, lb => lb.Series, SeriesListFormat.Formatter }, - { TemplateTags.FirstSeries, lb => lb.FirstSeries, FormattableFormatter }, - { TemplateTags.SeriesNumber, lb => lb.FirstSeries?.Order, FormattableFormatter }, + { TemplateTags.FirstSeries, lb => lb.FirstSeries, CommonFormatters.FormattableFormatter }, + { TemplateTags.SeriesNumber, lb => lb.FirstSeries?.Order, CommonFormatters.FormattableFormatter }, { TemplateTags.Language, lb => lb.Language }, //Don't allow formatting of LanguageShort - { TemplateTags.LanguageShort, lb => lb.Language, GetLanguageShort }, + { TemplateTags.LanguageShort, lb => lb.Language, CommonFormatters.LanguageShortFormatter }, { TemplateTags.Account, lb => lb.Account }, { TemplateTags.AccountNickname, lb => lb.AccountNickname }, { TemplateTags.Locale, lb => lb.Locale }, @@ -280,7 +280,7 @@ public abstract class Templates }; private static readonly PropertyTagCollection audioFilePropertyTags = - new(caseSensitive: true, StringFormatter, IntegerFormatter) + new(caseSensitive: true, CommonFormatters.StringFormatter, CommonFormatters.IntegerFormatter) { { TemplateTags.Bitrate, lb => lb.BitRate }, { TemplateTags.SampleRate, lb => lb.SampleRate }, @@ -292,17 +292,16 @@ public abstract class Templates private static readonly List chapterPropertyTags = [ - new PropertyTagCollection(caseSensitive: true, StringFormatter) + new PropertyTagCollection(caseSensitive: true, CommonFormatters.StringFormatter) { { TemplateTags.Title, lb => lb.TitleWithSubtitle }, { TemplateTags.TitleShort, lb => GetTitleShort(lb.Title) }, { TemplateTags.AudibleTitle, lb => lb.Title }, { TemplateTags.AudibleSubtitle, lb => lb.Subtitle }, { TemplateTags.Series, lb => lb.Series, SeriesListFormat.Formatter }, - { TemplateTags.FirstSeries, lb => lb.FirstSeries, FormattableFormatter }, + { TemplateTags.FirstSeries, lb => lb.FirstSeries, CommonFormatters.FormattableFormatter }, }, - - new PropertyTagCollection(caseSensitive: true, StringFormatter, IntegerFormatter, DateTimeFormatter) + new PropertyTagCollection(caseSensitive: true, CommonFormatters.StringFormatter, CommonFormatters.IntegerFormatter, CommonFormatters.DateTimeFormatter) { { TemplateTags.ChCount, m => m.PartsTotal }, { TemplateTags.ChNumber, m => m.PartsPosition }, @@ -361,67 +360,17 @@ public abstract class Templates private static string? GetTitleShort(string? title) => title?.IndexOf(':') > 0 ? title.Substring(0, title.IndexOf(':')) : title; - private static string GetLanguageShort(string? language) - { - if (language is null) - return ""; - - language = language.Trim(); - if (language.Length <= 3) - return language.ToUpper(); - return language[..3].ToUpper(); - } - - private static string FormattableFormatter(ITemplateTag _, IFormattable? value, string formatString) - => value?.ToString(formatString, null) ?? ""; - - private static string StringFormatter(ITemplateTag _, string? value, string formatString) - { - if (value is null) return ""; - var culture = CultureInfo.CurrentCulture; - - return formatString switch - { - "u" or "U" => value.ToUpper(culture), - "l" or "L" => value.ToLower(culture), - _ => value, - }; - } - - private static string IntegerFormatter(ITemplateTag templateTag, int value, string formatString) - => FloatFormatter(templateTag, value, formatString); - - private static string FloatFormatter(ITemplateTag _, float value, string formatString) - { - var culture = CultureInfo.CurrentCulture; - if (!int.TryParse(formatString, out var numDigits) || numDigits <= 0) return value.ToString(formatString, culture); - //Zero-pad the integer part - var strValue = value.ToString(culture); - var decIndex = culture.CompareInfo.IndexOf(strValue, culture.NumberFormat.NumberDecimalSeparator); - var zeroPad = decIndex == -1 ? int.Max(0, numDigits - strValue.Length) : int.Max(0, numDigits - decIndex); - - return new string('0', zeroPad) + strValue; - } - - private static string DateTimeFormatter(ITemplateTag _, DateTime value, string formatString) - { - var culture = CultureInfo.CurrentCulture; - if (string.IsNullOrEmpty(formatString)) - formatString = TemplateTags.DEFAULT_DATE_FORMAT; - return value.ToString(formatString, culture); - } - #endregion public class FolderTemplate : Templates, ITemplate { public static string Name { get; } = "Folder Template"; - public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)) ?? ""; + public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FolderTemplate)); public static string DefaultTemplate { get; } = " [<id>]"; public static IEnumerable<TagCollection> TagCollections { get; } = [filePropertyTags, audioFilePropertyTags, conditionalTags, folderConditionalTags, combinedConditionalTags]; public override IEnumerable<string> Errors - => TemplateText?.Length >= 2 && Path.IsPathFullyQualified(TemplateText) ? base.Errors.Append(ErrorFullPathIsInvalid) : base.Errors; + => TemplateText.Length >= 2 && Path.IsPathFullyQualified(TemplateText) ? base.Errors.Append(ErrorFullPathIsInvalid) : base.Errors; protected override List<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements) => parts @@ -435,7 +384,7 @@ public abstract class Templates public class FileTemplate : Templates, ITemplate { public static string Name { get; } = "File Template"; - public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate)) ?? ""; + public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.FileTemplate)); public static string DefaultTemplate { get; } = "<title> [<id>]"; public static IEnumerable<TagCollection> TagCollections { get; } = [filePropertyTags, audioFilePropertyTags, conditionalTags, combinedConditionalTags]; } @@ -443,7 +392,7 @@ public abstract class Templates public class ChapterFileTemplate : Templates, ITemplate { public static string Name { get; } = "Chapter File Template"; - public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)) ?? ""; + public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)); public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>"; public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(filePropertyTags).Append(audioFilePropertyTags).Append(conditionalTags).Append(combinedConditionalTags); @@ -457,7 +406,7 @@ public abstract class Templates public class ChapterTitleTemplate : Templates, ITemplate { public static string Name { get; } = "Chapter Title Template"; - public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate)) ?? ""; + public static string Description { get; } = Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate)); public static string DefaultTemplate => "<ch#> - <title short>: <ch title>"; public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(conditionalTags).Append(combinedConditionalTags); diff --git a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs index 20817eb6..bc3d6830 100644 --- a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs +++ b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs @@ -280,15 +280,7 @@ public class GetPortionFilename string FormatString(ITemplateTag templateTag, string? value, string format) { - if (value is null) return ""; - var culture = CultureInfo.CurrentCulture; - - return format switch - { - "u" or "U" => value.ToUpper(culture), - "l" or "L" => value.ToLower(culture), - _ => value, - }; + return CommonFormatters.StringFormatter(templateTag, value, format); } } }