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.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.*" />
<PackageVersion Include="Verify.SourceGenerators" Version="2.5.*" />
<PackageVersion Include="Verify.Xunit" Version="31.9.3" />
<PackageVersion Include="Verify.Xunit" Version="31.12.5" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
Expand Down
19 changes: 17 additions & 2 deletions src/AltaSoft.DomainPrimitives/ToXmlStringExt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,27 @@ public static class ToXmlStringExt
public static string ToXmlString(this DateOnly value) => value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);

/// <summary>
/// Converts a <see cref="TimeOnly" /> value to its XML string representation in the format "HH:mm:ss".
/// Converts a <see cref="TimeOnly" /> value to its XML string representation in the format "HH:mm:sszzz",
/// where the time zone offset reflects the current local offset from UTC.
/// </summary>
/// <param name="value">The TimeOnly value to convert.</param>
/// <returns>The XML string representation of the TimeOnly value.</returns>

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string ToXmlString(this TimeOnly value) => value.ToString("HH:mm:ss", CultureInfo.InvariantCulture);
public static string ToXmlString(this TimeOnly value)
{
// We avoid constructing a local DateTime for an arbitrary date to prevent
// possible exceptions on DST transition days (invalid or ambiguous local times).
var timePart = value.ToString("HH:mm:ss", CultureInfo.InvariantCulture);

// Derive the current local offset from UTC in a DST-safe way using the current instant.
var offset = TimeZoneInfo.Local.GetUtcOffset(DateTime.UtcNow);
var offsetSign = offset < TimeSpan.Zero ? "-" : "+";
var absoluteOffset = offset.Duration();
var offsetPart = absoluteOffset.ToString(@"hh\:mm", CultureInfo.InvariantCulture);

return $"{timePart}{offsetSign}{offsetPart}";
}

/// <summary>
/// Converts a <see cref="DateTimeOffset" /> value to its XML string representation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@ public void TimeOnly_ToXmlString_ReturnsExpectedFormat()
{
var t = new TimeOnly(13, 45, 30);
var xml = t.ToXmlString();
Assert.Equal("13:45:30", xml);

// Validate the overall shape: time followed by a timezone offset
Assert.Matches(@"^13:45:30[+-]\d{2}:\d{2}$", xml);

// Validate the offset suffix matches the actual current local offset
var offset = TimeZoneInfo.Local.GetUtcOffset(DateTime.UtcNow);
var offsetSign = offset < TimeSpan.Zero ? "-" : "+";
var expectedOffsetSuffix = $"{offsetSign}{offset.Duration():hh\\:mm}";
Assert.EndsWith(expectedOffsetSuffix, xml);
}

[Fact]
Expand Down
Loading