Introduce culture parameter for formatting.

This commit is contained in:
Jo-Be-Co
2026-03-12 02:58:35 +01:00
parent a6aa14c7f8
commit d161bdfaeb
13 changed files with 193 additions and 122 deletions

View File

@@ -9,12 +9,12 @@ 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 delegate string PropertyFormatter<in T>(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);
}
}

View File

@@ -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<T>(ITemplateTag templateTag, T value, string condition);
public delegate bool Conditional<T>(ITemplateTag templateTag, T value, string condition, CultureInfo? culture);
public class ConditionalTagCollection<TClass>(bool caseSensitive = true) : TagCollection(typeof(TClass), caseSensitive)
{
@@ -74,7 +75,8 @@ public class ConditionalTagCollection<TClass>(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)

View File

@@ -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
/// <summary>
/// Invoke the <see cref="NamingTemplate"/>
/// </summary>
/// <param name="culture"></param>
/// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param>
public TemplatePart Evaluate(params object?[] propertyClasses)
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<Type?>.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)
{

View File

@@ -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<TClass> : 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<T>({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<T>({nameof(ITemplateTag)}, T, {nameof(String)}, {nameof(CultureInfo)})]");
this._defaultFormatters[parameters[1].ParameterType] = formatter;
}
@@ -118,15 +121,15 @@ public class PropertyTagCollection<TClass> : TagCollection
/// <param name="object">The property class from which the tag's value is read</param>
/// <param name="value"><paramref name="tagName"/>'s string value if it is in this collection, otherwise null</param>
/// <returns>True if the <paramref name="tagName"/> is in this collection, otherwise false</returns>
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<Func<TClass, string>>(valueExpression, Parameter).Compile();
value = func(@object);
var func = Expression.Lambda<Func<TClass, CultureInfo?, string>>(valueExpression, Parameter, CultureParameter).Compile();
value = func(@object, culture);
return true;
}
@@ -145,7 +148,8 @@ public class PropertyTagCollection<TClass> : TagCollection
formatter.Method,
Expression.Constant(templateTag),
expVal,
Expression.Constant(format));
Expression.Constant(format),
CultureParameter);
}
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, Func<TPropertyValue, string> toString)

View File

@@ -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<ITemplateTag>
/// <summary>The <see cref="ParameterExpression"/> of the <see cref="TagCollection"/>'s TClass type.</summary>
internal ParameterExpression Parameter { get; }
internal static readonly ParameterExpression CultureParameter = Expression.Parameter(typeof(CultureInfo), "culture");
protected RegexOptions Options { get; } = RegexOptions.Compiled;
private List<IPropertyTag> PropertyTags { get; } = [];
@@ -34,7 +37,8 @@ public abstract class TagCollection : IEnumerable<ITemplateTag>
/// <param name="propertyTag"></param>
/// <param name="propertyValue">The <see cref="Expression"/> that returns the <paramref name="propertyTag"/>'s value</param>
/// <returns>True if the <paramref name="templateString"/> starts with a tag registered in this class.</returns>
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)
{

View File

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

View File

@@ -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<TList> where TList : IListFormat<TList>
{
static string Join<T>(string formatString, IEnumerable<T> items)
where T : IFormattable
static IEnumerable<T> FilteredList<T>(string formatString, IEnumerable<T> 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<T> Max(string formatString, IEnumerable<T> 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<string> FormattedList<T>(string formatString, IEnumerable<T> 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> regex)
{
var match = regex().Match(formatString);
return match.Success ? match.Groups[1].Value : null;
}
}
static string Join<T>(string formatString, IEnumerable<T> items, CultureInfo? culture) where T : IFormattable
{
return Join(", ", FormattedList(formatString, items, culture));
}
private static string Join(string separator, IEnumerable<string> 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();
/// <summary> Max must have a 1 or 2-digit number </summary>
[GeneratedRegex(@"[Mm]ax\(\s*([1-9]\d?)\s*\)")]
private static partial Regex MaxRegex();
/// <summary> Separator can be anything </summary>
[GeneratedRegex(@"[Ss]eparator\((.*?)\)")]
private static partial Regex SeparatorRegex();
/// <summary> Max must have a 1 or 2-digit number </summary>
[GeneratedRegex(@"[Mm]ax\(\s*(\d{1,2})\s*\)")]
private static partial Regex MaxRegex();
}

View File

@@ -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<NameListFormat>
{
public static string Formatter(ITemplateTag _, IEnumerable<ContributorDto>? names, string formatString)
public static string Formatter(ITemplateTag _, IEnumerable<ContributorDto>? names, string formatString, CultureInfo? culture)
=> names is null
? string.Empty
: IListFormat<NameListFormat>.Join(formatString, Sort(names, formatString));
: IListFormat<NameListFormat>.Join(formatString, Sort(names, formatString), culture);
private static IEnumerable<ContributorDto> Sort(IEnumerable<ContributorDto> names, string formatString)
{

View File

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

View File

@@ -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<SeriesListFormat>
{
public static string Formatter(ITemplateTag _, IEnumerable<SeriesDto>? series, string formatString)
public static string Formatter(ITemplateTag _, IEnumerable<SeriesDto>? series, string formatString, CultureInfo? culture)
=> series is null
? string.Empty
: IListFormat<SeriesListFormat>.Join(formatString, series);
: IListFormat<SeriesListFormat>.Join(formatString, series, culture);
/// <summary> Format must have at least one of the string {N}, {#}, {ID} </summary>
[GeneratedRegex(@"[Ff]ormat\((.*?(?:{#(?:\:.*?)?}|{N}|{ID})+.*?)\)")]

View File

@@ -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<string> GetTemplatePartsStrings(List<TemplatePart> 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<TagCollection> 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<string?, bool> check = s => !string.IsNullOrWhiteSpace(s);
Func<string?, CultureInfo?, bool> check = (s, _) => !string.IsNullOrWhiteSpace(s);
foreach (var c in allPropertyTags.OfType<PropertyTagCollection<LibraryBookDto>>())
{
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<PropertyTagCollection<MultiConvertFileProperties>>())
{
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;

View File

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

View File

@@ -116,7 +116,7 @@ namespace TemplatesTests
Templates.TryGetTemplate<Templates.FileTemplate>(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<bitrate> - <bitrate> 4", "", "", "1 8 - 1 8")]
[DataRow("<channels><channels><samplerate><channels><channels>", "", "", "100")]
[DataRow(" <channels> <channels> <samplerate> <channels> <channels>", "", "", "100")]
[DataRow(" <channels> - <channels> <samplerate> <channels> - <channels>", "", "", "- 100 -")]
[DataRow(" <channels> - <channels> <samplerate> <channels> - <channels>", "", "", "- 100 -")]
public void Tests_removeSpaces(string template, string dirFullPath, string extension, string expected)
{
@@ -152,7 +152,7 @@ namespace TemplatesTests
Templates.TryGetTemplate<Templates.FileTemplate>(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<Templates.FileTemplate>(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<Templates.FileTemplate>(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<Templates.FileTemplate>(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<Templates.FileTemplate>(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("<id> - <date added[MM/dd/yy HH:mm]>", @"/foo/bar", ".m4b", @"/foo/bar/asin - 060922 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<Templates.FileTemplate>(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<Templates.FileTemplate>(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<Templates.FileTemplate>("<author[format(Title={T}, First={F}, Middle={M} Last={L}, Suffix={S})]>", 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<Templates.FileTemplate>(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<Templates.FileTemplate>(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<Templates.FileTemplate>(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("<first series[{N}, {#:00.0}]>", "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<Templates.FileTemplate>(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("<series#>", " 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<Templates.FileTemplate>(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<Templates.FileTemplate>("foo<if series-><-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<Templates.FileTemplate>("foo<if series->-<series>-<id>-<-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<Templates.FileTemplate>("foo<if series->-<series>-<id>-<-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("<audibletitle [u]>", "I", "en-US", "i")]
[DataRow("<audibletitle [l]>", "ı", "tr-TR", "I")]
[DataRow("<audibletitle [u]>", "İ", "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<Templates.FileTemplate>(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<Templates.FolderTemplate>(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<Templates.ChapterFileTemplate>(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<Templates.FileTemplate>(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<Templates.ChapterFileTemplate>(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);
}