1use serde_json::{json, Map, Value};
5
6use super::components::Component;
7use super::components::{ComponentMap, ParsingComponents};
8use super::context::ParsingOption;
9use super::js_date::JsDate;
10use super::reference::{ParsingReference, ReferenceInput, ReferenceWithTimezone};
11use super::result::ParsingResult;
12use super::timezone::{TimezoneInput, TimezoneSpec};
13
14pub fn utf16_index(text: &str, byte_index: usize) -> usize {
16 text.get(..byte_index).map_or(0, |prefix| prefix.encode_utf16().count())
17}
18
19fn component_map_json(map: &ComponentMap) -> Value {
20 let mut obj = Map::new();
21 for (c, v) in map.iter() {
22 obj.insert(c.name().to_string(), json!(v));
23 }
24 Value::Object(obj)
25}
26
27pub fn components_to_json(c: &ParsingComponents) -> Value {
28 let mut tags: Vec<&str> = c.tags().iter().map(String::as_str).collect();
29 tags.sort_unstable();
30 json!({
31 "known": component_map_json(c.known_values()),
32 "implied": component_map_json(c.implied_values()),
33 "date": c.date().to_iso_string(),
34 "tags": tags,
35 })
36}
37
38pub fn result_to_json(text: &str, r: &ParsingResult) -> Value {
40 json!({
41 "index": utf16_index(text, r.index),
42 "text": r.text,
43 "start": components_to_json(&r.start),
44 "end": r.end.as_ref().map(components_to_json),
45 })
46}
47
48pub fn results_to_json(text: &str, results: &[ParsingResult]) -> Value {
49 Value::Array(results.iter().map(|r| result_to_json(text, r)).collect())
50}
51
52pub fn byte_index(text: &str, utf16: usize) -> Option<usize> {
54 let mut units = 0;
55 for (b, ch) in text.char_indices() {
56 if units == utf16 {
57 return Some(b);
58 }
59 units += ch.len_utf16();
60 if units > utf16 {
61 return None;
62 }
63 }
64 (units == utf16).then_some(text.len())
65}
66
67fn component_map_from_json(v: Option<&Value>) -> Result<ComponentMap, String> {
68 let mut map = ComponentMap::new();
69 let obj = v.and_then(Value::as_object).ok_or("components must be an object")?;
70 for (k, v) in obj {
71 let c = Component::from_name(k).ok_or_else(|| format!("unknown component {k}"))?;
72 map.set(
73 c,
74 v.as_i64().ok_or_else(|| format!("component {k} must be an integer"))?,
75 );
76 }
77 Ok(map)
78}
79
80fn components_from_json(reference: ReferenceWithTimezone, v: &Value) -> Result<ParsingComponents, String> {
81 let tags = v
82 .get("tags")
83 .and_then(Value::as_array)
84 .ok_or("tags must be an array")?
85 .iter()
86 .map(|t| t.as_str().map(str::to_string).ok_or("tags must be strings"))
87 .collect::<Result<Vec<_>, _>>()?;
88 Ok(ParsingComponents::from_parts(
89 reference,
90 component_map_from_json(v.get("known"))?,
91 component_map_from_json(v.get("implied"))?,
92 tags,
93 ))
94}
95
96pub fn result_from_json(text: &str, reference: ReferenceWithTimezone, v: &Value) -> Result<ParsingResult, String> {
99 let index = v.get("index").and_then(Value::as_u64).ok_or("missing index")? as usize;
100 let index = byte_index(text, index).ok_or_else(|| format!("index {index} isn't a character boundary"))?;
101 let matched = v.get("text").and_then(Value::as_str).ok_or("missing text")?;
102 let end = index.checked_add(matched.len()).ok_or("text too long")?;
104 if text.get(index..end) != Some(matched) {
105 return Err(format!("text {matched:?} isn't the source text at index {index}"));
106 }
107 let start = components_from_json(reference, v.get("start").ok_or("missing start")?)?;
108 let end = match v.get("end") {
109 None | Some(Value::Null) => None,
110 Some(e) => Some(components_from_json(reference, e)?),
111 };
112 Ok(ParsingResult::new(reference, index, matched, Some(start), end))
113}
114
115#[derive(Clone, Debug)]
117pub struct Request {
118 pub text: String,
119 pub config: String,
120 pub reference: ReferenceInput,
121 pub now: JsDate,
123 pub option: ParsingOption,
124}
125
126pub fn parse_request(v: &Value) -> Result<Request, String> {
128 let text = v.get("text").and_then(Value::as_str).ok_or("missing text")?.to_string();
129 let config = v
130 .get("config")
131 .and_then(Value::as_str)
132 .unwrap_or("en.casual")
133 .to_string();
134 if let Some(tz) = v.get("system_timezone").and_then(Value::as_str) {
135 if tz != "UTC" {
136 return Err(format!("system_timezone must be UTC, got {tz}"));
137 }
138 }
139
140 let r = v.get("reference").ok_or("missing reference")?;
141 let kind = r.get("kind").and_then(Value::as_str).ok_or("missing reference.kind")?;
142 let instant_str = r
143 .get("instant")
144 .and_then(Value::as_str)
145 .ok_or("missing reference.instant")?;
146 let instant = JsDate::parse_iso(instant_str).ok_or_else(|| format!("bad instant {instant_str}"))?;
147 let timezone = match r.get("timezone_input") {
148 None | Some(Value::Null) => None,
149 Some(Value::String(s)) => Some(TimezoneInput::Abbr(s.clone())),
150 Some(Value::Number(n)) => Some(TimezoneInput::Minutes(
151 n.as_i64().ok_or("timezone_input must be an integer")?,
152 )),
153 Some(other) => return Err(format!("bad timezone_input {other}")),
154 };
155 let reference = match kind {
156 "none" => ReferenceInput::Now,
157 "date" => ReferenceInput::Date(instant),
158 "object" => ReferenceInput::Reference(ParsingReference {
159 instant: Some(instant),
160 timezone,
161 }),
162 other => return Err(format!("bad reference.kind {other}")),
163 };
164
165 let mut option = ParsingOption::default();
166 if let Some(opts) = v.get("options").and_then(Value::as_object) {
167 for (key, value) in opts {
168 match key.as_str() {
169 "forwardDate" => option.forward_date = value.as_bool().ok_or("forwardDate must be a boolean")?,
170 "timezones" => {
171 for (abbr, offset) in value.as_object().ok_or("timezones must be an object")? {
172 let minutes = offset.as_i64().ok_or("timezone overrides must be integer minutes")?;
173 option.timezones.insert(abbr.clone(), TimezoneSpec::Fixed(minutes));
174 }
175 }
176 other => return Err(format!("unsupported option {other}")),
177 }
178 }
179 }
180
181 Ok(Request {
182 text,
183 config,
184 reference,
185 now: instant,
186 option,
187 })
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn utf16_indexes() {
196 let text = "😀 é now";
197 assert_eq!(utf16_index(text, text.find("now").unwrap()), 5);
198 }
199
200 #[test]
201 fn byte_indexes_invert_utf16_indexes() {
202 let text = "😀 é now";
203 for (b, _) in text.char_indices().chain([(text.len(), ' ')]) {
204 assert_eq!(byte_index(text, utf16_index(text, b)), Some(b));
205 }
206 assert_eq!(byte_index(text, 1), None); assert_eq!(byte_index(text, 99), None);
208 }
209
210 #[test]
211 fn results_round_trip_through_json() {
212 let text = "до 😀 завтра";
213 let reference = ReferenceWithTimezone::from_input(
214 &ReferenceInput::Date(JsDate::parse_iso("2012-08-10T12:00:00.000Z").unwrap()),
215 &Default::default(),
216 JsDate::parse_iso("2012-08-10T12:00:00.000Z").unwrap(),
217 );
218 let mut start = ParsingComponents::new(reference, &[(Component::Day, 11)]);
219 start.add_tag("casualReference/tomorrow");
220 let end = ParsingComponents::new(reference, &[(Component::Hour, 9)]);
221 let r = ParsingResult::new(
222 reference,
223 text.find("завтра").unwrap(),
224 "завтра",
225 Some(start),
226 Some(end),
227 );
228 let v = result_to_json(text, &r);
229 let back = result_from_json(text, reference, &v).unwrap();
231 assert_eq!(result_to_json(text, &back), v);
232 assert_eq!((back.index, back.start.tags()), (r.index, r.start.tags()));
233 assert!(result_from_json(
234 text,
235 reference,
236 &json!({ "index": 4, "text": "x", "start": v["start"] })
237 )
238 .is_err());
239 let at = |index: usize, t: &str| json!({ "index": index, "text": t, "start": v["start"], "end": null });
241 assert!(result_from_json("a", reference, &at(0, "aa")).is_err());
242 assert!(result_from_json("éa", reference, &at(0, "e")).is_err());
243 assert!(result_from_json(text, reference, &at(9, "завтр")).is_err());
244 }
245
246 #[test]
247 fn request_roundtrip() {
248 let req = parse_request(&json!({
249 "text": "Friday at 4pm",
250 "config": "en.casual",
251 "reference": {"kind": "object", "instant": "2021-06-09T12:00:00.000Z", "timezone_input": "CDT", "timezone_offset": -300},
252 "system_timezone": "UTC",
253 "options": {"forwardDate": true, "timezones": {"XYZ": 60}},
254 }))
255 .unwrap();
256 assert!(req.option.forward_date);
257 assert_eq!(req.option.timezones.get("XYZ"), Some(&TimezoneSpec::Fixed(60)));
258 assert!(matches!(req.reference, ReferenceInput::Reference(_)));
259 assert!(parse_request(&json!({"text": "x", "reference": {"kind": "none", "instant": "2012-08-10T12:00:00.000Z"}, "options": {"debug": true}})).is_err());
260 }
261}