Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<Product>Domain Primitives</Product>
<Company>ALTA Software llc.</Company>
<Copyright>Copyright © 2024 ALTA Software llc.</Copyright>
<Version>7.1.0</Version>
<Version>7.1.1</Version>
</PropertyGroup>

<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)");
}
Expand All @@ -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))");
}
Expand Down Expand Up @@ -1110,6 +1106,7 @@ public static void GenerateIXmlSerializableMethods(GeneratorData data, SourceCod
"string" => "ReadElementContentAsString",
"bool" => "ReadElementContentAsBoolean",
"DateOnly" => "ReadElementContentAsDateOnly",
"TimeOnly" => "ReadElementContentAsTimeOnly",
_ => $"ReadElementContentAs<{data.PrimitiveTypeFriendlyName}>"
};
}
Expand All @@ -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)}));");
Expand Down
26 changes: 26 additions & 0 deletions src/AltaSoft.DomainPrimitives/XmlReaderExt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,24 @@ public static DateOnly ReadElementContentAsDateOnly(this XmlReader reader)
return DateOnly.FromDateTime(DateTime.Parse(str, CultureInfo.InvariantCulture));
}

/// <summary>
/// Reads the content of the current XML element as a <see cref="TimeOnly"/> value.
/// </summary>
/// <param name="reader">The <see cref="XmlReader"/> instance.</param>
/// <returns>
/// A <see cref="TimeOnly"/> value parsed from the current element's content.
/// </returns>
[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);
}

/// <summary>
/// Reads the content of the current XML element as a <see cref="DateOnly"/> value.
/// </summary>
Expand Down Expand Up @@ -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)
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ public void ReadElementContentAsDateOnly_WithTimezoneString_ReturnsDateOnly()

Assert.Equal(new DateOnly(2024, 4, 1), result);
}
}
}
68 changes: 68 additions & 0 deletions tests/AltaSoft.DomainPrimitives.UnitTests/ParseTimeOnlyTests.cs
Original file line number Diff line number Diff line change
@@ -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<FormatException>(() => 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<FormatException>(() => ParseTimeOnly(input));
}
private static TimeOnly ParseTimeOnly(string content)
{
var xml = $"<root>{content}</root>";
var reader = XmlReader.Create(new StringReader(xml));
reader.ReadToFollowing("root");
return reader.ReadElementContentAsTimeOnly();
}
}