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
41 changes: 41 additions & 0 deletions src/ApplyScrubbersTests/TrimmedFractionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Formats ending in an upper case fraction produce a second scrubber for the trimmed
// format, since those fractions render as empty when zero. A one character format
// string is a standard format specifier, so the trimmed format has to be escaped.
public class TrimmedFractionTests
{
static readonly CultureInfo enUs = new("en-US");

[Fact]
public void SingleCharTrimmedFormatDoesNotBecomeStandardFormat()
{
var scrubbers = DateMatchers.DateTimes("s.F", enUs);

// "s" as a standard format is the sortable pattern, so the trimmed scrubber
// used to swallow every full sortable date-time in the output
var result = EngineRunner.Run("2020-01-01T10:20:30", scrubbers);
Assert.NotEqual("DateTime_1", result);

// the seconds the format actually asks for still scrub
Assert.Equal("DateTime_1", EngineRunner.Run("9", scrubbers));
}

[Fact]
public void SingleCharTrimmedFormatIsNotRejected()
{
// "H" is not a standard format specifier, so trimming used to throw
// "Invalid format: H" at registration
var scrubbers = DateMatchers.DateTimes("H.F", enUs);

Assert.Equal(2, scrubbers.Length);
Assert.Equal("DateTime_1", EngineRunner.Run("9", scrubbers));
}

[Fact]
public void FractionOnlyFormatHasNoTrimmedScrubber()
{
// trimming leaves nothing to parse
var scrubbers = DateMatchers.DateTimes(".F", enUs);

Assert.Single(scrubbers);
}
}
41 changes: 28 additions & 13 deletions src/Verify/Serialization/Scrubbers/DateMatchers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -268,31 +268,46 @@ static bool StartsWithNumericToken(string format)

static bool TryGetFormatWithUpperMillisecondsTrimmed(string format, [NotNullWhen(true)] out string? trimmedFormat)
{
string trimmed;
if (format.EndsWith(".FFFF", StringComparison.Ordinal))
{
trimmedFormat = format[..^5];
return true;
trimmed = format[..^5];
}

if (format.EndsWith(".FFF", StringComparison.Ordinal))
else if (format.EndsWith(".FFF", StringComparison.Ordinal))
{
trimmedFormat = format[..^4];
return true;
trimmed = format[..^4];
}
else if (format.EndsWith(".FF", StringComparison.Ordinal))
{
trimmed = format[..^3];
}
else if (format.EndsWith(".F", StringComparison.Ordinal))
{
trimmed = format[..^2];
}
else
{
trimmedFormat = null;
return false;
}

if (format.EndsWith(".FF", StringComparison.Ordinal))
// Nothing is left to parse, so the untrimmed scrubber is the only one
if (trimmed.Length == 0)
{
trimmedFormat = format[..^3];
return true;
trimmedFormat = null;
return false;
}

if (format.EndsWith(".F", StringComparison.Ordinal))
// A one character format string is read as a standard format specifier, so "s.F"
// would trim to the culture's sortable pattern and "H.F" to nothing valid at all.
// `%` forces the character to be read as the custom specifier it was written as.
if (trimmed.Length == 1)
{
trimmedFormat = format[..^2];
trimmedFormat = $"%{trimmed}";
return true;
}

trimmedFormat = null;
return false;
trimmedFormat = trimmed;
return true;
}
}
2 changes: 1 addition & 1 deletion src/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ All six resolved 2026-08-16 (five fixed here; the inline item resolved as by-des
- [ ] **`#` in parameter values collides with the indexed-target namespace.**
`Verify/Naming/MatchingFileFinder.cs:9,20` — `indexedPattern: "{prefix}#"` matches by prefix, and `#` is not sanitized. Cases `"x"` and `"x#1"` on one method: running `"x"` deletes `C.M_p=x#1.received.txt` and sweeps `C.M_p=x#1.verified.txt` into the stale set (deleted under AutoVerify).

- [ ] **Trimmed fraction format collapses into a standard format specifier.**
- [x] **Trimmed fraction format collapses into a standard format specifier.**
`Verify/Serialization/Scrubbers/DateMatchers.cs:269-297` (consumed at 118-127) — `ScrubInlineDateTimes("s.F")` builds a secondary scrubber for `"s"`; length-1 formats are standard specifiers, so it scrubs every full sortable date-time in the output. `"H.F"` trims to `"H"` and throws `Invalid format: H` at registration despite passing up-front validation.

- [ ] **`MemberConverter` has no exact-type precedence.**
Expand Down
Loading