Use let for never-mutated bindings in DateTimeParser, HTMLEntities, and the XSLT parser, and drop the redundant Unicode.Scalar Comparable conformance that the standard library already provides. No behavior change.
175 lines
6.7 KiB
Swift
175 lines
6.7 KiB
Swift
extension PureXML.Schema {
|
|
/// Parses an XSD date/time lexical form for a given ``DateKind``, validating
|
|
/// field ranges (including leap years and the `24:00:00` midnight form) and
|
|
/// the timezone. Returns nil when the form is invalid.
|
|
struct DateTimeParser {
|
|
private let chars: [Character]
|
|
private var index = 0
|
|
|
|
private init(_ lexical: String) {
|
|
chars = Array(lexical)
|
|
}
|
|
|
|
static func parse(_ lexical: String, kind: DateKind) -> DateTimeValue? {
|
|
var parser = DateTimeParser(lexical)
|
|
guard var value = parser.parseFields(kind) else { return nil }
|
|
guard parser.parseTimezone(into: &value) else { return nil }
|
|
return parser.isAtEnd ? value : nil
|
|
}
|
|
|
|
private mutating func parseFields(_ kind: DateKind) -> DateTimeValue? {
|
|
switch kind {
|
|
case .dateTime: parseDateTime()
|
|
case .date: parseDate()
|
|
case .time: parseTimeOnly()
|
|
case .gYearMonth: parseYearMonth()
|
|
case .gYear: parseYearOnly()
|
|
case .gMonthDay: parseMonthDay()
|
|
case .gDay: parseDayOnly()
|
|
case .gMonth: parseMonthOnly()
|
|
}
|
|
}
|
|
|
|
// MARK: Composite forms
|
|
|
|
private mutating func parseDateTime() -> DateTimeValue? {
|
|
guard var value = parseDate(), consume("T") else { return nil }
|
|
return parseTime(into: &value) ? value : nil
|
|
}
|
|
|
|
private mutating func parseDate() -> DateTimeValue? {
|
|
guard var value = parseYearMonth(), consume("-"), let day = readFixed(2) else { return nil }
|
|
value.day = day
|
|
return dayInRange(value) ? value : nil
|
|
}
|
|
|
|
private mutating func parseYearMonth() -> DateTimeValue? {
|
|
guard var value = parseYearOnly(), consume("-"), let month = readFixed(2), (1 ... 12).contains(month) else {
|
|
return nil
|
|
}
|
|
value.month = month
|
|
return value
|
|
}
|
|
|
|
private mutating func parseYearOnly() -> DateTimeValue? {
|
|
var value = DateTimeValue()
|
|
let negative = consume("-")
|
|
guard let digits = readDigits(minimum: 4) else { return nil }
|
|
value.year = (negative ? -1 : 1) * digits
|
|
return value
|
|
}
|
|
|
|
private mutating func parseTimeOnly() -> DateTimeValue? {
|
|
var value = DateTimeValue()
|
|
return parseTime(into: &value) ? value : nil
|
|
}
|
|
|
|
// MARK: g* forms
|
|
|
|
private mutating func parseMonthDay() -> DateTimeValue? {
|
|
guard consume("--"), let month = readFixed(2), (1 ... 12).contains(month), consume("-"),
|
|
let day = readFixed(2) else { return nil }
|
|
let value = DateTimeValue(year: 2000, month: month, day: day)
|
|
return dayInRange(value) ? value : nil
|
|
}
|
|
|
|
private mutating func parseMonthOnly() -> DateTimeValue? {
|
|
guard consume("--"), let month = readFixed(2), (1 ... 12).contains(month) else { return nil }
|
|
return DateTimeValue(month: month)
|
|
}
|
|
|
|
private mutating func parseDayOnly() -> DateTimeValue? {
|
|
guard consume("---"), let day = readFixed(2), (1 ... 31).contains(day) else { return nil }
|
|
return DateTimeValue(day: day)
|
|
}
|
|
|
|
// MARK: Time and timezone
|
|
|
|
private mutating func parseTime(into value: inout DateTimeValue) -> Bool {
|
|
guard let hour = readFixed(2), consume(":"), let minute = readFixed(2), consume(":"),
|
|
let second = readFixed(2) else { return false }
|
|
guard (0 ... 24).contains(hour), (0 ... 59).contains(minute), (0 ... 59).contains(second) else { return false }
|
|
if hour == 24, minute != 0 || second != 0 { return false }
|
|
value.hour = hour
|
|
value.minute = minute
|
|
value.second = second
|
|
if consume(".") {
|
|
guard let fraction = readFraction() else { return false }
|
|
value.fraction = fraction
|
|
}
|
|
return true
|
|
}
|
|
|
|
private mutating func parseTimezone(into value: inout DateTimeValue) -> Bool {
|
|
if consume("Z") { value.offsetMinutes = 0
|
|
return true
|
|
}
|
|
guard peek() == "+" || peek() == "-" else { return true }
|
|
let negative = consume("-")
|
|
if !negative { advance() }
|
|
guard let hour = readFixed(2), consume(":"), let minute = readFixed(2) else { return false }
|
|
guard (0 ... 14).contains(hour), (0 ... 59).contains(minute), hour * 60 + minute <= 14 * 60 else {
|
|
return false
|
|
}
|
|
value.offsetMinutes = (negative ? -1 : 1) * (hour * 60 + minute)
|
|
return true
|
|
}
|
|
|
|
private func dayInRange(_ value: DateTimeValue) -> Bool {
|
|
(1 ... DateTimeValue.daysInMonth(year: value.year, month: value.month)).contains(value.day)
|
|
}
|
|
|
|
// MARK: Cursor
|
|
|
|
private var isAtEnd: Bool {
|
|
index >= chars.count
|
|
}
|
|
|
|
private func peek() -> Character? {
|
|
index < chars.count ? chars[index] : nil
|
|
}
|
|
|
|
private mutating func advance() {
|
|
if index < chars.count { index += 1 }
|
|
}
|
|
|
|
private mutating func consume(_ literal: String) -> Bool {
|
|
let target = Array(literal)
|
|
guard index + target.count <= chars.count, Array(chars[index ..< index + target.count]) == target else {
|
|
return false
|
|
}
|
|
index += target.count
|
|
return true
|
|
}
|
|
|
|
/// Reads exactly `count` digits as an integer, or nil.
|
|
private mutating func readFixed(_ count: Int) -> Int? {
|
|
guard index + count <= chars.count else { return nil }
|
|
let slice = chars[index ..< index + count]
|
|
guard slice.allSatisfy(\.isNumber) else { return nil }
|
|
index += count
|
|
return Int(String(slice))
|
|
}
|
|
|
|
/// Reads at least `minimum` digits as an integer.
|
|
private mutating func readDigits(minimum: Int) -> Int? {
|
|
let start = index
|
|
while let character = peek(), character.isNumber {
|
|
advance()
|
|
}
|
|
guard index - start >= minimum else { return nil }
|
|
return Int(String(chars[start ..< index]))
|
|
}
|
|
|
|
/// Reads the fractional seconds after the decimal point.
|
|
private mutating func readFraction() -> Double? {
|
|
let start = index
|
|
while let character = peek(), character.isNumber {
|
|
advance()
|
|
}
|
|
guard index > start else { return nil }
|
|
return Double("0." + String(chars[start ..< index]))
|
|
}
|
|
}
|
|
}
|