1use crate::context::{State, Update};
6use crate::node::{Kind, Validator};
7use crate::value::{js_number, Value};
8use regex::Regex;
9use std::sync::{Arc, OnceLock};
10
11fn iso_re() -> &'static Regex {
14 static RE: OnceLock<Regex> = OnceLock::new();
15 RE.get_or_init(|| {
16 Regex::new(
17 r"^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})$",
18 )
19 .unwrap()
20 })
21}
22
23struct IsoParts {
24 y: i64,
25 mo: i64,
26 d: i64,
27 h: i64,
28 mi: i64,
29 s: i64,
30 ms: i64,
31 off: i64,
33}
34
35fn iso_parts(s: &str) -> Option<IsoParts> {
36 let m = iso_re().captures(s)?;
37 let num = |i: usize| {
38 m.get(i)
39 .map(|x| x.as_str().parse::<i64>().unwrap())
40 .unwrap_or(0)
41 };
42 let (y, mo, d, h, mi, sec) = (num(1), num(2), num(3), num(4), num(5), num(6));
43 if !(1..=12).contains(&mo) || h > 23 || mi > 59 || sec > 59 {
44 return None;
45 }
46 let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
47 let mut days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][(mo - 1) as usize];
48 if leap && mo == 2 {
49 days = 29;
50 }
51 if d < 1 || days < d {
52 return None;
53 }
54 let ms = m
56 .get(7)
57 .map(|f| {
58 let digits: String = f.as_str().chars().take(3).collect();
59 let padded = format!("{:0<3}", digits);
60 padded.parse::<i64>().unwrap()
61 })
62 .unwrap_or(0);
63 let zone = m.get(8).unwrap().as_str();
64 let off = if zone == "Z" {
65 0
66 } else {
67 let oh: i64 = zone[1..3].parse().unwrap();
68 let om: i64 = zone[4..6].parse().unwrap();
69 if oh > 23 || om > 59 {
70 return None;
71 }
72 let total = oh * 60 + om;
73 if zone.starts_with('-') {
74 -total
75 } else {
76 total
77 }
78 };
79 Some(IsoParts {
80 y,
81 mo,
82 d,
83 h,
84 mi,
85 s: sec,
86 ms,
87 off,
88 })
89}
90
91pub(crate) fn is_iso_date_time(s: &str) -> bool {
92 iso_parts(s).is_some()
93}
94
95fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
97 let y = if m <= 2 { y - 1 } else { y };
98 let era = if y >= 0 { y } else { y - 399 } / 400;
99 let yoe = y - era * 400;
100 let mp = (m + 9) % 12;
101 let doy = (153 * mp + 2) / 5 + d - 1;
102 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
103 era * 146097 + doe - 719468
104}
105
106pub(crate) fn parse_iso_date_time(s: &str) -> Option<i64> {
108 let p = iso_parts(s)?;
109 let days = days_from_civil(p.y, p.mo, p.d);
110 let secs = days * 86400 + p.h * 3600 + p.mi * 60 + p.s - p.off * 60;
111 Some(secs * 1000 + p.ms)
112}
113
114fn numeric_re() -> &'static Regex {
117 static RE: OnceLock<Regex> = OnceLock::new();
118 RE.get_or_init(|| Regex::new(r"^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$").unwrap())
119}
120
121pub(crate) fn coerce_to(k: Kind, val: &Value) -> Option<Value> {
124 match k {
125 Kind::Number | Kind::Integer => match val {
126 Value::Str(v) => {
127 let s = v.trim();
128 if numeric_re().is_match(s) {
129 return s
130 .parse::<f64>()
131 .ok()
132 .filter(|f| f.is_finite())
133 .map(Value::Num);
134 }
135 None
136 }
137 Value::Bool(b) => Some(Value::Num(if *b { 1.0 } else { 0.0 })),
138 _ => None,
139 },
140 Kind::String => match val {
141 Value::Bool(b) => Some(Value::Str(b.to_string())),
142 Value::Num(f) if f.is_finite() => Some(Value::Str(js_number(*f))),
143 _ => None,
144 },
145 Kind::Boolean => match val {
146 Value::Str(s) => match s.trim().to_lowercase().as_str() {
147 "true" | "1" => Some(Value::Bool(true)),
148 "false" | "0" => Some(Value::Bool(false)),
149 _ => None,
150 },
151 Value::Num(f) if *f == 1.0 => Some(Value::Bool(true)),
152 Value::Num(f) if *f == 0.0 => Some(Value::Bool(false)),
153 _ => None,
154 },
155 Kind::Date => match val {
156 Value::Str(s) => parse_iso_date_time(s.trim()).map(Value::Date),
157 Value::Num(f) if f.is_finite() => Some(Value::Date(f.trunc() as i64)),
159 _ => None,
160 },
161 _ => None,
162 }
163}
164
165pub(crate) fn coerce_validator() -> Validator {
168 Validator {
169 name: "Coerce".to_string(),
170 func: Arc::new(|state: &mut State<'_>, update: &mut Update| {
171 if let Some(c) = coerce_to(state.node.kind, state.value) {
172 update.val = Some(c);
173 }
174 true
175 }),
176 args: Vec::new(),
177 suffix: None,
178 inner: None,
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn iso_date_times() {
188 assert_eq!(parse_iso_date_time("1970-01-01T00:00:00Z"), Some(0));
189 assert_eq!(
190 parse_iso_date_time("2020-01-01T00:00:00Z"),
191 Some(1577836800000)
192 );
193 assert_eq!(
194 parse_iso_date_time("2020-01-01T12:30:00.5+02:00"),
195 Some(1577874600500)
196 );
197 assert_eq!(
198 parse_iso_date_time("2020-01-01T00:00:00.123456-01:30"),
199 Some(1577836800123 + 90 * 60 * 1000)
200 );
201 assert_eq!(parse_iso_date_time("1969-12-31T23:59:59Z"), Some(-1000));
202 assert_eq!(
203 parse_iso_date_time("0001-03-01T00:00:00Z"),
204 Some(-719103 * 86400000)
205 );
206 assert!(parse_iso_date_time("2020-02-30T00:00:00Z").is_none());
207 assert!(parse_iso_date_time("2020-02-29T00:00:00Z").is_some());
208 assert!(parse_iso_date_time("2021-02-29T00:00:00Z").is_none());
209 assert!(parse_iso_date_time("2020-13-01T00:00:00Z").is_none());
210 assert!(parse_iso_date_time("2020-01-01T24:00:00Z").is_none());
211 assert!(parse_iso_date_time("2020-01-01T00:00:00+24:00").is_none());
212 assert!(parse_iso_date_time("2020-01-01T00:00:00+01:60").is_none());
213 assert!(parse_iso_date_time("2020-01-01").is_none());
214 assert!(is_iso_date_time("2000-02-29T00:00:00Z"));
215 assert!(!is_iso_date_time("1900-02-29T00:00:00Z"));
216 }
217
218 #[test]
219 fn conversions() {
220 assert_eq!(
221 coerce_to(Kind::Number, &Value::from(" 5 ")),
222 Some(Value::Num(5.0))
223 );
224 assert_eq!(coerce_to(Kind::Number, &Value::from("0x10")), None);
225 assert_eq!(coerce_to(Kind::Number, &Value::from("1e999")), None);
226 assert_eq!(
227 coerce_to(Kind::Integer, &Value::from(true)),
228 Some(Value::Num(1.0))
229 );
230 assert_eq!(
231 coerce_to(Kind::Number, &Value::from(false)),
232 Some(Value::Num(0.0))
233 );
234 assert_eq!(coerce_to(Kind::Number, &Value::Null), None);
235 assert_eq!(
236 coerce_to(Kind::String, &Value::from(true)),
237 Some(Value::from("true"))
238 );
239 assert_eq!(
240 coerce_to(Kind::String, &Value::from(1.5)),
241 Some(Value::from("1.5"))
242 );
243 assert_eq!(coerce_to(Kind::String, &Value::Num(f64::NAN)), None);
244 assert_eq!(coerce_to(Kind::String, &Value::Null), None);
245 assert_eq!(
246 coerce_to(Kind::Boolean, &Value::from(" TRUE ")),
247 Some(Value::Bool(true))
248 );
249 assert_eq!(
250 coerce_to(Kind::Boolean, &Value::from("0")),
251 Some(Value::Bool(false))
252 );
253 assert_eq!(coerce_to(Kind::Boolean, &Value::from("yes")), None);
254 assert_eq!(
255 coerce_to(Kind::Boolean, &Value::from(1)),
256 Some(Value::Bool(true))
257 );
258 assert_eq!(
259 coerce_to(Kind::Boolean, &Value::from(0)),
260 Some(Value::Bool(false))
261 );
262 assert_eq!(coerce_to(Kind::Boolean, &Value::from(2)), None);
263 assert_eq!(coerce_to(Kind::Boolean, &Value::Null), None);
264 assert_eq!(
265 coerce_to(Kind::Date, &Value::from("2020-01-01T00:00:00Z")),
266 Some(Value::Date(1577836800000))
267 );
268 assert_eq!(coerce_to(Kind::Date, &Value::from("nope")), None);
269 assert_eq!(
270 coerce_to(Kind::Date, &Value::from(1.9)),
271 Some(Value::Date(1))
272 );
273 assert_eq!(coerce_to(Kind::Date, &Value::Num(f64::INFINITY)), None);
274 assert_eq!(coerce_to(Kind::Date, &Value::Null), None);
275 assert_eq!(coerce_to(Kind::Any, &Value::from("1")), None);
276 }
277}