use edtf_core::{Bound, Edtf, Relation};
use serde::Serialize;
use wasm_bindgen::prelude::wasm_bindgen;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Summary {
pub canonical: String,
pub level: u8,
pub kind: &'static str,
pub precision: Option<&'static str>,
pub earliest: Option<String>,
pub latest: Option<String>,
pub uncertain: bool,
pub approximate: bool,
pub unspecified: bool,
}
fn bound_json(b: Bound) -> Option<String> {
match b {
Bound::Date(d) => Some(d.to_string()),
Bound::NegativeInfinity => Some("-infinity".into()),
Bound::PositiveInfinity => Some("infinity".into()),
Bound::Unknown => None,
}
}
pub fn summarize(input: &str) -> Option<Summary> {
let parsed = Edtf::parse(input).ok()?;
let bounds = parsed.bounds();
let (kind, precision) = match &parsed {
Edtf::Date(d) => ("date", Some(precision_str(d))),
Edtf::DateTime(dt) => ("datetime", Some(precision_str(&dt.date))),
Edtf::Interval(_) => ("interval", None),
Edtf::Set(_) => ("set", None),
};
Some(Summary {
canonical: parsed.to_string(),
level: parsed.level(),
kind,
precision,
earliest: bound_json(bounds.earliest),
latest: bound_json(bounds.latest),
uncertain: parsed.is_uncertain(),
approximate: parsed.is_approximate(),
unspecified: parsed.has_unspecified(),
})
}
fn precision_str(d: &edtf_core::Date) -> &'static str {
match d.precision() {
edtf_core::Precision::Year => "year",
edtf_core::Precision::Month => "month",
edtf_core::Precision::Season => "season",
edtf_core::Precision::Day => "day",
}
}
#[derive(Debug, Serialize)]
pub struct RelationSummary {
pub before: &'static str,
pub after: &'static str,
pub overlaps: &'static str,
pub contains: &'static str,
pub within: &'static str,
pub equal: &'static str,
}
pub fn relate(a: &str, b: &str) -> Option<RelationSummary> {
let rel = Edtf::parse(a).ok()?.relation(&Edtf::parse(b).ok()?);
let m = |r: Relation| rel.modality(r).as_str();
Some(RelationSummary {
before: m(Relation::Before),
after: m(Relation::After),
overlaps: m(Relation::Overlaps),
contains: m(Relation::Contains),
within: m(Relation::Within),
equal: m(Relation::Equal),
})
}
#[wasm_bindgen(js_name = isValid)]
pub fn is_valid(input: &str) -> bool {
edtf_core::is_valid(input)
}
#[wasm_bindgen]
pub fn level(input: &str) -> i32 {
edtf_core::level(input).map_or(-1, i32::from)
}
#[wasm_bindgen]
pub fn canonical(input: &str) -> Option<String> {
Some(Edtf::parse(input).ok()?.to_string())
}
#[wasm_bindgen]
pub fn parse(input: &str) -> Option<String> {
let summary = summarize(input)?;
serde_json::to_string(&summary).ok()
}
#[wasm_bindgen]
pub fn relation(a: &str, b: &str) -> Option<String> {
serde_json::to_string(&relate(a, b)?).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn summary_shape() {
let s = summarize("2004-06~-11").unwrap();
assert_eq!(s.canonical, "2004-06~-11");
assert_eq!(s.level, 2);
assert_eq!(s.kind, "date");
assert_eq!(s.precision, Some("day"));
assert_eq!(s.earliest.as_deref(), Some("2004-06-11"));
assert_eq!(s.latest.as_deref(), Some("2004-06-11"));
assert!(s.approximate);
assert!(!s.uncertain);
assert!(!s.unspecified);
}
#[test]
fn open_interval_summary() {
let s = summarize("1985-04-12/..").unwrap();
assert_eq!(s.kind, "interval");
assert_eq!(s.earliest.as_deref(), Some("1985-04-12"));
assert_eq!(s.latest.as_deref(), Some("infinity"));
}
#[test]
fn unknown_bound_is_null() {
let s = summarize("1986-04/").unwrap();
assert_eq!(s.latest, None);
}
#[test]
fn invalid_yields_none() {
assert!(summarize("1985-02-30").is_none());
assert_eq!(level("1985-02-30"), -1);
assert!(is_valid("1985-04-12"));
}
#[test]
fn relation_shape() {
let r = relate("1985~", "199X").unwrap();
assert_eq!(r.before, "definite");
assert_eq!(r.after, "impossible");
assert_eq!(r.equal, "impossible");
let j = relation("198X", "1985").unwrap();
assert_eq!(
j,
"{\"before\":\"possible\",\"after\":\"possible\",\
\"overlaps\":\"possible\",\"contains\":\"possible\",\
\"within\":\"possible\",\"equal\":\"possible\"}"
);
assert!(relation("junk", "1985").is_none());
assert!(relation("1985", "junk").is_none());
}
#[test]
fn json_is_camel_case() {
let j = parse("1985").unwrap();
assert!(j.contains("\"canonical\":\"1985\""));
assert!(!j.contains("\"level\":1"), "1985 is level 0: {j}");
assert!(j.contains("\"level\":0"));
}
}