1use std::ops::Deref;
6use std::sync::LazyLock;
7
8use num_traits::Zero;
9use regex::Regex;
10pub use script_bindings::str::*;
11use time::{Date, Month, OffsetDateTime, Time, Weekday};
12
13fn parse_month_component(value: &str) -> Option<(i32, u32)> {
15 let mut iterator = value.split('-');
17 let year = iterator.next()?;
18 let month = iterator.next()?;
19
20 let year_int = year.parse::<i32>().ok()?;
22 if year.len() < 4 || year_int == 0 {
23 return None;
24 }
25
26 let month_int = month.parse::<u32>().ok()?;
28 if month.len() != 2 || !(1..=12).contains(&month_int) {
29 return None;
30 }
31
32 Some((year_int, month_int))
34}
35
36fn parse_date_component(value: &str) -> Option<(i32, u32, u32)> {
38 let (year_int, month_int) = parse_month_component(value)?;
40
41 let day = value.split('-').nth(2)?;
43 let day_int = day.parse::<u32>().ok()?;
44 if day.len() != 2 {
45 return None;
46 }
47
48 let max_day = max_day_in_month(year_int, month_int)?;
50 if day_int == 0 || day_int > max_day {
51 return None;
52 }
53
54 Some((year_int, month_int, day_int))
56}
57
58fn parse_time_component(value: &str) -> Option<(u8, u8, u8, u16)> {
60 let mut iterator = value.split(':');
65 let hour = iterator.next()?;
66 if hour.len() != 2 {
67 return None;
68 }
69 let hour_int = hour.parse::<u8>().ok()?;
71 if hour_int > 23 {
72 return None;
73 }
74
75 let minute = iterator.next()?;
84 if minute.len() != 2 {
85 return None;
86 }
87 let minute_int = minute.parse::<u8>().ok()?;
88 if minute_int > 59 {
89 return None;
90 }
91
92 let Some(seconds_and_milliseconds) = iterator.next() else {
95 return Some((hour_int, minute_int, 0, 0));
96 };
97
98 let mut second_iterator = seconds_and_milliseconds.split('.');
100 let second = second_iterator.next()?;
101 if second.len() != 2 {
102 return None;
103 }
104 let second_int = second.parse::<u8>().ok()?;
105
106 let Some(millisecond) = second_iterator.next() else {
109 return Some((hour_int, minute_int, second_int, 0));
110 };
111 let millisecond_length = millisecond.len() as u32;
112 if millisecond_length > 3 {
113 return None;
114 }
115 let millisecond_int = millisecond.parse::<u16>().ok()?;
116 let millisecond_int = millisecond_int * 10_u16.pow(3 - millisecond_length);
117
118 Some((hour_int, minute_int, second_int, millisecond_int))
121}
122
123fn max_day_in_month(year_num: i32, month_num: u32) -> Option<u32> {
124 match month_num {
125 1 | 3 | 5 | 7 | 8 | 10 | 12 => Some(31),
126 4 | 6 | 9 | 11 => Some(30),
127 2 => {
128 if is_leap_year(year_num) {
129 Some(29)
130 } else {
131 Some(28)
132 }
133 },
134 _ => None,
135 }
136}
137
138fn max_week_in_year(year: i32) -> u32 {
146 let Ok(date) = Date::from_calendar_date(year, Month::January, 1) else {
147 return 52;
148 };
149
150 match OffsetDateTime::new_utc(date, Time::MIDNIGHT).weekday() {
151 Weekday::Thursday => 53,
152 Weekday::Wednesday if is_leap_year(year) => 53,
153 _ => 52,
154 }
155}
156
157#[inline]
158fn is_leap_year(year: i32) -> bool {
159 year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)
160}
161
162pub(crate) trait ToInputValueString {
163 fn to_date_string(&self) -> String;
164 fn to_month_string(&self) -> String;
165 fn to_week_string(&self) -> String;
166 fn to_time_string(&self) -> String;
167
168 fn to_local_date_time_string(&self) -> String;
172}
173
174impl ToInputValueString for OffsetDateTime {
175 fn to_date_string(&self) -> String {
176 format!(
177 "{:04}-{:02}-{:02}",
178 self.year(),
179 self.month() as u8,
180 self.day()
181 )
182 }
183
184 fn to_month_string(&self) -> String {
185 format!("{:04}-{:02}", self.year(), self.month() as u8)
186 }
187
188 fn to_week_string(&self) -> String {
189 let (year, week, _) = self.to_iso_week_date();
191 format!("{:04}-W{:02}", year, week)
192 }
193
194 fn to_time_string(&self) -> String {
195 if self.second().is_zero() && self.millisecond().is_zero() {
196 format!("{:02}:{:02}", self.hour(), self.minute())
197 } else {
198 format!(
200 "{:02}:{:02}:{:02}.{:03}",
201 self.hour(),
202 self.minute(),
203 self.second(),
204 self.millisecond()
205 )
206 .trim_end_matches(['.', '0'])
207 .to_owned()
208 }
209 }
210
211 fn to_local_date_time_string(&self) -> String {
212 format!("{}T{}", self.to_date_string(), self.to_time_string())
213 }
214}
215
216pub(crate) trait FromInputValueString {
217 fn parse_date_string(&self) -> Option<OffsetDateTime>;
226
227 fn parse_month_string(&self) -> Option<OffsetDateTime>;
235
236 fn parse_week_string(&self) -> Option<OffsetDateTime>;
245
246 fn parse_time_string(&self) -> Option<OffsetDateTime>;
249
250 fn parse_local_date_time_string(&self) -> Option<OffsetDateTime>;
254
255 fn is_valid_date_string(&self) -> bool {
258 self.parse_date_string().is_some()
259 }
260
261 fn is_valid_month_string(&self) -> bool {
264 self.parse_month_string().is_some()
265 }
266 fn is_valid_week_string(&self) -> bool {
269 self.parse_week_string().is_some()
270 }
271 fn is_valid_time_string(&self) -> bool;
274
275 fn is_valid_local_date_time_string(&self) -> bool {
278 self.parse_local_date_time_string().is_some()
279 }
280
281 fn is_valid_simple_color_string(&self) -> bool;
283
284 fn is_valid_email_address_string(&self) -> bool;
286}
287
288impl<T: Deref<Target = str>> FromInputValueString for T {
289 fn parse_date_string(&self) -> Option<OffsetDateTime> {
290 let (year_int, month_int, day_int) = parse_date_component(self)?;
292
293 if self.split('-').nth(3).is_some() {
295 return None;
296 }
297
298 let month = (month_int as u8).try_into().ok()?;
300 let date = Date::from_calendar_date(year_int, month, day_int as u8).ok()?;
301 Some(OffsetDateTime::new_utc(date, Time::MIDNIGHT))
302 }
303
304 fn parse_month_string(&self) -> Option<OffsetDateTime> {
305 let (year_int, month_int) = parse_month_component(self)?;
307
308 if self.split('-').nth(2).is_some() {
310 return None;
311 }
312 let month = (month_int as u8).try_into().ok()?;
314 let date = Date::from_calendar_date(year_int, month, 1).ok()?;
315 Some(OffsetDateTime::new_utc(date, Time::MIDNIGHT))
316 }
317
318 fn parse_week_string(&self) -> Option<OffsetDateTime> {
319 let mut iterator = self.split('-');
321 let year = iterator.next()?;
322
323 let year_int = year.parse::<i32>().ok()?;
325 if year.len() < 4 || year_int == 0 {
326 return None;
327 }
328
329 let week = iterator.next()?;
331 let (week_first, week_last) = week.split_at(1);
332 if week_first != "W" {
333 return None;
334 }
335
336 let week_int = week_last.parse::<u32>().ok()?;
338 if week_last.len() != 2 {
339 return None;
340 }
341
342 let max_week = max_week_in_year(year_int);
344
345 if week_int < 1 || week_int > max_week {
347 return None;
348 }
349
350 if iterator.next().is_some() {
352 return None;
353 }
354
355 let date = Date::from_iso_week_date(year_int, week_int as u8, Weekday::Monday).ok()?;
357 Some(OffsetDateTime::new_utc(date, Time::MIDNIGHT))
358 }
359
360 fn parse_time_string(&self) -> Option<OffsetDateTime> {
361 let (hour, minute, second, millisecond) = parse_time_component(self)?;
363
364 if self.split(':').nth(3).is_some() {
366 return None;
367 }
368
369 let time = Time::from_hms_milli(hour, minute, second, millisecond).ok()?;
371 Some(OffsetDateTime::new_utc(
372 OffsetDateTime::UNIX_EPOCH.date(),
373 time,
374 ))
375 }
376
377 fn parse_local_date_time_string(&self) -> Option<OffsetDateTime> {
378 let mut iterator = if self.contains('T') {
380 self.split('T')
381 } else {
382 self.split(' ')
383 };
384
385 let date = iterator.next()?;
387 let (year, month, day) = parse_date_component(date)?;
388
389 let time = iterator.next()?;
391 let (hour, minute, second, millisecond) = parse_time_component(time)?;
392
393 if iterator.next().is_some() {
395 return None;
396 }
397
398 let month = (month as u8).try_into().ok()?;
401 let date = Date::from_calendar_date(year, month, day as u8).ok()?;
402 let time = Time::from_hms_milli(hour, minute, second, millisecond).ok()?;
403 Some(OffsetDateTime::new_utc(date, time))
404 }
405
406 fn is_valid_time_string(&self) -> bool {
407 enum State {
408 HourHigh,
409 HourLow09,
410 HourLow03,
411 MinuteColon,
412 MinuteHigh,
413 MinuteLow,
414 SecondColon,
415 SecondHigh,
416 SecondLow,
417 MilliStop,
418 MilliHigh,
419 MilliMiddle,
420 MilliLow,
421 Done,
422 Error,
423 }
424 let next_state =
425 |valid: bool, next: State| -> State { if valid { next } else { State::Error } };
426
427 let state = self.chars().fold(State::HourHigh, |state, c| {
428 match state {
429 State::HourHigh => match c {
431 '0' | '1' => State::HourLow09,
432 '2' => State::HourLow03,
433 _ => State::Error,
434 },
435 State::HourLow09 => next_state(c.is_ascii_digit(), State::MinuteColon),
436 State::HourLow03 => next_state(c.is_digit(4), State::MinuteColon),
437
438 State::MinuteColon => next_state(c == ':', State::MinuteHigh),
440
441 State::MinuteHigh => next_state(c.is_digit(6), State::MinuteLow),
443 State::MinuteLow => next_state(c.is_ascii_digit(), State::SecondColon),
444
445 State::SecondColon => next_state(c == ':', State::SecondHigh),
447 State::SecondHigh => next_state(c.is_digit(6), State::SecondLow),
449 State::SecondLow => next_state(c.is_ascii_digit(), State::MilliStop),
450
451 State::MilliStop => next_state(c == '.', State::MilliHigh),
453 State::MilliHigh => next_state(c.is_ascii_digit(), State::MilliMiddle),
455 State::MilliMiddle => next_state(c.is_ascii_digit(), State::MilliLow),
456 State::MilliLow => next_state(c.is_ascii_digit(), State::Done),
457
458 _ => State::Error,
459 }
460 });
461
462 match state {
463 State::Done |
464 State::SecondColon |
466 State::MilliStop |
468 State::MilliMiddle | State::MilliLow => true,
470 _ => false
471 }
472 }
473
474 fn is_valid_simple_color_string(&self) -> bool {
475 let mut chars = self.chars();
476 if self.len() == 7 && chars.next() == Some('#') {
477 chars.all(|c| c.is_ascii_hexdigit())
478 } else {
479 false
480 }
481 }
482
483 fn is_valid_email_address_string(&self) -> bool {
484 static RE: LazyLock<Regex> = LazyLock::new(|| {
485 Regex::new(concat!(
486 r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?",
487 r"(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
488 ))
489 .unwrap()
490 });
491 RE.is_match(self)
492 }
493}