mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-03-30 04:45:05 -04:00
QS second part:
Made Evaluate() more resilient. Added tests to CommonFormatters.
This commit is contained in:
@@ -17,7 +17,7 @@ public class NamingTemplate
|
||||
private Delegate? _templateToString;
|
||||
private readonly List<string> _warnings = [];
|
||||
private readonly List<string> _errors = [];
|
||||
private readonly IEnumerable<TagCollection> _tagCollections;
|
||||
private readonly List<TagCollection> _tagCollections;
|
||||
private readonly List<ITemplateTag> _tagsInUse = [];
|
||||
|
||||
public const string ErrorNullIsInvalid = "Null template is invalid.";
|
||||
@@ -36,35 +36,48 @@ public class NamingTemplate
|
||||
|
||||
// Match propertyClasses to the arguments required by templateToString.DynamicInvoke().
|
||||
// First parameter is "this", so ignore it.
|
||||
var parameters = _templateToString.Method.GetParameters();
|
||||
var delegateArgTypes = parameters.Skip(1).ToList();
|
||||
var delegateArgTypes = _templateToString.Method.GetParameters().Skip(1).Select(p => p.ParameterType).ToList();
|
||||
var delegateArgs = new object?[delegateArgTypes.Count];
|
||||
|
||||
var args = new object?[delegateArgTypes.Count];
|
||||
var availableObjects = propertyClasses.Where(pc => pc is not null).Cast<object>().ToList();
|
||||
for (var i = 0; i < delegateArgTypes.Count; i++)
|
||||
{
|
||||
var p = delegateArgTypes[i];
|
||||
args[i] = propertyClasses.FirstOrDefault(pc => pc != null && p.ParameterType.IsInstanceOfType(pc));
|
||||
var index = availableObjects.FindIndex(pc => p.IsInstanceOfType(pc));
|
||||
if (index < 0)
|
||||
{
|
||||
if (CanBeNull(p))
|
||||
delegateArgs[i] = null;
|
||||
else
|
||||
throw new ArgumentException(
|
||||
$"No matching object found for parameter type {p.Name}. Available objects: {string.Join(", ", availableObjects.Select(o => o.GetType().Name))}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var candidate = availableObjects[index];
|
||||
availableObjects.RemoveAt(index);
|
||||
availableObjects.Add(candidate); // Re-add to the end to allow reuse if needed later
|
||||
delegateArgs[i] = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return (_templateToString.DynamicInvoke(delegateArgs) as TemplatePart)!.FirstPart;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Parse a template string to a <see cref="NamingTemplate"/></summary>
|
||||
/// <param name="template">The template string to parse</param>
|
||||
/// <param name="tagCollections">A collection of <see cref="TagCollection"/> with
|
||||
/// properties registered to match to the <paramref name="template"/></param>
|
||||
public static NamingTemplate Parse(string? template, IEnumerable<TagCollection> tagCollections)
|
||||
{
|
||||
var namingTemplate = new NamingTemplate(tagCollections);
|
||||
var listOfTagCollections = tagCollections.ToList();
|
||||
var namingTemplate = new NamingTemplate(listOfTagCollections);
|
||||
try
|
||||
{
|
||||
var intermediate = namingTemplate.IntermediateParse(template);
|
||||
var evalTree = GetExpressionTree(intermediate);
|
||||
|
||||
namingTemplate._templateToString = Expression.Lambda(evalTree, tagCollections.Select(tc => tc.Parameter).Append(TagCollection.CultureParameter)).Compile();
|
||||
namingTemplate._templateToString = Expression.Lambda(evalTree, listOfTagCollections.Select(tc => tc.Parameter).Append(TagCollection.CultureParameter)).Compile();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -73,7 +86,7 @@ public class NamingTemplate
|
||||
return namingTemplate;
|
||||
}
|
||||
|
||||
private NamingTemplate(IEnumerable<TagCollection> properties)
|
||||
private NamingTemplate(List<TagCollection> properties)
|
||||
{
|
||||
_tagCollections = properties;
|
||||
}
|
||||
@@ -127,23 +140,23 @@ public class NamingTemplate
|
||||
{
|
||||
CheckAndAddLiterals();
|
||||
|
||||
var lastParenth = currentNode;
|
||||
var lastParent = currentNode;
|
||||
|
||||
while (lastParenth?.IsConditional is false)
|
||||
lastParenth = lastParenth.Parent;
|
||||
while (lastParent?.IsConditional is false)
|
||||
lastParent = lastParent.Parent;
|
||||
|
||||
if (lastParenth?.Parent is null)
|
||||
if (lastParent?.Parent is null)
|
||||
{
|
||||
_warnings.Add($"Missing <{closingPropertyTag.TemplateTag.TagName}-> open conditional.");
|
||||
break;
|
||||
}
|
||||
else if (lastParenth.Name != closingPropertyTag.TemplateTag.TagName)
|
||||
else if (lastParent.Name != closingPropertyTag.TemplateTag.TagName)
|
||||
{
|
||||
_warnings.Add($"Missing <-{lastParenth.Name}> closing conditional.");
|
||||
_warnings.Add($"Missing <-{lastParent.Name}> closing conditional.");
|
||||
break;
|
||||
}
|
||||
|
||||
currentNode = lastParenth.Parent;
|
||||
currentNode = lastParent.Parent;
|
||||
templateString = templateString[exactPropertyName.Length..];
|
||||
}
|
||||
else
|
||||
@@ -282,4 +295,6 @@ public class NamingTemplate
|
||||
return newNode.IsConditional ? newNode : currentNode;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CanBeNull(Type type) => !type.IsValueType || Nullable.GetUnderlyingType(type) != null;
|
||||
}
|
||||
|
||||
312
Source/_Tests/FileManager.Tests/CommonFormattersTests.cs
Normal file
312
Source/_Tests/FileManager.Tests/CommonFormattersTests.cs
Normal file
@@ -0,0 +1,312 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using AssertionHelper;
|
||||
using FileManager.NamingTemplate;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace FileManager.Tests;
|
||||
|
||||
[TestClass]
|
||||
public class CommonFormattersTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void TemplateStringFormatter_UnknownTag_RemainsUnchanged()
|
||||
{
|
||||
// Arrange
|
||||
var template = "Author: {AUTHOR}, Unknown: {UNKNOWN}, Title: {TITLE}";
|
||||
var replacements = new Dictionary<string, Func<TestClass, object?>>
|
||||
{
|
||||
["AUTHOR"] = obj => obj.Author,
|
||||
["TITLE"] = obj => obj.Title
|
||||
};
|
||||
var testObj = new TestClass { Author = "John Doe", Title = "Test Book" };
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.TemplateStringFormatter(testObj, template, CultureInfo.InvariantCulture, replacements);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("Author: John Doe, Unknown: {UNKNOWN}, Title: Test Book", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MinutesFormatter_Boundaries_ZeroMinutes()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "MINUTES" };
|
||||
var value = 0;
|
||||
var format = "{H}:{M}";
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.MinutesFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("0:0", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MinutesFormatter_Boundaries_OneDay()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "MINUTES" };
|
||||
var value = 1440; // 24 hours
|
||||
var format = "{D}d {H}h {M}m";
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.MinutesFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("1d 0h 0m", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MinutesFormatter_Boundaries_LargeValue()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "MINUTES" };
|
||||
var value = 3000; // 50 hours
|
||||
var format = "{D}d {H}h {M}m";
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.MinutesFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("2d 2h 0m", result); // 2 days, 2 hours
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringFormatter_InvalidCombinedFormat_ReturnsOriginal()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "STRING" };
|
||||
var value = "TestString";
|
||||
var invalidFormat = "invalid format with spaces and numbers 123";
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.StringFormatter(templateTag, value, invalidFormat, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("TestString", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TemplateStringFormatter_InvalidCombinedFormat_HandlesGracefully()
|
||||
{
|
||||
// Arrange
|
||||
var template = "{AUTHOR:invalid:format}, {TITLE}";
|
||||
var replacements = new Dictionary<string, Func<TestClass, object?>>
|
||||
{
|
||||
["AUTHOR"] = obj => obj.Author,
|
||||
["TITLE"] = obj => obj.Title
|
||||
};
|
||||
var testObj = new TestClass { Author = "John Doe", Title = "Test Book" };
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.TemplateStringFormatter(testObj, template, CultureInfo.InvariantCulture, replacements);
|
||||
|
||||
// Assert
|
||||
// Since AUTHOR is IFormattable? No, it's string, so uses _StringFormatter with invalid format
|
||||
Assert.AreEqual("John Doe, Test Book", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringFormatter_Uppercase()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "STRING" };
|
||||
var value = "test string";
|
||||
var format = "U";
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.StringFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("TEST STRING", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringFormatter_Lowercase()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "STRING" };
|
||||
var value = "TEST STRING";
|
||||
var format = "L";
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.StringFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("test string", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringFormatter_TitleCase()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "STRING" };
|
||||
var value = "test string";
|
||||
var format = "T";
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.StringFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("Test String", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringFormatter_TitleCaseWithLength()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "STRING" };
|
||||
var value = "test string longer";
|
||||
var format = "10T";
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.StringFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("Test Strin", result); // Title case first 10 chars
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringFormatter_MaxLength()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "STRING" };
|
||||
var value = "this is a very long string";
|
||||
var format = "20";
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.StringFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("this is a very long ", result); // Truncated to 20 chars
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FormattableFormatter_Standard()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "FORMATTABLE" };
|
||||
var value = 123.45;
|
||||
var format = "F2";
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.FormattableFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("123.45", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IntegerFormatter_WithLengthAndPadding()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "INTEGER" };
|
||||
var value = 42;
|
||||
var format = "5"; // Zero-padded to 5 digits
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.IntegerFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("00042", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IntegerFormatter_StandardFormat()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "INTEGER" };
|
||||
var value = 1234;
|
||||
var format = "N0"; // Number format
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.IntegerFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("1,234", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FloatFormatter_WithLengthAndPadding()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "FLOAT" };
|
||||
var value = 12.34f;
|
||||
var format = "F3"; // Fixed-point with 3 decimals
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.FloatFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("12.340", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FloatFormatter_StandardFormat()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "FLOAT" };
|
||||
var value = 1234.567f;
|
||||
var format = "N2"; // Number format with 2 decimals
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.FloatFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("1,234.57", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DateTimeFormatter_Standard()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "DATETIME" };
|
||||
var value = new DateTime(2023, 10, 15, 14, 30, 0);
|
||||
var format = "yyyy-MM-dd";
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.DateTimeFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("2023-10-15", result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LanguageShortFormatter_TrimToThreeAndUppercase()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "LANGUAGE" };
|
||||
var value = "english";
|
||||
var format = ""; // Assuming default or empty
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.LanguageShortFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("ENG", result); // First 3 chars uppercase
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LanguageShortFormatter_ShortLanguage()
|
||||
{
|
||||
// Arrange
|
||||
var templateTag = new TemplateTag { TagName = "LANGUAGE" };
|
||||
var value = "de";
|
||||
var format = "";
|
||||
|
||||
// Act
|
||||
var result = CommonFormatters.LanguageShortFormatter(templateTag, value, format, CultureInfo.InvariantCulture);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual("DE", result); // Uppercase, no trim needed
|
||||
}
|
||||
|
||||
private class TestClass
|
||||
{
|
||||
public string? Author { get; set; }
|
||||
public string? Title { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user