diff --git a/Directory.Build.props b/Directory.Build.props index b9f8fd3..25b9bc4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,7 @@ Domain Primitives ALTA Software llc. Copyright © 2024 ALTA Software llc. - 7.1.0 + 7.1.1 diff --git a/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs b/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs index 0450ba0..ba08ae7 100644 --- a/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs +++ b/src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs @@ -871,13 +871,11 @@ public static void GenerateParsable(GeneratorData data, SourceCodeBuilder builde { builder.Append("s"); } - else - if (isChar) + else if (isChar) { builder.Append("char.Parse(s)"); } - else - if (isBool) + else if (isBool) { builder.Append("bool.Parse(s)"); } @@ -899,13 +897,11 @@ public static void GenerateParsable(GeneratorData data, SourceCodeBuilder builde { builder.AppendLine("if (s is null)"); } - else - if (isChar) + else if (isChar) { builder.AppendLine("if (!char.TryParse(s, out var value))"); } - else - if (isBool) + else if (isBool) { builder.AppendLine("if (!bool.TryParse(s, out var value))"); } @@ -1110,6 +1106,7 @@ public static void GenerateIXmlSerializableMethods(GeneratorData data, SourceCod "string" => "ReadElementContentAsString", "bool" => "ReadElementContentAsBoolean", "DateOnly" => "ReadElementContentAsDateOnly", + "TimeOnly" => "ReadElementContentAsTimeOnly", _ => $"ReadElementContentAs<{data.PrimitiveTypeFriendlyName}>" }; } @@ -1129,8 +1126,7 @@ public static void GenerateIXmlSerializableMethods(GeneratorData data, SourceCod if (string.Equals(data.PrimitiveTypeFriendlyName, "string", System.StringComparison.Ordinal)) builder.AppendLine($"public void WriteXml(XmlWriter writer) => writer.WriteString({data.FieldName});"); - else - if (data.SerializationFormat is null) + else if (data.SerializationFormat is null) builder.AppendLine($"public void WriteXml(XmlWriter writer) => writer.WriteValue((({data.PrimitiveTypeFriendlyName}){data.FieldName}).ToXmlString());"); else builder.AppendLine($"public void WriteXml(XmlWriter writer) => writer.WriteString({data.FieldName}.ToString({QuoteAndEscape(data.SerializationFormat)}));"); diff --git a/src/AltaSoft.DomainPrimitives/XmlReaderExt.cs b/src/AltaSoft.DomainPrimitives/XmlReaderExt.cs index acf997e..37c3577 100644 --- a/src/AltaSoft.DomainPrimitives/XmlReaderExt.cs +++ b/src/AltaSoft.DomainPrimitives/XmlReaderExt.cs @@ -81,6 +81,24 @@ public static DateOnly ReadElementContentAsDateOnly(this XmlReader reader) return DateOnly.FromDateTime(DateTime.Parse(str, CultureInfo.InvariantCulture)); } + /// + /// Reads the content of the current XML element as a value. + /// + /// The instance. + /// + /// A value parsed from the current element's content. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TimeOnly ReadElementContentAsTimeOnly(this XmlReader reader) + { + var str = reader.ReadElementContentAsString(); + if (TimeOnly.TryParse(str, CultureInfo.InvariantCulture, out var result)) + return result; + + var dt = DateTimeOffset.ParseExact(str, s_acceptedFormats, CultureInfo.InvariantCulture, DateTimeStyles.None); + return TimeOnly.FromTimeSpan(dt.TimeOfDay); + } + /// /// Reads the content of the current XML element as a value. /// @@ -140,4 +158,12 @@ public static TimeSpan ReadElementContentAsTimeSpan(this XmlReader reader, strin return TimeSpan.Parse(str, CultureInfo.InvariantCulture); } + + private static readonly string[] s_acceptedFormats = + [ + "HH:mm:ss", + "HH:mm:sszzz", // 15:00:00+04:00 + "HH:mm:ssz", // 15:00:00Z + "HH:mm:ss'+'", // 15:00:00+ (bare plus) + ]; } diff --git a/tests/AltaSoft.DomainPrimitives.UnitTests/DateOnlyConversionTests.cs b/tests/AltaSoft.DomainPrimitives.UnitTests/DateOnlyConversionTests.cs index bd01d39..1c0a2f0 100644 --- a/tests/AltaSoft.DomainPrimitives.UnitTests/DateOnlyConversionTests.cs +++ b/tests/AltaSoft.DomainPrimitives.UnitTests/DateOnlyConversionTests.cs @@ -28,4 +28,4 @@ public void ReadElementContentAsDateOnly_WithTimezoneString_ReturnsDateOnly() Assert.Equal(new DateOnly(2024, 4, 1), result); } -} +} \ No newline at end of file diff --git a/tests/AltaSoft.DomainPrimitives.UnitTests/ParseTimeOnlyTests.cs b/tests/AltaSoft.DomainPrimitives.UnitTests/ParseTimeOnlyTests.cs new file mode 100644 index 0000000..5044dfc --- /dev/null +++ b/tests/AltaSoft.DomainPrimitives.UnitTests/ParseTimeOnlyTests.cs @@ -0,0 +1,68 @@ +using System.Xml; +using Xunit; + +namespace AltaSoft.DomainPrimitives.UnitTests; + +public class ParseTimeOnlyTests +{ + // ─── Valid cases ─────────────────────────────────────────────────────────── + + [Theory] + [InlineData("15:00:00", 15, 0, 0)] + [InlineData("00:00:00", 0, 0, 0)] + [InlineData("23:59:59", 23, 59, 59)] + [InlineData("01:02:03", 1, 2, 3)] + // With positive offset + [InlineData("15:00:00+04:00", 15, 0, 0)] + [InlineData("15:00:00+00:00", 15, 0, 0)] + [InlineData("00:00:00+05:30", 0, 0, 0)] + [InlineData("23:59:59+14:00", 23, 59, 59)] + // With negative offset + [InlineData("15:00:00-04:00", 15, 0, 0)] + [InlineData("15:00:00-00:00", 15, 0, 0)] + [InlineData("00:00:00-05:30", 0, 0, 0)] + + // With bare plus + [InlineData("15:00:00+", 15, 0, 0)] + [InlineData("00:00:00+", 0, 0, 0)] + public void Parse_ValidInput_ReturnsExpectedTimeOnly(string input, int hour, int minute, int second) + { + var result = ParseTimeOnly(input); + + Assert.Equal(new TimeOnly(hour, minute, second), result); + } + + // ─── Invalid cases: has date part ───────────────────────────────────────── + + [Theory] + [InlineData("2025/01/11")] + [InlineData("11/01/2025")] + public void Parse_InputWithDatePart_Throws(string input) + { + Assert.Throws(() => ParseTimeOnly(input)); + } + + // ─── Invalid cases: malformed time ──────────────────────────────────────── + + [Theory] + [InlineData("99:00:00")] // invalid hour + [InlineData("15:60:00")] // invalid minute + [InlineData("15:00:60")] // invalid second + [InlineData("abc")] // garbage + [InlineData("1500:00")] // malformed + [InlineData("15:00:00++04:00")] // double operator + [InlineData("15:00:00+25:00")] // invalid offset hour + [InlineData("")] // empty + [InlineData(" ")] // whitespace + public void Parse_MalformedInput_Throws(string input) + { + Assert.Throws(() => ParseTimeOnly(input)); + } + private static TimeOnly ParseTimeOnly(string content) + { + var xml = $"{content}"; + var reader = XmlReader.Create(new StringReader(xml)); + reader.ReadToFollowing("root"); + return reader.ReadElementContentAsTimeOnly(); + } +}