///|
/// Validate arbitrarily long lexical years without narrowing them to machine ints.
/// Arithmetic limits in parse_date are reported separately from conformance.
fn normalized_date_point(s : String, ctx : SchemaContext) -> String? {
let ts = s.split(" ").map(fn(x) { x.to_owned() }).to_array()
if ts.any(fn(x) { x == "" }) {
return None
}
let mut cal = "GREGORIAN"
if ["GREGORIAN", "JULIAN", "HEBREW", "FRENCH_R"].contains(ts[0]) ||
ts[0].has_prefix("_") {
cal = ts[0]
if ctx.declarations.get(cal) is Some(uri) &&
uri.has_prefix("https://gedcom.io/terms/v7/cal-") {
cal = uri[31:].to_owned()
}
ts.remove(0) |> ignore
}
let mut epoch = ""
if ts.get(ts.length() - 1) is Some(t) && (t == "BCE" || t.has_prefix("_")) {
epoch = t
ts.remove(ts.length() - 1) |> ignore
}
if ts.length() < 1 || ts.length() > 3 {
return None
}
let year = ts[ts.length() - 1]
if !ascii_digits(year) || year.code_units().all(fn(c) { c == 48 }) {
return None
}
let mut n = 0
let cycle = if cal == "HEBREW" {
689472
} else if cal == "JULIAN" {
4
} else {
400
}
for c in year.code_units() {
n = (n * 10 + c.to_int() - 48) % cycle
}
if n == 0 {
n = cycle
}
// Use actual year when bounded; French leap history has no agreed prolepsis.
let bounded = @string.parse_int(year[:]) catch { _ => 0 }
if bounded > 0 && bounded <= 9999999 {
n = bounded
} else if cal == "FRENCH_R" {
n = 9999999
}
ts[ts.length() - 1] = n.to_string()
if ts.length() >= 2 {
let m = ts.length() - 2
if ts[m].has_prefix("_") &&
ctx.declarations.get(ts[m]) is Some(uri) &&
uri.has_prefix("https://gedcom.io/terms/v7/month-") {
ts[m] = uri[33:].to_owned()
}
}
Some(cal + " " + ts.join(" ") + (if epoch == "" { "" } else { " " + epoch }))
}
///|
fn valid_date_payload(s : String, kind : String, ctx : SchemaContext) -> Bool {
if s == "" {
return kind != "type-Date#exact"
}
if kind == "type-Date#exact" {
let ts = s.split(" ").to_array()
if ts.length() != 3 ||
!ascii_digits(ts[0].to_owned()) ||
!ascii_digits(ts[2].to_owned()) {
return false
}
if ![
"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV",
"DEC",
].contains(ts[1].to_owned()) {
return false
}
}
let mut points = [s]
let mut period = false
if s.has_prefix("FROM ") {
period = true
points = s[5:].split(" TO ").map(fn(x) { x.to_owned() }).to_array()
if points.length() > 2 {
return false
}
} else if s.has_prefix("TO ") {
period = true
points = [s[3:].to_owned()]
} else if s.has_prefix("BET ") {
points = s[4:].split(" AND ").map(fn(x) { x.to_owned() }).to_array()
if points.length() != 2 {
return false
}
} else {
for prefix in ["BEF ", "AFT ", "ABT ", "CAL ", "EST "] {
if s.has_prefix(prefix) {
points = [s[prefix.length():].to_owned()]
break
}
}
}
if kind == "type-Date#period" && !period {
return false
}
for point in points {
guard normalized_date_point(point, ctx) is Some(p) else { return false }
let valid = try {
parse_point(p) |> ignore
true
} catch {
_ => false
}
if !valid {
return false
}
}
true
}