1use crate::host::{self, with_host};
20use fusevm::Value;
21
22const ERA: usize = 0;
24const YEAR: usize = 1;
25const QUARTER: usize = 2;
26const MONTH: usize = 3;
27const WEEK_OF_YEAR: usize = 4;
28const WEEK_OF_MONTH: usize = 5;
29const WEEKDAY: usize = 6;
30const DAY_OF_YEAR: usize = 7;
31const DAY_OF_WEEK_IN_MONTH: usize = 8;
32const DAY: usize = 9;
33const DAYPERIOD: usize = 10;
34const HOUR: usize = 11;
35const MINUTE: usize = 12;
36const SECOND: usize = 13;
37const FRACTIONAL: usize = 14;
38const ZONE: usize = 15;
39const FIELD_COUNT: usize = 16;
40
41const DATE_MASK: u32 = (1 << DAYPERIOD) - 1;
42const TIME_MASK: u32 = ((1 << FIELD_COUNT) - 1) & !DATE_MASK;
43const SECOND_AND_FRACTIONAL: u32 = (1 << SECOND) | (1 << FRACTIONAL);
44
45const NUMERIC: i32 = 0x100;
49const DELTA: i32 = 0x10;
50const NARROW: i32 = -0x101;
51const SHORTER: i32 = -0x102;
52const SHORT: i32 = -0x103;
53const LONG: i32 = -0x104;
54const EXTRA_FIELD: i32 = 0x10000;
55const MISSING_FIELD: i32 = 0x1000;
56
57const TYPES: &[(char, usize, i32, usize)] = &[
61 ('G', ERA, SHORT, 1),
62 ('G', ERA, LONG, 4),
63 ('G', ERA, NARROW, 5),
64 ('y', YEAR, NUMERIC, 1),
65 ('Q', QUARTER, NUMERIC, 1),
66 ('Q', QUARTER, SHORT, 3),
67 ('Q', QUARTER, LONG, 4),
68 ('Q', QUARTER, NARROW, 5),
69 ('M', MONTH, NUMERIC, 1),
70 ('M', MONTH, SHORT, 3),
71 ('M', MONTH, LONG, 4),
72 ('M', MONTH, NARROW, 5),
73 ('L', MONTH, NUMERIC + DELTA, 1),
74 ('L', MONTH, SHORT - DELTA, 3),
75 ('L', MONTH, LONG - DELTA, 4),
76 ('L', MONTH, NARROW - DELTA, 5),
77 ('w', WEEK_OF_YEAR, NUMERIC, 1),
78 ('W', WEEK_OF_MONTH, NUMERIC, 1),
79 ('E', WEEKDAY, SHORT, 1),
80 ('E', WEEKDAY, LONG, 4),
81 ('E', WEEKDAY, NARROW, 5),
82 ('E', WEEKDAY, SHORTER, 6),
83 ('c', WEEKDAY, NUMERIC + 2 * DELTA, 1),
84 ('c', WEEKDAY, SHORT - 2 * DELTA, 3),
85 ('c', WEEKDAY, LONG - 2 * DELTA, 4),
86 ('c', WEEKDAY, NARROW - 2 * DELTA, 5),
87 ('c', WEEKDAY, SHORTER - 2 * DELTA, 6),
88 ('d', DAY, NUMERIC, 1),
89 ('D', DAY_OF_YEAR, NUMERIC, 1),
90 ('F', DAY_OF_WEEK_IN_MONTH, NUMERIC, 1),
91 ('a', DAYPERIOD, SHORT, 1),
92 ('a', DAYPERIOD, LONG, 4),
93 ('a', DAYPERIOD, NARROW, 5),
94 ('B', DAYPERIOD, SHORT - 3 * DELTA, 1),
95 ('B', DAYPERIOD, LONG - 3 * DELTA, 4),
96 ('B', DAYPERIOD, NARROW - 3 * DELTA, 5),
97 ('H', HOUR, NUMERIC + 10 * DELTA, 1),
98 ('k', HOUR, NUMERIC + 11 * DELTA, 1),
99 ('h', HOUR, NUMERIC, 1),
100 ('K', HOUR, NUMERIC + DELTA, 1),
101 ('m', MINUTE, NUMERIC, 1),
102 ('s', SECOND, NUMERIC, 1),
103 ('S', FRACTIONAL, NUMERIC, 1),
104 ('v', ZONE, SHORT - 2 * DELTA, 1),
105 ('v', ZONE, LONG - 2 * DELTA, 4),
106 ('z', ZONE, SHORT, 1),
107 ('z', ZONE, LONG, 4),
108 ('O', ZONE, SHORT - 3 * DELTA, 1),
109 ('O', ZONE, LONG - 3 * DELTA, 4),
110];
111
112fn type_row(c: char, len: usize) -> Option<(usize, i32, usize)> {
114 TYPES
115 .iter()
116 .rev()
117 .find(|r| r.0 == c && r.3 <= len)
118 .or_else(|| TYPES.iter().find(|r| r.0 == c))
119 .map(|r| (r.1, r.2, r.3))
120}
121
122#[derive(Clone, Debug, PartialEq)]
125enum Tok {
126 Field(char, usize),
127 Lit(String),
128}
129
130fn tokenize(pattern: &str) -> Vec<Tok> {
131 let cs: Vec<char> = pattern.chars().collect();
132 let mut out = Vec::new();
133 let mut i = 0;
134 while i < cs.len() {
135 let c = cs[i];
136 if c.is_ascii_alphabetic() {
137 let start = i;
138 while i < cs.len() && cs[i] == c {
139 i += 1;
140 }
141 out.push(Tok::Field(c, i - start));
142 } else if c == '\'' {
143 let start = i;
145 i += 1;
146 while i < cs.len() {
147 if cs[i] == '\'' {
148 if cs.get(i + 1) == Some(&'\'') {
149 i += 2;
150 continue;
151 }
152 i += 1;
153 break;
154 }
155 i += 1;
156 }
157 out.push(Tok::Lit(cs[start..i].iter().collect()));
158 } else {
159 out.push(Tok::Lit(c.to_string()));
160 i += 1;
161 }
162 }
163 out
164}
165
166#[derive(Clone, Debug, PartialEq, Default)]
169struct Skeleton {
170 orig: [Option<(char, usize)>; FIELD_COUNT],
171 ty: [i32; FIELD_COUNT],
172 base: [Option<(char, usize)>; FIELD_COUNT],
173 added_day_period: bool,
175}
176
177impl Skeleton {
178 fn of(pattern: &str) -> Skeleton {
181 let mut s = Skeleton::default();
182 for t in tokenize(pattern) {
183 let Tok::Field(c, len) = t else { continue };
184 let Some((field, ty, min)) = type_row(c, len) else {
185 continue;
186 };
187 s.orig[field] = Some((c, len));
188 let base_char = TYPES
189 .iter()
190 .find(|r| r.1 == field && r.2 == ty)
191 .map_or(c, |r| r.0);
192 s.base[field] = Some((base_char, min));
193 s.ty[field] = if ty > 0 { ty + len as i32 } else { ty };
194 }
195 if s.orig[MINUTE].is_some() && s.orig[FRACTIONAL].is_some() && s.orig[SECOND].is_none() {
198 s.orig[SECOND] = Some(('s', 1));
199 s.base[SECOND] = Some(('s', 1));
200 s.ty[SECOND] = NUMERIC + 1;
201 }
202 if let Some((h, _)) = s.orig[HOUR] {
205 if h == 'h' || h == 'K' {
206 if s.orig[DAYPERIOD].is_none() {
207 s.orig[DAYPERIOD] = Some(('a', 1));
208 s.base[DAYPERIOD] = Some(('a', 1));
209 s.ty[DAYPERIOD] = SHORT;
210 s.added_day_period = true;
211 }
212 } else if s.orig[DAYPERIOD].is_some() {
213 s.orig[DAYPERIOD] = None;
214 s.base[DAYPERIOD] = None;
215 s.ty[DAYPERIOD] = 0;
216 }
217 }
218 s
219 }
220
221 fn mask(&self) -> u32 {
222 (0..FIELD_COUNT)
223 .filter(|&i| self.ty[i] != 0)
224 .fold(0, |m, i| m | (1 << i))
225 }
226
227 fn base_string(&self) -> String {
229 self.base
230 .iter()
231 .flatten()
232 .map(|&(c, n)| c.to_string().repeat(n))
233 .collect()
234 }
235
236 fn distance(&self, other: &Skeleton, include: u32) -> (i32, u32, u32) {
238 let (mut d, mut missing, mut extra) = (0, 0u32, 0u32);
239 for i in 0..FIELD_COUNT {
240 let mine = if include & (1 << i) == 0 {
241 0
242 } else {
243 self.ty[i]
244 };
245 let theirs = other.ty[i];
246 if mine == theirs {
247 continue;
248 }
249 if mine == 0 {
250 d += EXTRA_FIELD;
251 extra |= 1 << i;
252 } else if theirs == 0 {
253 d += MISSING_FIELD;
254 missing |= 1 << i;
255 } else {
256 d += (mine - theirs).abs();
257 }
258 }
259 (d, missing, extra)
260 }
261}
262
263struct Entry {
267 skel: Skeleton,
268 pattern: &'static str,
269 specified: bool,
270}
271
272const AVAILABLE_FORMATS: &[(&str, &str)] = &[
276 ("Bh", "h B"),
277 ("Bhm", "h:mm B"),
278 ("Bhms", "h:mm:ss B"),
279 ("E", "ccc"),
280 ("EBh", "E h B"),
281 ("EBhm", "E h:mm B"),
282 ("EBhms", "E h:mm:ss B"),
283 ("EHm", "E HH:mm"),
284 ("EHms", "E HH:mm:ss"),
285 ("Ed", "d E"),
286 ("Eh", "E h a"),
287 ("Ehm", "E h:mm a"),
288 ("Ehms", "E h:mm:ss a"),
289 ("Gy", "y G"),
290 ("GyM", "M/y G"),
291 ("GyMEd", "E, M/d/y G"),
292 ("GyMMM", "MMM y G"),
293 ("GyMMMEd", "E, MMM d, y G"),
294 ("GyMMMd", "MMM d, y G"),
295 ("GyMd", "M/d/y G"),
296 ("H", "HH"),
297 ("Hm", "HH:mm"),
298 ("Hms", "HH:mm:ss"),
299 ("Hmsv", "HH:mm:ss v"),
300 ("Hmv", "HH:mm v"),
301 ("Hv", "HH v"),
302 ("M", "L"),
303 ("MEd", "E, M/d"),
304 ("MMM", "LLL"),
305 ("MMMEd", "E, MMM d"),
306 ("MMMMd", "MMMM d"),
307 ("MMMd", "MMM d"),
308 ("Md", "M/d"),
309 ("d", "d"),
310 ("h", "h a"),
311 ("hm", "h:mm a"),
312 ("hms", "h:mm:ss a"),
313 ("hmsv", "h:mm:ss a v"),
314 ("hmv", "h:mm a v"),
315 ("hv", "h a v"),
316 ("ms", "mm:ss"),
317 ("y", "y"),
318 ("yM", "M/y"),
319 ("yMEd", "E, M/d/y"),
320 ("yMMM", "MMM y"),
321 ("yMMMEd", "E, MMM d, y"),
322 ("yMMMM", "MMMM y"),
323 ("yMMMd", "MMM d, y"),
324 ("yMd", "M/d/y"),
325 ("yQQQ", "QQQ y"),
326 ("yQQQQ", "QQQQ y"),
327];
328
329const DATE_STYLES: [&str; 4] = ["EEEE, MMMM d, y", "MMMM d, y", "MMM d, y", "M/d/yy"];
332const TIME_STYLES: [&str; 4] = ["h:mm:ss a zzzz", "h:mm:ss a z", "h:mm:ss a", "h:mm a"];
333
334const DATE_TIME_GLUE: [&str; 4] = ["{1} 'at' {0}", "{1} 'at' {0}", "{1}, {0}", "{1}, {0}"];
337
338fn entries() -> &'static [Entry] {
344 static ENTRIES: std::sync::OnceLock<Vec<Entry>> = std::sync::OnceLock::new();
345 ENTRIES.get_or_init(|| {
346 let mut list: Vec<Entry> = Vec::new();
347 let add_derived = |list: &mut Vec<Entry>, pattern: &'static str| {
350 let skel = Skeleton::of(pattern);
351 let base = skel.base_string();
352 if list.iter().any(|e| e.skel.base_string() == base) {
353 return;
354 }
355 list.push(Entry {
356 skel,
357 pattern,
358 specified: false,
359 });
360 };
361 const CANONICAL: &[&str] = &[
362 "G", "y", "Q", "M", "w", "W", "E", "D", "F", "d", "a", "H", "m", "s", "S", "v",
363 ];
364 for p in CANONICAL {
365 add_derived(&mut list, p);
366 }
367 for i in 0..4 {
368 add_derived(&mut list, DATE_STYLES[i]);
369 add_derived(&mut list, TIME_STYLES[i]);
370 }
371 add_derived(&mut list, "mm:ss");
373 for &(key, pattern) in AVAILABLE_FORMATS {
374 let skel = Skeleton::of(key);
375 match list.iter_mut().find(|e| e.skel == skel) {
376 Some(e) => {
377 e.pattern = pattern;
378 e.specified = true;
379 }
380 None => list.push(Entry {
381 skel,
382 pattern,
383 specified: true,
384 }),
385 }
386 }
387 let bucket = |e: &Entry| {
388 let c = e.skel.base_string().chars().next().unwrap_or('a');
389 if c.is_ascii_uppercase() {
390 c as u32 - 'A' as u32
391 } else {
392 26 + c as u32 - 'a' as u32
393 }
394 };
395 list.sort_by_key(bucket);
396 list
397 })
398}
399
400fn best_raw(req: &Skeleton, include: u32) -> (&'static Entry, u32, u32) {
403 let mut best: Option<(&Entry, i32, u32, u32)> = None;
404 for e in entries() {
405 let (d, missing, extra) = req.distance(&e.skel, include);
406 let better = match best {
407 None => true,
408 Some((_, bd, bmissing, _)) => d < bd || (d == bd && bmissing < missing),
409 };
410 if better {
411 best = Some((e, d, missing, extra));
412 if d == 0 {
413 break;
414 }
415 }
416 }
417 let (e, _, missing, extra) = best.expect("the generator has entries");
418 (e, missing, extra)
419}
420
421fn adjust(
425 pattern: &str,
426 entry_skel: Option<&Skeleton>,
427 req: &Skeleton,
428 fix_fraction: bool,
429) -> String {
430 let mut out = String::new();
431 for t in tokenize(pattern) {
432 let (c, len) = match t {
433 Tok::Lit(s) => {
434 out.push_str(&s);
435 continue;
436 }
437 Tok::Field(c, len) => (c, len),
438 };
439 let Some((field, row_ty, _)) = type_row(c, len) else {
440 out.push_str(&c.to_string().repeat(len));
441 continue;
442 };
443 if fix_fraction && field == SECOND {
444 out.push_str(&c.to_string().repeat(len));
445 out.push('.');
446 if let Some((fc, flen)) = req.orig[FRACTIONAL] {
447 out.push_str(&fc.to_string().repeat(flen));
448 }
449 continue;
450 }
451 if req.ty[field] == 0 {
452 out.push_str(&c.to_string().repeat(len));
453 continue;
454 }
455 let (req_char, mut req_len) = req.orig[field].unwrap_or((c, len));
456 if req_char == 'E' && req_len < 3 {
457 req_len = 3;
458 }
459 let mut adj_len = req_len;
460 if field == MINUTE || field == SECOND {
461 adj_len = len;
462 } else if let Some(spec) = entry_skel {
463 let skel_len = spec.orig[field].map_or(0, |(_, n)| n);
464 let pat_numeric = row_ty > 0;
465 let skel_numeric = spec.ty[field] > 0;
466 if skel_len == req_len || pat_numeric != skel_numeric {
467 adj_len = len;
468 }
469 }
470 let keep_pattern_char = matches!(field, HOUR | MONTH | WEEKDAY | YEAR);
471 let mut ch = if keep_pattern_char { c } else { req_char };
472 if field == HOUR {
473 ch = match req_char {
475 'h' => 'h',
476 'K' => 'h',
477 _ => ch,
478 };
479 }
480 out.push_str(&ch.to_string().repeat(adj_len));
481 }
482 out
483}
484
485fn best_appending(req: &Skeleton, fields: u32) -> String {
488 if fields == 0 {
489 return String::new();
490 }
491 let (e, mut missing, extra) = best_raw(req, fields);
492 let spec = |e: &Entry| e.specified.then_some(&e.skel).cloned();
493 let mut spec_skel = spec(e);
494 let mut result = adjust(e.pattern, spec_skel.as_ref(), req, false);
495 if missing == 0 && extra == 0 {
496 return result;
497 }
498 let mut last_missing = 0;
499 while missing != 0 {
500 if last_missing == missing {
501 break;
502 }
503 if missing & SECOND_AND_FRACTIONAL == 1 << FRACTIONAL
504 && fields & SECOND_AND_FRACTIONAL == SECOND_AND_FRACTIONAL
505 {
506 result = adjust(&result, spec_skel.as_ref(), req, true);
507 missing &= !(1 << FRACTIONAL);
508 continue;
509 }
510 let starting = missing;
511 let (e, m, _) = best_raw(req, missing);
512 missing = m;
513 spec_skel = spec(e);
514 let temp = adjust(e.pattern, spec_skel.as_ref(), req, false);
515 let found = starting & !missing;
516 let top = 31 - found.leading_zeros() as usize;
517 result = append_item(top, &result, &temp);
518 last_missing = missing;
519 }
520 result
521}
522
523fn append_item(field: usize, result: &str, temp: &str) -> String {
525 let name = match field {
526 ERA => "era",
527 YEAR => "year",
528 QUARTER => "quarter",
529 MONTH => "month",
530 WEEK_OF_YEAR => "week",
531 WEEK_OF_MONTH => "week of month",
532 WEEKDAY => "day of the week",
533 DAY_OF_YEAR => "day of year",
534 DAY_OF_WEEK_IN_MONTH => "weekday of the month",
535 DAY => "day",
536 DAYPERIOD => "AM/PM",
537 HOUR => "hour",
538 MINUTE => "minute",
539 SECOND => "second",
540 FRACTIONAL => "F14",
541 _ => "time zone",
542 };
543 match field {
544 ERA | YEAR | WEEKDAY | ZONE => format!("{result} {temp}"),
545 QUARTER | MONTH | WEEK_OF_YEAR | DAY | HOUR | MINUTE | SECOND => {
546 format!("{result} ('{name}': {temp})")
547 }
548 _ => format!("{result} \u{251C}'{name}': {temp}\u{2524}"),
549 }
550}
551
552fn best_pattern(skeleton: &str) -> String {
554 let req = Skeleton::of(skeleton);
555 let (e, missing, extra) = best_raw(&req, u32::MAX);
556 if missing == 0 && extra == 0 {
557 return adjust(e.pattern, e.specified.then_some(&e.skel), &req, false);
558 }
559 let needed = req.mask();
560 let date = best_appending(&req, needed & DATE_MASK);
561 let time = best_appending(&req, needed & TIME_MASK);
562 if date.is_empty() {
563 return time;
564 }
565 if time.is_empty() {
566 return date;
567 }
568 let month_len = req.base[MONTH].map_or(0, |(_, n)| n);
569 let style = match month_len {
570 4 if req.base[WEEKDAY].is_some() => 0,
571 4 => 1,
572 3 => 2,
573 _ => 3,
574 };
575 glue(style, &date, &time)
576}
577
578fn glue(style: usize, date: &str, time: &str) -> String {
579 DATE_TIME_GLUE[style]
580 .replace("{1}", date)
581 .replace("{0}", time)
582}
583
584#[derive(Clone, Copy, PartialEq)]
589pub enum Required {
590 Any,
591 Date,
592 Time,
593}
594
595#[derive(Clone, Copy, PartialEq)]
597enum Hc {
598 H11,
599 H12,
600 H23,
601 H24,
602}
603
604#[derive(Clone)]
606enum Zone {
607 Local,
609 Utc,
610 Fixed(i64),
612 Named(std::rc::Rc<crate::tzif::Tz>),
614}
615
616fn out_of_range(method: &str, value: &str, name: &str) -> String {
617 host::range_error(&format!(
618 "Value {value} out of range for {method} options property {name}"
619 ))
620}
621
622fn string_option(
623 opts: &Value,
624 method: &str,
625 name: &str,
626 allowed: &[&str],
627) -> Result<Option<String>, String> {
628 let v = crate::builtins::get_property(opts, name)?;
629 if matches!(v, Value::Undef) {
630 return Ok(None);
631 }
632 let s = host::to_string_value(&v)?;
633 let s = with_host(|h| h.str_of(&s));
634 if !allowed.is_empty() && !allowed.contains(&s.as_str()) {
635 return Err(out_of_range(method, &s, name));
636 }
637 Ok(Some(s))
638}
639
640pub struct Format {
642 pattern: String,
643 zone: Zone,
644}
645
646const TEXT_WIDTHS: &[&str] = &["narrow", "short", "long"];
647const NUMERIC_WIDTHS: &[&str] = &["numeric", "2-digit"];
648
649pub fn resolve(opts: &Value, required: Required, method: &str) -> Result<Format, String> {
653 if with_host(|h| h.is_null(opts)) {
654 return Err(host::type_error(&format!(
655 "{method} called on null or undefined"
656 )));
657 }
658 let is_object = matches!(opts, Value::Obj(_)) && !with_host(|h| host::is_primitive(h, opts));
659 let undef = Value::Undef;
660 let o = if is_object { opts } else { &undef };
661 let get = |name: &str, allowed: &[&str]| -> Result<Option<String>, String> {
662 if is_object {
663 string_option(o, method, name, allowed)
664 } else {
665 Ok(None)
666 }
667 };
668 let hour12 = if is_object {
669 let v = crate::builtins::get_property(o, "hour12")?;
670 (!matches!(v, Value::Undef)).then(|| with_host(|h| h.truthy(&v)))
671 } else {
672 None
673 };
674 let hour_cycle = get("hourCycle", &["h11", "h12", "h23", "h24"])?;
675 let time_zone = if is_object {
676 let v = crate::builtins::get_property(o, "timeZone")?;
677 if matches!(v, Value::Undef) {
678 None
679 } else {
680 let s = host::to_string_value(&v)?;
681 Some(with_host(|h| h.str_of(&s)))
682 }
683 } else {
684 None
685 };
686 let weekday = get("weekday", TEXT_WIDTHS)?;
687 let era = get("era", TEXT_WIDTHS)?;
688 let mut year = get("year", NUMERIC_WIDTHS)?;
689 let mut month = get("month", &["numeric", "2-digit", "narrow", "short", "long"])?;
690 let mut day = get("day", NUMERIC_WIDTHS)?;
691 let day_period = get("dayPeriod", TEXT_WIDTHS)?;
692 let mut hour = get("hour", NUMERIC_WIDTHS)?;
693 let mut minute = get("minute", NUMERIC_WIDTHS)?;
694 let mut second = get("second", NUMERIC_WIDTHS)?;
695 let fsd = if is_object {
696 let v = crate::builtins::get_property(o, "fractionalSecondDigits")?;
697 if matches!(v, Value::Undef) {
698 None
699 } else {
700 let n = host::to_number_value(&v)?;
701 if n.is_nan() || !(1.0..=3.0).contains(&n) {
702 return Err(host::range_error(
703 "fractionalSecondDigits value is out of range.",
704 ));
705 }
706 Some(n.floor() as usize)
707 }
708 } else {
709 None
710 };
711 let tz_name = get(
712 "timeZoneName",
713 &[
714 "short",
715 "long",
716 "shortOffset",
717 "longOffset",
718 "shortGeneric",
719 "longGeneric",
720 ],
721 )?;
722 let style_names = &["full", "long", "medium", "short"];
723 let date_style = get("dateStyle", style_names)?;
724 let time_style = get("timeStyle", style_names)?;
725
726 let zone = match time_zone {
727 None => Zone::Local,
728 Some(z) => parse_zone(&z)?,
729 };
730
731 let mut hc = match (hour12, hour_cycle.as_deref()) {
732 (Some(true), _) => Some(Hc::H12),
733 (Some(false), _) => Some(Hc::H23),
734 (None, Some("h11")) => Some(Hc::H11),
735 (None, Some("h12")) => Some(Hc::H12),
736 (None, Some("h23")) => Some(Hc::H23),
737 (None, Some("h24")) => Some(Hc::H24),
738 _ => None,
739 };
740
741 if date_style.is_some() || time_style.is_some() {
742 let explicit = weekday.is_some()
743 || era.is_some()
744 || year.is_some()
745 || month.is_some()
746 || day.is_some()
747 || day_period.is_some()
748 || hour.is_some()
749 || minute.is_some()
750 || second.is_some()
751 || fsd.is_some()
752 || tz_name.is_some();
753 if explicit {
754 return Err(host::type_error("Invalid option : option"));
755 }
756 if required == Required::Date && time_style.is_some() {
757 return Err(host::type_error("Invalid option : timeStyle"));
758 }
759 if required == Required::Time && date_style.is_some() {
760 return Err(host::type_error("Invalid option : dateStyle"));
761 }
762 let index = |s: &Option<String>| {
763 s.as_deref()
764 .and_then(|s| style_names.iter().position(|n| *n == s))
765 };
766 let (ds, ts) = (index(&date_style), index(&time_style));
767 let time = ts.map(|t| {
768 let p = TIME_STYLES[t];
769 match hc {
770 None | Some(Hc::H12) => p.to_string(),
773 Some(c) => {
774 let skel: String = Skeleton::of(p)
775 .orig
776 .iter()
777 .flatten()
778 .filter(|(ch, _)| *ch != 'a')
779 .map(|&(ch, n)| {
780 let ch = if ch == 'h' { hour_char(c) } else { ch };
781 ch.to_string().repeat(n)
782 })
783 .collect();
784 best_pattern(&skel)
785 }
786 }
787 });
788 let pattern = match (ds, time) {
789 (Some(d), Some(t)) => glue(d, DATE_STYLES[d], &t),
790 (Some(d), None) => DATE_STYLES[d].to_string(),
791 (None, Some(t)) => t,
792 (None, None) => unreachable!("a style was given"),
793 };
794 return Ok(Format {
795 pattern: replace_hour_cycle(&pattern, hc),
796 zone,
797 });
798 }
799
800 let mut need_defaults = true;
802 if matches!(required, Required::Date | Required::Any)
803 && (weekday.is_some() || year.is_some() || month.is_some() || day.is_some())
804 {
805 need_defaults = false;
806 }
807 if matches!(required, Required::Time | Required::Any)
808 && (day_period.is_some()
809 || hour.is_some()
810 || minute.is_some()
811 || second.is_some()
812 || fsd.is_some())
813 {
814 need_defaults = false;
815 }
816 if need_defaults && matches!(required, Required::Date | Required::Any) {
817 year = Some("numeric".into());
818 month = Some("numeric".into());
819 day = Some("numeric".into());
820 }
821 if need_defaults && matches!(required, Required::Time | Required::Any) {
822 hour = Some("numeric".into());
823 minute = Some("numeric".into());
824 second = Some("numeric".into());
825 hc = hc.map(|c| match c {
829 Hc::H11 | Hc::H12 => Hc::H12,
830 Hc::H23 | Hc::H24 => Hc::H23,
831 });
832 }
833
834 let mut skel = String::new();
835 let mut push = |c: char, n: usize| skel.push_str(&c.to_string().repeat(n));
836 let text_len = |w: &str| match w {
837 "narrow" => 5,
838 "long" => 4,
839 _ => 3,
840 };
841 if let Some(w) = &era {
842 push('G', text_len(w));
843 }
844 if let Some(w) = &year {
845 push('y', if w == "2-digit" { 2 } else { 1 });
846 }
847 if let Some(w) = &month {
848 push(
849 'M',
850 match w.as_str() {
851 "numeric" => 1,
852 "2-digit" => 2,
853 w => text_len(w),
854 },
855 );
856 }
857 if let Some(w) = &weekday {
858 push('E', text_len(w));
859 }
860 if let Some(w) = &day {
861 push('d', if w == "2-digit" { 2 } else { 1 });
862 }
863 if let Some(w) = &day_period {
864 push('B', text_len(w));
865 }
866 if let Some(w) = &hour {
867 push(
868 hour_char(hc.unwrap_or(Hc::H12)),
869 if w == "2-digit" { 2 } else { 1 },
870 );
871 }
872 if let Some(w) = &minute {
873 push('m', if w == "2-digit" { 2 } else { 1 });
874 }
875 if let Some(w) = &second {
876 push('s', if w == "2-digit" { 2 } else { 1 });
877 }
878 if let Some(n) = fsd {
879 push('S', n);
880 }
881 if let Some(w) = &tz_name {
882 let (c, n) = match w.as_str() {
883 "short" => ('z', 1),
884 "long" => ('z', 4),
885 "shortOffset" => ('O', 1),
886 "longOffset" => ('O', 4),
887 "shortGeneric" => ('v', 1),
888 _ => ('v', 4),
889 };
890 push(c, n);
891 }
892 let pattern = best_pattern(&skel);
893 Ok(Format {
894 pattern: replace_hour_cycle(&pattern, if hour.is_some() { hc } else { None }),
895 zone,
896 })
897}
898
899fn hour_char(hc: Hc) -> char {
900 match hc {
901 Hc::H11 => 'K',
902 Hc::H12 => 'h',
903 Hc::H23 => 'H',
904 Hc::H24 => 'k',
905 }
906}
907
908fn replace_hour_cycle(pattern: &str, hc: Option<Hc>) -> String {
911 let Some(hc) = hc else {
912 return pattern.to_string();
913 };
914 let want = hour_char(hc);
915 let mut quoted = false;
916 pattern
917 .chars()
918 .map(|c| match c {
919 '\'' => {
920 quoted = !quoted;
921 c
922 }
923 'H' | 'h' | 'K' | 'k' if !quoted => want,
924 _ => c,
925 })
926 .collect()
927}
928
929fn parse_zone(z: &str) -> Result<Zone, String> {
932 let upper = z.to_ascii_uppercase();
933 const UTC_NAMES: &[&str] = &[
934 "UTC",
935 "ETC/UTC",
936 "GMT",
937 "ETC/GMT",
938 "UCT",
939 "ETC/UCT",
940 "ETC/UNIVERSAL",
941 "UNIVERSAL",
942 "ETC/ZULU",
943 "ZULU",
944 "ETC/GREENWICH",
945 "GREENWICH",
946 "GMT0",
947 "ETC/GMT0",
948 "GMT+0",
949 "GMT-0",
950 "ETC/GMT+0",
951 "ETC/GMT-0",
952 ];
953 if UTC_NAMES.contains(&upper.as_str()) {
954 return Ok(Zone::Utc);
955 }
956 if let Some(rest) = upper.strip_prefix("ETC/GMT") {
958 let (sign, digits) = rest.split_at(1.min(rest.len()));
959 if let (Some(s), Ok(h)) = (
960 match sign {
961 "+" => Some(-1),
962 "-" => Some(1),
963 _ => None,
964 },
965 digits.parse::<i64>(),
966 ) {
967 if !digits.is_empty() && digits.len() <= 2 && h <= 14 && !digits.starts_with('0') {
968 return Ok(Zone::Fixed(s * h * 60));
969 }
970 }
971 }
972 let b = z.as_bytes();
974 if matches!(b.first(), Some(b'+') | Some(b'-')) {
975 let sign = if b[0] == b'-' { -1 } else { 1 };
976 let rest = &z[1..];
977 let digits: String = rest.chars().filter(|c| *c != ':').collect();
978 let well_formed = match rest.len() {
979 2 => rest.bytes().all(|c| c.is_ascii_digit()),
980 4 => rest.bytes().all(|c| c.is_ascii_digit()),
981 5 => rest.as_bytes()[2] == b':' && digits.bytes().all(|c| c.is_ascii_digit()),
982 _ => false,
983 };
984 if well_formed {
985 let h: i64 = digits[..2].parse().unwrap_or(99);
986 let m: i64 = digits
987 .get(2..)
988 .filter(|s| !s.is_empty())
989 .map_or(Ok(0), str::parse)
990 .unwrap_or(99);
991 if h <= 23 && m <= 59 {
992 return Ok(Zone::Fixed(sign * (h * 60 + m)));
993 }
994 }
995 }
996 match crate::tzif::load(z) {
997 Some(tz) => Ok(Zone::Named(tz)),
998 None => Err(host::range_error(&format!(
999 "Invalid time zone specified: {z}"
1000 ))),
1001 }
1002}
1003
1004const MONTHS_LONG: [&str; 12] = [
1007 "January",
1008 "February",
1009 "March",
1010 "April",
1011 "May",
1012 "June",
1013 "July",
1014 "August",
1015 "September",
1016 "October",
1017 "November",
1018 "December",
1019];
1020const DAYS_LONG: [&str; 7] = [
1021 "Sunday",
1022 "Monday",
1023 "Tuesday",
1024 "Wednesday",
1025 "Thursday",
1026 "Friday",
1027 "Saturday",
1028];
1029
1030struct Fields {
1032 year: i64,
1033 month: usize,
1034 day: i64,
1035 weekday: usize,
1036 hour: i64,
1037 minute: i64,
1038 second: i64,
1039 millis: i64,
1040 offset_sec: i64,
1043 abbrev: Option<String>,
1046 utc: bool,
1048}
1049
1050impl Format {
1051 pub fn format(&self, ms: f64) -> String {
1053 let (offset_ms, abbrev) = match &self.zone {
1054 Zone::Local => (
1055 crate::stdlib::date::zone_offset_ms(ms),
1056 crate::stdlib::date::zone_abbrev(ms),
1057 ),
1058 Zone::Utc => (0.0, None),
1059 Zone::Fixed(m) => (*m as f64 * 60_000.0, None),
1060 Zone::Named(tz) => {
1061 let ty = tz.at((ms / 1000.0).floor() as i64);
1062 (ty.offset as f64 * 1000.0, Some(ty.abbrev))
1063 }
1064 };
1065 let local = ms + offset_ms;
1066 let (day_num, rem) = crate::stdlib::date::split_day(local);
1067 let (year, month, day) = crate::stdlib::date::civil_from_days(day_num);
1068 let f = Fields {
1069 year,
1070 month: month as usize,
1071 day,
1072 weekday: (((day_num % 7) + 4 + 7) % 7) as usize,
1073 hour: rem / 3_600_000,
1074 minute: rem / 60_000 % 60,
1075 second: rem / 1000 % 60,
1076 millis: rem % 1000,
1077 offset_sec: (offset_ms / 1000.0) as i64,
1078 utc: match &self.zone {
1081 Zone::Utc => true,
1082 Zone::Fixed(_) => false,
1083 Zone::Local => offset_ms == 0.0 && abbrev.as_deref().map_or(true, |a| a == "UTC"),
1084 Zone::Named(_) => offset_ms == 0.0 && abbrev.as_deref() == Some("UTC"),
1085 },
1086 abbrev: abbrev.filter(|_| ms >= 0.0),
1087 };
1088 let pattern_has_minute = self.pattern.contains('m');
1089 let pattern_has_second = self.pattern.contains('s');
1090 let mut out = String::new();
1091 for t in tokenize(&self.pattern) {
1092 match t {
1093 Tok::Lit(s) => {
1094 if s.len() >= 2 && s.starts_with('\'') {
1095 let inner = &s[1..s.len() - 1];
1096 if inner.is_empty() {
1097 out.push('\'');
1098 } else {
1099 out.push_str(&inner.replace("''", "'"));
1100 }
1101 } else {
1102 out.push_str(&s);
1103 }
1104 }
1105 Tok::Field(c, n) => {
1106 out.push_str(&self.field(c, n, &f, pattern_has_minute, pattern_has_second))
1107 }
1108 }
1109 }
1110 out
1111 }
1112
1113 fn field(&self, c: char, n: usize, f: &Fields, has_min: bool, has_sec: bool) -> String {
1114 let num = |v: i64, n: usize| format!("{:0width$}", v, width = n);
1115 let bc = f.year <= 0;
1116 match c {
1117 'G' => match (n, bc) {
1118 (1..=3, false) => "AD".into(),
1119 (1..=3, true) => "BC".into(),
1120 (4, false) => "Anno Domini".into(),
1121 (4, true) => "Before Christ".into(),
1122 (_, false) => "A".into(),
1123 (_, true) => "B".into(),
1124 },
1125 'y' => {
1126 let y = if bc { 1 - f.year } else { f.year };
1127 if n == 2 {
1128 num(y % 100, 2)
1129 } else {
1130 num(y, n)
1131 }
1132 }
1133 'M' | 'L' => match n {
1134 1 | 2 => num(f.month as i64 + 1, n),
1135 3 => MONTHS_LONG[f.month][..3].to_string(),
1136 4 => MONTHS_LONG[f.month].to_string(),
1137 _ => MONTHS_LONG[f.month][..1].to_string(),
1138 },
1139 'E' | 'c' => match n {
1140 4 => DAYS_LONG[f.weekday].to_string(),
1141 5 => DAYS_LONG[f.weekday][..1].to_string(),
1142 6 => DAYS_LONG[f.weekday][..2].to_string(),
1143 _ => DAYS_LONG[f.weekday][..3].to_string(),
1144 },
1145 'd' => num(f.day, n),
1146 'a' => match (n, f.hour < 12) {
1147 (5, true) => "a".into(),
1148 (5, false) => "p".into(),
1149 (_, true) => "AM".into(),
1150 (_, false) => "PM".into(),
1151 },
1152 'B' => {
1153 let noon =
1157 f.hour == 12 && (!has_min || f.minute == 0) && (!has_sec || f.second == 0);
1158 if noon {
1159 return if n == 5 { "n".into() } else { "noon".into() };
1160 }
1161 match f.hour {
1162 0..=11 => "in the morning",
1163 12..=17 => "in the afternoon",
1164 18..=20 => "in the evening",
1165 _ => "at night",
1166 }
1167 .into()
1168 }
1169 'h' => num(if f.hour % 12 == 0 { 12 } else { f.hour % 12 }, n),
1170 'K' => num(f.hour % 12, n),
1171 'H' => num(f.hour, n),
1172 'k' => num(if f.hour == 0 { 24 } else { f.hour }, n),
1173 'm' => num(f.minute, n),
1174 's' => num(f.second, n),
1175 'S' => {
1176 let digits = format!("{:03}", f.millis);
1177 let mut s: String = digits.chars().take(n).collect();
1178 while s.len() < n {
1179 s.push('0');
1180 }
1181 s
1182 }
1183 'z' | 'O' | 'v' => self.zone_name(c, n, f),
1184 _ => c.to_string().repeat(n),
1185 }
1186 }
1187
1188 fn zone_name(&self, c: char, n: usize, f: &Fields) -> String {
1192 let long = n >= 4;
1193 let utc = f.utc;
1194 let named = f.abbrev.as_deref().and_then(named_zone);
1195 match (c, utc, named) {
1196 ('z', true, _) if long => "Coordinated Universal Time".into(),
1197 ('z', true, _) => "UTC".into(),
1198 ('z', false, Some((_, long_name, _, _))) if long => long_name.into(),
1199 ('z', false, Some((Some(short), _, _, _))) => short.into(),
1200 ('v', false, Some((_, _, _, generic_long))) if long => generic_long.into(),
1201 ('v', false, Some((_, _, generic, _))) => generic.into(),
1202 _ => gmt_offset(f.offset_sec, long),
1203 }
1204 }
1205}
1206
1207fn gmt_offset(secs: i64, long: bool) -> String {
1210 let sign = if secs < 0 { '-' } else { '+' };
1211 let a = secs.abs();
1212 let (h, m, s) = (a / 3600, a / 60 % 60, a % 60);
1213 let tail = if s != 0 {
1214 format!(":{s:02}")
1215 } else {
1216 String::new()
1217 };
1218 if long {
1219 format!("GMT{sign}{h:02}:{m:02}{tail}")
1220 } else if m == 0 && s == 0 {
1221 format!("GMT{sign}{h}")
1222 } else {
1223 format!("GMT{sign}{h}:{m:02}{tail}")
1224 }
1225}
1226
1227type ZoneNames = (
1231 Option<&'static str>,
1232 &'static str,
1233 &'static str,
1234 &'static str,
1235);
1236
1237fn named_zone(abbrev: &str) -> Option<ZoneNames> {
1238 let us = |short, long, region: &'static str| -> ZoneNames {
1239 let generic: &'static str = match region {
1240 "Eastern" => "ET",
1241 "Central" => "CT",
1242 "Mountain" => "MT",
1243 "Pacific" => "PT",
1244 _ => "AKT",
1245 };
1246 let generic_long: &'static str = match region {
1247 "Eastern" => "Eastern Time",
1248 "Central" => "Central Time",
1249 "Mountain" => "Mountain Time",
1250 "Pacific" => "Pacific Time",
1251 _ => "Alaska Time",
1252 };
1253 (Some(short), long, generic, generic_long)
1254 };
1255 Some(match abbrev {
1256 "EST" => us("EST", "Eastern Standard Time", "Eastern"),
1257 "EDT" => us("EDT", "Eastern Daylight Time", "Eastern"),
1258 "CST" => us("CST", "Central Standard Time", "Central"),
1259 "CDT" => us("CDT", "Central Daylight Time", "Central"),
1260 "MST" => us("MST", "Mountain Standard Time", "Mountain"),
1261 "MDT" => us("MDT", "Mountain Daylight Time", "Mountain"),
1262 "PST" => us("PST", "Pacific Standard Time", "Pacific"),
1263 "PDT" => us("PDT", "Pacific Daylight Time", "Pacific"),
1264 "AKST" => us("AKST", "Alaska Standard Time", "Alaska"),
1265 "AKDT" => us("AKDT", "Alaska Daylight Time", "Alaska"),
1266 "HST" => (
1269 Some("HST"),
1270 "Hawaii-Aleutian Standard Time",
1271 "HST",
1272 "Hawaii-Aleutian Standard Time",
1273 ),
1274 "GMT" => (
1275 Some("GMT"),
1276 "Greenwich Mean Time",
1277 "GMT",
1278 "Greenwich Mean Time",
1279 ),
1280 _ => return None,
1281 })
1282}
1283
1284pub fn date_to_locale(
1286 ms: f64,
1287 opts: &Value,
1288 required: Required,
1289 method: &str,
1290) -> Result<String, String> {
1291 if ms.is_nan() {
1292 return Ok("Invalid Date".into());
1293 }
1294 Ok(resolve(opts, required, method)?.format(ms))
1295}
1296
1297#[cfg(test)]
1298mod tests {
1299 use super::*;
1300
1301 #[test]
1302 fn patterns_match_icu() {
1303 assert_eq!(best_pattern("yMd"), "M/d/y");
1304 assert_eq!(best_pattern("yMMMMEEEEd"), "EEEE, MMMM d, y");
1305 assert_eq!(best_pattern("hms"), "h:mm:ss a");
1306 assert_eq!(best_pattern("yMdhms"), "M/d/y, h:mm:ss a");
1307 assert_eq!(best_pattern("Hms"), "HH:mm:ss");
1308 assert_eq!(best_pattern("MMMMMh"), "LLLLL, h a");
1309 assert_eq!(best_pattern("ss"), "s");
1310 assert_eq!(best_pattern("ddHHs"), "dd, s ('hour': HH)");
1311 }
1312}