Skip to main content

razeri_plugin_contract/
date_format.rs

1// Copyright (C) 2026 Gabriel Shea
2// Licensed under either of the Apache License, Version 2.0 or the MIT
3// license, at your option.
4
5//! **How a source spells a day** — the format a design states, read once and
6//! handed to every engine as a list rather than as text.
7//!
8//! One import arrives with `1/1/2015` in one column and `2015-01-01T00:00:00Z`
9//! in the next, and a warehouse that converts either of them by asking the
10//! engine to guess gets a different answer on every engine and on some of them
11//! a different answer on a different server. So the design says the spelling,
12//! and this is what the spelling is.
13//!
14//! ## Why a list and not a string
15//!
16//! No two engines share a pattern language. `%m/%d/%Y` is `MM/DD/YYYY` on one
17//! family, `MM/dd/yyyy` on another, and a style number on a third. Handing the
18//! authored text through to an engine would mean each engine parsing it again,
19//! each one accepting a slightly different set of directives, and a directive
20//! nobody supports arriving at the database rather than at the door. Reading
21//! it once, here, gives every engine the same list to translate directive by
22//! directive — and gives a directive an engine cannot spell a refusal that
23//! names it.
24//!
25//! ## The two patterns
26//!
27//! Some engines answer nothing for a value they cannot read; some read it
28//! anyway and hand back the wrong day. For the second kind a guard has to be
29//! written beside the parse, and it has to come from the same list, or the
30//! guard and the parse would be two opinions about one spelling.
31//! [`DateFormat::shape_pattern`] is that guard, and
32//! [`DateFormat::capture_pattern`] reads one field back out of the value so a
33//! parse can be checked against what was written.
34
35use std::fmt;
36
37/// One thing a format says, in the strftime family's own vocabulary.
38///
39/// The list is what the family agrees on. A directive outside it is refused
40/// by name when the design is read, rather than being passed to an engine that
41/// would either ignore it or read it as something else.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub enum FormatDirective {
44    /// `%Y` — the year in four digits.
45    Year4,
46    /// `%y` — the year in two digits.
47    Year2,
48    /// `%m` — the month as a number.
49    MonthNumber,
50    /// `%d` — the day of the month, zero-padded or not.
51    DayOfMonth,
52    /// `%e` — the day of the month, space-padded or not.
53    DayOfMonthSpaced,
54    /// `%H` — the hour on a twenty-four hour clock.
55    Hour24,
56    /// `%I` — the hour on a twelve hour clock, read with [`Self::Meridiem`].
57    Hour12,
58    /// `%M` — the minute.
59    Minute,
60    /// `%S` — the second.
61    Second,
62    /// `%f` — the fraction of a second.
63    Fraction,
64    /// `%p` — `AM` or `PM`.
65    Meridiem,
66    /// `%j` — the day of the year.
67    DayOfYear,
68    /// `%b` — the month's name, abbreviated.
69    MonthAbbrev,
70    /// `%B` — the month's name in full.
71    MonthName,
72    /// `%a` — the weekday's name, abbreviated.
73    WeekdayAbbrev,
74    /// `%A` — the weekday's name in full.
75    WeekdayName,
76    /// `%z` — the offset from UTC, as `+0000` or `+00:00`.
77    Offset,
78    /// `%Z` — the time zone's name.
79    ZoneName,
80}
81
82impl FormatDirective {
83    /// The directive as a format writes it, `%` and all.
84    #[must_use]
85    pub fn spelled(self) -> &'static str {
86        match self {
87            Self::Year4 => "%Y",
88            Self::Year2 => "%y",
89            Self::MonthNumber => "%m",
90            Self::DayOfMonth => "%d",
91            Self::DayOfMonthSpaced => "%e",
92            Self::Hour24 => "%H",
93            Self::Hour12 => "%I",
94            Self::Minute => "%M",
95            Self::Second => "%S",
96            Self::Fraction => "%f",
97            Self::Meridiem => "%p",
98            Self::DayOfYear => "%j",
99            Self::MonthAbbrev => "%b",
100            Self::MonthName => "%B",
101            Self::WeekdayAbbrev => "%a",
102            Self::WeekdayName => "%A",
103            Self::Offset => "%z",
104            Self::ZoneName => "%Z",
105        }
106    }
107
108    /// What this directive is, in the words a refusal uses.
109    #[must_use]
110    pub fn means(self) -> &'static str {
111        match self {
112            Self::Year4 => "a four-digit year",
113            Self::Year2 => "a two-digit year",
114            Self::MonthNumber => "the month as a number",
115            Self::DayOfMonth | Self::DayOfMonthSpaced => "the day of the month",
116            Self::Hour24 => "the hour on a twenty-four hour clock",
117            Self::Hour12 => "the hour on a twelve hour clock",
118            Self::Minute => "the minute",
119            Self::Second => "the second",
120            Self::Fraction => "the fraction of a second",
121            Self::Meridiem => "AM or PM",
122            Self::DayOfYear => "the day of the year",
123            Self::MonthAbbrev => "the month's name, abbreviated",
124            Self::MonthName => "the month's name in full",
125            Self::WeekdayAbbrev => "the weekday's name, abbreviated",
126            Self::WeekdayName => "the weekday's name in full",
127            Self::Offset => "the offset from UTC",
128            Self::ZoneName => "the time zone's name",
129        }
130    }
131
132    /// **True where this directive says something about a moment within a
133    /// day** — an offset or a zone name, which a column holding a day alone
134    /// has nowhere to put.
135    #[must_use]
136    pub fn names_a_zone(self) -> bool {
137        matches!(self, Self::Offset | Self::ZoneName)
138    }
139
140    /// The directive one letter stands for, or `None` for a letter the family
141    /// does not use.
142    fn from_letter(letter: char) -> Option<Self> {
143        Some(match letter {
144            'Y' => Self::Year4,
145            'y' => Self::Year2,
146            'm' => Self::MonthNumber,
147            'd' => Self::DayOfMonth,
148            'e' => Self::DayOfMonthSpaced,
149            'H' => Self::Hour24,
150            'I' => Self::Hour12,
151            'M' => Self::Minute,
152            'S' => Self::Second,
153            'f' => Self::Fraction,
154            'p' => Self::Meridiem,
155            'j' => Self::DayOfYear,
156            'b' => Self::MonthAbbrev,
157            'B' => Self::MonthName,
158            'a' => Self::WeekdayAbbrev,
159            'A' => Self::WeekdayName,
160            'z' => Self::Offset,
161            'Z' => Self::ZoneName,
162            _ => return None,
163        })
164    }
165
166    /// What a value spelled this way looks like, as a POSIX regular
167    /// expression that holds each field to its own range. Grouped, because
168    /// several of them are alternations; the group is never read back.
169    fn shape(self) -> &'static str {
170        match self {
171            Self::Year4 => "([0-9]{4})",
172            Self::Year2 => "([0-9]{2})",
173            Self::MonthNumber | Self::Hour12 => "(0?[1-9]|1[0-2])",
174            Self::DayOfMonth => "(0?[1-9]|[12][0-9]|3[01])",
175            Self::DayOfMonthSpaced => "( ?[1-9]|[12][0-9]|3[01])",
176            Self::Hour24 => "([01]?[0-9]|2[0-3])",
177            Self::Minute | Self::Second => "([0-5]?[0-9])",
178            Self::Fraction => "([0-9]{1,9})",
179            Self::Meridiem => "([AaPp][Mm])",
180            Self::DayOfYear => "(0{0,2}[1-9]|0?[1-9][0-9]|[12][0-9]{2}|3[0-5][0-9]|36[0-6])",
181            // **The names, written out.** A shape that accepted any run of
182            // letters would hand `Xyz` to a reader that raises on a month it
183            // does not know, which is the one thing a guard exists to stop.
184            // Written in both cases per letter because the engines reading
185            // this pattern match case as it stands.
186            Self::MonthAbbrev => {
187                "([Jj][Aa][Nn]|[Ff][Ee][Bb]|[Mm][Aa][Rr]|[Aa][Pp][Rr]|[Mm][Aa][Yy]|[Jj][Uu][Nn]|[Jj][Uu][Ll]|[Aa][Uu][Gg]|[Ss][Ee][Pp]|[Oo][Cc][Tt]|[Nn][Oo][Vv]|[Dd][Ee][Cc])"
188            }
189            Self::MonthName => {
190                "([Jj][Aa][Nn][Uu][Aa][Rr][Yy]|[Ff][Ee][Bb][Rr][Uu][Aa][Rr][Yy]|[Mm][Aa][Rr][Cc][Hh]|[Aa][Pp][Rr][Ii][Ll]|[Mm][Aa][Yy]|[Jj][Uu][Nn][Ee]|[Jj][Uu][Ll][Yy]|[Aa][Uu][Gg][Uu][Ss][Tt]|[Ss][Ee][Pp][Tt][Ee][Mm][Bb][Ee][Rr]|[Oo][Cc][Tt][Oo][Bb][Ee][Rr]|[Nn][Oo][Vv][Ee][Mm][Bb][Ee][Rr]|[Dd][Ee][Cc][Ee][Mm][Bb][Ee][Rr])"
191            }
192            Self::WeekdayAbbrev => {
193                "([Mm][Oo][Nn]|[Tt][Uu][Ee]|[Ww][Ee][Dd]|[Tt][Hh][Uu]|[Ff][Rr][Ii]|[Ss][Aa][Tt]|[Ss][Uu][Nn])"
194            }
195            Self::WeekdayName => {
196                "([Mm][Oo][Nn][Dd][Aa][Yy]|[Tt][Uu][Ee][Ss][Dd][Aa][Yy]|[Ww][Ee][Dd][Nn][Ee][Ss][Dd][Aa][Yy]|[Tt][Hh][Uu][Rr][Ss][Dd][Aa][Yy]|[Ff][Rr][Ii][Dd][Aa][Yy]|[Ss][Aa][Tt][Uu][Rr][Dd][Aa][Yy]|[Ss][Uu][Nn][Dd][Aa][Yy])"
197            }
198            Self::Offset => "([+-][0-9]{2}:?[0-9]{2}|[Zz])",
199            Self::ZoneName => "([A-Za-z_/+-]+)",
200        }
201    }
202
203    /// The loosest thing this directive can stand for, written without a
204    /// group so that a pattern reading one field back has exactly one.
205    fn loose(self) -> &'static str {
206        match self {
207            Self::Year4 => "[0-9]{4}",
208            Self::Year2 => "[0-9]{2}",
209            Self::MonthNumber
210            | Self::DayOfMonth
211            | Self::Hour24
212            | Self::Hour12
213            | Self::Minute
214            | Self::Second => "[0-9]{1,2}",
215            Self::DayOfMonthSpaced => "[ 0-9]{1,2}",
216            Self::Fraction => "[0-9]{1,9}",
217            Self::Meridiem => "[AaPp][Mm]",
218            Self::DayOfYear => "[0-9]{1,3}",
219            Self::MonthAbbrev | Self::MonthName | Self::WeekdayAbbrev | Self::WeekdayName => {
220                "[A-Za-z]+"
221            }
222            Self::Offset => "[Zz+:0-9-]{1,6}",
223            Self::ZoneName => "[A-Za-z_/+-]+",
224        }
225    }
226}
227
228impl fmt::Display for FormatDirective {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        f.write_str(self.spelled())
231    }
232}
233
234/// One piece of a format: something the source writes literally, or one of the
235/// things the format says.
236#[derive(Debug, Clone, PartialEq, Eq)]
237pub enum FormatPart {
238    /// Text the value carries as it stands — a slash, a dash, a space, a `T`.
239    Literal(String),
240    /// One of the values the format reads.
241    Directive(FormatDirective),
242}
243
244/// What is wrong with a format, in the words the refusal uses.
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub enum DateFormatError {
247    /// A `%` followed by a letter no format directive uses.
248    UnknownDirective(char),
249    /// A `%` with nothing after it.
250    DanglingPercent,
251    /// A format saying nothing at all.
252    Empty,
253}
254
255impl fmt::Display for DateFormatError {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        match self {
258            Self::UnknownDirective(letter) => write!(
259                f,
260                "`%{letter}` is not something a format can say. The directives are `%Y` `%y` \
261                 `%m` `%d` `%e` `%H` `%I` `%M` `%S` `%f` `%p` `%j` `%b` `%B` `%a` `%A` `%z` `%Z` \
262                 and `%%` for a literal percent sign; everything else in a format is written the \
263                 way the value writes it"
264            ),
265            Self::DanglingPercent => f.write_str(
266                "the format ends in a `%` with nothing after it. Name what comes there, or write \
267                 `%%` for a literal percent sign",
268            ),
269            Self::Empty => f.write_str(
270                "the format is empty. Write how the source spells the value — `%m/%d/%Y` for \
271                 `1/1/2015`, `%Y-%m-%d` for `2015-01-01`",
272            ),
273        }
274    }
275}
276
277impl std::error::Error for DateFormatError {}
278
279/// **A source's spelling, read once.**
280///
281/// Built from the authored text and never rebuilt: every engine translates the
282/// same list, so two engines cannot disagree about what the design said.
283#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct DateFormat {
285    spelled: String,
286    parts: Vec<FormatPart>,
287}
288
289impl DateFormat {
290    /// Read a format, or say what is wrong with it.
291    ///
292    /// # Errors
293    ///
294    /// A `%` followed by a letter outside the family, a `%` at the end with
295    /// nothing after it, or nothing at all.
296    pub fn parse(spelled: &str) -> Result<Self, DateFormatError> {
297        if spelled.is_empty() {
298            return Err(DateFormatError::Empty);
299        }
300        let mut parts: Vec<FormatPart> = Vec::new();
301        let mut literal = String::new();
302        let mut letters = spelled.chars();
303        while let Some(character) = letters.next() {
304            if character != '%' {
305                literal.push(character);
306                continue;
307            }
308            let Some(next) = letters.next() else {
309                return Err(DateFormatError::DanglingPercent);
310            };
311            if next == '%' {
312                literal.push('%');
313                continue;
314            }
315            let Some(directive) = FormatDirective::from_letter(next) else {
316                return Err(DateFormatError::UnknownDirective(next));
317            };
318            if !literal.is_empty() {
319                parts.push(FormatPart::Literal(std::mem::take(&mut literal)));
320            }
321            parts.push(FormatPart::Directive(directive));
322        }
323        if !literal.is_empty() {
324            parts.push(FormatPart::Literal(literal));
325        }
326        Ok(Self { spelled: spelled.to_owned(), parts })
327    }
328
329    /// The format as the design wrote it — what a refusal quotes back.
330    #[must_use]
331    pub fn spelled(&self) -> &str {
332        &self.spelled
333    }
334
335    /// The pieces, in the order the value writes them.
336    #[must_use]
337    pub fn parts(&self) -> &[FormatPart] {
338        &self.parts
339    }
340
341    /// Every directive the format says, in order, literals passed over.
342    pub fn directives(&self) -> impl Iterator<Item = FormatDirective> + '_ {
343        self.parts.iter().filter_map(|part| match part {
344            FormatPart::Directive(directive) => Some(*directive),
345            FormatPart::Literal(_) => None,
346        })
347    }
348
349    /// **True where the format carries an offset or a zone name** — the two
350    /// things a column holding a day alone has nowhere to put.
351    #[must_use]
352    pub fn names_a_zone(&self) -> bool {
353        self.directives().any(FormatDirective::names_a_zone)
354    }
355
356    /// **True where the format says anything about the time within a day.**
357    #[must_use]
358    pub fn says_a_time_of_day(&self) -> bool {
359        self.directives().any(|directive| {
360            matches!(
361                directive,
362                FormatDirective::Hour24
363                    | FormatDirective::Hour12
364                    | FormatDirective::Minute
365                    | FormatDirective::Second
366                    | FormatDirective::Fraction
367                    | FormatDirective::Meridiem
368            )
369        })
370    }
371
372    /// **Translate the format into one engine's own pattern language**,
373    /// directive by directive.
374    ///
375    /// `spell` answers with this engine's token for one directive, or `None`
376    /// where the engine has no way to say it; `literal` writes a run of
377    /// literal text the way this engine's patterns want it, which on some is
378    /// the text as it stands and on others is the text in quotes.
379    ///
380    /// # Errors
381    ///
382    /// The first directive `spell` answered `None` for — which is what the
383    /// refusal names, so an author is told which part of their format this
384    /// engine cannot read rather than that the format failed.
385    pub fn translate(
386        &self,
387        spell: &dyn Fn(FormatDirective) -> Option<&'static str>,
388        literal: &dyn Fn(&str) -> String,
389    ) -> Result<String, FormatDirective> {
390        let mut pattern = String::new();
391        for part in &self.parts {
392            match part {
393                FormatPart::Literal(text) => pattern.push_str(&literal(text)),
394                FormatPart::Directive(directive) => {
395                    pattern.push_str(spell(*directive).ok_or(*directive)?);
396                }
397            }
398        }
399        Ok(pattern)
400    }
401
402    /// **A guard against a value spelled some other way**, as an anchored
403    /// POSIX regular expression that holds every field to its own range.
404    ///
405    /// It is for the engines that read a value they cannot read: hand
406    /// `13/45/2015` to a parse that rolls fields over and it answers a day in
407    /// 2016 rather than answering nothing. The guard is built from the same
408    /// list the parse is, so the two cannot disagree about what the design
409    /// said.
410    ///
411    /// **What it holds and what it does not.** Every field is held to its own
412    /// range — a month is 1 to 12, an hour is 0 to 23 — so a value written
413    /// some other way, or holding a number no calendar has, is caught. A day
414    /// that is in range and yet not in its month, the thirty-first of a
415    /// thirty-day month, is not a question a shape can answer; the engines
416    /// that need this guard say in their own rendering what they do about it.
417    #[must_use]
418    pub fn shape_pattern(&self) -> String {
419        let mut pattern = String::from("^");
420        for part in &self.parts {
421            match part {
422                FormatPart::Literal(text) => pattern.push_str(&escape_posix(text)),
423                FormatPart::Directive(directive) => pattern.push_str(directive.shape()),
424            }
425        }
426        pattern.push('$');
427        pattern
428    }
429
430    /// **Read one field back out of the value**, as an anchored POSIX regular
431    /// expression with exactly one capturing group around the field asked for.
432    ///
433    /// Everything else in it is loose and ungrouped, so the group an engine
434    /// reads back is the first one and the only one — which is the most any
435    /// of the engines needing it can address.
436    ///
437    /// `None` where the format does not say that field at all.
438    #[must_use]
439    pub fn capture_pattern(&self, wanted: FormatDirective) -> Option<String> {
440        let mut found = false;
441        let mut pattern = String::from("^");
442        for part in &self.parts {
443            match part {
444                FormatPart::Literal(text) => pattern.push_str(&escape_posix(text)),
445                FormatPart::Directive(directive) if *directive == wanted && !found => {
446                    found = true;
447                    pattern.push('(');
448                    pattern.push_str(directive.loose());
449                    pattern.push(')');
450                }
451                FormatPart::Directive(directive) => pattern.push_str(directive.loose()),
452            }
453        }
454        pattern.push('$');
455        found.then_some(pattern)
456    }
457}
458
459impl fmt::Display for DateFormat {
460    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
461        f.write_str(&self.spelled)
462    }
463}
464
465/// Write literal text so a regular expression reads it as itself.
466fn escape_posix(text: &str) -> String {
467    let mut escaped = String::with_capacity(text.len());
468    for character in text.chars() {
469        if "\\^$.[]|()*+?{}".contains(character) {
470            escaped.push('\\');
471        }
472        escaped.push(character);
473    }
474    escaped
475}
476
477#[cfg(test)]
478mod tests {
479    use super::{DateFormat, DateFormatError, FormatDirective, FormatPart};
480
481    /// The retailer's own spelling, read into the three things it says and the
482    /// two slashes between them.
483    #[test]
484    fn a_day_spelled_with_slashes_reads_as_five_pieces() {
485        let format = DateFormat::parse("%m/%d/%Y").expect("a lawful format");
486        assert_eq!(
487            format.parts(),
488            [
489                FormatPart::Directive(FormatDirective::MonthNumber),
490                FormatPart::Literal("/".into()),
491                FormatPart::Directive(FormatDirective::DayOfMonth),
492                FormatPart::Literal("/".into()),
493                FormatPart::Directive(FormatDirective::Year4),
494            ]
495        );
496        assert_eq!(format.spelled(), "%m/%d/%Y");
497        assert!(!format.says_a_time_of_day());
498        assert!(!format.names_a_zone());
499    }
500
501    /// Every directive the family has is read, and each one keeps its letter.
502    #[test]
503    fn every_directive_in_the_family_is_read() {
504        let every = "%Y %y %m %d %e %H %I %M %S %f %p %j %b %B %a %A %z %Z";
505        let format = DateFormat::parse(every).expect("a lawful format");
506        let read: Vec<_> = format.directives().map(FormatDirective::spelled).collect();
507        assert_eq!(
508            read,
509            [
510                "%Y", "%y", "%m", "%d", "%e", "%H", "%I", "%M", "%S", "%f", "%p", "%j", "%b", "%B",
511                "%a", "%A", "%z", "%Z"
512            ]
513        );
514        assert!(format.names_a_zone());
515        assert!(format.says_a_time_of_day());
516    }
517
518    /// A doubled percent sign is one literal percent sign, and says nothing.
519    #[test]
520    fn a_doubled_percent_is_a_literal_one() {
521        let format = DateFormat::parse("%d%%%m").expect("a lawful format");
522        assert_eq!(
523            format.parts(),
524            [
525                FormatPart::Directive(FormatDirective::DayOfMonth),
526                FormatPart::Literal("%".into()),
527                FormatPart::Directive(FormatDirective::MonthNumber),
528            ]
529        );
530    }
531
532    /// A letter the family does not use is refused by name rather than passed
533    /// through to an engine that would read it as something else.
534    #[test]
535    fn a_letter_the_family_does_not_use_is_refused_by_name() {
536        assert_eq!(DateFormat::parse("%Q-%m"), Err(DateFormatError::UnknownDirective('Q')));
537        assert!(DateFormat::parse("%Q").unwrap_err().to_string().contains("`%Q`"));
538    }
539
540    /// A trailing percent sign says nothing and is refused as itself.
541    #[test]
542    fn a_trailing_percent_is_refused() {
543        assert_eq!(DateFormat::parse("%Y-%m-%d %"), Err(DateFormatError::DanglingPercent));
544        assert_eq!(DateFormat::parse(""), Err(DateFormatError::Empty));
545    }
546
547    /// A translation is directive by directive, and the first directive an
548    /// engine cannot say is what comes back.
549    #[test]
550    fn a_translation_names_the_directive_the_engine_cannot_say() {
551        let format = DateFormat::parse("%Y-%m-%dT%H:%M:%S%z").expect("a lawful format");
552        let spell = |directive: FormatDirective| match directive {
553            FormatDirective::Year4 => Some("yyyy"),
554            FormatDirective::MonthNumber => Some("MM"),
555            FormatDirective::DayOfMonth => Some("dd"),
556            FormatDirective::Hour24 => Some("HH"),
557            FormatDirective::Minute => Some("mm"),
558            FormatDirective::Second => Some("ss"),
559            _ => None,
560        };
561        assert_eq!(
562            format.translate(&spell, &|text| format!("'{text}'")),
563            Err(FormatDirective::Offset)
564        );
565        let without = DateFormat::parse("%Y-%m-%d").expect("a lawful format");
566        assert_eq!(
567            without.translate(&spell, &|text: &str| text.to_owned()),
568            Ok("yyyy-MM-dd".to_owned())
569        );
570    }
571
572    /// The guard holds each field to its own range, and the field it reads
573    /// back is the only group in its own pattern.
574    #[test]
575    fn the_guard_and_the_read_back_come_from_one_list() {
576        let format = DateFormat::parse("%m/%d/%Y").expect("a lawful format");
577        assert_eq!(
578            format.shape_pattern(),
579            "^(0?[1-9]|1[0-2])/(0?[1-9]|[12][0-9]|3[01])/([0-9]{4})$"
580        );
581        assert_eq!(
582            format.capture_pattern(FormatDirective::DayOfMonth).as_deref(),
583            Some("^[0-9]{1,2}/([0-9]{1,2})/[0-9]{4}$")
584        );
585        assert_eq!(format.capture_pattern(FormatDirective::Hour24), None);
586    }
587
588    /// A literal that would mean something to a regular expression is written
589    /// so that it means itself.
590    #[test]
591    fn a_literal_dot_means_a_dot() {
592        let format = DateFormat::parse("%d.%m.%Y").expect("a lawful format");
593        assert!(format.shape_pattern().contains("\\."));
594    }
595}