diff --git a/Sources/Schema/DurationValue.swift b/Sources/Schema/DurationValue.swift index 4f4cf67..80b5254 100644 --- a/Sources/Schema/DurationValue.swift +++ b/Sources/Schema/DurationValue.swift @@ -1,4 +1,7 @@ extension PureXML.Schema { + /// The outcome of comparing two durations, which form a partial order. + enum DurationOrder { case lessThan, equal, greaterThan, incomparable } + /// An `xs:duration` as its two independent signed components, months and /// seconds, with the XSD 1.0 order relation. Because a month is 28 to 31 /// days, the order is partial: P and Q compare by adding each to the four @@ -10,14 +13,13 @@ extension PureXML.Schema { let months: Int let seconds: Double - enum Order { case lessThan, equal, greaterThan, incomparable } - - /// The four reference dateTimes (year, month, day) at T00:00:00Z. - private static let references: [(year: Int, month: Int, day: Int)] = [ - (1696, 9, 1), (1697, 2, 1), (1903, 3, 1), (1903, 7, 1), + /// The four reference points (year, month); each is the first of its + /// month at T00:00:00Z, so the day is always 1 and need not be carried. + private static let references: [(year: Int, month: Int)] = [ + (1696, 9), (1697, 2), (1903, 3), (1903, 7), ] - func compare(to other: DurationValue) -> Order { + func compare(to other: DurationValue) -> DurationOrder { var sawLess = false var sawGreater = false var sawEqual = false @@ -40,11 +42,11 @@ extension PureXML.Schema { return .incomparable } - /// The instant, in seconds, of `reference` + (months, seconds): add the - /// months with calendar rollover (clamping the day to the resulting - /// month's length, per the spec), then add the seconds component. + /// The instant, in seconds, of `reference` (day 1) + (months, seconds): + /// add the months with calendar rollover (clamping the day to the + /// resulting month's length, per the spec), then add the seconds. private static func instant( - _ reference: (year: Int, month: Int, day: Int), + _ reference: (year: Int, month: Int), addingMonths addMonths: Int, seconds: Double, ) -> Double { @@ -52,7 +54,7 @@ extension PureXML.Schema { let yearsCarried = Int((Double(total) / 12.0).rounded(.down)) let newYear = reference.year + yearsCarried let newMonth = total - yearsCarried * 12 + 1 - let newDay = Swift.min(reference.day, DateTimeValue.daysInMonth(year: newYear, month: newMonth)) + let newDay = Swift.min(1, DateTimeValue.daysInMonth(year: newYear, month: newMonth)) let days = DateTimeValue.daysFromCivil(year: newYear, month: newMonth, day: newDay) return Double(days) * 86400 + seconds } diff --git a/Sources/Schema/SimpleType.swift b/Sources/Schema/SimpleType.swift index 1906434..ec7a513 100644 --- a/Sources/Schema/SimpleType.swift +++ b/Sources/Schema/SimpleType.swift @@ -266,7 +266,7 @@ public extension PureXML.Schema { /// the ordered range), so an incomparable result is a violation. private func durationRangeError(_ value: String) -> String? { guard let actual = DurationValue(value) else { return nil } - func order(_ bound: String?) -> DurationValue.Order? { + func order(_ bound: String?) -> DurationOrder? { bound.flatMap(DurationValue.init).map(actual.compare(to:)) } if let order = order(facets.minInclusive), order == .lessThan || order == .incomparable { diff --git a/Tests/SchemaDurationFacetTests.swift b/Tests/SchemaDurationFacetTests.swift index d32c939..aa3f2f9 100644 --- a/Tests/SchemaDurationFacetTests.swift +++ b/Tests/SchemaDurationFacetTests.swift @@ -21,25 +21,25 @@ struct SchemaDurationFacetTests { @Test("minInclusive enforces the lower bound") func test_minInclusive() throws { - let s = try schema(#""#) - #expect(try s.validate("P1Y").isEmpty) // equal - #expect(try s.validate("P2Y").isEmpty) // greater - #expect(try !s.validate("P6M").isEmpty) // less + let doc = try schema(#""#) + #expect(try doc.validate("P1Y").isEmpty) // equal + #expect(try doc.validate("P2Y").isEmpty) // greater + #expect(try !doc.validate("P6M").isEmpty) // less } @Test("minExclusive rejects the bound itself") func test_minExclusive() throws { - let s = try schema(#""#) - #expect(try !s.validate("P1Y").isEmpty) // equal: rejected - #expect(try s.validate("P2Y").isEmpty) // greater + let doc = try schema(#""#) + #expect(try !doc.validate("P1Y").isEmpty) // equal: rejected + #expect(try doc.validate("P2Y").isEmpty) // greater } @Test("maxInclusive enforces the upper bound and rejects incomparable values") func test_maxInclusivePartialOrder() throws { - let s = try schema(#""#) - #expect(try s.validate("P20D").isEmpty) // less - #expect(try !s.validate("P40D").isEmpty) // greater + let doc = try schema(#""#) + #expect(try doc.validate("P20D").isEmpty) // less + #expect(try !doc.validate("P40D").isEmpty) // greater // P1M is incomparable to P30D (28..31 days) so it is not <= P30D. - #expect(try !s.validate("P1M").isEmpty) + #expect(try !doc.validate("P1M").isEmpty) } } diff --git a/Tests/SchemaListFacetTests.swift b/Tests/SchemaListFacetTests.swift index 77c2543..0a4caf9 100644 --- a/Tests/SchemaListFacetTests.swift +++ b/Tests/SchemaListFacetTests.swift @@ -21,18 +21,18 @@ struct SchemaListFacetTests { @Test("length on NMTOKENS counts items, not characters") func test_nmtokensLength() throws { - let s = try schema("xs:NMTOKENS", #""#) - #expect(try s.validate("alpha beta gamma").isEmpty) // 3 items: valid - #expect(try !s.validate("alpha beta").isEmpty) // 2 items: invalid - #expect(try !s.validate("alpha beta gamma delta").isEmpty) // 4 items: invalid + let doc = try schema("xs:NMTOKENS", #""#) + #expect(try doc.validate("alpha beta gamma").isEmpty) // 3 items: valid + #expect(try !doc.validate("alpha beta").isEmpty) // 2 items: invalid + #expect(try !doc.validate("alpha beta gamma delta").isEmpty) // 4 items: invalid } @Test("minLength and maxLength on IDREFS count items") func test_idrefsMinMax() throws { - let s = try schema("xs:IDREFS", #""#) - #expect(try s.validate("a b").isEmpty) // 2 items: valid - #expect(try s.validate("a b c").isEmpty) // 3 items: valid - #expect(try !s.validate("a").isEmpty) // 1 item: invalid - #expect(try !s.validate("a b c d").isEmpty) // 4 items: invalid + let doc = try schema("xs:IDREFS", #""#) + #expect(try doc.validate("a b").isEmpty) // 2 items: valid + #expect(try doc.validate("a b c").isEmpty) // 3 items: valid + #expect(try !doc.validate("a").isEmpty) // 1 item: invalid + #expect(try !doc.validate("a b c d").isEmpty) // 4 items: invalid } } diff --git a/Tests/SchemaUnionFacetTests.swift b/Tests/SchemaUnionFacetTests.swift index f60e480..8e07115 100644 --- a/Tests/SchemaUnionFacetTests.swift +++ b/Tests/SchemaUnionFacetTests.swift @@ -23,31 +23,31 @@ struct SchemaUnionFacetTests { @Test("Enumeration on a union restricts to the listed values") func test_enumeration() throws { - let s = try schema(#""#) - #expect(try s.validate("5").isEmpty) // listed - #expect(try s.validate("foo").isEmpty) // listed - #expect(try !s.validate("7").isEmpty) // valid int, not listed - #expect(try !s.validate("bar").isEmpty) // valid string, not listed + let doc = try schema(#""#) + #expect(try doc.validate("5").isEmpty) // listed + #expect(try doc.validate("foo").isEmpty) // listed + #expect(try !doc.validate("7").isEmpty) // valid int, not listed + #expect(try !doc.validate("bar").isEmpty) // valid string, not listed } @Test("Union enumeration compares in value space, not lexically") func test_enumerationValueSpace() throws { - let s = try schema(#""#) + let doc = try schema(#""#) // 05 is a valid xs:int equal to the enumerated 5, so it is in the set. - #expect(try s.validate("05").isEmpty) + #expect(try doc.validate("05").isEmpty) } @Test("Pattern on a union constrains the lexical value") func test_pattern() throws { - let s = try schema(#""#) - #expect(try s.validate("abc").isEmpty) // valid string, matches - #expect(try !s.validate("5").isEmpty) // valid int, fails pattern + let doc = try schema(#""#) + #expect(try doc.validate("abc").isEmpty) // valid string, matches + #expect(try !doc.validate("5").isEmpty) // valid int, fails pattern } @Test("A union with no facets still admits any member value") func test_noFacets() throws { - let s = try schema("") - #expect(try s.validate("42").isEmpty) - #expect(try s.validate("anything").isEmpty) + let doc = try schema("") + #expect(try doc.validate("42").isEmpty) + #expect(try doc.validate("anything").isEmpty) } }