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();
+ }
+}