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:
parent
d9ebfc1998
commit
01650a454e
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 |
|
||||
|
||||
Loading…
Reference in New Issue
Block a user