fix(schema): reject year 0000 in dateTime, date, gYearMonth, gYear

XSD 1.0 has no year zero (the year before 0001 is -0001), so 0000-01-01T00:00:00
and the other 0000 forms are not valid values. DateTimeParser.parseYearOnly now
rejects a zero magnitude for both signs, so negative years like -0001 still parse.

Closes the XSTS dateTime011 false negative: invalid-instances-accepted 31 -> 30,
no other bucket moved (valid-schemas-rejected and valid-instances-rejected held at
0). 1449 unit tests pass; the full XSTS suite settles at 0/43/0/30.
This commit is contained in:
Mihaela Mihaljevic 2026-06-20 01:09:43 +02:00
parent d9ebfc1998
commit 01650a454e
5 changed files with 27 additions and 5 deletions

View File

@ -28,6 +28,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- `dateTime`, `date`, `gYearMonth`, and `gYear` now reject the year `0000`. XSD
1.0 has no year zero (the year before `0001` is `-0001`), so `0000-01-01T00:00:00`
and the other `0000` forms are not valid values. `DateTimeParser.parseYearOnly`
rejects a zero magnitude for both signs, so `-0001` and other negative years
still parse. XSTS invalid-instances-accepted 31 -> 30 (`dateTime011`), no other
bucket moved (valid-rejected held at 0).
- Fixed a dead `rootLocation` fallback in `XSDParser.containerLocationMap`. The
map value type is itself `String?`, so `loc ?? rootLocation` bound the
coalescing at the outer optional and never used `rootLocation` (the compiler

View File

@ -54,7 +54,10 @@ extension PureXML.Schema {
private mutating func parseYearOnly() -> DateTimeValue? {
var value = DateTimeValue()
let negative = consume("-")
guard let digits = readDigits(minimum: 4) else { return nil }
// XSD 1.0 has no year zero: the lexical year 0000 (and -0000) is not a
// valid value for dateTime, date, gYearMonth, or gYear. `digits` is the
// magnitude, so rejecting 0 covers both signs.
guard let digits = readDigits(minimum: 4), digits != 0 else { return nil }
value.year = (negative ? -1 : 1) * digits
return value
}

View File

@ -57,6 +57,18 @@ struct XSDDatatypeTests {
#expect(!valid("2026-06-08T12:30:00+15:00", .dateTime))
}
@Test("XSD 1.0 has no year zero: 0000 is rejected across the year-bearing types")
func test_yearZero() {
// Rejected: the prohibited year 0000 (and -0000) wherever a year appears.
#expect(!valid("0000-01-01T00:00:00", .dateTime))
#expect(!valid("0000-01-01", .date))
#expect(!valid("0000-01", .gYearMonth))
#expect(!valid("0000", .gYear) && !valid("-0000", .gYear))
// Accepted: the smallest magnitudes either side of zero, and a normal year.
#expect(valid("0001-01-01T00:00:00", .dateTime) && valid("-0001-12-31", .date))
#expect(valid("0001", .gYear) && valid("2026", .gYear) && valid("-0044-03", .gYearMonth))
}
@Test("the g* date types parse their reduced forms")
func test_gTypes() {
#expect(valid("--02-29", .gMonthDay) && !valid("--02-30", .gMonthDay))

View File

@ -150,13 +150,13 @@ struct XSTSSuiteTests {
private let knownSchemaValidRejected = 0
private let knownSchemaInvalidAccepted = 43
private let knownInstanceValidRejected = 0
/// Instance bucket (133 -> 31), per-step deltas in CHANGELOG.md: xsi:type must derive from the
/// Instance bucket (133 -> 30), per-step deltas in CHANGELOG.md: xsi:type must derive from the
/// declared type (complex, then atomic and list/union, resolved through the element-ref chain);
/// block/blockDefault (incl. "substitution" and an untyped head) bar substitutions after the
/// derivation backbone was re-keyed by namespace; ur-type `xsd:anyType` wildcards are `lax`;
/// identity fields compare in the decimal value space and take a declared default/fixed when
/// absent or empty; a complex type's attribute wildcard is the INTERSECTION of its sources.
private let knownInstanceInvalidAccepted = 31
/// absent/empty; an attribute wildcard is the INTERSECTION of its sources; year 0000 is invalid.
private let knownInstanceInvalidAccepted = 30
@Test("Every XSTS case behaves: compile, reject, validate, invalidate")
func test_suite() throws {

View File

@ -94,7 +94,7 @@ classification is wrong.
| Nillability | `nillable00201m/nillable00201m2_n` | `XSDParserHelpers`, instance validator |
| Type derivation | `typedef00204m/typeDef00204m1_n` | `ComplexValidatorXSIType`, derivation tables |
| Additional-suite instance triage | `addB065/addB065.i` | to classify from fixture |
| Datatype value-space edges | `anyURI_a004_1339/anyURI_a004_1339.i`, `dateTime011_2008/dateTime011_2008.i` | `SimpleType` |
| Datatype value-space edges | `anyURI_a004_1339/anyURI_a004_1339.i` (open); ~~`dateTime011_2008/dateTime011_2008.i`~~ closed: XSD 1.0 has no year zero, `0000` now rejected | `SimpleType`, `DateTimeParser` |
| Element constraints | `elemO011/elemO011.i`, `elemT074/elemT074.i` | instance validator, element declarations |
| Identity constraints | `idG006/idG006.i`, `idK012/idK012.i`, `idZ010/idZ010.i`, `idZ012/idZ012.i` | `IdentityValidator*` |
| Model group instance matching | `mgO013/mgO013.i`, `mgZ001/mgZ001.i` | `ContentMatcher`, `ParticleRestriction` fallout |