Skip to main content

mz_expr/scalar/func/
format.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Data type formatting functions.
11//!
12//! <https://www.postgresql.org/docs/current/functions-formatting.html>
13
14#![allow(non_camel_case_types)]
15
16use std::fmt;
17
18use aho_corasick::AhoCorasickBuilder;
19use enum_iterator::Sequence;
20use mz_ore::cast::CastFrom;
21use num_enum::{IntoPrimitive, TryFromPrimitive};
22use serde::{Deserialize, Serialize};
23
24use crate::scalar::func::TimestampLike;
25
26/// The raw tokens that can appear in a format string. Many of these tokens
27/// overlap, in which case the longest matching token should be selected.
28#[repr(u8)]
29#[derive(Eq, PartialEq, TryFromPrimitive, IntoPrimitive, Sequence)]
30enum DateTimeToken {
31    a_d,
32    A_D,
33    a_m,
34    A_M,
35    ad,
36    AD,
37    am,
38    AM,
39    b_c,
40    B_C,
41    bc,
42    BC,
43    cc,
44    CC,
45    d,
46    D,
47    day,
48    Day,
49    DAY,
50    dd,
51    DD,
52    ddd,
53    DDD,
54    dy,
55    Dy,
56    DY,
57    fm,
58    FM,
59    fx,
60    FX,
61    hh,
62    HH,
63    hh12,
64    HH12,
65    hh24,
66    HH24,
67    i,
68    I,
69    id,
70    ID,
71    iddd,
72    IDDD,
73    iw,
74    IW,
75    iy,
76    IY,
77    iyy,
78    IYY,
79    iyyy,
80    IYYY,
81    j,
82    J,
83    mi,
84    MI,
85    mm,
86    MM,
87    mon,
88    Mon,
89    MON,
90    month,
91    Month,
92    MONTH,
93    ms,
94    MS,
95    OF,
96    p_m,
97    P_M,
98    pm,
99    PM,
100    q,
101    Q,
102    rm,
103    RM,
104    ss,
105    SS,
106    ssss,
107    SSSS,
108    sssss,
109    SSSSS,
110    tz,
111    TZ,
112    TZH,
113    TZM,
114    us,
115    US,
116    w,
117    W,
118    ww,
119    WW,
120    y_yyy,
121    Y_YYY,
122    y,
123    Y,
124    yy,
125    YY,
126    yyy,
127    YYY,
128    yyyy,
129    YYYY,
130    th,
131    TH,
132    EscQuote,
133    Quote,
134}
135
136impl DateTimeToken {
137    /// Returns the literal sequence of characters that this `DateTimeToken`
138    /// matches.
139    const fn pattern(&self) -> &'static str {
140        match self {
141            DateTimeToken::AD => "AD",
142            DateTimeToken::ad => "ad",
143            DateTimeToken::A_D => "A.D.",
144            DateTimeToken::a_d => "a.d.",
145            DateTimeToken::AM => "AM",
146            DateTimeToken::am => "am",
147            DateTimeToken::A_M => "A.M.",
148            DateTimeToken::a_m => "a.m.",
149            DateTimeToken::BC => "BC",
150            DateTimeToken::bc => "bc",
151            DateTimeToken::B_C => "B.C.",
152            DateTimeToken::b_c => "b.c.",
153            DateTimeToken::CC => "CC",
154            DateTimeToken::cc => "cc",
155            DateTimeToken::D => "D",
156            DateTimeToken::d => "d",
157            DateTimeToken::DAY => "DAY",
158            DateTimeToken::Day => "Day",
159            DateTimeToken::day => "day",
160            DateTimeToken::DD => "DD",
161            DateTimeToken::dd => "dd",
162            DateTimeToken::DDD => "DDD",
163            DateTimeToken::ddd => "ddd",
164            DateTimeToken::DY => "DY",
165            DateTimeToken::Dy => "Dy",
166            DateTimeToken::dy => "dy",
167            DateTimeToken::FM => "FM",
168            DateTimeToken::fm => "fm",
169            DateTimeToken::FX => "FX",
170            DateTimeToken::fx => "fx",
171            DateTimeToken::HH => "HH",
172            DateTimeToken::hh => "hh",
173            DateTimeToken::HH12 => "HH12",
174            DateTimeToken::hh12 => "hh12",
175            DateTimeToken::HH24 => "HH24",
176            DateTimeToken::hh24 => "hh24",
177            DateTimeToken::I => "I",
178            DateTimeToken::i => "i",
179            DateTimeToken::ID => "ID",
180            DateTimeToken::id => "id",
181            DateTimeToken::IDDD => "IDDD",
182            DateTimeToken::iddd => "iddd",
183            DateTimeToken::IW => "IW",
184            DateTimeToken::iw => "iw",
185            DateTimeToken::IY => "IY",
186            DateTimeToken::iy => "iy",
187            DateTimeToken::IYY => "IYY",
188            DateTimeToken::iyy => "iyy",
189            DateTimeToken::IYYY => "IYYY",
190            DateTimeToken::iyyy => "iyyy",
191            DateTimeToken::J => "J",
192            DateTimeToken::j => "j",
193            DateTimeToken::MI => "MI",
194            DateTimeToken::mi => "mi",
195            DateTimeToken::MM => "MM",
196            DateTimeToken::mm => "mm",
197            DateTimeToken::MON => "MON",
198            DateTimeToken::Mon => "Mon",
199            DateTimeToken::mon => "mon",
200            DateTimeToken::MONTH => "MONTH",
201            DateTimeToken::Month => "Month",
202            DateTimeToken::month => "month",
203            DateTimeToken::MS => "MS",
204            DateTimeToken::ms => "ms",
205            DateTimeToken::OF => "OF",
206            DateTimeToken::PM => "PM",
207            DateTimeToken::pm => "pm",
208            DateTimeToken::P_M => "P.M.",
209            DateTimeToken::p_m => "p.m.",
210            DateTimeToken::Q => "Q",
211            DateTimeToken::q => "q",
212            DateTimeToken::rm => "rm",
213            DateTimeToken::RM => "RM",
214            DateTimeToken::SS => "ss",
215            DateTimeToken::ss => "SS",
216            DateTimeToken::SSSS => "SSSS",
217            DateTimeToken::ssss => "ssss",
218            DateTimeToken::SSSSS => "SSSSS",
219            DateTimeToken::sssss => "sssss",
220            DateTimeToken::TZ => "TZ",
221            DateTimeToken::tz => "tz",
222            DateTimeToken::TZH => "TZH",
223            DateTimeToken::TZM => "TZM",
224            DateTimeToken::US => "US",
225            DateTimeToken::us => "us",
226            DateTimeToken::W => "W",
227            DateTimeToken::w => "w",
228            DateTimeToken::WW => "ww",
229            DateTimeToken::ww => "WW",
230            DateTimeToken::Y => "Y",
231            DateTimeToken::y => "y",
232            DateTimeToken::Y_YYY => "Y,YYY",
233            DateTimeToken::y_yyy => "y,yyy",
234            DateTimeToken::YY => "YY",
235            DateTimeToken::yy => "yy",
236            DateTimeToken::YYY => "YYY",
237            DateTimeToken::yyy => "yyy",
238            DateTimeToken::YYYY => "YYYY",
239            DateTimeToken::yyyy => "yyyy",
240            DateTimeToken::Quote => "\"",
241            DateTimeToken::EscQuote => "\\\"",
242            DateTimeToken::TH => "TH",
243            DateTimeToken::th => "th",
244        }
245    }
246
247    /// Returns the list of all known patterns, in the same order as the enum
248    /// variants.
249    fn patterns() -> Vec<&'static str> {
250        enum_iterator::all::<Self>().map(|v| v.pattern()).collect()
251    }
252
253    /// Returns the `DateTimeField` associated with this token, if any.
254    ///
255    /// Some tokens do not correspond directly to a field, but instead modify
256    /// other fields.
257    fn field(&self) -> Option<DateTimeField> {
258        use DateTimeToken::*;
259        use WordCaps::*;
260        match self {
261            AD | BC => Some(DateTimeField::Era {
262                dots: false,
263                caps: true,
264            }),
265            ad | bc => Some(DateTimeField::Era {
266                dots: false,
267                caps: false,
268            }),
269            A_D | B_C => Some(DateTimeField::Era {
270                dots: true,
271                caps: true,
272            }),
273            a_d | b_c => Some(DateTimeField::Era {
274                dots: true,
275                caps: false,
276            }),
277            AM | PM => Some(DateTimeField::Meridiem {
278                dots: false,
279                caps: true,
280            }),
281            am | pm => Some(DateTimeField::Meridiem {
282                dots: false,
283                caps: false,
284            }),
285            A_M | P_M => Some(DateTimeField::Meridiem {
286                dots: true,
287                caps: true,
288            }),
289            a_m | p_m => Some(DateTimeField::Meridiem {
290                dots: true,
291                caps: false,
292            }),
293            cc | CC => Some(DateTimeField::Century),
294            d | D => Some(DateTimeField::DayOfWeek),
295            day => Some(DateTimeField::DayName {
296                abbrev: false,
297                caps: NoCaps,
298            }),
299            Day => Some(DateTimeField::DayName {
300                abbrev: false,
301                caps: FirstCaps,
302            }),
303            DAY => Some(DateTimeField::DayName {
304                abbrev: false,
305                caps: AllCaps,
306            }),
307            dy => Some(DateTimeField::DayName {
308                abbrev: true,
309                caps: NoCaps,
310            }),
311            Dy => Some(DateTimeField::DayName {
312                abbrev: true,
313                caps: FirstCaps,
314            }),
315            DY => Some(DateTimeField::DayName {
316                abbrev: true,
317                caps: AllCaps,
318            }),
319            dd | DD => Some(DateTimeField::DayOfMonth),
320            ddd | DDD => Some(DateTimeField::DayOfYear),
321            fm | FM | fx | FX | th | TH | Quote | EscQuote => None,
322            hh | HH | hh12 | HH12 => Some(DateTimeField::Hour12),
323            hh24 | HH24 => Some(DateTimeField::Hour24),
324            id | ID => Some(DateTimeField::IsoDayOfWeek),
325            iddd | IDDD => Some(DateTimeField::IsoDayOfYear),
326            iw | IW => Some(DateTimeField::IsoWeekOfYear),
327            j | J => Some(DateTimeField::JulianDay),
328            mi | MI => Some(DateTimeField::Minute),
329            mm | MM => Some(DateTimeField::MonthOfYear),
330            mon => Some(DateTimeField::MonthName {
331                abbrev: true,
332                caps: NoCaps,
333            }),
334            Mon => Some(DateTimeField::MonthName {
335                abbrev: true,
336                caps: FirstCaps,
337            }),
338            MON => Some(DateTimeField::MonthName {
339                abbrev: true,
340                caps: AllCaps,
341            }),
342            month => Some(DateTimeField::MonthName {
343                abbrev: false,
344                caps: NoCaps,
345            }),
346            Month => Some(DateTimeField::MonthName {
347                abbrev: false,
348                caps: FirstCaps,
349            }),
350            MONTH => Some(DateTimeField::MonthName {
351                abbrev: false,
352                caps: AllCaps,
353            }),
354            ms | MS => Some(DateTimeField::Millisecond),
355            OF => Some(DateTimeField::TimezoneOffset),
356            q | Q => Some(DateTimeField::Quarter),
357            rm => Some(DateTimeField::MonthInRomanNumerals { caps: false }),
358            RM => Some(DateTimeField::MonthInRomanNumerals { caps: true }),
359            ss | SS => Some(DateTimeField::Second),
360            ssss | SSSS | sssss | SSSSS => Some(DateTimeField::SecondsPastMidnight),
361            tz => Some(DateTimeField::Timezone { caps: false }),
362            TZ => Some(DateTimeField::Timezone { caps: true }),
363            TZH => Some(DateTimeField::TimezoneHours),
364            TZM => Some(DateTimeField::TimezoneMinutes),
365            us | US => Some(DateTimeField::Microsecond),
366            w | W => Some(DateTimeField::WeekOfMonth),
367            ww | WW => Some(DateTimeField::WeekOfYear),
368            y | Y => Some(DateTimeField::Year1),
369            yy | YY => Some(DateTimeField::Year2),
370            yyy | YYY => Some(DateTimeField::Year3),
371            yyyy | YYYY => Some(DateTimeField::Year4 { separator: false }),
372            y_yyy | Y_YYY => Some(DateTimeField::Year4 { separator: true }),
373            i | I => Some(DateTimeField::IsoYear1),
374            iy | IY => Some(DateTimeField::IsoYear2),
375            iyy | IYY => Some(DateTimeField::IsoYear3),
376            iyyy | IYYY => Some(DateTimeField::IsoYear4),
377        }
378    }
379
380    /// Returns how this token should be rendered if it appears within quotes.
381    /// This is usually the same string as the `pattern` method returns, but
382    /// not always.
383    fn as_literal(&self) -> &'static str {
384        match self {
385            DateTimeToken::Quote => "",
386            DateTimeToken::EscQuote => "\"",
387            _ => self.pattern(),
388        }
389    }
390
391    /// Returns whether this token is a fill mode toggle.
392    fn is_fill_mode_toggle(&self) -> bool {
393        matches!(self, DateTimeToken::fm | DateTimeToken::FM)
394    }
395
396    /// Returns how this token affects the ordinal mode, if at all.
397    fn ordinal_mode(&self) -> OrdinalMode {
398        match self {
399            DateTimeToken::th => OrdinalMode::Lower,
400            DateTimeToken::TH => OrdinalMode::Upper,
401            _ => OrdinalMode::None,
402        }
403    }
404}
405
406/// Specifies the ordinal suffix that should be attached to numeric fields.
407#[derive(
408    Debug,
409    Eq,
410    PartialEq,
411    PartialOrd,
412    Ord,
413    Copy,
414    Clone,
415    Hash,
416    Serialize,
417    Deserialize
418)]
419enum OrdinalMode {
420    /// No ordinal suffix.
421    None,
422    /// A lowercase ordinal suffix.
423    Lower,
424    /// An uppercase ordinal suffix.
425    Upper,
426}
427
428impl OrdinalMode {
429    fn render(self, out: &mut impl fmt::Write, n: impl Into<i64>) -> Result<(), fmt::Error> {
430        let n = n.into();
431        // Numbers that end in teen always use "th" as their ordinal suffix.
432        // Otherwise the last digit determines the ordinal suffix.
433        let n = match n % 100 {
434            10..=19 => 0,
435            _ => n % 10,
436        };
437        match self {
438            OrdinalMode::None => Ok(()),
439            OrdinalMode::Lower => match n {
440                1 => out.write_str("st"),
441                2 => out.write_str("nd"),
442                3 => out.write_str("rd"),
443                _ => out.write_str("th"),
444            },
445            OrdinalMode::Upper => match n {
446                1 => out.write_str("ST"),
447                2 => out.write_str("ND"),
448                3 => out.write_str("RD"),
449                _ => out.write_str("TH"),
450            },
451        }
452    }
453}
454
455/// Specifies the capitalization of a word.
456#[allow(clippy::enum_variant_names)] // Having "Caps" in the variant names is clarifying.
457#[derive(
458    Debug,
459    Eq,
460    PartialEq,
461    PartialOrd,
462    Ord,
463    Copy,
464    Clone,
465    Hash,
466    Serialize,
467    Deserialize
468)]
469enum WordCaps {
470    /// All of the letters should be capitalized.
471    AllCaps,
472    /// Only the first letter should be capitalized.
473    FirstCaps,
474    /// None of the letters should be capitalized.
475    NoCaps,
476}
477
478/// A date-time field.
479///
480/// The variants are largely self-evident, but are described in detail in the
481/// PostgreSQL documentation if necessary.
482#[derive(
483    Debug,
484    Eq,
485    PartialEq,
486    PartialOrd,
487    Ord,
488    Clone,
489    Hash,
490    Serialize,
491    Deserialize
492)]
493enum DateTimeField {
494    Hour12,
495    Hour24,
496    Minute,
497    Second,
498    Millisecond,
499    Microsecond,
500    SecondsPastMidnight,
501    Meridiem { dots: bool, caps: bool },
502    Year1,
503    Year2,
504    Year3,
505    Year4 { separator: bool },
506    IsoYear1,
507    IsoYear2,
508    IsoYear3,
509    IsoYear4,
510    Era { dots: bool, caps: bool },
511    MonthName { abbrev: bool, caps: WordCaps },
512    MonthOfYear,
513    DayName { abbrev: bool, caps: WordCaps },
514    DayOfWeek,
515    IsoDayOfWeek,
516    DayOfMonth,
517    DayOfYear,
518    IsoDayOfYear,
519    WeekOfMonth,
520    WeekOfYear,
521    IsoWeekOfYear,
522    Century,
523    JulianDay,
524    Quarter,
525    MonthInRomanNumerals { caps: bool },
526    Timezone { caps: bool },
527    TimezoneHours,
528    TimezoneMinutes,
529    TimezoneOffset,
530}
531
532/// An element of a date-time format string.
533#[derive(
534    Clone,
535    Debug,
536    PartialEq,
537    Eq,
538    PartialOrd,
539    Ord,
540    Hash,
541    Serialize,
542    Deserialize
543)]
544enum DateTimeFormatNode {
545    /// A field whose value will be computed from the input timestamp.
546    Field {
547        /// The inner field.
548        field: DateTimeField,
549        /// Whether the field should be padded with spaces to its maximum width.
550        /// Does not have an effect for all fields, as the width of some fields
551        /// is unknowable.
552        fill: bool,
553        /// Whether the field should be followed with an ordinal suffix, like
554        /// "th." Only meaningful for numeric fields.
555        ordinal: OrdinalMode,
556    },
557    /// A literal character.
558    Literal(char),
559}
560
561const WEEKDAYS_ALL_CAPS: [&str; 7] = [
562    "SUNDAY",
563    "MONDAY",
564    "TUESDAY",
565    "WEDNESDAY",
566    "THURSDAY",
567    "FRIDAY",
568    "SATURDAY",
569];
570
571const WEEKDAYS_FIRST_CAPS: [&str; 7] = [
572    "Sunday",
573    "Monday",
574    "Tuesday",
575    "Wednesday",
576    "Thursday",
577    "Friday",
578    "Saturday",
579];
580
581const WEEKDAYS_NO_CAPS: [&str; 7] = [
582    "sunday",
583    "monday",
584    "tuesday",
585    "wednesday",
586    "thursday",
587    "friday",
588    "saturday",
589];
590
591const WEEKDAYS_ABBREV_ALL_CAPS: [&str; 7] = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
592
593const WEEKDAYS_ABBREV_FIRST_CAPS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
594
595const WEEKDAYS_ABBREV_NO_CAPS: [&str; 7] = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"];
596
597const MONTHS_ALL_CAPS: [&str; 12] = [
598    "JANUARY",
599    "FEBRUARY",
600    "MARCH",
601    "APRIL",
602    "MAY",
603    "JUNE",
604    "JULY",
605    "AUGUST",
606    "SEPTEMBER",
607    "OCTOBER",
608    "NOVEMBER",
609    "DECEMBER",
610];
611
612const MONTHS_FIRST_CAPS: [&str; 12] = [
613    "January",
614    "February",
615    "March",
616    "April",
617    "May",
618    "June",
619    "July",
620    "August",
621    "September",
622    "October",
623    "November",
624    "December",
625];
626
627const MONTHS_NO_CAPS: [&str; 12] = [
628    "january",
629    "february",
630    "march",
631    "april",
632    "may",
633    "june",
634    "july",
635    "august",
636    "september",
637    "october",
638    "november",
639    "december",
640];
641
642const MONTHS_ABBREV_ALL_CAPS: [&str; 12] = [
643    "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC",
644];
645
646const MONTHS_ABBREV_FIRST_CAPS: [&str; 12] = [
647    "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
648];
649
650const MONTHS_ABBREV_NO_CAPS: [&str; 12] = [
651    "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec",
652];
653
654const MONTHS_ROMAN_NO_CAPS: [&str; 12] = [
655    "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix", "x", "xi", "xii",
656];
657
658const MONTHS_ROMAN_CAPS: [&str; 12] = [
659    "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII",
660];
661
662impl DateTimeFormatNode {
663    fn render(&self, buf: &mut impl fmt::Write, ts: &impl TimestampLike) -> Result<(), fmt::Error> {
664        use WordCaps::*;
665        match self {
666            DateTimeFormatNode::Literal(ch) => buf.write_char(*ch),
667            DateTimeFormatNode::Field {
668                field,
669                fill,
670                ordinal,
671            } => {
672                macro_rules! write_num {
673                    ($n:expr, $width:expr) => {{
674                        write!(
675                            buf,
676                            "{:0width$}",
677                            $n,
678                            width = if *fill { $width } else { 0 }
679                        )?;
680                        ordinal.render(buf, $n)
681                    }};
682                    ($n:expr) => {
683                        write_num!($n, 0)
684                    };
685                }
686
687                macro_rules! write_str {
688                    ($s:expr, $width:expr) => {{ write!(buf, "{:width$}", $s, width = if *fill { $width } else { 0 }) }};
689                    ($s:expr) => {
690                        write_str!($s, 0)
691                    };
692                }
693
694                match field {
695                    DateTimeField::Era {
696                        dots: false,
697                        caps: true,
698                    } => write_str!(if ts.year_ce().0 { "AD" } else { "BC" }),
699                    DateTimeField::Era {
700                        dots: false,
701                        caps: false,
702                    } => write_str!(if ts.year_ce().0 { "ad" } else { "bc" }),
703                    DateTimeField::Era {
704                        dots: true,
705                        caps: true,
706                    } => write_str!(if ts.year_ce().0 { "A.D." } else { "B.C." }),
707                    DateTimeField::Era {
708                        dots: true,
709                        caps: false,
710                    } => write_str!(if ts.year_ce().0 { "a.d." } else { "b.c." }),
711                    DateTimeField::Meridiem {
712                        dots: false,
713                        caps: true,
714                    } => write_str!(if ts.hour12().0 { "PM" } else { "AM" }),
715                    DateTimeField::Meridiem {
716                        dots: false,
717                        caps: false,
718                    } => write_str!(if ts.hour12().0 { "pm" } else { "am" }),
719                    DateTimeField::Meridiem {
720                        dots: true,
721                        caps: true,
722                    } => write_str!(if ts.hour12().0 { "P.M." } else { "A.M." }),
723                    DateTimeField::Meridiem {
724                        dots: true,
725                        caps: false,
726                    } => write_str!(if ts.hour12().0 { "p.m." } else { "a.m." }),
727                    DateTimeField::Century => {
728                        let n = if ts.year() > 0 {
729                            (ts.year() - 1) / 100 + 1
730                        } else {
731                            ts.year() / 100 - 1
732                        };
733                        write_num!(n, if n >= 0 { 2 } else { 3 })
734                    }
735                    DateTimeField::DayOfWeek => write_num!(ts.weekday().number_from_sunday(), 1),
736                    DateTimeField::IsoDayOfWeek => write_num!(ts.weekday().number_from_monday(), 1),
737                    DateTimeField::DayName {
738                        abbrev: false,
739                        caps: AllCaps,
740                    } => write_str!(WEEKDAYS_ALL_CAPS[ts.weekday0()], 9),
741                    DateTimeField::DayName {
742                        abbrev: false,
743                        caps: FirstCaps,
744                    } => write_str!(WEEKDAYS_FIRST_CAPS[ts.weekday0()], 9),
745                    DateTimeField::DayName {
746                        abbrev: false,
747                        caps: NoCaps,
748                    } => write_str!(WEEKDAYS_NO_CAPS[ts.weekday0()], 9),
749                    DateTimeField::DayName {
750                        abbrev: true,
751                        caps: AllCaps,
752                    } => write_str!(WEEKDAYS_ABBREV_ALL_CAPS[ts.weekday0()]),
753                    DateTimeField::DayName {
754                        abbrev: true,
755                        caps: FirstCaps,
756                    } => write_str!(WEEKDAYS_ABBREV_FIRST_CAPS[ts.weekday0()]),
757                    DateTimeField::DayName {
758                        abbrev: true,
759                        caps: NoCaps,
760                    } => write_str!(WEEKDAYS_ABBREV_NO_CAPS[ts.weekday0()]),
761                    DateTimeField::DayOfMonth => write_num!(ts.day(), 2),
762                    DateTimeField::DayOfYear => write_num!(ts.ordinal(), 3),
763                    DateTimeField::Hour12 => write_num!(ts.hour12().1, 2),
764                    DateTimeField::Hour24 => write_num!(ts.hour(), 2),
765                    DateTimeField::IsoYear1 => write_num!(ts.iso_year_ce() % 10, 1),
766                    DateTimeField::IsoYear2 => write_num!(ts.iso_year_ce() % 100, 2),
767                    DateTimeField::IsoYear3 => write_num!(ts.iso_year_ce() % 1000, 3),
768                    DateTimeField::IsoYear4 => write_num!(ts.iso_year_ce(), 4),
769                    DateTimeField::IsoDayOfYear => write_num!(
770                        ts.iso_week().week0() * 7 + ts.weekday().number_from_monday(),
771                        3
772                    ),
773                    DateTimeField::IsoWeekOfYear => write_num!(ts.iso_week().week(), 2),
774                    DateTimeField::JulianDay => write_num!(ts.num_days_from_ce() + 1_721_425),
775                    DateTimeField::Minute => write_num!(ts.minute(), 2),
776                    DateTimeField::MonthOfYear => write_num!(ts.month(), 2),
777                    DateTimeField::MonthName {
778                        abbrev: true,
779                        caps: AllCaps,
780                    } => write_str!(MONTHS_ABBREV_ALL_CAPS[usize::cast_from(ts.month0())]),
781                    DateTimeField::MonthName {
782                        abbrev: true,
783                        caps: FirstCaps,
784                    } => write_str!(MONTHS_ABBREV_FIRST_CAPS[usize::cast_from(ts.month0())]),
785                    DateTimeField::MonthName {
786                        abbrev: true,
787                        caps: NoCaps,
788                    } => write_str!(MONTHS_ABBREV_NO_CAPS[usize::cast_from(ts.month0())]),
789                    DateTimeField::MonthName {
790                        abbrev: false,
791                        caps: AllCaps,
792                    } => write_str!(MONTHS_ALL_CAPS[usize::cast_from(ts.month0())], 9),
793                    DateTimeField::MonthName {
794                        abbrev: false,
795                        caps: FirstCaps,
796                    } => write_str!(MONTHS_FIRST_CAPS[usize::cast_from(ts.month0())], 9),
797                    DateTimeField::MonthName {
798                        abbrev: false,
799                        caps: NoCaps,
800                    } => write_str!(MONTHS_NO_CAPS[usize::cast_from(ts.month0())], 9),
801                    DateTimeField::Millisecond => write_num!(ts.nanosecond() / 1_000_000, 3),
802                    DateTimeField::Quarter => write_num!(ts.month0() / 3 + 1),
803                    DateTimeField::MonthInRomanNumerals { caps: true } => {
804                        write_str!(MONTHS_ROMAN_CAPS[usize::cast_from(ts.month0())], 4)
805                    }
806                    DateTimeField::MonthInRomanNumerals { caps: false } => {
807                        write_str!(MONTHS_ROMAN_NO_CAPS[usize::cast_from(ts.month0())], 4)
808                    }
809                    DateTimeField::Second => write_num!(ts.second(), 2),
810                    DateTimeField::SecondsPastMidnight => {
811                        write_num!(ts.num_seconds_from_midnight())
812                    }
813                    DateTimeField::Timezone { caps } => write_str!(ts.timezone_name(*caps)),
814                    DateTimeField::TimezoneOffset => write_str!(ts.timezone_offset()),
815                    DateTimeField::TimezoneHours => write_str!(ts.timezone_hours()),
816                    DateTimeField::TimezoneMinutes => write_str!(ts.timezone_minutes()),
817                    DateTimeField::Microsecond => write_num!(ts.nanosecond() / 1_000, 6),
818                    DateTimeField::WeekOfMonth => write_num!(ts.day0() / 7 + 1, 1),
819                    DateTimeField::WeekOfYear => write_num!(ts.ordinal0() / 7 + 1, 2),
820                    DateTimeField::Year1 => write_num!(ts.year_ce().1 % 10, 1),
821                    DateTimeField::Year2 => write_num!(ts.year_ce().1 % 100, 2),
822                    DateTimeField::Year3 => write_num!(ts.year_ce().1 % 1000, 3),
823                    DateTimeField::Year4 { separator: false } => write_num!(ts.year_ce().1, 4),
824                    DateTimeField::Year4 { separator: true } => {
825                        let n = ts.year_ce().1;
826                        write!(buf, "{},{:03}", n / 1000, n % 1000)?;
827                        ordinal.render(buf, n)
828                    }
829                }
830            }
831        }
832    }
833}
834
835/// A compiled date-time format string.
836#[derive(
837    Clone,
838    Debug,
839    PartialEq,
840    Eq,
841    PartialOrd,
842    Ord,
843    Hash,
844    Serialize,
845    Deserialize
846)]
847pub struct DateTimeFormat(Vec<DateTimeFormatNode>);
848
849impl DateTimeFormat {
850    /// Compiles a new `DateTimeFormat` from the input string `s`.
851    pub fn compile(s: &str) -> DateTimeFormat {
852        // The approach here uses the Aho-Corasick string searching algorithm to
853        // repeatedly and efficiently find the next token of interest. Tokens of
854        // interest are typically field specifiers, like "DDDD", or field
855        // modifiers, like "FM". Characters in between tokens of interest are
856        // recorded as literals. We also consider a double quote a token of
857        // interest, as a double quote disables matching of field
858        // specifiers/modifiers until the next double quote.
859
860        struct Match {
861            start: usize,
862            end: usize,
863            token: DateTimeToken,
864        }
865
866        let matcher = AhoCorasickBuilder::new()
867            .match_kind(aho_corasick::MatchKind::LeftmostLongest)
868            .build(DateTimeToken::patterns())
869            .unwrap_or_else(|e| panic!("automaton build error: {e}"));
870
871        let matches: Vec<_> = matcher
872            .find_iter(&s)
873            .map(|m| Match {
874                start: m.start(),
875                end: m.end(),
876                token: DateTimeToken::try_from(
877                    u8::try_from(m.pattern().as_u32()).expect("match index fits in a u8"),
878                )
879                .expect("match pattern missing"),
880            })
881            .collect();
882
883        let mut out = Vec::new();
884        let mut pos = 0;
885        let mut in_quotes = false;
886        for i in 0..matches.len() {
887            // Any characters since the last match are to be taken literally.
888            for c in s[pos..matches[i].start].chars() {
889                if !(in_quotes && c == '\\') {
890                    // Backslash is an escape character inside of quotes.
891                    out.push(DateTimeFormatNode::Literal(c));
892                }
893            }
894
895            if in_quotes {
896                // If we see a format specifier inside of a quoted block, it
897                // is taken literally.
898                for c in matches[i].token.as_literal().chars() {
899                    out.push(DateTimeFormatNode::Literal(c))
900                }
901            } else if let Some(field) = matches[i].token.field() {
902                // We found a format specifier. Look backwards for a fill mode
903                // toggle (fill mode is on by default), and forwards for an
904                // ordinal suffix specifier (default is no ordinal suffix).
905                let fill = i == 0
906                    || matches[i - 1].end != matches[i].start
907                    || !matches[i - 1].token.is_fill_mode_toggle();
908                let ordinal = match matches.get(i + 1) {
909                    Some(m) if m.start == matches[i].end => m.token.ordinal_mode(),
910                    _ => OrdinalMode::None,
911                };
912                out.push(DateTimeFormatNode::Field {
913                    field,
914                    fill,
915                    ordinal,
916                });
917            }
918
919            if matches[i].token == DateTimeToken::Quote {
920                in_quotes = !in_quotes;
921            }
922            pos = matches[i].end;
923        }
924        for c in s[pos..].chars() {
925            out.push(DateTimeFormatNode::Literal(c));
926        }
927        DateTimeFormat(out)
928    }
929
930    /// Renders the format string using the timestamp `ts` as the input. The
931    /// placeholders in the format string will be filled in appropriately
932    /// according to the value of `ts`.
933    pub fn render(&self, ts: &impl TimestampLike) -> String {
934        let mut out = String::new();
935        for node in &self.0 {
936            node.render(&mut out, ts)
937                .expect("rendering to string cannot fail");
938        }
939        out
940    }
941}