Add the PureXML.Schema namespace validating XSD Part 2 built-in datatypes and constraining facets. Built-ins span string/boolean/decimal, the bounded integer family with intrinsic bounds, float/double, duration, the eight date/time types, hexBinary, base64Binary, anyURI, QName, and the Name/NCName/NMTOKEN/language family. Facets: length/minLength/maxLength, pattern (via the regex engine), enumeration (value-space comparison), whiteSpace, min/maxInclusive/Exclusive, totalDigits, fractionDigits. Decimals compare exactly with no floating-point loss; date/time fields are range-validated with leap years, 24:00:00, and timezone bounds. Closes #8
93 lines
4.2 KiB
Swift
93 lines
4.2 KiB
Swift
extension PureXML.Schema {
|
|
/// An exact decimal value, parsed from an XSD `decimal`/`integer` lexical form
|
|
/// and compared without floating-point loss. Holds the sign and the
|
|
/// significant integer and fraction digits (leading and trailing zeros
|
|
/// removed), which is also what `totalDigits` and `fractionDigits` count.
|
|
struct DecimalValue: Equatable, Comparable, Sendable {
|
|
let negative: Bool
|
|
let integerDigits: [Character]
|
|
let fractionDigits: [Character]
|
|
|
|
/// Parses a decimal lexical form. With `allowFraction` false the value must
|
|
/// be an integer (no decimal point).
|
|
init?(_ lexical: String, allowFraction: Bool) {
|
|
var characters = Array(lexical)
|
|
guard !characters.isEmpty else { return nil }
|
|
|
|
var negative = false
|
|
if characters.first == "+" || characters.first == "-" {
|
|
negative = characters.first == "-"
|
|
characters.removeFirst()
|
|
}
|
|
guard !characters.isEmpty else { return nil }
|
|
|
|
let parts = Self.split(characters)
|
|
guard let (whole, fraction) = parts, allowFraction || fraction == nil else { return nil }
|
|
guard whole.allSatisfy(\.isNumber), (fraction ?? []).allSatisfy(\.isNumber) else { return nil }
|
|
guard !whole.isEmpty || !(fraction ?? []).isEmpty else { return nil }
|
|
|
|
let integerDigits = Self.dropLeadingZeros(whole)
|
|
let fractionDigits = Self.dropTrailingZeros(fraction ?? [])
|
|
self.integerDigits = integerDigits
|
|
self.fractionDigits = fractionDigits
|
|
// Negative zero is the same value as zero.
|
|
self.negative = (integerDigits.isEmpty && fractionDigits.isEmpty) ? false : negative
|
|
}
|
|
|
|
/// Splits the digits on a single decimal point, rejecting a second point.
|
|
/// Returns nil when malformed.
|
|
private static func split(_ characters: [Character]) -> (whole: [Character], fraction: [Character]?)? {
|
|
guard let dot = characters.firstIndex(of: ".") else { return (characters, nil) }
|
|
let rest = characters[(dot + 1)...]
|
|
guard !rest.contains(".") else { return nil }
|
|
return (Array(characters[..<dot]), Array(rest))
|
|
}
|
|
|
|
private static func dropLeadingZeros(_ digits: [Character]) -> [Character] {
|
|
Array(digits.drop { $0 == "0" })
|
|
}
|
|
|
|
private static func dropTrailingZeros(_ digits: [Character]) -> [Character] {
|
|
var digits = digits
|
|
while digits.last == "0" {
|
|
digits.removeLast()
|
|
}
|
|
return digits
|
|
}
|
|
|
|
var isZero: Bool {
|
|
integerDigits.isEmpty && fractionDigits.isEmpty
|
|
}
|
|
|
|
/// The XSD `totalDigits` of the value: every significant digit, at least one.
|
|
var totalDigits: Int {
|
|
Swift.max(1, integerDigits.count + fractionDigits.count)
|
|
}
|
|
|
|
static func < (lhs: DecimalValue, rhs: DecimalValue) -> Bool {
|
|
if lhs.negative != rhs.negative { return lhs.negative }
|
|
let magnitude = compareMagnitude(lhs, rhs)
|
|
return lhs.negative ? magnitude > 0 : magnitude < 0
|
|
}
|
|
|
|
/// Negative when `lhs` has the smaller magnitude, positive when larger.
|
|
private static func compareMagnitude(_ lhs: DecimalValue, _ rhs: DecimalValue) -> Int {
|
|
if lhs.integerDigits.count != rhs.integerDigits.count {
|
|
return lhs.integerDigits.count < rhs.integerDigits.count ? -1 : 1
|
|
}
|
|
if let order = lexicographic(lhs.integerDigits, rhs.integerDigits) { return order }
|
|
return lexicographic(lhs.fractionDigits, rhs.fractionDigits) ?? 0
|
|
}
|
|
|
|
/// Digit-by-digit comparison; a proper prefix is the smaller value. Returns
|
|
/// nil when the sequences are identical.
|
|
private static func lexicographic(_ lhs: [Character], _ rhs: [Character]) -> Int? {
|
|
for (left, right) in zip(lhs, rhs) where left != right {
|
|
return left < right ? -1 : 1
|
|
}
|
|
if lhs.count == rhs.count { return nil }
|
|
return lhs.count < rhs.count ? -1 : 1
|
|
}
|
|
}
|
|
}
|