1use edtf_core::{Bound, Edtf, Relation};
12use serde::Serialize;
13use wasm_bindgen::prelude::wasm_bindgen;
14
15#[derive(Debug, Serialize)]
17#[serde(rename_all = "camelCase")]
18pub struct Summary {
19 pub canonical: String,
21 pub level: u8,
23 pub kind: &'static str,
25 pub precision: Option<&'static str>,
27 pub earliest: Option<String>,
29 pub latest: Option<String>,
31 pub uncertain: bool,
33 pub approximate: bool,
35 pub unspecified: bool,
37}
38
39fn bound_json(b: Bound) -> Option<String> {
40 match b {
41 Bound::Date(d) => Some(d.to_string()),
42 Bound::NegativeInfinity => Some("-infinity".into()),
43 Bound::PositiveInfinity => Some("infinity".into()),
44 Bound::Unknown => None,
45 }
46}
47
48pub fn summarize(input: &str) -> Option<Summary> {
50 let parsed = Edtf::parse(input).ok()?;
51 let bounds = parsed.bounds();
52 let (kind, precision) = match &parsed {
53 Edtf::Date(d) => ("date", Some(precision_str(d))),
54 Edtf::DateTime(dt) => ("datetime", Some(precision_str(&dt.date))),
55 Edtf::Interval(_) => ("interval", None),
56 Edtf::Set(_) => ("set", None),
57 };
58 Some(Summary {
59 canonical: parsed.to_string(),
60 level: parsed.level(),
61 kind,
62 precision,
63 earliest: bound_json(bounds.earliest),
64 latest: bound_json(bounds.latest),
65 uncertain: parsed.is_uncertain(),
66 approximate: parsed.is_approximate(),
67 unspecified: parsed.has_unspecified(),
68 })
69}
70
71fn precision_str(d: &edtf_core::Date) -> &'static str {
72 match d.precision() {
73 edtf_core::Precision::Year => "year",
74 edtf_core::Precision::Month => "month",
75 edtf_core::Precision::Season => "season",
76 edtf_core::Precision::Day => "day",
77 }
78}
79
80#[derive(Debug, Serialize)]
86pub struct RelationSummary {
87 pub before: &'static str,
89 pub after: &'static str,
91 pub overlaps: &'static str,
93 pub contains: &'static str,
95 pub within: &'static str,
97 pub equal: &'static str,
99}
100
101pub fn relate(a: &str, b: &str) -> Option<RelationSummary> {
103 let rel = Edtf::parse(a).ok()?.relation(&Edtf::parse(b).ok()?);
104 let m = |r: Relation| rel.modality(r).as_str();
105 Some(RelationSummary {
106 before: m(Relation::Before),
107 after: m(Relation::After),
108 overlaps: m(Relation::Overlaps),
109 contains: m(Relation::Contains),
110 within: m(Relation::Within),
111 equal: m(Relation::Equal),
112 })
113}
114
115#[wasm_bindgen(js_name = isValid)]
117pub fn is_valid(input: &str) -> bool {
118 edtf_core::is_valid(input)
119}
120
121#[wasm_bindgen]
123pub fn level(input: &str) -> i32 {
124 edtf_core::level(input).map_or(-1, i32::from)
125}
126
127#[wasm_bindgen]
129pub fn canonical(input: &str) -> Option<String> {
130 Some(Edtf::parse(input).ok()?.to_string())
131}
132
133#[wasm_bindgen]
136pub fn parse(input: &str) -> Option<String> {
137 let summary = summarize(input)?;
138 serde_json::to_string(&summary).ok()
139}
140
141#[wasm_bindgen]
145pub fn relation(a: &str, b: &str) -> Option<String> {
146 serde_json::to_string(&relate(a, b)?).ok()
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn summary_shape() {
155 let s = summarize("2004-06~-11").unwrap();
156 assert_eq!(s.canonical, "2004-06~-11");
157 assert_eq!(s.level, 2);
158 assert_eq!(s.kind, "date");
159 assert_eq!(s.precision, Some("day"));
160 assert_eq!(s.earliest.as_deref(), Some("2004-06-11"));
161 assert_eq!(s.latest.as_deref(), Some("2004-06-11"));
162 assert!(s.approximate);
163 assert!(!s.uncertain);
164 assert!(!s.unspecified);
165 }
166
167 #[test]
168 fn open_interval_summary() {
169 let s = summarize("1985-04-12/..").unwrap();
170 assert_eq!(s.kind, "interval");
171 assert_eq!(s.earliest.as_deref(), Some("1985-04-12"));
172 assert_eq!(s.latest.as_deref(), Some("infinity"));
173 }
174
175 #[test]
176 fn unknown_bound_is_null() {
177 let s = summarize("1986-04/").unwrap();
178 assert_eq!(s.latest, None);
179 }
180
181 #[test]
182 fn invalid_yields_none() {
183 assert!(summarize("1985-02-30").is_none());
184 assert_eq!(level("1985-02-30"), -1);
185 assert!(is_valid("1985-04-12"));
186 }
187
188 #[test]
189 fn relation_shape() {
190 let r = relate("1985~", "199X").unwrap();
191 assert_eq!(r.before, "definite");
192 assert_eq!(r.after, "impossible");
193 assert_eq!(r.equal, "impossible");
194 let j = relation("198X", "1985").unwrap();
195 assert_eq!(
196 j,
197 "{\"before\":\"possible\",\"after\":\"possible\",\
198 \"overlaps\":\"possible\",\"contains\":\"possible\",\
199 \"within\":\"possible\",\"equal\":\"possible\"}"
200 );
201 assert!(relation("junk", "1985").is_none());
202 assert!(relation("1985", "junk").is_none());
203 }
204
205 #[test]
206 fn json_is_camel_case() {
207 let j = parse("1985").unwrap();
208 assert!(j.contains("\"canonical\":\"1985\""));
209 assert!(!j.contains("\"level\":1"), "1985 is level 0: {j}");
210 assert!(j.contains("\"level\":0"));
211 }
212}