PureXML/Sources/Schema/DateTimeValue.swift
Mihaela Mihaljevic e862f364aa feat(schema): XSD simple-type datatype library with facets
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
2026-06-08 06:29:37 +02:00

63 lines
2.2 KiB
Swift

extension PureXML.Schema {
/// The eight XSD date/time datatypes, distinguished by which fields their
/// lexical form carries.
enum DateKind: Sendable {
case dateTime
case date
case time
case gYearMonth
case gYear
case gMonthDay
case gDay
case gMonth
}
/// A parsed and range-validated XSD date/time value, with a UTC-normalized key
/// for ordering facet bounds. Values without a timezone are ordered as if in
/// UTC, which gives the total order facet checking needs.
struct DateTimeValue: Equatable, Comparable, Sendable {
var year = 1
var month = 1
var day = 1
var hour = 0
var minute = 0
var second = 0
var fraction = 0.0
var offsetMinutes = 0
static func < (lhs: DateTimeValue, rhs: DateTimeValue) -> Bool {
lhs.key < rhs.key
}
/// Seconds from a fixed epoch in UTC, the ordering key.
private var key: Double {
let days = Self.daysFromCivil(year: year, month: month, day: day)
let seconds = Double(days * 86400 + hour * 3600 + minute * 60 + second)
return seconds + fraction - Double(offsetMinutes * 60)
}
/// The civil-date-to-day-count algorithm (proleptic Gregorian), valid for
/// any year. Day 0 is 1970-01-01.
static func daysFromCivil(year: Int, month: Int, day: Int) -> Int {
let shifted = month <= 2 ? year - 1 : year
let era = (shifted >= 0 ? shifted : shifted - 399) / 400
let yearOfEra = shifted - era * 400
let dayOfYear = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1
let dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear
return era * 146_097 + dayOfEra - 719_468
}
static func isLeapYear(_ year: Int) -> Bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
static func daysInMonth(year: Int, month: Int) -> Int {
switch month {
case 1, 3, 5, 7, 8, 10, 12: 31
case 4, 6, 9, 11: 30
default: isLeapYear(year) ? 29 : 28
}
}
}
}