xs:unique/xs:key/xs:keyref were quadratic over a wide list of targets. The duplicate check compared each value tuple against every tuple already seen (and a keyref against every referenced key), and the error-location helper rescanned the parent's whole child list to position each target. Key tuples in a Set by their raw string when field equality provably reduces to a raw string comparison (whitespace-preserving lexical types, no colon, which is most ids); value-space types (numeric, boolean, date), whitespace-collapsing types, and QName-bearing values keep the exact pairwise comparison, so which documents pass is unchanged. Position each target through a per-parent, per-name index built once. A 16000-item xs:unique document drops from about 4.0 s to about 0.12 s (~34x), and the cost is now linear. The full local suite and the W3C XSTS datatype/identity conformance corpus are unchanged.
120 lines
4.4 KiB
Swift
120 lines
4.4 KiB
Swift
extension PureXML.Schema {
|
|
/// A value compared for the ordering facets (`minInclusive` and friends).
|
|
enum OrderedValue: Equatable, Comparable {
|
|
case decimal(DecimalValue)
|
|
case double(Double)
|
|
case dateTime(DateTimeValue)
|
|
|
|
static func < (lhs: OrderedValue, rhs: OrderedValue) -> Bool {
|
|
switch (lhs, rhs) {
|
|
case let (.decimal(left), .decimal(right)): left < right
|
|
case let (.double(left), .double(right)): left < right
|
|
case let (.dateTime(left), .dateTime(right)): left < right
|
|
default: false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An XSD primitive value space: the lexical recognizer, and for the ordered
|
|
/// spaces a value usable by the ordering facets.
|
|
enum Primitive: Sendable {
|
|
case string
|
|
case boolean
|
|
case decimal
|
|
case integer
|
|
case double
|
|
case float
|
|
case duration
|
|
case dateKind(DateKind)
|
|
case hexBinary
|
|
case base64Binary
|
|
case anyURI
|
|
case qName
|
|
case name
|
|
case ncName
|
|
case nmtoken
|
|
case language
|
|
case notation
|
|
|
|
func isValid(_ value: String) -> Bool {
|
|
stringValidity(value) ?? numericValidity(value)
|
|
}
|
|
|
|
/// Whether two valid values of this primitive compare by their
|
|
/// whitespace-processed lexical form rather than by a value space. These
|
|
/// are exactly the `default` cases of `SimpleType.atomicValueMatches`:
|
|
/// every primitive except the numeric, date, and boolean spaces, which
|
|
/// compare two distinct lexical forms (`3.0`/`3`, `true`/`1`) as equal.
|
|
var comparesLexically: Bool {
|
|
switch self {
|
|
case .decimal, .integer, .double, .float, .boolean, .dateKind: false
|
|
default: true
|
|
}
|
|
}
|
|
|
|
/// Validity for the string-like and boolean spaces; nil when this primitive
|
|
/// is handled by ``numericValidity(_:)``.
|
|
private func stringValidity(_ value: String) -> Bool? {
|
|
switch self {
|
|
case .string, .anyURI: true
|
|
case .boolean: Lexical.isBoolean(value)
|
|
case .qName, .notation: Lexical.isQName(value)
|
|
case .name: Lexical.isName(value)
|
|
case .ncName: Lexical.isNCName(value)
|
|
case .nmtoken: Lexical.isNMToken(value)
|
|
case .language: Lexical.isLanguage(value)
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
private func numericValidity(_ value: String) -> Bool {
|
|
switch self {
|
|
case .decimal: DecimalValue(value, allowFraction: true) != nil
|
|
case .integer: DecimalValue(value, allowFraction: false) != nil
|
|
case .double, .float: Lexical.isFloating(value)
|
|
case .duration: Lexical.isDuration(value)
|
|
case let .dateKind(kind): DateTimeParser.parse(value, kind: kind) != nil
|
|
case .hexBinary: Lexical.isHexBinary(value)
|
|
case .base64Binary: Lexical.isBase64Binary(value)
|
|
default: false
|
|
}
|
|
}
|
|
|
|
/// The ordering value for a lexically valid string, or nil for the
|
|
/// unordered spaces.
|
|
func ordered(_ value: String) -> OrderedValue? {
|
|
switch self {
|
|
case .decimal: DecimalValue(value, allowFraction: true).map(OrderedValue.decimal)
|
|
case .integer: DecimalValue(value, allowFraction: false).map(OrderedValue.decimal)
|
|
case .double, .float: Self.floatingValue(value).map(OrderedValue.double)
|
|
case let .dateKind(kind): DateTimeParser.parse(value, kind: kind).map(OrderedValue.dateTime)
|
|
default: nil
|
|
}
|
|
}
|
|
|
|
private static func floatingValue(_ value: String) -> Double? {
|
|
switch value {
|
|
case "INF": .infinity
|
|
case "-INF": -.infinity
|
|
case "NaN": .nan
|
|
default: Double(value)
|
|
}
|
|
}
|
|
|
|
/// The unit `length`, `minLength`, and `maxLength` count: octets for binary
|
|
/// types, characters otherwise.
|
|
func measuredLength(_ value: String) -> Int {
|
|
switch self {
|
|
case .hexBinary: value.count / 2
|
|
case .base64Binary: Self.base64Octets(value)
|
|
default: value.count
|
|
}
|
|
}
|
|
|
|
private static func base64Octets(_ value: String) -> Int {
|
|
let padding = value.reversed().prefix { $0 == "=" }.count
|
|
return value.count / 4 * 3 - padding
|
|
}
|
|
}
|
|
}
|