Skip to content

Commit bad98be

Browse files
authored
Merge pull request #42 from altasoft/bugfix/TimeOnlyXmlSerialization
TimeOnly xml deserialization bug fix
2 parents 1337707 + bb21e69 commit bad98be

5 files changed

Lines changed: 102 additions & 12 deletions

File tree

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<Product>Domain Primitives</Product>
1212
<Company>ALTA Software llc.</Company>
1313
<Copyright>Copyright © 2024 ALTA Software llc.</Copyright>
14-
<Version>7.1.0</Version>
14+
<Version>7.1.1</Version>
1515
</PropertyGroup>
1616

1717
<PropertyGroup>

src/AltaSoft.DomainPrimitives.Generator/Helpers/MethodGeneratorHelper.cs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -871,13 +871,11 @@ public static void GenerateParsable(GeneratorData data, SourceCodeBuilder builde
871871
{
872872
builder.Append("s");
873873
}
874-
else
875-
if (isChar)
874+
else if (isChar)
876875
{
877876
builder.Append("char.Parse(s)");
878877
}
879-
else
880-
if (isBool)
878+
else if (isBool)
881879
{
882880
builder.Append("bool.Parse(s)");
883881
}
@@ -899,13 +897,11 @@ public static void GenerateParsable(GeneratorData data, SourceCodeBuilder builde
899897
{
900898
builder.AppendLine("if (s is null)");
901899
}
902-
else
903-
if (isChar)
900+
else if (isChar)
904901
{
905902
builder.AppendLine("if (!char.TryParse(s, out var value))");
906903
}
907-
else
908-
if (isBool)
904+
else if (isBool)
909905
{
910906
builder.AppendLine("if (!bool.TryParse(s, out var value))");
911907
}
@@ -1110,6 +1106,7 @@ public static void GenerateIXmlSerializableMethods(GeneratorData data, SourceCod
11101106
"string" => "ReadElementContentAsString",
11111107
"bool" => "ReadElementContentAsBoolean",
11121108
"DateOnly" => "ReadElementContentAsDateOnly",
1109+
"TimeOnly" => "ReadElementContentAsTimeOnly",
11131110
_ => $"ReadElementContentAs<{data.PrimitiveTypeFriendlyName}>"
11141111
};
11151112
}
@@ -1129,8 +1126,7 @@ public static void GenerateIXmlSerializableMethods(GeneratorData data, SourceCod
11291126

11301127
if (string.Equals(data.PrimitiveTypeFriendlyName, "string", System.StringComparison.Ordinal))
11311128
builder.AppendLine($"public void WriteXml(XmlWriter writer) => writer.WriteString({data.FieldName});");
1132-
else
1133-
if (data.SerializationFormat is null)
1129+
else if (data.SerializationFormat is null)
11341130
builder.AppendLine($"public void WriteXml(XmlWriter writer) => writer.WriteValue((({data.PrimitiveTypeFriendlyName}){data.FieldName}).ToXmlString());");
11351131
else
11361132
builder.AppendLine($"public void WriteXml(XmlWriter writer) => writer.WriteString({data.FieldName}.ToString({QuoteAndEscape(data.SerializationFormat)}));");

src/AltaSoft.DomainPrimitives/XmlReaderExt.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,24 @@ public static DateOnly ReadElementContentAsDateOnly(this XmlReader reader)
8181
return DateOnly.FromDateTime(DateTime.Parse(str, CultureInfo.InvariantCulture));
8282
}
8383

84+
/// <summary>
85+
/// Reads the content of the current XML element as a <see cref="TimeOnly"/> value.
86+
/// </summary>
87+
/// <param name="reader">The <see cref="XmlReader"/> instance.</param>
88+
/// <returns>
89+
/// A <see cref="TimeOnly"/> value parsed from the current element's content.
90+
/// </returns>
91+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
92+
public static TimeOnly ReadElementContentAsTimeOnly(this XmlReader reader)
93+
{
94+
var str = reader.ReadElementContentAsString();
95+
if (TimeOnly.TryParse(str, CultureInfo.InvariantCulture, out var result))
96+
return result;
97+
98+
var dt = DateTimeOffset.ParseExact(str, s_acceptedFormats, CultureInfo.InvariantCulture, DateTimeStyles.None);
99+
return TimeOnly.FromTimeSpan(dt.TimeOfDay);
100+
}
101+
84102
/// <summary>
85103
/// Reads the content of the current XML element as a <see cref="DateOnly"/> value.
86104
/// </summary>
@@ -140,4 +158,12 @@ public static TimeSpan ReadElementContentAsTimeSpan(this XmlReader reader, strin
140158

141159
return TimeSpan.Parse(str, CultureInfo.InvariantCulture);
142160
}
161+
162+
private static readonly string[] s_acceptedFormats =
163+
[
164+
"HH:mm:ss",
165+
"HH:mm:sszzz", // 15:00:00+04:00
166+
"HH:mm:ssz", // 15:00:00Z
167+
"HH:mm:ss'+'", // 15:00:00+ (bare plus)
168+
];
143169
}

tests/AltaSoft.DomainPrimitives.UnitTests/DateOnlyConversionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ public void ReadElementContentAsDateOnly_WithTimezoneString_ReturnsDateOnly()
2828

2929
Assert.Equal(new DateOnly(2024, 4, 1), result);
3030
}
31-
}
31+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System.Xml;
2+
using Xunit;
3+
4+
namespace AltaSoft.DomainPrimitives.UnitTests;
5+
6+
public class ParseTimeOnlyTests
7+
{
8+
// ─── Valid cases ───────────────────────────────────────────────────────────
9+
10+
[Theory]
11+
[InlineData("15:00:00", 15, 0, 0)]
12+
[InlineData("00:00:00", 0, 0, 0)]
13+
[InlineData("23:59:59", 23, 59, 59)]
14+
[InlineData("01:02:03", 1, 2, 3)]
15+
// With positive offset
16+
[InlineData("15:00:00+04:00", 15, 0, 0)]
17+
[InlineData("15:00:00+00:00", 15, 0, 0)]
18+
[InlineData("00:00:00+05:30", 0, 0, 0)]
19+
[InlineData("23:59:59+14:00", 23, 59, 59)]
20+
// With negative offset
21+
[InlineData("15:00:00-04:00", 15, 0, 0)]
22+
[InlineData("15:00:00-00:00", 15, 0, 0)]
23+
[InlineData("00:00:00-05:30", 0, 0, 0)]
24+
25+
// With bare plus
26+
[InlineData("15:00:00+", 15, 0, 0)]
27+
[InlineData("00:00:00+", 0, 0, 0)]
28+
public void Parse_ValidInput_ReturnsExpectedTimeOnly(string input, int hour, int minute, int second)
29+
{
30+
var result = ParseTimeOnly(input);
31+
32+
Assert.Equal(new TimeOnly(hour, minute, second), result);
33+
}
34+
35+
// ─── Invalid cases: has date part ─────────────────────────────────────────
36+
37+
[Theory]
38+
[InlineData("2025/01/11")]
39+
[InlineData("11/01/2025")]
40+
public void Parse_InputWithDatePart_Throws(string input)
41+
{
42+
Assert.Throws<FormatException>(() => ParseTimeOnly(input));
43+
}
44+
45+
// ─── Invalid cases: malformed time ────────────────────────────────────────
46+
47+
[Theory]
48+
[InlineData("99:00:00")] // invalid hour
49+
[InlineData("15:60:00")] // invalid minute
50+
[InlineData("15:00:60")] // invalid second
51+
[InlineData("abc")] // garbage
52+
[InlineData("1500:00")] // malformed
53+
[InlineData("15:00:00++04:00")] // double operator
54+
[InlineData("15:00:00+25:00")] // invalid offset hour
55+
[InlineData("")] // empty
56+
[InlineData(" ")] // whitespace
57+
public void Parse_MalformedInput_Throws(string input)
58+
{
59+
Assert.Throws<FormatException>(() => ParseTimeOnly(input));
60+
}
61+
private static TimeOnly ParseTimeOnly(string content)
62+
{
63+
var xml = $"<root>{content}</root>";
64+
var reader = XmlReader.Create(new StringReader(xml));
65+
reader.ReadToFollowing("root");
66+
return reader.ReadElementContentAsTimeOnly();
67+
}
68+
}

0 commit comments

Comments
 (0)