From 9a80f18e1c13b2ba4f46a44e77d3589f6a355f8b Mon Sep 17 00:00:00 2001 From: Michael Bucari-Tovo Date: Sat, 4 Feb 2023 12:51:48 -0700 Subject: [PATCH] Rename ConditionalTagClass to ConditionalTagCollection --- Documentation/NamingTemplates.md | 16 +-- ...cs => ConditionalTagCollection[TClass].cs} | 4 +- .../NamingTemplate/NamingTemplate.cs | 2 +- .../PropertyTagCollection[TClass].cs | 116 +++++++++--------- Source/LibationFileManager/Templates.cs | 23 ++-- .../FileNamingTemplateTests.cs | 24 +++- 6 files changed, 101 insertions(+), 84 deletions(-) rename Source/FileManager/NamingTemplate/{ConditionalTagClass[TClass].cs => ConditionalTagCollection[TClass].cs} (93%) diff --git a/Documentation/NamingTemplates.md b/Documentation/NamingTemplates.md index 02e93017..4c0fea50 100644 --- a/Documentation/NamingTemplates.md +++ b/Documentation/NamingTemplates.md @@ -23,7 +23,7 @@ These tags will be replaced in the template with the audiobook's values. |Tag|Description|Type| |-|-|-| -|\|Audible book ID (ASIN)|Text| +|\ **†**|Audible book ID (ASIN)|Text| |\|Full title|Text| |\|Title. Stop at first colon|Text| |\<author\>|Author(s)|Text| @@ -39,16 +39,18 @@ These tags will be replaced in the template with the audiobook's values. |\<locale\>|Region/country|Text| |\<year\>|Year published|Integer| |\<language\>|Book's language|Text| -|\<language short\>|Book's language abbreviated. Eg: ENG|Text| +|\<language short\> **†**|Book's language abbreviated. Eg: ENG|Text| |\<file date\>|File creation date/time.|DateTime| |\<pub date\>|Audiobook publication date|DateTime| |\<date added\>|Date the book added to your Audible account|DateTime| -|\<ch count\>|Number of chapters **†**|Integer| -|\<ch title\>|Chapter title **†**|Text| -|\<ch#\>|Chapter number **†**|Integer| -|\<ch# 0\>|Chapter number with leading zeros **†**|Integer| +|\<ch count\> **‡**|Number of chapters|Integer| +|\<ch title\> **‡**|Chapter title|Text| +|\<ch#\> **‡**|Chapter number|Integer| +|\<ch# 0\> **‡**|Chapter number with leading zeros|Integer| -**†** Only valid for Chapter Filename and Chapter Tile Metadata +**†** Does not support custom formatting + +**‡** Only valid for Chapter Filename and Chapter Tile Metadata To change how these properties are displayed, [read about custom formatters](#tag-formatters) diff --git a/Source/FileManager/NamingTemplate/ConditionalTagClass[TClass].cs b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs similarity index 93% rename from Source/FileManager/NamingTemplate/ConditionalTagClass[TClass].cs rename to Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs index 7acb4fec..c2d2a8b1 100644 --- a/Source/FileManager/NamingTemplate/ConditionalTagClass[TClass].cs +++ b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs @@ -20,9 +20,9 @@ internal interface IClosingPropertyTag : IPropertyTag bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag propertyTag); } -public class ConditionalTagClass<TClass> : TagCollection +public class ConditionalTagCollection<TClass> : TagCollection { - public ConditionalTagClass(bool caseSensative = true) :base(typeof(TClass), caseSensative) { } + public ConditionalTagCollection(bool caseSensative = true) :base(typeof(TClass), caseSensative) { } /// <summary> /// Register a conditional tag. diff --git a/Source/FileManager/NamingTemplate/NamingTemplate.cs b/Source/FileManager/NamingTemplate/NamingTemplate.cs index 646166c5..8e4a3d0c 100644 --- a/Source/FileManager/NamingTemplate/NamingTemplate.cs +++ b/Source/FileManager/NamingTemplate/NamingTemplate.cs @@ -27,7 +27,7 @@ public class NamingTemplate /// <summary> /// Invoke the <see cref="NamingTemplate"/> to /// </summary> - /// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagClass{TClass}"/></param> + /// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param> /// <returns></returns> public TemplatePart Evaluate(params object[] propertyClasses) { diff --git a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs index 32d0ee0a..58683634 100644 --- a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs @@ -11,9 +11,9 @@ public delegate string PropertyFormatter<T>(ITemplateTag templateTag, T value, s public class PropertyTagCollection<TClass> : TagCollection { - private readonly Dictionary<Type, Delegate> defaultFormatters = new(); + private readonly Dictionary<Type, MulticastDelegate> defaultFormatters = new(); - public PropertyTagCollection(bool caseSensative = true, params Delegate[] defaultFormatters) : base(typeof(TClass), caseSensative) + public PropertyTagCollection(bool caseSensative = true, params MulticastDelegate[] defaultFormatters) : base(typeof(TClass), caseSensative) { foreach (var formatter in defaultFormatters) { @@ -30,101 +30,94 @@ public class PropertyTagCollection<TClass> : TagCollection } /// <summary> - /// Register a nullable value type property. + /// Register a nullable value type <typeparamref name="TClass"/> property. /// </summary> - /// <typeparam name="U">Type of the property from <see cref="TClass"/></typeparam> + /// <typeparam name="TProperty">Type of the property from <see cref="TClass"/></typeparam> /// <param name="propertyGetter">A Func to get the property value from <see cref="TClass"/></param> - /// <param name="formatter">Optional formatting function that accepts the <typeparamref name="U"/> property + /// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property /// and a formatting string and returnes the value the formatted string. If <see cref="null"/>, use the default - /// <typeparamref name="U"/> formatter if present, or <see cref="object.ToString"/></param> - public void Add<U>(ITemplateTag templateTag, Func<TClass, U?> propertyGetter, PropertyFormatter<U> formatter = null) - where U : struct + /// <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) + where TProperty : struct => RegisterWithFormatter(templateTag, propertyGetter, formatter); /// <summary> - /// Register a nullable value type property. + /// Register a nullable value type <typeparamref name="TClass"/> property. /// </summary> /// <param name="propertyGetter">A Func to get the string property from <see cref="TClass"/></param> - /// <param name="toString">ToString function that accepts the <typeparamref name="U"/> property and returnes a string</param> - public void Add<U>(ITemplateTag templateTag, Func<TClass, U?> propertyGetter, Func<U, string> toString) - where U : struct + /// <param name="toString">ToString function that accepts the <typeparamref name="TProperty"/> property and returnes a string</param> + public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, Func<TProperty, string> toString) + where TProperty : struct => RegisterWithToString(templateTag, propertyGetter, toString); /// <summary> - /// Register a non-nullable value type property + /// Register a <typeparamref name="TClass"/> property /// </summary> - /// <typeparam name="U">Type of the property from <see cref="TClass"/></typeparam> + /// <typeparam name="TProperty">Type of the property from <see cref="TClass"/></typeparam> /// <param name="propertyGetter">A Func to get the property value from <see cref="TClass"/></param> - /// <param name="formatter">Optional formatting function that accepts the <typeparamref name="U"/> property + /// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property /// and a formatting string and returnes the value formatted to string. If <see cref="null"/>, use the default - /// <typeparamref name="U"/> formatter if present, or <see cref="object.ToString"/></param> - public void Add<U>(ITemplateTag templateTag, Func<TClass, U> propertyGetter, PropertyFormatter<U> formatter = null) - where U : struct + /// <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) => RegisterWithFormatter(templateTag, propertyGetter, formatter); /// <summary> - /// Register a non-nullable value type property. + /// Register a <typeparamref name="TClass"/> property. /// </summary> /// <param name="propertyGetter">A Func to get the string property from <see cref="TClass"/></param> - /// <param name="toString">ToString function that accepts the <typeparamref name="U"/> property and returnes a string</param> - public void Add<U>(ITemplateTag templateTag, Func<TClass, U> propertyGetter, Func<U, string> toString) - where U : struct + /// <param name="toString">ToString function that accepts the <typeparamref name="TProperty"/> property and returnes a string</param> + public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, Func<TProperty, string> toString) => RegisterWithToString(templateTag, propertyGetter, toString); - /// <summary> - /// Register a string type property - /// </summary> - /// <param name="propertyGetter">A Func to get the string property from <see cref="TClass"/></param> - /// <param name="formatter">Optional formatting function that accepts the string property and a formatting - /// string and returnes the value formatted to string. If <see cref="null"/>, use the default <see cref="string"/> - /// formatter if present, or <see cref="string.ToString"/></param> - public void Add(ITemplateTag templateTag, Func<TClass, string> propertyGetter, PropertyFormatter<string> formatter = null) - => RegisterWithFormatter(templateTag, propertyGetter, formatter); - - /// <summary> - /// Register a string type property. - /// </summary> - /// <param name="propertyGetter">A Func to get the string property from <see cref="TClass"/></param> - /// <param name="toString">ToString function that accepts the string property and returnes a string</param> - public void Add(ITemplateTag templateTag, Func<TClass, string> propertyGetter, Func<string, string> toString) - => RegisterWithToString(templateTag, propertyGetter, toString); - - private void RegisterWithFormatter<T,U>(ITemplateTag templateTag, Func<TClass, T> propertyGetter, PropertyFormatter<U> formatter) + private void RegisterWithFormatter<TProperty, TPropertyValue> + (ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TPropertyValue> formatter) { - static string ToStringFunc(U value) => value is string str ? str : value.ToString(); ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag)); ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter)); - var formatDelegate = formatter ?? defaultFormatters.FirstOrDefault(kvp => kvp.Key == typeof(U)).Value; + formatter ??= GetDefaultFormatter<TPropertyValue>(); - var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter); - - if (formatDelegate is null) - AddPropertyTag(PropertyTag.CreateWithToString(templateTag, Options, expr, ToStringFunc)); + if (formatter is null) + RegisterWithToString<TProperty, TPropertyValue>(templateTag, propertyGetter, null); else - AddPropertyTag(PropertyTag.CreateWithFormatter(templateTag, Options, expr, formatDelegate)); + { + var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter); + AddPropertyTag(PropertyTag.Create(templateTag, Options, expr, formatter)); + } } - private void RegisterWithToString<T,U>(ITemplateTag templateTag, Func<TClass, T> propertyGetter, Func<U, string> toString) + private PropertyFormatter<T> GetDefaultFormatter<T>() { - static string ToStringFunc(U value) => value is string str ? str : value.ToString(); + 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>; + } + catch + { + return null; + } + } + + private void RegisterWithToString<TProperty, TPropertyValue> + (ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, Func<TPropertyValue, string> toString) + { + static string ToStringFunc(TPropertyValue value) => value?.ToString() ?? ""; ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag)); ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter)); - toString ??= ToStringFunc; - var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter); - AddPropertyTag(PropertyTag.CreateWithToString(templateTag, Options, expr, toString)); + AddPropertyTag(PropertyTag.Create(templateTag, Options, expr, toString ?? ToStringFunc)); } private class PropertyTag : TagBase { private Func<Expression, string, Expression> CreateToStringExpression { get; init; } - private PropertyTag(ITemplateTag templateTag, Expression propertyExpression) : base(templateTag, propertyExpression) { } + private PropertyTag(ITemplateTag templateTag, Expression propertyGetter) : base(templateTag, propertyGetter) { } - public static PropertyTag CreateWithFormatter(ITemplateTag templateTag, RegexOptions options, Expression propertyExpression, Delegate formatter) + public static PropertyTag Create<TPropertyValue>(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PropertyFormatter<TPropertyValue> formatter) { - return new PropertyTag(templateTag, propertyExpression) + return new PropertyTag(templateTag, propertyGetter) { NameMatcher = new Regex(@$"^<{templateTag.TagName.Replace(" ", "\\s*?")}\s*?(?:\[([^\[\]]*?)\]\s*?)?>", options), CreateToStringExpression = (expVal, format) => @@ -137,9 +130,9 @@ public class PropertyTagCollection<TClass> : TagCollection }; } - public static PropertyTag CreateWithToString(ITemplateTag templateTag, RegexOptions options, Expression propertyExpression, Delegate toString) + public static PropertyTag Create<TPropertyValue>(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, Func<TPropertyValue, string> toString) { - return new PropertyTag(templateTag, propertyExpression) + return new PropertyTag(templateTag, propertyGetter) { NameMatcher = new Regex(@$"^<{templateTag.TagName}>", options), CreateToStringExpression = (expVal, _) => @@ -153,8 +146,11 @@ public class PropertyTagCollection<TClass> : TagCollection protected override Expression GetTagExpression(string exactName, string formatString) { Expression toStringExpression - = ReturnType == typeof(string) - ? CreateToStringExpression(Expression.Coalesce(ValueExpression, Expression.Constant("")), formatString) + = !ReturnType.IsValueType + ? Expression.Condition( + Expression.Equal(ValueExpression, Expression.Constant(null)), + Expression.Constant(""), + CreateToStringExpression(ValueExpression, formatString)) : Nullable.GetUnderlyingType(ReturnType) is null ? CreateToStringExpression(ValueExpression, formatString) : Expression.Condition( diff --git a/Source/LibationFileManager/Templates.cs b/Source/LibationFileManager/Templates.cs index 09393b55..df672814 100644 --- a/Source/LibationFileManager/Templates.cs +++ b/Source/LibationFileManager/Templates.cs @@ -12,7 +12,7 @@ namespace LibationFileManager public interface ITemplate { static abstract string DefaultTemplate { get; } - static abstract IEnumerable<TagCollection> TagClass { get; } + static abstract IEnumerable<TagCollection> TagCollections { get; } } public abstract class Templates @@ -40,14 +40,14 @@ namespace LibationFileManager public static bool TryGetTemplate<T>(string templateText, out T template) where T : Templates, ITemplate, new() { - var namingTemplate = NamingTemplate.Parse(templateText, T.TagClass); + var namingTemplate = NamingTemplate.Parse(templateText, T.TagCollections); template = new() { Template = namingTemplate }; return !namingTemplate.Errors.Any(); } private static T GetDefaultTemplate<T>() where T : Templates, ITemplate, new() - => new() { Template = NamingTemplate.Parse(T.DefaultTemplate, T.TagClass) }; + => new() { Template = NamingTemplate.Parse(T.DefaultTemplate, T.TagCollections) }; static Templates() { @@ -197,7 +197,7 @@ namespace LibationFileManager //Don't allow formatting of Id { TemplateTags.Id, lb => lb.AudibleProductId, v => v }, { TemplateTags.Title, lb => lb.Title }, - { TemplateTags.TitleShort, lb => lb.Title.IndexOf(':') < 1 ? lb.Title : lb.Title.Substring(0, lb.Title.IndexOf(':')) }, + { TemplateTags.TitleShort, lb => getTitleShort(lb.Title) }, { TemplateTags.Author, lb => lb.AuthorNames }, { TemplateTags.FirstAuthor, lb => lb.FirstAuthor }, { TemplateTags.Narrator, lb => lb.NarratorNames }, @@ -223,7 +223,7 @@ namespace LibationFileManager new PropertyTagCollection<LibraryBookDto>(caseSensative: true, StringFormatter) { { TemplateTags.Title, lb => lb.Title }, - { TemplateTags.TitleShort, lb => lb ?.Title ?.IndexOf(':') > 0 ? lb.Title.Substring(0, lb.Title.IndexOf(':')) : lb.Title }, + { TemplateTags.TitleShort, lb => getTitleShort(lb.Title) }, { TemplateTags.Series, lb => lb.SeriesName }, }, new PropertyTagCollection<MultiConvertFileProperties>(caseSensative: true, StringFormatter, IntegerFormatter, DateTimeFormatter) @@ -236,7 +236,7 @@ namespace LibationFileManager } }; - private static readonly ConditionalTagClass<LibraryBookDto> conditionalTags = new() + private static readonly ConditionalTagCollection<LibraryBookDto> conditionalTags = new() { { TemplateTags.IfSeries, lb => lb.IsSeries }, { TemplateTags.IfPodcast, lb => lb.IsPodcast }, @@ -247,6 +247,9 @@ namespace LibationFileManager #region Tag Formatters + 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) @@ -286,7 +289,7 @@ namespace LibationFileManager public override string Name => "Folder Template"; public override string Description => Configuration.GetDescription(nameof(Configuration.FolderTemplate)); public static string DefaultTemplate { get; } = "<title short> [<id>]"; - public static IEnumerable<TagCollection> TagClass => new TagCollection[] { filePropertyTags, conditionalTags }; + public static IEnumerable<TagCollection> TagCollections => new TagCollection[] { filePropertyTags, conditionalTags }; public override IEnumerable<string> Errors => TemplateText?.Length >= 2 && Path.IsPathFullyQualified(TemplateText) ? base.Errors.Append(ERROR_FULL_PATH_IS_INVALID) : base.Errors; @@ -305,7 +308,7 @@ namespace LibationFileManager public override string Name => "File Template"; public override string Description => Configuration.GetDescription(nameof(Configuration.FileTemplate)); public static string DefaultTemplate { get; } = "<title> [<id>]"; - public static IEnumerable<TagCollection> TagClass { get; } = new TagCollection[] { filePropertyTags, conditionalTags }; + public static IEnumerable<TagCollection> TagCollections { get; } = new TagCollection[] { filePropertyTags, conditionalTags }; } public class ChapterFileTemplate : Templates, ITemplate @@ -313,7 +316,7 @@ namespace LibationFileManager public override string Name => "Chapter File Template"; public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterFileTemplate)); public static string DefaultTemplate { get; } = "<title> [<id>] - <ch# 0> - <ch title>"; - public static IEnumerable<TagCollection> TagClass { get; } = chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags); + public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(filePropertyTags).Append(conditionalTags); public override IEnumerable<string> Warnings => Template.TagsInUse.Any(t => t.TagName.In(TemplateTags.ChNumber.TagName, TemplateTags.ChNumber0.TagName)) @@ -326,7 +329,7 @@ namespace LibationFileManager public override string Name => "Chapter Title Template"; public override string Description => Configuration.GetDescription(nameof(Configuration.ChapterTitleTemplate)); public static string DefaultTemplate => "<ch#> - <title short>: <ch title>"; - public static IEnumerable<TagCollection> TagClass { get; } = chapterPropertyTags.Append(conditionalTags); + public static IEnumerable<TagCollection> TagCollections { get; } = chapterPropertyTags.Append(conditionalTags); protected override IEnumerable<string> GetTemplatePartsStrings(List<TemplatePart> parts, ReplacementCharacters replacements) => parts.Select(p => p.Value); diff --git a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs index 6f9e70ac..4470436b 100644 --- a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs +++ b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs @@ -33,9 +33,17 @@ namespace NamingTemplateTests public string Item2 { get; set; } public string Item3 { get; set; } public string Item4 { get; set; } + public ReferenceType RefType { get; set; } public int? Int2 { get; set; } public bool Condition { get; set; } } + class ReferenceType + { + public override string ToString() + { + return nameof(ReferenceType); + } + } [TestClass] @@ -55,22 +63,23 @@ namespace NamingTemplateTests { new TemplateTag { TagName = "item3" }, i => i.Item3 }, { new TemplateTag { TagName = "item4" }, i => i.Item4 }, }; - PropertyTagCollection<PropertyClass3> props3 = new() + PropertyTagCollection<PropertyClass3> props3 = new(true, GetVal) { { new TemplateTag { TagName = "item3_1" }, i => i.Item1 }, { new TemplateTag { TagName = "item3_2" }, i => i.Item2 }, { new TemplateTag { TagName = "item3_3" }, i => i.Item3 }, { new TemplateTag { TagName = "item3_4" }, i => i.Item4 }, + { new TemplateTag { TagName = "reftype" }, i => i.RefType }, }; - ConditionalTagClass<PropertyClass1> conditional1 = new() + ConditionalTagCollection<PropertyClass1> conditional1 = new() { { new TemplateTag { TagName = "ifc1" }, i => i.Condition }, }; - ConditionalTagClass<PropertyClass2> conditional2 = new() + ConditionalTagCollection<PropertyClass2> conditional2 = new() { { new TemplateTag { TagName = "ifc2" }, i => i.Condition }, }; - ConditionalTagClass<PropertyClass3> conditional3 = new() + ConditionalTagCollection<PropertyClass3> conditional3 = new() { { new TemplateTag { TagName = "ifc3" }, i => i.Condition }, }; @@ -143,6 +152,12 @@ namespace NamingTemplateTests template.Errors.Should().HaveCount(0); template.Warnings.Should().BeEquivalentTo(warnings); } + + static string GetVal(ITemplateTag templateTag, ReferenceType referenceType, string format) + { + return ""; + } + [TestMethod] [DataRow("<int1>", "55")] [DataRow("<int1[]>", "55")] @@ -158,6 +173,7 @@ namespace NamingTemplateTests [DataRow("<item2_2_null>", "")] [DataRow("<item2_2_null[]>", "")] [DataRow("<item2_2_null[l]>", "")] + [DataRow("<reftype[l]>", "")] public void formatting(string inStr, string outStr) { props1.Add(new TemplateTag { TagName = "int1" }, i => i.Int1, formatInt);