From d161bdfaebd2fde866c3510798640592ab767c2a Mon Sep 17 00:00:00 2001 From: Jo-Be-Co Date: Thu, 12 Mar 2026 02:58:35 +0100 Subject: [PATCH] Introduce culture parameter for formatting. --- .../NamingTemplate/CommonFormatters.cs | 32 ++++---- .../ConditionalTagCollection[TClass].cs | 6 +- .../NamingTemplate/NamingTemplate.cs | 28 +++++-- .../PropertyTagCollection[TClass].cs | 18 +++-- .../NamingTemplate/TagCollection.cs | 6 +- .../Templates/ContributorDto.cs | 2 +- .../Templates/IListFormat[TList].cs | 78 ++++++++++++------- .../Templates/NameListFormat.cs | 5 +- .../Templates/SeriesDto.cs | 2 +- .../Templates/SeriesListFormat.cs | 5 +- .../Templates/Templates.cs | 33 +++++--- .../FileNamingTemplateTests.cs | 32 ++++---- .../TemplatesTests.cs | 68 +++++++++------- 13 files changed, 193 insertions(+), 122 deletions(-) diff --git a/Source/FileManager/NamingTemplate/CommonFormatters.cs b/Source/FileManager/NamingTemplate/CommonFormatters.cs index fbf7ba9e..6168a295 100644 --- a/Source/FileManager/NamingTemplate/CommonFormatters.cs +++ b/Source/FileManager/NamingTemplate/CommonFormatters.cs @@ -9,12 +9,12 @@ public static partial class CommonFormatters { public const string DefaultDateFormat = "yyyy-MM-dd"; - public delegate string PropertyFormatter(ITemplateTag templateTag, T value, string formatString); + public delegate string PropertyFormatter(ITemplateTag templateTag, T? value, string formatString, CultureInfo? culture); - public static string StringFormatter(ITemplateTag _, string? value, string formatString) + public static string StringFormatter(ITemplateTag _, string? value, string formatString, CultureInfo? culture) { if (value is null) return ""; - var culture = CultureInfo.CurrentCulture; + culture ??= CultureInfo.CurrentCulture; return formatString switch { @@ -24,15 +24,15 @@ public static partial class CommonFormatters }; } - public static string FormattableFormatter(ITemplateTag _, IFormattable? value, string formatString) - => value?.ToString(formatString, null) ?? ""; + public static string FormattableFormatter(ITemplateTag _, IFormattable? value, string formatString, CultureInfo? culture) + => value?.ToString(formatString, culture) ?? ""; - public static string IntegerFormatter(ITemplateTag templateTag, int value, string formatString) - => FloatFormatter(templateTag, value, formatString); + public static string IntegerFormatter(ITemplateTag templateTag, int value, string formatString, CultureInfo? culture) + => FloatFormatter(templateTag, value, formatString, culture); - public static string FloatFormatter(ITemplateTag _, float value, string formatString) + public static string FloatFormatter(ITemplateTag _, float value, string formatString, CultureInfo? culture) { - var culture = CultureInfo.CurrentCulture; + 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); @@ -42,22 +42,22 @@ public static partial class CommonFormatters return new string('0', zeroPad) + strValue; } - public static string DateTimeFormatter(ITemplateTag _, DateTime value, string formatString) + public static string DateTimeFormatter(ITemplateTag _, DateTime value, string formatString, CultureInfo? culture) { - var culture = CultureInfo.CurrentCulture; + culture ??= CultureInfo.InvariantCulture; if (string.IsNullOrEmpty(formatString)) - formatString = CommonFormatters.DefaultDateFormat; + formatString = DefaultDateFormat; return value.ToString(formatString, culture); } - public static string LanguageShortFormatter(string? language) + public static string LanguageShortFormatter(ITemplateTag templateTag, string? language, string formatString, CultureInfo? culture) { if (language is null) return ""; language = language.Trim(); - if (language.Length <= 3) - return language.ToUpper(); - return language[..3].ToUpper(); + if (language.Length > 3) language = language[..3]; + + return StringFormatter(templateTag, language, formatString, culture); } } \ No newline at end of file diff --git a/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs index 8196bd03..ce98ce10 100644 --- a/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/ConditionalTagCollection[TClass].cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq.Expressions; using System.Text.RegularExpressions; @@ -21,7 +22,7 @@ internal interface IClosingPropertyTag : IPropertyTag bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag); } -public delegate bool Conditional(ITemplateTag templateTag, T value, string condition); +public delegate bool Conditional(ITemplateTag templateTag, T value, string condition, CultureInfo? culture); public class ConditionalTagCollection(bool caseSensitive = true) : TagCollection(typeof(TClass), caseSensitive) { @@ -74,7 +75,8 @@ public class ConditionalTagCollection(bool caseSensitive = true) : TagCo conditional.Method, Expression.Constant(templateTag), parameter, - Expression.Constant(condition)); + Expression.Constant(condition), + CultureParameter); } public bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag) diff --git a/Source/FileManager/NamingTemplate/NamingTemplate.cs b/Source/FileManager/NamingTemplate/NamingTemplate.cs index 255b29ee..7538b9f8 100644 --- a/Source/FileManager/NamingTemplate/NamingTemplate.cs +++ b/Source/FileManager/NamingTemplate/NamingTemplate.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using System.Linq.Expressions; @@ -28,19 +29,36 @@ public class NamingTemplate /// /// Invoke the /// + /// /// Instances of the TClass used in and - public TemplatePart Evaluate(params object?[] propertyClasses) + public TemplatePart Evaluate(params object?[] propertyClasses) //CultureInfo? culture, { if (_templateToString is null) throw new InvalidOperationException(); // Match propertyClasses to the arguments required by templateToString.DynamicInvoke(). // First parameter is "this", so ignore it. - var delegateArgTypes = _templateToString.Method.GetParameters().Skip(1); + var parameters = _templateToString.Method.GetParameters(); + int skip = _templateToString.Target == null ? 0 : 1; + var delegateArgTypes = parameters.Skip(skip).ToList(); - object?[] args = delegateArgTypes.Join(propertyClasses, o => o.ParameterType, i => i?.GetType(), (_, i) => i).ToArray(); + object?[] args = new object?[delegateArgTypes.Count]; + // args = delegateArgTypes.Join(propertyClasses, dat => dat.ParameterType, pc => pc?.GetType(), (_, i) => i, + // EqualityComparer.Create((datType, pcType) => datType!.IsAssignableFrom(pcType))).ToArray(); + 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)); + } + } - if (args.Length != delegateArgTypes.Count()) + if (args.Length != delegateArgTypes.Count) throw new ArgumentException($"This instance of {nameof(NamingTemplate)} requires the following arguments: {string.Join(", ", delegateArgTypes.Select(t => t.Name).Distinct())}"); return (_templateToString.DynamicInvoke(args) as TemplatePart)!.FirstPart; @@ -58,7 +76,7 @@ public class NamingTemplate BinaryNode intermediate = namingTemplate.IntermediateParse(template); Expression evalTree = GetExpressionTree(intermediate); - namingTemplate._templateToString = Expression.Lambda(evalTree, tagCollections.Select(tc => tc.Parameter)).Compile(); + namingTemplate._templateToString = Expression.Lambda(evalTree, tagCollections.Select(tc => tc.Parameter).Append(TagCollection.CultureParameter)).Compile(); } catch (Exception ex) { diff --git a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs index 2aa01fb3..ced3bb81 100644 --- a/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs +++ b/Source/FileManager/NamingTemplate/PropertyTagCollection[TClass].cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Text.RegularExpressions; @@ -20,10 +21,12 @@ public class PropertyTagCollection : TagCollection var parameters = formatter.Method.GetParameters(); if (formatter.Method.ReturnType != typeof(string) - || parameters.Length != 3 + || parameters.Length != 4 || parameters[0].ParameterType != typeof(ITemplateTag) - || parameters[2].ParameterType != typeof(string)) - throw new ArgumentException($"{nameof(defaultFormatters)} must have a signature of [{nameof(String)} PropertyFormatter({nameof(ITemplateTag)}, T, {nameof(String)})]"); + || parameters[2].ParameterType != typeof(string) + || !typeof(CultureInfo).IsAssignableFrom(parameters[3].ParameterType)) + throw new ArgumentException( + $"{nameof(defaultFormatters)} must have a signature of [{nameof(String)} PropertyFormatter({nameof(ITemplateTag)}, T, {nameof(String)}, {nameof(CultureInfo)})]"); this._defaultFormatters[parameters[1].ParameterType] = formatter; } @@ -118,15 +121,15 @@ public class PropertyTagCollection : TagCollection /// The property class from which the tag's value is read /// 's string value if it is in this collection, otherwise null /// True if the is in this collection, otherwise false - public bool TryGetValue(string tagName, TClass @object, [NotNullWhen(true)] out string? value) + public bool TryGetValue(string tagName, TClass @object, CultureInfo? culture, [NotNullWhen(true)] out string? value) { value = null; if (!StartsWith($"<{tagName}>", out _, out _, out var valueExpression)) return false; - var func = Expression.Lambda>(valueExpression, Parameter).Compile(); - value = func(@object); + var func = Expression.Lambda>(valueExpression, Parameter, CultureParameter).Compile(); + value = func(@object, culture); return true; } @@ -145,7 +148,8 @@ public class PropertyTagCollection : TagCollection formatter.Method, Expression.Constant(templateTag), expVal, - Expression.Constant(format)); + Expression.Constant(format), + CultureParameter); } public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, Func toString) diff --git a/Source/FileManager/NamingTemplate/TagCollection.cs b/Source/FileManager/NamingTemplate/TagCollection.cs index 1935ca27..066d46b5 100644 --- a/Source/FileManager/NamingTemplate/TagCollection.cs +++ b/Source/FileManager/NamingTemplate/TagCollection.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Text.RegularExpressions; @@ -16,6 +17,8 @@ public abstract class TagCollection : IEnumerable /// The of the 's TClass type. internal ParameterExpression Parameter { get; } + + internal static readonly ParameterExpression CultureParameter = Expression.Parameter(typeof(CultureInfo), "culture"); protected RegexOptions Options { get; } = RegexOptions.Compiled; private List PropertyTags { get; } = []; @@ -34,7 +37,8 @@ public abstract class TagCollection : IEnumerable /// /// The that returns the 's value /// True if the starts with a tag registered in this class. - internal bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag, [NotNullWhen(true)] out Expression? propertyValue) + internal bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag, + [NotNullWhen(true)] out Expression? propertyValue) { foreach (var p in PropertyTags) { diff --git a/Source/LibationFileManager/Templates/ContributorDto.cs b/Source/LibationFileManager/Templates/ContributorDto.cs index 77fe7a2c..b578a66b 100644 --- a/Source/LibationFileManager/Templates/ContributorDto.cs +++ b/Source/LibationFileManager/Templates/ContributorDto.cs @@ -11,7 +11,7 @@ public class ContributorDto(string name, string? audibleContributorId) : IFormat public override string ToString() => ToString("{T} {F} {M} {L} {S}", null); - public string ToString(string? format, IFormatProvider? _) + public string ToString(string? format, IFormatProvider? provider) { if (string.IsNullOrWhiteSpace(format)) return ToString(); diff --git a/Source/LibationFileManager/Templates/IListFormat[TList].cs b/Source/LibationFileManager/Templates/IListFormat[TList].cs index 74aa2387..a18f0315 100644 --- a/Source/LibationFileManager/Templates/IListFormat[TList].cs +++ b/Source/LibationFileManager/Templates/IListFormat[TList].cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text.RegularExpressions; @@ -7,46 +8,63 @@ namespace LibationFileManager.Templates; internal partial interface IListFormat where TList : IListFormat { - static string Join(string formatString, IEnumerable items) - where T : IFormattable + static IEnumerable FilteredList(string formatString, IEnumerable items) { - var itemFormatter = Formatter(formatString); - var separatorString = Separator(formatString) ?? ", "; - var maxValues = Max(formatString) ?? items.Count(); + return Max(formatString, items); - var formattedValues = string.Join(separatorString, items.Take(maxValues).Select(n => n.ToString(itemFormatter, null))); - - while (formattedValues.Contains(" ")) - formattedValues = formattedValues.Replace(" ", " "); - - return formattedValues; - - static string? Formatter(string formatString) - { - var formatMatch = TList.FormatRegex().Match(formatString); - return formatMatch.Success ? formatMatch.Groups[1].Value : null; - } - - static int? Max(string formatString) + static IEnumerable Max(string formatString, IEnumerable items) { var maxMatch = MaxRegex().Match(formatString); - return maxMatch.Success && int.TryParse(maxMatch.Groups[1].Value, out var max) ? int.Max(1, max) : null; - } - - static string? Separator(string formatString) - { - var separatorMatch = SeparatorRegex().Match(formatString); - return separatorMatch.Success ? separatorMatch.Groups[1].Value : ", "; + return maxMatch.Success && int.TryParse(maxMatch.Groups[1].ValueSpan, out var max) + ? items.Take(max) + : items; } } + static IEnumerable FormattedList(string formatString, IEnumerable items, CultureInfo? culture) where T : IFormattable + { + var format = FormatElement(formatString, TList.FormatRegex); + var separator = FormatElement(formatString, SeparatorRegex); + var formattedItems = FilteredList(formatString, items).Select(ItemFormatter); + + // ReSharper disable PossibleMultipleEnumeration + return separator is null + ? formattedItems + : formattedItems.Any() + ? [Join(separator, formattedItems)] + : []; + // ReSharper restore PossibleMultipleEnumeration + + string ItemFormatter(T n) => n.ToString(format, culture); + + static string? FormatElement(string formatString, Func regex) + { + var match = regex().Match(formatString); + return match.Success ? match.Groups[1].Value : null; + } + } + + static string Join(string formatString, IEnumerable items, CultureInfo? culture) where T : IFormattable + { + return Join(", ", FormattedList(formatString, items, culture)); + } + + private static string Join(string separator, IEnumerable strings) + { + return CollapseSpacesRegex().Replace(string.Join(separator, strings), " "); + } + + // Collapses runs of 2+ spaces into a single space (does NOT touch tabs/newlines). + [GeneratedRegex(@" {2,}")] + private static partial Regex CollapseSpacesRegex(); + static abstract Regex FormatRegex(); + /// Max must have a 1 or 2-digit number + [GeneratedRegex(@"[Mm]ax\(\s*([1-9]\d?)\s*\)")] + private static partial Regex MaxRegex(); + /// Separator can be anything [GeneratedRegex(@"[Ss]eparator\((.*?)\)")] private static partial Regex SeparatorRegex(); - - /// Max must have a 1 or 2-digit number - [GeneratedRegex(@"[Mm]ax\(\s*(\d{1,2})\s*\)")] - private static partial Regex MaxRegex(); } diff --git a/Source/LibationFileManager/Templates/NameListFormat.cs b/Source/LibationFileManager/Templates/NameListFormat.cs index 15e49d27..4f6fa938 100644 --- a/Source/LibationFileManager/Templates/NameListFormat.cs +++ b/Source/LibationFileManager/Templates/NameListFormat.cs @@ -1,5 +1,6 @@ using FileManager.NamingTemplate; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text.RegularExpressions; @@ -7,10 +8,10 @@ namespace LibationFileManager.Templates; internal partial class NameListFormat : IListFormat { - public static string Formatter(ITemplateTag _, IEnumerable? names, string formatString) + public static string Formatter(ITemplateTag _, IEnumerable? names, string formatString, CultureInfo? culture) => names is null ? string.Empty - : IListFormat.Join(formatString, Sort(names, formatString)); + : IListFormat.Join(formatString, Sort(names, formatString), culture); private static IEnumerable Sort(IEnumerable names, string formatString) { diff --git a/Source/LibationFileManager/Templates/SeriesDto.cs b/Source/LibationFileManager/Templates/SeriesDto.cs index 93734de2..22b653d7 100644 --- a/Source/LibationFileManager/Templates/SeriesDto.cs +++ b/Source/LibationFileManager/Templates/SeriesDto.cs @@ -8,7 +8,7 @@ public partial record SeriesDto(string? Name, string? Number, string AudibleSeri public SeriesOrder Order { get; } = SeriesOrder.Parse(Number); public override string? ToString() => Name?.Trim(); - public string ToString(string? format, IFormatProvider? _) + public string ToString(string? format, IFormatProvider? provider) => string.IsNullOrWhiteSpace(format) ? ToString() ?? string.Empty : FormatRegex().Replace(format, MatchEvaluator) .Replace("{N}", Name) diff --git a/Source/LibationFileManager/Templates/SeriesListFormat.cs b/Source/LibationFileManager/Templates/SeriesListFormat.cs index 5646129c..a844b04a 100644 --- a/Source/LibationFileManager/Templates/SeriesListFormat.cs +++ b/Source/LibationFileManager/Templates/SeriesListFormat.cs @@ -1,15 +1,16 @@ using FileManager.NamingTemplate; using System.Collections.Generic; +using System.Globalization; using System.Text.RegularExpressions; namespace LibationFileManager.Templates; internal partial class SeriesListFormat : IListFormat { - public static string Formatter(ITemplateTag _, IEnumerable? series, string formatString) + public static string Formatter(ITemplateTag _, IEnumerable? series, string formatString, CultureInfo? culture) => series is null ? string.Empty - : IListFormat.Join(formatString, series); + : IListFormat.Join(formatString, series, culture); /// Format must have at least one of the string {N}, {#}, {ID} [GeneratedRegex(@"[Ff]ormat\((.*?(?:{#(?:\:.*?)?}|{N}|{ID})+.*?)\)")] diff --git a/Source/LibationFileManager/Templates/Templates.cs b/Source/LibationFileManager/Templates/Templates.cs index ae2929b2..e1257586 100644 --- a/Source/LibationFileManager/Templates/Templates.cs +++ b/Source/LibationFileManager/Templates/Templates.cs @@ -103,23 +103,33 @@ public abstract class Templates #region to file name public string GetName(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps) + => GetName(libraryBookDto, multiChapProps, null); + + public string GetName(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, CultureInfo? culture) { ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto)); ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps)); - return string.Concat(NamingTemplate.Evaluate(libraryBookDto, multiChapProps, new CombinedDto(libraryBookDto, multiChapProps)).Select(p => p.Value)); + return string.Concat(NamingTemplate.Evaluate(culture, libraryBookDto, multiChapProps, new CombinedDto(libraryBookDto, multiChapProps)).Select(p => p.Value)); } public LongPath GetFilename(LibraryBookDto libraryBookDto, string baseDir, string fileExtension, ReplacementCharacters? replacements = null, bool returnFirstExisting = false) + => GetFilename(libraryBookDto, baseDir, fileExtension, culture: null, replacements: replacements, returnFirstExisting: returnFirstExisting); + + public LongPath GetFilename(LibraryBookDto libraryBookDto, string baseDir, string fileExtension, CultureInfo? culture, ReplacementCharacters? replacements = null, bool returnFirstExisting = false) { ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto)); ArgumentValidator.EnsureNotNull(baseDir, nameof(baseDir)); ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension)); replacements ??= Configuration.Instance.ReplacementCharacters; - return GetFilename(baseDir, fileExtension, replacements, returnFirstExisting, libraryBookDto); + return GetFilename(baseDir, fileExtension, replacements, returnFirstExisting, libraryBookDto, culture); } public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir, string fileExtension, ReplacementCharacters? replacements = null, bool returnFirstExisting = false) + => GetFilename(libraryBookDto, multiChapProps, baseDir, fileExtension, culture: null, replacements: replacements, returnFirstExisting: returnFirstExisting); + + public LongPath GetFilename(LibraryBookDto libraryBookDto, MultiConvertFileProperties multiChapProps, string baseDir, string fileExtension, CultureInfo? culture, + ReplacementCharacters? replacements = null, bool returnFirstExisting = false) { ArgumentValidator.EnsureNotNull(libraryBookDto, nameof(libraryBookDto)); ArgumentValidator.EnsureNotNull(multiChapProps, nameof(multiChapProps)); @@ -127,17 +137,18 @@ public abstract class Templates ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension)); replacements ??= Configuration.Instance.ReplacementCharacters; - return GetFilename(baseDir, fileExtension, replacements, returnFirstExisting, libraryBookDto, multiChapProps); + return GetFilename(baseDir, fileExtension, replacements, returnFirstExisting, libraryBookDto, culture, multiChapProps); } protected virtual IEnumerable GetTemplatePartsStrings(List parts, ReplacementCharacters replacements) => parts.Select(p => replacements.ReplaceFilenameChars(p.Value)); - private LongPath GetFilename(string baseDir, string fileExtension, ReplacementCharacters replacements, bool returnFirstExisting, LibraryBookDto lbDto, MultiConvertFileProperties? multiDto = null) + private LongPath GetFilename(string baseDir, string fileExtension, ReplacementCharacters replacements, bool returnFirstExisting, LibraryBookDto lbDto, CultureInfo? culture, + MultiConvertFileProperties? multiDto = null) { fileExtension = FileUtility.GetStandardizedExtension(fileExtension); - var parts = NamingTemplate.Evaluate(lbDto, multiDto, new CombinedDto(lbDto, multiDto)).ToList(); + var parts = NamingTemplate.Evaluate(culture, lbDto, multiDto, new CombinedDto(lbDto, multiDto)).ToList(); var pathParts = GetPathParts(GetTemplatePartsStrings(parts, replacements)); //Remove 1 character from the end of the longest filename part until @@ -331,14 +342,14 @@ public abstract class Templates private static readonly List allPropertyTags = chapterPropertyTags.Append(filePropertyTags).Append(audioFilePropertyTags).ToList(); - private static bool HasValue(ITemplateTag _, CombinedDto dtos, string property) + private static bool HasValue(ITemplateTag _, CombinedDto dtos, string property, CultureInfo? culture) { - Func check = s => !string.IsNullOrWhiteSpace(s); + Func check = (s, _) => !string.IsNullOrWhiteSpace(s); foreach (var c in allPropertyTags.OfType>()) { - if (c.TryGetValue(property, dtos.LibraryBook, out var value)) - return check(value); + if (c.TryGetValue(property, dtos.LibraryBook, culture, out var value)) + return check(value, culture ?? CultureInfo.CurrentCulture); } if (dtos.MultiConvert is null) @@ -346,8 +357,8 @@ public abstract class Templates foreach (var c in allPropertyTags.OfType>()) { - if (c.TryGetValue(property, dtos.MultiConvert, out var value)) - return check(value); + if (c.TryGetValue(property, dtos.MultiConvert, culture, out var value)) + return check(value, culture ?? CultureInfo.CurrentCulture); } return false; diff --git a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs index bc3d6830..bf7105bb 100644 --- a/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs +++ b/Source/_Tests/FileManager.Tests/FileNamingTemplateTests.cs @@ -98,14 +98,14 @@ public class GetPortionFilename { new TemplateTag { TagName = "has3" }, HasValue } }; - private static bool HasValue(ITemplateTag templateTag, PropertyClass1 referenceType, string condition) - => props1.TryGetValue(condition, referenceType, out var value) && !string.IsNullOrEmpty(value); + private static bool HasValue(ITemplateTag templateTag, PropertyClass1 referenceType, string condition, CultureInfo? culture) + => props1.TryGetValue(condition, referenceType, culture, out var value) && !string.IsNullOrEmpty(value); - private static bool HasValue(ITemplateTag templateTag, PropertyClass2 referenceType, string condition) - => props2.TryGetValue(condition, referenceType, out var value) && !string.IsNullOrEmpty(value); + private static bool HasValue(ITemplateTag templateTag, PropertyClass2 referenceType, string condition, CultureInfo? culture) + => props2.TryGetValue(condition, referenceType, culture, out var value) && !string.IsNullOrEmpty(value); - private static bool HasValue(ITemplateTag templateTag, PropertyClass3 referenceType, string condition) - => props3.TryGetValue(condition, referenceType, out var value) && !string.IsNullOrEmpty(value); + private static bool HasValue(ITemplateTag templateTag, PropertyClass3 referenceType, string condition, CultureInfo? culture) + => props3.TryGetValue(condition, referenceType, culture, out var value) && !string.IsNullOrEmpty(value); private readonly PropertyClass1 _propertyClass1 = new() { @@ -156,7 +156,7 @@ public class GetPortionFilename template.Warnings.Should().HaveCount(numTags > 0 ? 0 : 1); template.Errors.Should().HaveCount(0); - var templateText = string.Concat(template.Evaluate(_propertyClass3, _propertyClass2, _propertyClass1).Select(v => v.Value)); + var templateText = string.Concat(template.Evaluate(null, _propertyClass3, _propertyClass2, _propertyClass1).Select(v => v.Value)); templateText.Should().Be(outStr); } @@ -186,7 +186,7 @@ public class GetPortionFilename template.Warnings.Should().HaveCount(1); template.Errors.Should().HaveCount(0); - var templateText = string.Concat(template.Evaluate(_propertyClass3, _propertyClass2, _propertyClass1).Select(v => v.Value)); + var templateText = string.Concat(template.Evaluate(null, _propertyClass3, _propertyClass2, _propertyClass1).Select(v => v.Value)); templateText.Should().Be(outStr); } @@ -210,7 +210,7 @@ public class GetPortionFilename template.Warnings.Should().HaveCount(2); template.Errors.Should().HaveCount(0); - var templateText = string.Concat(template.Evaluate(_propertyClass3, _propertyClass2, _propertyClass1).Select(v => v.Value)); + var templateText = string.Concat(template.Evaluate(null, _propertyClass3, _propertyClass2, _propertyClass1).Select(v => v.Value)); templateText.Should().Be(outStr); } @@ -234,7 +234,7 @@ public class GetPortionFilename template.Warnings.Should().BeEquivalentTo(warnings); } - static string GetVal(ITemplateTag templateTag, ReferenceType referenceType, string format) + static string GetVal(ITemplateTag templateTag, ReferenceType referenceType, string format, CultureInfo? culture) { return ""; } @@ -267,20 +267,20 @@ public class GetPortionFilename template.Warnings.Should().HaveCount(0); template.Errors.Should().HaveCount(0); - var templateText = string.Concat(template.Evaluate(_propertyClass3, _propertyClass2, _propertyClass1).Select(v => v.Value)); + var templateText = string.Concat(template.Evaluate(null, _propertyClass3, _propertyClass2, _propertyClass1).Select(v => v.Value)); templateText.Should().Be(outStr); - string FormatInt(ITemplateTag templateTag, int value, string format) + string FormatInt(ITemplateTag templateTag, int value, string format, CultureInfo? culture) { if (int.TryParse(format, out var numDecs)) - return value.ToString($"D{numDecs}"); - return value.ToString(); + return value.ToString($"D{numDecs}", culture); + return value.ToString(culture); } - string FormatString(ITemplateTag templateTag, string? value, string format) + string FormatString(ITemplateTag templateTag, string? value, string format, CultureInfo? culture) { - return CommonFormatters.StringFormatter(templateTag, value, format); + return CommonFormatters.StringFormatter(templateTag, value, format, culture); } } } diff --git a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs index 665641bc..2cf36e72 100644 --- a/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs +++ b/Source/_Tests/LibationFileManager.Tests/TemplatesTests.cs @@ -116,7 +116,7 @@ namespace TemplatesTests Templates.TryGetTemplate(template, out var fileTemplate).Should().BeTrue(); fileTemplate - .GetFilename(GetLibraryBook(), dirFullPath, extension, Replacements) + .GetFilename(GetLibraryBook(), dirFullPath, extension, culture: null, replacements: Replacements) .PathWithoutPrefix .Should().Be(expected); } @@ -131,7 +131,7 @@ namespace TemplatesTests [DataRow("4 - 4", "", "", "1 8 - 1 8")] [DataRow("", "", "", "100")] [DataRow(" ", "", "", "100")] - [DataRow(" - - ", "", "", "- 100 -")] + [DataRow(" - - ", "", "", "- 100 -")] public void Tests_removeSpaces(string template, string dirFullPath, string extension, string expected) { @@ -152,7 +152,7 @@ namespace TemplatesTests Templates.TryGetTemplate(template, out var fileTemplate).Should().BeTrue(); fileTemplate - .GetFilename(GetLibraryBook(), dirFullPath, extension, replacements) + .GetFilename(GetLibraryBook(), dirFullPath, extension, culture: null, replacements: replacements) .PathWithoutPrefix .Should().Be(expected); } @@ -168,7 +168,7 @@ namespace TemplatesTests public void FormatTags(string template, string expected) { Templates.TryGetTemplate(template, out var fileTemplate).Should().BeTrue(); - fileTemplate.GetFilename(GetLibraryBook(), "", "", Replacements).PathWithoutPrefix.Should().Be(expected); + fileTemplate.GetFilename(GetLibraryBook(), "", "", culture: null, replacements: Replacements).PathWithoutPrefix.Should().Be(expected); } [TestMethod] @@ -193,7 +193,7 @@ namespace TemplatesTests Templates.TryGetTemplate(template, out var fileTemplate).Should().BeTrue(); fileTemplate - .GetFilename(GetLibraryBook(), dirFullPath, extension, Replacements) + .GetFilename(GetLibraryBook(), dirFullPath, extension, culture: null, replacements: Replacements) .PathWithoutPrefix .Should().Be(expected); } @@ -222,7 +222,7 @@ namespace TemplatesTests Templates.TryGetTemplate(template, out var fileTemplate).Should().BeTrue(); fileTemplate - .GetFilename(GetLibraryBook(), dirFullPath, extension, Replacements) + .GetFilename(GetLibraryBook(), dirFullPath, extension, culture: null, replacements: Replacements) .PathWithoutPrefix .Should().Be(expected); } @@ -241,7 +241,7 @@ namespace TemplatesTests Templates.TryGetTemplate(template, out var fileTemplate).Should().BeTrue(); fileTemplate - .GetFilename(GetLibraryBook(), dirFullPath, extension, Replacements) + .GetFilename(GetLibraryBook(), dirFullPath, extension, culture: null, replacements: Replacements) .PathWithoutPrefix .Should().Be(expected); } @@ -255,15 +255,13 @@ namespace TemplatesTests [DataRow(" - ", @"/foo/bar", ".m4b", @"/foo/bar/asin - 06∕09∕22 00:00.m4b", PlatformID.Unix)] public void DateFormat_illegal(string template, string dirFullPath, string extension, string expected, PlatformID platformId) { - CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; - if (Environment.OSVersion.Platform == platformId) { Templates.TryGetTemplate(template, out var fileTemplate).Should().BeTrue(); fileTemplate.HasWarnings.Should().BeFalse(); fileTemplate - .GetFilename(GetLibraryBook(), dirFullPath, extension, Replacements) + .GetFilename(GetLibraryBook(), dirFullPath, extension, culture: CultureInfo.InvariantCulture, replacements: Replacements) .PathWithoutPrefix .Should().Be(expected); } @@ -285,7 +283,7 @@ namespace TemplatesTests Templates.TryGetTemplate(template, out var fileTemplate).Should().BeTrue(); fileTemplate - .GetFilename(lbDto, dirFullPath, extension, Replacements) + .GetFilename(lbDto, dirFullPath, extension, culture: null, replacements: Replacements) .PathWithoutPrefix .Should().Be(expected); } @@ -326,7 +324,7 @@ namespace TemplatesTests bookDto.Authors = [new(author, null)]; Templates.TryGetTemplate("", out var fileTemplate).Should().BeTrue(); fileTemplate - .GetFilename(bookDto, "", "", Replacements) + .GetFilename(bookDto, "", "", culture: null, replacements: Replacements) .PathWithoutPrefix .Should().Be(expected); } @@ -375,7 +373,7 @@ namespace TemplatesTests Templates.TryGetTemplate(template, out var fileTemplate).Should().BeTrue(); fileTemplate - .GetFilename(bookDto, "", "", Replacements) + .GetFilename(bookDto, "", "", culture: null, replacements: Replacements) .PathWithoutPrefix .Should().Be(expected); } @@ -395,7 +393,7 @@ namespace TemplatesTests Templates.TryGetTemplate(template, out var fileTemplate).Should().BeTrue(); fileTemplate - .GetFilename(bookDto, multiDto, "", "", Replacements) + .GetFilename(bookDto, multiDto, "", "", culture: null, replacements: Replacements) .PathWithoutPrefix .Should().Be(expected); } @@ -445,7 +443,7 @@ namespace TemplatesTests Templates.TryGetTemplate(template, out var fileTemplate).Should().BeTrue(); fileTemplate - .GetFilename(bookDto, multiDto, "", "", Replacements) + .GetFilename(bookDto, multiDto, "", "", culture: null, replacements: Replacements) .PathWithoutPrefix .Should().Be(expected); } @@ -466,8 +464,6 @@ namespace TemplatesTests [DataRow("", "Series A, 01.0")] public void SeriesFormat_formatters(string template, string expected) { - CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; - var bookDto = GetLibraryBook(); bookDto.Series = [ @@ -479,7 +475,7 @@ namespace TemplatesTests Templates.TryGetTemplate(template, out var fileTemplate).Should().BeTrue(); fileTemplate - .GetFilename(bookDto, "", "", Replacements) + .GetFilename(bookDto, "", "", culture: CultureInfo.InvariantCulture, replacements: Replacements) .PathWithoutPrefix .Should().Be(expected); } @@ -499,14 +495,12 @@ namespace TemplatesTests [DataRow("", " 1 6 ", "1 6")] public void SeriesOrder_formatters(string template, string seriesOrder, string expected) { - CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; - var bookDto = GetLibraryBook(); bookDto.Series = [new("Series A", seriesOrder, "B1")]; Templates.TryGetTemplate(template, out var fileTemplate).Should().BeTrue(); fileTemplate - .GetFilename(bookDto, "", "", Replacements) + .GetFilename(bookDto, "", "", culture: CultureInfo.InvariantCulture, replacements: Replacements) .PathWithoutPrefix .Should().Be(expected); } @@ -521,7 +515,7 @@ namespace TemplatesTests Templates.TryGetTemplate("foo<-if series>bar", out var fileTemplate).Should().BeTrue(); fileTemplate - .GetFilename(GetLibraryBook(), directory, "ext", Replacements) + .GetFilename(GetLibraryBook(), directory, "ext", culture: null, replacements: Replacements) .PathWithoutPrefix .Should().Be(expected); } @@ -536,7 +530,7 @@ namespace TemplatesTests { Templates.TryGetTemplate("foo---<-if series>bar", out var fileTemplate).Should().BeTrue(); - fileTemplate.GetFilename(GetLibraryBook(null), directory, "ext", Replacements) + fileTemplate.GetFilename(GetLibraryBook(null), directory, "ext", culture: null, replacements: Replacements) .PathWithoutPrefix .Should().Be(expected); } @@ -552,11 +546,28 @@ namespace TemplatesTests Templates.TryGetTemplate("foo---<-if series>bar", out var fileTemplate).Should().BeTrue(); fileTemplate - .GetFilename(GetLibraryBook(), directory, "ext", Replacements) + .GetFilename(GetLibraryBook(), directory, "ext", culture: null, replacements: Replacements) .PathWithoutPrefix .Should().Be(expected); } } + + [TestMethod] + [DataRow("", "I", "en-US", "i")] + [DataRow("", "ı", "tr-TR", "I")] + [DataRow("", "İ", "tr-TR", "i")] + public void Tag_culture_test(string template, string expected, string cultureName, string title) + { + var bookDto = Shared.GetLibraryBook(); + bookDto.Title = title; + var culture = new System.Globalization.CultureInfo(cultureName); + + Templates.TryGetTemplate(template, out var fileTemplate).Should().BeTrue(); + + fileTemplate + .GetName(bookDto, new MultiConvertFileProperties { OutputFileName = string.Empty }, culture) + .Should().Be(expected); + } } } @@ -614,7 +625,7 @@ namespace Templates_Other Templates.TryGetTemplate(template, out var fileNamingTemplate).Should().BeTrue(); - return fileNamingTemplate.GetFilename(lbDto, dirFullPath, extension, Replacements).PathWithoutPrefix; + return fileNamingTemplate.GetFilename(lbDto, dirFullPath, extension, culture: null, replacements: Replacements).PathWithoutPrefix; } [TestMethod] @@ -642,7 +653,8 @@ namespace Templates_Other Templates.TryGetTemplate(template, out var chapterFileTemplate).Should().BeTrue(); return chapterFileTemplate - .GetFilename(lbDto, new MultiConvertFileProperties { Title = suffix, PartsTotal = partsTotal, PartsPosition = partsPosition, OutputFileName = string.Empty }, dir, estension, Replacements) + .GetFilename(lbDto, new MultiConvertFileProperties { Title = suffix, PartsTotal = partsTotal, PartsPosition = partsPosition, OutputFileName = string.Empty }, dir, estension, + culture: null, replacements: Replacements) .PathWithoutPrefix; } @@ -661,7 +673,7 @@ namespace Templates_Other Templates.TryGetTemplate(fileName, out var fileNamingTemplate).Should().BeTrue(); - fileNamingTemplate.GetFilename(lbDto, directory, "txt", Replacements).PathWithoutPrefix.Should().Be(outStr); + fileNamingTemplate.GetFilename(lbDto, directory, "txt", culture: null, replacements: Replacements).PathWithoutPrefix.Should().Be(outStr); } } } @@ -1011,7 +1023,7 @@ namespace Templates_ChapterFile_Tests { Templates.TryGetTemplate(template, out var chapterTemplate).Should().BeTrue(); chapterTemplate - .GetFilename(GetLibraryBook(), new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir, ext, Default) + .GetFilename(GetLibraryBook(), new() { OutputFileName = $"xyz.{ext}", PartsPosition = pos, PartsTotal = total, Title = chapter }, dir, ext, culture: null, replacements: Default) .PathWithoutPrefix .Should().Be(expected); }