Collect formatters into CommonFormatters

This commit is contained in:
Jo-Be-Co
2026-03-11 01:47:02 +01:00
parent 7dd55a2545
commit a6aa14c7f8
6 changed files with 92 additions and 88 deletions

View File

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

View File

@@ -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<T>(ITemplateTag templateTag, T value, string formatString);
public class PropertyTagCollection<TClass> : TagCollection
{
private readonly Dictionary<Type, MulticastDelegate> _defaultFormatters = new();
@@ -39,7 +38,7 @@ public class PropertyTagCollection<TClass> : TagCollection
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
/// and a formatting string and returns the value the formatted string. If <c>null</c>, use the default
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, PropertyFormatter<TProperty>? formatter = null)
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, PF.PropertyFormatter<TProperty>? formatter = null)
where TProperty : struct
=> RegisterWithFormatter(templateTag, propertyGetter, formatter);
@@ -63,7 +62,7 @@ public class PropertyTagCollection<TClass> : TagCollection
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
/// and a formatting string and returns the value formatted to string. If <c>null</c>, use the default
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TProperty>? formatter = null)
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PF.PropertyFormatter<TProperty>? formatter = null)
=> RegisterWithFormatter(templateTag, propertyGetter, formatter);
/// <summary>
@@ -77,7 +76,7 @@ public class PropertyTagCollection<TClass> : TagCollection
=> RegisterWithToString(templateTag, propertyGetter, toString);
private void RegisterWithFormatter<TProperty, TPropertyValue>
(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TPropertyValue>? formatter)
(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PF.PropertyFormatter<TPropertyValue>? formatter)
{
ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag));
ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter));
@@ -102,12 +101,12 @@ public class PropertyTagCollection<TClass> : TagCollection
private static string ToStringFunc<T>(T propertyValue) => propertyValue?.ToString() ?? "";
private PropertyFormatter<T>? GetDefaultFormatter<T>()
private PF.PropertyFormatter<T>? GetDefaultFormatter<T>()
{
try
{
var del = _defaultFormatters.FirstOrDefault(kvp => kvp.Key == typeof(T)).Value;
return del is null ? null : Delegate.CreateDelegate(typeof(PropertyFormatter<T>), del.Target, del.Method) as PropertyFormatter<T>;
return del is null ? null : Delegate.CreateDelegate(typeof(PF.PropertyFormatter<T>), del.Target, del.Method) as PF.PropertyFormatter<T>;
}
catch { return null; }
}
@@ -136,7 +135,7 @@ public class PropertyTagCollection<TClass> : TagCollection
public override Regex NameMatcher { get; }
private Func<Expression, string, Expression> CreateToStringExpression { get; }
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PropertyFormatter<TPropertyValue> formatter)
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PF.PropertyFormatter<TPropertyValue> formatter)
: base(templateTag, propertyGetter)
{
NameMatcher = new Regex(@$"^<{templateTag.TagName.Replace(" ", "\\s*?")}\s*?(?:\[([^\[\]]*?)\]\s*?)?>", options | RegexOptions.Compiled);

View File

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

View File

@@ -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", $"<file date [{DEFAULT_DATE_FORMAT}]>", "<file date [...]>");
public static TemplateTags DatePublished { get; } = new TemplateTags("pub date", "Publication date. e.g. yyyy-MM-dd", $"<pub date [{DEFAULT_DATE_FORMAT}]>", "<pub date [...]>");
public static TemplateTags DateAdded { get; } = new TemplateTags("date added", "Date added to your Audible account. e.g. yyyy-MM-dd", $"<date added [{DEFAULT_DATE_FORMAT}]>", "<date added [...]>");
public static TemplateTags FileDate { get; } = new TemplateTags("file date", "File date/time. e.g. yyyy-MM-dd HH-mm", $"<file date [{CommonFormatters.DefaultDateFormat}]>", "<file date [...]>");
public static TemplateTags DatePublished { get; } = new TemplateTags("pub date", "Publication date. e.g. yyyy-MM-dd", $"<pub date [{CommonFormatters.DefaultDateFormat}]>", "<pub date [...]>");
public static TemplateTags DateAdded { get; } =
new TemplateTags("date added", "Date added to your Audible account. e.g. yyyy-MM-dd", $"<date added [{CommonFormatters.DefaultDateFormat}]>", "<date added [...]>");
public static TemplateTags IfSeries { get; } = new TemplateTags("if series", "Only include if part of a book series or podcast", "<if series-><-if series>", "<if series->...<-if series>");
public static TemplateTags IfPodcast { get; } = new TemplateTags("if podcast", "Only include if part of a podcast", "<if podcast-><-if 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>", "<if podcastparent->...<-if podcastparent>");

View File

@@ -252,7 +252,7 @@ public abstract class Templates
#region Registered Template Properties
private static readonly PropertyTagCollection<LibraryBookDto> 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<LibraryBookDto> 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<TagCollection> chapterPropertyTags =
[
new PropertyTagCollection<LibraryBookDto>(caseSensitive: true, StringFormatter)
new PropertyTagCollection<LibraryBookDto>(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<MultiConvertFileProperties>(caseSensitive: true, StringFormatter, IntegerFormatter, DateTimeFormatter)
new PropertyTagCollection<MultiConvertFileProperties>(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; } = "<title short> [<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);

View File

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