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
33 changes: 13 additions & 20 deletions docs/type-to-string-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,39 +278,32 @@ public static partial class DateFormatter
}

// Interpolation formats with the current culture, and NumberFormatInfo.NegativeSign is not "-"
// everywhere: sv-SE renders U+2212 and ar-SA prefixes U+061C. A negative offset carries that
// sign into both snapshot content and parameter file names, so the culture is pinned here the
// same way it is for every date part above
// everywhere: sv-SE renders U+2212 and ar-SA prefixes U+061C. The offset reaches both snapshot
// content and parameter file names, so the culture is pinned here the same way it is for every
// date part above, and the sign is written as a literal rather than taken from a negative number.
static string GetDateOffset(DateTimeOffset value)
{
var offset = value.Offset;

if (offset > TimeSpan.Zero)
if (offset == TimeSpan.Zero)
{
if (offset.Minutes == 0)
{
return FormattableString.Invariant($"+{offset.TotalHours:0}");
}

return FormattableString.Invariant($"+{offset.Hours:0}-{offset.Minutes:00}");
return "+0";
}

if (offset < TimeSpan.Zero)
{
if (offset.Minutes == 0)
{
return FormattableString.Invariant($"{offset.Hours:0}");
}
// The sign belongs to the offset as a whole. Taking it from the hour component
// instead loses it for a sub hour offset, where that component is zero.
var sign = offset < TimeSpan.Zero ? '-' : '+';

// Minutes is negative too, which is what renders the separator
return FormattableString.Invariant($"{offset.Hours:0}{offset.Minutes:00}");
if (offset.Minutes == 0)
{
return FormattableString.Invariant($"{sign}{Math.Abs(offset.TotalHours):0}");
}

return "+0";
return FormattableString.Invariant($"{sign}{Math.Abs(offset.Hours):0}-{Math.Abs(offset.Minutes):00}");
}
}
```
<sup><a href='/src/Verify/Serialization/DateFormatter_DateTimeOffset.cs#L1-L96' title='Snippet source file'>snippet source</a> | <a href='#snippet-DateFormatter_DateTimeOffset.cs' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify/Serialization/DateFormatter_DateTimeOffset.cs#L1-L89' title='Snippet source file'>snippet source</a> | <a href='#snippet-DateFormatter_DateTimeOffset.cs' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


Expand Down
18 changes: 18 additions & 0 deletions src/Verify.Tests/DateFormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,24 @@ public Task SubMillisecondTicks()
TimeSpan.TicksPerMinute + 1
];

// A sub hour offset has a zero hour component, so the sign has to come from
// the offset itself
[Fact]
public void SubHourOffsets()
{
var negative = new DateTimeOffset(2000, 10, 1, 0, 0, 0, TimeSpan.FromMinutes(-30));
Assert.Equal("2000-10-01 -0-30", DateFormatter.Convert(negative));
Assert.Equal("2000-10-01-0-30", DateFormatter.ToParameterString(negative));

var positive = new DateTimeOffset(2000, 10, 1, 0, 0, 0, TimeSpan.FromMinutes(30));
Assert.Equal("2000-10-01 +0-30", DateFormatter.Convert(positive));
Assert.Equal("2000-10-01+0-30", DateFormatter.ToParameterString(positive));

Assert.NotEqual(
DateFormatter.ToParameterString(negative),
DateFormatter.ToParameterString(positive));
}

static bool[] bools =
[
true,
Expand Down
31 changes: 12 additions & 19 deletions src/Verify/Serialization/DateFormatter_DateTimeOffset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,34 +63,27 @@ static string GetParameterDatePart(DateTimeOffset value)
}

// Interpolation formats with the current culture, and NumberFormatInfo.NegativeSign is not "-"
// everywhere: sv-SE renders U+2212 and ar-SA prefixes U+061C. A negative offset carries that
// sign into both snapshot content and parameter file names, so the culture is pinned here the
// same way it is for every date part above
// everywhere: sv-SE renders U+2212 and ar-SA prefixes U+061C. The offset reaches both snapshot
// content and parameter file names, so the culture is pinned here the same way it is for every
// date part above, and the sign is written as a literal rather than taken from a negative number.
static string GetDateOffset(DateTimeOffset value)
{
var offset = value.Offset;

if (offset > TimeSpan.Zero)
if (offset == TimeSpan.Zero)
{
if (offset.Minutes == 0)
{
return FormattableString.Invariant($"+{offset.TotalHours:0}");
}

return FormattableString.Invariant($"+{offset.Hours:0}-{offset.Minutes:00}");
return "+0";
}

if (offset < TimeSpan.Zero)
{
if (offset.Minutes == 0)
{
return FormattableString.Invariant($"{offset.Hours:0}");
}
// The sign belongs to the offset as a whole. Taking it from the hour component
// instead loses it for a sub hour offset, where that component is zero.
var sign = offset < TimeSpan.Zero ? '-' : '+';

// Minutes is negative too, which is what renders the separator
return FormattableString.Invariant($"{offset.Hours:0}{offset.Minutes:00}");
if (offset.Minutes == 0)
{
return FormattableString.Invariant($"{sign}{Math.Abs(offset.TotalHours):0}");
}

return "+0";
return FormattableString.Invariant($"{sign}{Math.Abs(offset.Hours):0}-{Math.Abs(offset.Minutes):00}");
}
}
2 changes: 1 addition & 1 deletion src/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ All six resolved 2026-08-16 (five fixed here; the inline item resolved as by-des
- [x] **`FlattenMessage` omits the joining space after a line ending in `.`.**
`Verify/Combinations/CombinationResultsConverter.cs:168-183` — two-line messages (net48 `ArgumentNullException`) render as `"Value cannot be null.Parameter name: p"`.

- [ ] **Negative sub-hour offsets render unsigned.**
- [x] **Negative sub-hour offsets render unsigned.**
`Verify/Serialization/DateFormatter_DateTimeOffset.cs:74-82` — `TimeSpan.FromMinutes(-30)` renders `0-30` (positive twin is `+0-30`). No real timezone in that range; constructed offsets only.

- [x] **Mismatch crash for handle-based `FileStream` received streams.**
Expand Down
Loading