///|
fn pad4(value : Int) -> String {
if value < 10 {
"000" + value.to_string()
} else if value < 100 {
"00" + value.to_string()
} else if value < 1000 {
"0" + value.to_string()
} else {
value.to_string()
}
}
///|
fn exact_year_to_string(value : Int) -> String {
if value < 0 {
let magnitude = -value
if magnitude < 10000 {
"-" + pad4(magnitude)
} else {
"Y-" + magnitude.to_string()
}
} else if value < 10000 {
pad4(value)
} else {
"Y" + value.to_string()
}
}
///|
fn qualifier_to_string(q : Qualifier) -> String {
if q.uncertain && q.approximate {
"%"
} else if q.uncertain {
"?"
} else if q.approximate {
"~"
} else {
""
}
}
///|
fn year_to_string(year : YearSpec) -> String {
match year {
Exact(value) => exact_year_to_string(value)
Masked(prefix, x_count) => prefix + "X".repeat(x_count)
}
}
///|
fn month_to_string(month : MonthSpec) -> String {
match month {
Unspecified => ""
Exact(value) => pad4(value)[2:4].to_owned()
Season(value) => pad4(value)[2:4].to_owned()
}
}
///|
fn day_to_string(day : DaySpec) -> String {
match day {
Unspecified => ""
Exact(value) => pad4(value)[2:4].to_owned()
}
}
///|
fn date_to_string(date : EdtfDate) -> String {
// Omit suffix bits already encoded by a qualifier to the right.
let year_suffix = {
uncertain: date.year_qualifier.uncertain && !date.month_qualifier.uncertain,
approximate: date.year_qualifier.approximate &&
!date.month_qualifier.approximate,
}
let month_suffix = {
uncertain: date.month_qualifier.uncertain && !date.day_qualifier.uncertain,
approximate: date.month_qualifier.approximate &&
!date.day_qualifier.approximate,
}
let year = year_to_string(date.year) + qualifier_to_string(year_suffix)
match date.month {
Unspecified => year
_ => {
let month = "-" +
month_to_string(date.month) +
qualifier_to_string(month_suffix)
match date.day {
Unspecified => year + month
_ =>
year +
month +
"-" +
day_to_string(date.day) +
qualifier_to_string(date.day_qualifier)
}
}
}
}
///|
fn endpoint_to_string(endpoint : Endpoint) -> String {
match endpoint {
Known(date) => date_to_string(date)
Open => ".."
Unknown => ""
}
}
///|
fn value_to_string(value : EdtfValue) -> String {
match value {
Date(date) => date_to_string(date)
Interval(interval) =>
endpoint_to_string(interval.start) +
"/" +
endpoint_to_string(interval.end)
Set(items) => {
let mut out = "["
for i = 0; i < items.length(); i = i + 1 {
if i > 0 {
out = out + ","
}
out = out + value_to_string(items[i])
}
out + "]"
}
Range(start, end) => date_to_string(start) + ".." + date_to_string(end)
}
}
///|
/// Return a normalized EDTF spelling. Normalization never promotes an
/// uncertain/approximate/masked value to a precise timestamp.
pub fn EdtfValue::to_string(self : EdtfValue) -> String {
value_to_string(self)
}
///|
/// Convenience alias for `value.to_string()`.
pub fn normalize(value : EdtfValue) -> String {
value.to_string()
}
///|
/// Return a single diagnostic record. Invalid rows carry a stable error code
/// and the original UTF-16 offset; valid rows carry the normalized EDTF string.
pub fn diagnose(input : String) -> Diagnostic {
try {
let value = parse(input)
{
input,
valid: true,
error_code: "",
error_offset: -1,
normalized: value.to_string(),
}
} catch {
Syntax(code, offset) =>
{
input,
valid: false,
error_code: code,
error_offset: offset,
normalized: "",
}
Unsupported(code, offset) =>
{
input,
valid: false,
error_code: code,
error_offset: offset,
normalized: "",
}
}
}
///|
/// Batch-diagnose an array of EDTF candidate strings without throwing.
pub fn diagnose_batch(inputs : Array[String]) -> Array[Diagnostic] {
let out : Array[Diagnostic] = []
for i = 0; i < inputs.length(); i = i + 1 {
out.push(diagnose(inputs[i]))
}
out
}
///|
fn as_date(value : EdtfValue) -> EdtfDate raise EdtfError {
match value {
Date(date) => date
_ => raise Unsupported("COMPARE_UNSUPPORTED", 0)
}
}
///|
fn assert_unqualified(date : EdtfDate) -> Unit raise EdtfError {
if date.year_qualifier.uncertain ||
date.year_qualifier.approximate ||
date.month_qualifier.uncertain ||
date.month_qualifier.approximate ||
date.day_qualifier.uncertain ||
date.day_qualifier.approximate {
raise Unsupported("COMPARE_QUALIFIED", 0)
}
}
///|
fn exact_year(date : EdtfDate) -> Int raise EdtfError {
match date.year {
Exact(value) => value
Masked(_, _) => raise Unsupported("COMPARE_PRECISION", 0)
}
}
///|
fn exact_month(date : EdtfDate) -> Int raise EdtfError {
match date.month {
Exact(value) => value
_ => raise Unsupported("COMPARE_PRECISION", 0)
}
}
///|
fn exact_day(date : EdtfDate) -> Int raise EdtfError {
match date.day {
Exact(value) => value
_ => raise Unsupported("COMPARE_PRECISION", 0)
}
}
///|
/// Compare only complete, unqualified exact dates. Qualified, partial, masked,
/// open, unknown, set, and interval values raise `Unsupported` instead of
/// pretending to have a precise order.
pub fn compare(left : EdtfValue, right : EdtfValue) -> Int raise EdtfError {
let a = as_date(left)
let b = as_date(right)
assert_unqualified(a)
assert_unqualified(b)
let year_a = exact_year(a)
let year_b = exact_year(b)
let month_a = exact_month(a)
let month_b = exact_month(b)
let day_a = exact_day(a)
let day_b = exact_day(b)
if year_a != year_b {
return if year_a < year_b { -1 } else { 1 }
}
if month_a != month_b {
return if month_a < month_b { -1 } else { 1 }
}
if day_a != day_b {
return if day_a < day_b { -1 } else { 1 }
}
0
}