Skip to content

Format UTC offsets with the invariant culture - #1854

Merged
SimonCropp merged 2 commits into
mainfrom
fix-negative-offset-culture
Aug 17, 2026
Merged

Format UTC offsets with the invariant culture#1854
SimonCropp merged 2 commits into
mainfrom
fix-negative-offset-culture

Conversation

@SimonCropp

Copy link
Copy Markdown
Member

GetDateOffset in src/Verify/Serialization/DateFormatter_DateTimeOffset.cs built the offset with plain interpolation, so the numbers went through CurrentCulture, while every other part of the rendered date in that file passes Culture.InvariantCulture explicitly.

NumberFormatInfo.NegativeSign is not - everywhere:

Culture Negative sign -01:30 renders as
invariant / en-US U+002D -1-30
sv-SE U+2212 −1−30
ar-SA U+061C U+002D ؜-1؜-30
fa-IR U+200E U+2212 ‎−1‎−30

Only negative offsets are affected; the positive and zero branches write a literal +.

Impact

The string reaches two places.

  • Snapshot content, via DateFormatter.Convert. A DateTimeOffset serialized on a Swedish machine differs from the one committed from an en-US machine even though the values are equal.
  • Parameter file names, via DateFormatter.ToParameterString. The verified file for a parameter with offset -05:00 is looked up under a name that machine can never produce a match for, so the test reports the snapshot as new on every run.

Change

The four interpolations now go through FormattableString.Invariant, which keeps the format strings in place and is available on every target framework down to net462. Output under the invariant culture is byte identical, so no existing snapshot moves.

Test

DateFormatterTests.OffsetDoesNotTakeTheCurrentCultureNegativeSign pins all five branches (-1.5, -5, +1.5, +5, 0) against expected literals while CurrentCulture carries U+2212 as its negative sign. The sign is assigned onto a clone of the invariant culture rather than picking a real culture, so the assertion does not move with the ICU data the test runs against.

Confirmed the test fails on unpatched source (both negative cases, positives unaffected) and passes after. Full Verify.Tests suite green: 1304 passed, 0 failed. Verify.csproj builds clean across all nine target frameworks.

GetDateOffset built the offset with plain interpolation, so the numbers went
through CurrentCulture while every other part of the rendered date passed
Culture.InvariantCulture explicitly.

NumberFormatInfo.NegativeSign is not "-" everywhere. sv-SE renders U+2212
MINUS SIGN, ar-SA prefixes U+061C ARABIC LETTER MARK, and fa-IR uses
U+200E + U+2212. Only negative offsets are affected, since the positive and
zero branches write a literal "+".

The string reaches two places. As snapshot content, via Convert, a
DateTimeOffset serialized on a Swedish machine differs from the one committed
from an en-US machine even though the values are equal. As a parameter file
name, via ToParameterString, the verified file for offset -05:00 is looked up
under a name that machine can never produce a match for.
@SimonCropp SimonCropp added this to the 32.0.0 milestone Aug 17, 2026
@SimonCropp
SimonCropp merged commit 1e6d61e into main Aug 17, 2026
1 of 3 checks passed
@SimonCropp
SimonCropp deleted the fix-negative-offset-culture branch August 17, 2026 13:33
This was referenced Aug 26, 2026
tom-wolfe pushed a commit to tom-wolfe/Wolfe.CommandLine that referenced this pull request Aug 27, 2026
Updated [Verify.XunitV3](https://github.com/VerifyTests/Verify) from
31.28.0 to 32.0.0.

<details>
<summary>Release notes</summary>

_Sourced from [Verify.XunitV3's
releases](https://github.com/VerifyTests/Verify/releases)._

## 32.0.0

- [x] [#​1832](VerifyTests/Verify#1832) Add
Inline snpashot support
- [x] [#​1835](VerifyTests/Verify#1835) Add
SettingsTask.AddContext
- [x] [#​1847](VerifyTests/Verify#1847) Honor
explicit DefaultValueHandling for bools
- [x] [#​1850](VerifyTests/Verify#1850) Stray
extension-less snapshot file crashes every run of its test
- [x] [#​1851](VerifyTests/Verify#1851) Static
UniqueForTargetFramework(Assembly) overloads ignore the assembly passed
to them
- [x] [#​1852](VerifyTests/Verify#1852)
Appended files skip scrubbing and newline normalization when a stream
converter is registered
- [x] [#​1853](VerifyTests/Verify#1853)
IgnoreParameters() ignore-all sentinel is lost when combined with
another ignore source
- [x] [#​1854](VerifyTests/Verify#1854) Format
UTC offsets with the invariant culture
- [x] [#​1855](VerifyTests/Verify#1855) Format
UTC offsets with invariant culture
- [x] [#​1856](VerifyTests/Verify#1856) Use
ticks to decide date precision
- [x] [#​1857](VerifyTests/Verify#1857) Clean #
out of file name parameters
- [x] [#​1858](VerifyTests/Verify#1858) Escape
one character trimmed date formats
- [x] [#​1859](VerifyTests/Verify#1859) Prefer
the exact type when resolving a member converter
- [x] [#​1860](VerifyTests/Verify#1860) Keep
empty collections ignored alongside instance predicates
- [x] [#​1861](VerifyTests/Verify#1861) Key the
combinations name cache by reference
- [x] [#​1862](VerifyTests/Verify#1862) Match
MSTest overloads against the test data
- [x] [#​1863](VerifyTests/Verify#1863) Report
deletes as directory relative paths
- [x] [#​1864](VerifyTests/Verify#1864) Name the
guarded API in ThrowIfVerifyHasBeenRun
- [x] [#​1865](VerifyTests/Verify#1865) Keep
stack frames that have no parameter list
- [x] [#​1866](VerifyTests/Verify#1866) Generate
the TestContext property for record test classes
- [x] [#​1867](VerifyTests/Verify#1867) Fix two
CombinationResultsConverter edge cases
- [x] [#​1868](VerifyTests/Verify#1868) Sign
sub-hour UTC offsets
- [x] [#​1869](VerifyTests/Verify#1869) Write
the received file from the stream for handle based FileStreams
- [x] [#​1870](VerifyTests/Verify#1870) Compare
prefixes ignoring case
- [x] [#​1871](VerifyTests/Verify#1871) Guard
the counter value caches with a lock
- [x] [#​1872](VerifyTests/Verify#1872) Sign
sub-hour UTC offsets

## 32.0.0-beta.16



## 32.0.0-beta.15



## 32.0.0-beta.14



## 32.0.0-beta.13



## 32.0.0-beta.12



## 32.0.0-beta.10



## 32.0.0-beta.8



## 32.0.0-beta.7



## 32.0.0-beta.6



## 32.0.0-beta.5



## 32.0.0-beta.4



## 32.0.0-beta.3



## 32.0.0-beta.2



## 32.0.0-beta.1



Commits viewable in [compare
view](VerifyTests/Verify@31.28.0...32.0.0).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=Verify.XunitV3&package-manager=nuget&previous-version=31.28.0&new-version=32.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants