jiff/fmt/strtime/mod.rs
1/*!
2Support for "printf"-style parsing and formatting.
3
4While the routines exposed in this module very closely resemble the
5corresponding [`strptime`] and [`strftime`] POSIX functions, it is not a goal
6for the formatting machinery to precisely match POSIX semantics.
7
8If there is a conversion specifier you need that Jiff doesn't support, please
9[create a new issue][create-issue].
10
11The formatting and parsing in this module does not currently support any
12form of localization. Please see [this issue][locale] about the topic of
13localization in Jiff.
14
15[create-issue]: https://github.com/BurntSushi/jiff/issues/new
16[locale]: https://github.com/BurntSushi/jiff/issues/4
17
18# Example
19
20This shows how to parse a civil date and its weekday:
21
22```
23use jiff::civil::Date;
24
25let date = Date::strptime("%Y-%m-%d is a %A", "2024-07-15 is a Monday")?;
26assert_eq!(date.to_string(), "2024-07-15");
27// Leading zeros are optional for numbers in all cases:
28let date = Date::strptime("%Y-%m-%d is a %A", "2024-07-15 is a Monday")?;
29assert_eq!(date.to_string(), "2024-07-15");
30// Parsing does error checking! 2024-07-15 was not a Tuesday.
31assert!(Date::strptime("%Y-%m-%d is a %A", "2024-07-15 is a Tuesday").is_err());
32
33# Ok::<(), Box<dyn std::error::Error>>(())
34```
35
36And this shows how to format a zoned datetime with a time zone abbreviation:
37
38```
39use jiff::civil::date;
40
41let zdt = date(2024, 7, 15).at(17, 30, 59, 0).in_tz("Australia/Tasmania")?;
42// %-I instead of %I means no padding.
43let string = zdt.strftime("%A, %B %d, %Y at %-I:%M%P %Z").to_string();
44assert_eq!(string, "Monday, July 15, 2024 at 5:30pm AEST");
45
46# Ok::<(), Box<dyn std::error::Error>>(())
47```
48
49Or parse a zoned datetime with an IANA time zone identifier:
50
51```
52use jiff::{civil::date, Zoned};
53
54let zdt = Zoned::strptime(
55 "%A, %B %d, %Y at %-I:%M%P %:Q",
56 "Monday, July 15, 2024 at 5:30pm Australia/Tasmania",
57)?;
58assert_eq!(
59 zdt,
60 date(2024, 7, 15).at(17, 30, 0, 0).in_tz("Australia/Tasmania")?,
61);
62
63# Ok::<(), Box<dyn std::error::Error>>(())
64```
65
66# Usage
67
68For most cases, you can use the `strptime` and `strftime` methods on the
69corresponding datetime type. For example, [`Zoned::strptime`] and
70[`Zoned::strftime`]. However, the [`BrokenDownTime`] type in this module
71provides a little more control.
72
73For example, assuming `t` is a `civil::Time`, then
74`t.strftime("%Y").to_string()` will actually panic because a `civil::Time` does
75not have a year. While the underlying formatting machinery actually returns
76an error, this error gets turned into a panic by virtue of going through the
77`std::fmt::Display` and `std::string::ToString` APIs.
78
79In contrast, [`BrokenDownTime::format`] (or just [`format`](format())) can
80report the error to you without any panicking:
81
82```
83use jiff::{civil::time, fmt::strtime};
84
85let t = time(23, 59, 59, 0);
86assert_eq!(
87 strtime::format("%Y", t).unwrap_err().to_string(),
88 "strftime formatting failed: %Y failed: requires date to format",
89);
90```
91
92# Advice
93
94The formatting machinery supported by this module is not especially expressive.
95The pattern language is a simple sequence of conversion specifiers interspersed
96by literals and arbitrary whitespace. This means that you sometimes need
97delimiters or spaces between components. For example, this is fine:
98
99```
100use jiff::fmt::strtime;
101
102let date = strtime::parse("%Y%m%d", "20240715")?.to_date()?;
103assert_eq!(date.to_string(), "2024-07-15");
104# Ok::<(), Box<dyn std::error::Error>>(())
105```
106
107But this is ambiguous (is the year `999` or `9990`?):
108
109```
110use jiff::fmt::strtime;
111
112assert!(strtime::parse("%Y%m%d", "9990715").is_err());
113```
114
115In this case, since years greedily consume up to 4 digits by default, `9990`
116is parsed as the year. And since months greedily consume up to 2 digits by
117default, `71` is parsed as the month, which results in an invalid day. If you
118expect your datetimes to always use 4 digits for the year, then it might be
119okay to skip on the delimiters. For example, the year `999` could be written
120with a leading zero:
121
122```
123use jiff::fmt::strtime;
124
125let date = strtime::parse("%Y%m%d", "09990715")?.to_date()?;
126assert_eq!(date.to_string(), "0999-07-15");
127// Indeed, the leading zero is written by default when
128// formatting, since years are padded out to 4 digits
129// by default:
130assert_eq!(date.strftime("%Y%m%d").to_string(), "09990715");
131
132# Ok::<(), Box<dyn std::error::Error>>(())
133```
134
135The main advice here is that these APIs can come in handy for ad hoc tasks that
136would otherwise be annoying to deal with. For example, I once wrote a tool to
137extract data from an XML dump of my SMS messages, and one of the date formats
138used was `Apr 1, 2022 20:46:15`. That doesn't correspond to any standard, and
139while parsing it with a regex isn't that difficult, it's pretty annoying,
140especially because of the English abbreviated month name. That's exactly the
141kind of use case where this module shines.
142
143If the formatting machinery in this module isn't flexible enough for your use
144case and you don't control the format, it is recommended to write a bespoke
145parser (possibly with regex). It is unlikely that the expressiveness of this
146formatting machinery will be improved much. (Although it is plausible to add
147new conversion specifiers.)
148
149# Conversion specifications
150
151This table lists the complete set of conversion specifiers supported in the
152format. While most conversion specifiers are supported as is in both parsing
153and formatting, there are some differences. Where differences occur, they are
154noted in the table below.
155
156When parsing, and whenever a conversion specifier matches an enumeration of
157strings, the strings are matched without regard to ASCII case.
158
159| Specifier | Example | Description |
160| --------- | ------- | ----------- |
161| `%%` | `%%` | A literal `%`. |
162| `%A`, `%a` | `Sunday`, `Sun` | The full and abbreviated weekday, respectively. |
163| `%B`, `%b`, `%h` | `June`, `Jun`, `Jun` | The full and abbreviated month name, respectively. |
164| `%C` | `20` | The century of the year. No padding. |
165| `%c` | `2024 M07 14, Sun 17:31:59` | The date and clock time via [`Custom`]. Supported when formatting only. |
166| `%D` | `7/14/24` | Equivalent to `%m/%d/%y`. |
167| `%d`, `%e` | `25`, ` 5` | The day of the month. `%d` is zero-padded, `%e` is space padded. |
168| `%F` | `2024-07-14` | Equivalent to `%Y-%m-%d`. |
169| `%f` | `000456` | Fractional seconds, up to nanosecond precision. |
170| `%.f` | `.000456` | Optional fractional seconds, with dot, up to nanosecond precision. |
171| `%G` | `2024` | An [ISO 8601 week-based] year. Zero padded to 4 digits. |
172| `%g` | `24` | A two-digit [ISO 8601 week-based] year. Represents only 1969-2068. Zero padded. |
173| `%H` | `23` | The hour in a 24 hour clock. Zero padded. |
174| `%I` | `11` | The hour in a 12 hour clock. Zero padded. |
175| `%j` | `060` | The day of the year. Range is `1..=366`. Zero padded to 3 digits. |
176| `%k` | `15` | The hour in a 24 hour clock. Space padded. |
177| `%l` | ` 3` | The hour in a 12 hour clock. Space padded. |
178| `%M` | `04` | The minute. Zero padded. |
179| `%m` | `01` | The month. Zero padded. |
180| `%N` | `123456000` | Fractional seconds, up to nanosecond precision. Alias for `%9f`. |
181| `%n` | `\n` | Formats as a newline character. Parses arbitrary whitespace. |
182| `%P` | `am` | Whether the time is in the AM or PM, lowercase. |
183| `%p` | `PM` | Whether the time is in the AM or PM, uppercase. |
184| `%Q` | `America/New_York`, `+0530` | An IANA time zone identifier, or `%z` if one doesn't exist. |
185| `%:Q` | `America/New_York`, `+05:30` | An IANA time zone identifier, or `%:z` if one doesn't exist. |
186| `%q` | `4` | The quarter of the year. Supported when formatting only. |
187| `%R` | `23:30` | Equivalent to `%H:%M`. |
188| `%r` | `8:30:00 AM` | The 12-hour clock time via [`Custom`]. Supported when formatting only. |
189| `%S` | `59` | The second. Zero padded. |
190| `%s` | `1737396540` | A Unix timestamp, in seconds. |
191| `%T` | `23:30:59` | Equivalent to `%H:%M:%S`. |
192| `%t` | `\t` | Formats as a tab character. Parses arbitrary whitespace. |
193| `%U` | `03` | Week number. Week 1 is the first week starting with a Sunday. Zero padded. |
194| `%u` | `7` | The day of the week beginning with Monday at `1`. |
195| `%V` | `05` | Week number in the [ISO 8601 week-based] calendar. Zero padded. |
196| `%W` | `03` | Week number. Week 1 is the first week starting with a Monday. Zero padded. |
197| `%w` | `0` | The day of the week beginning with Sunday at `0`. |
198| `%X` | `17:31:59` | The clock time via [`Custom`]. Supported when formatting only. |
199| `%x` | `2024 M07 14` | The date via [`Custom`]. Supported when formatting only. |
200| `%Y` | `2024` | A full year, including century. Zero padded to 4 digits. |
201| `%y` | `24` | A two-digit year. Represents only 1969-2068. Zero padded. |
202| `%Z` | `EDT` | A time zone abbreviation. Supported when formatting only. |
203| `%z` | `+0530` | A time zone offset in the format `[+-]HHMM[SS]`. |
204| `%:z` | `+05:30` | A time zone offset in the format `[+-]HH:MM[:SS]`. |
205| `%::z` | `+05:30:00` | A time zone offset in the format `[+-]HH:MM:SS`. |
206| `%:::z` | `-04`, `+05:30` | A time zone offset in the format `[+-]HH:[MM[:SS]]`. |
207
208When formatting, the following flags can be inserted immediately after the `%`
209and before the directive:
210
211* `_` - Pad a numeric result to the left with spaces.
212* `-` - Do not pad a numeric result.
213* `0` - Pad a numeric result to the left with zeros.
214* `^` - Use alphabetic uppercase for all relevant strings.
215* `#` - Swap the case of the result string. This is typically only useful with
216`%p` or `%Z`, since they are the only conversion specifiers that emit strings
217entirely in uppercase by default.
218
219The above flags override the "default" settings of a specifier. For example,
220`%_d` pads with spaces instead of zeros, and `%0e` pads with zeros instead of
221spaces. The exceptions are the locale (`%c`, `%r`, `%X`, `%x`), and time zone
222(`%z`, `%:z`) specifiers. They are unaffected by any flags.
223
224Moreover, any number of decimal digits can be inserted after the (possibly
225absent) flag and before the directive, so long as the parsed number is less
226than 256. The number formed by these digits will correspond to the minimum
227amount of padding (to the left). Note that padding is clamped to a maximum of
228`20`.
229
230The flags and padding amount above may be used when parsing as well. Most
231settings are ignored during parsing except for padding. For example, if one
232wanted to parse `003` as the day `3`, then one should use `%03d`. Otherwise, by
233default, `%d` will only try to consume at most 2 digits.
234
235The `%f` and `%.f` flags also support specifying the precision, up to
236nanoseconds. For example, `%3f` and `%.3f` will both always print a fractional
237second component to exactly 3 decimal places. When no precision is specified,
238then `%f` will always emit at least one digit, even if it's zero. But `%.f`
239will emit the empty string when the fractional component is zero. Otherwise, it
240will include the leading `.`. For parsing, `%f` does not include the leading
241dot, but `%.f` does. Note that all of the options above are still parsed for
242`%f` and `%.f`, but they are all no-ops (except for the padding for `%f`, which
243is instead interpreted as a precision setting). When using a precision setting,
244truncation is used. If you need a different rounding mode, you should use
245higher level APIs like [`Timestamp::round`] or [`Zoned::round`].
246
247# Conditionally unsupported
248
249Jiff does not support `%Q` or `%:Q` (IANA time zone identifier) when the
250`alloc` crate feature is not enabled. This is because a time zone identifier
251is variable width data. If you have a use case for this, please
252[detail it in a new issue](https://github.com/BurntSushi/jiff/issues/new).
253
254# Unsupported
255
256The following things are currently unsupported:
257
258* Parsing or formatting fractional seconds in the time time zone offset.
259* The `%+` conversion specifier is not supported since there doesn't seem to
260 be any consistent definition for it.
261* With only Jiff, the `%c`, `%r`, `%X` and `%x` locale oriented specifiers
262 use a default "unknown" locale via the [`DefaultCustom`] implementation
263 of the [`Custom`] trait. An example of the default locale format for `%c`
264 is `2024 M07 14, Sun 17:31:59`. One can either switch the POSIX locale
265 via [`PosixCustom`] (e.g., `Sun Jul 14 17:31:59 2024`), or write your own
266 implementation of [`Custom`] powered by [`icu`] and glued together with Jiff
267 via [`jiff-icu`].
268* The `E` and `O` locale modifiers are not supported.
269
270[`strftime`]: https://pubs.opengroup.org/onlinepubs/009695399/functions/strftime.html
271[`strptime`]: https://pubs.opengroup.org/onlinepubs/009695399/functions/strptime.html
272[ISO 8601 week-based]: https://en.wikipedia.org/wiki/ISO_week_date
273[`icu`]: https://docs.rs/icu
274[`jiff-icu`]: https://docs.rs/jiff-icu
275*/
276
277use crate::{
278 civil::{Date, DateTime, ISOWeekDate, Time, Weekday},
279 error::{fmt::strtime::Error as E, ErrorContext},
280 fmt::{
281 buffer::{ArrayBuffer, BorrowedWriter},
282 strtime::{parse::Parser, printer::Formatter},
283 Write,
284 },
285 tz::{Offset, OffsetConflict, TimeZone, TimeZoneDatabase},
286 util::{self, b, escape},
287 Error, Timestamp, Zoned,
288};
289
290mod parse;
291mod printer;
292
293/// Parse the given `input` according to the given `format` string.
294///
295/// See the [module documentation](self) for details on what's supported.
296///
297/// This routine is the same as [`BrokenDownTime::parse`], but may be more
298/// convenient to call.
299///
300/// # Errors
301///
302/// This returns an error when parsing failed. This might happen because
303/// the format string itself was invalid, or because the input didn't match
304/// the format string.
305///
306/// # Example
307///
308/// This example shows how to parse something resembling a RFC 2822 datetime:
309///
310/// ```
311/// use jiff::{civil::date, fmt::strtime, tz};
312///
313/// let zdt = strtime::parse(
314/// "%a, %d %b %Y %T %z",
315/// "Mon, 15 Jul 2024 16:24:59 -0400",
316/// )?.to_zoned()?;
317///
318/// let tz = tz::offset(-4).to_time_zone();
319/// assert_eq!(zdt, date(2024, 7, 15).at(16, 24, 59, 0).to_zoned(tz)?);
320///
321/// # Ok::<(), Box<dyn std::error::Error>>(())
322/// ```
323///
324/// Of course, one should prefer using the [`fmt::rfc2822`](super::rfc2822)
325/// module, which contains a dedicated RFC 2822 parser. For example, the above
326/// format string does not part all valid RFC 2822 datetimes, since, e.g.,
327/// the leading weekday is optional and so are the seconds in the time, but
328/// `strptime`-like APIs have no way of expressing such requirements.
329///
330/// [RFC 2822]: https://datatracker.ietf.org/doc/html/rfc2822
331///
332/// # Example: parse RFC 3339 timestamp with fractional seconds
333///
334/// ```
335/// use jiff::{civil::date, fmt::strtime};
336///
337/// let zdt = strtime::parse(
338/// "%Y-%m-%dT%H:%M:%S%.f%:z",
339/// "2024-07-15T16:24:59.123456789-04:00",
340/// )?.to_zoned()?;
341/// assert_eq!(
342/// zdt,
343/// date(2024, 7, 15).at(16, 24, 59, 123_456_789).in_tz("America/New_York")?,
344/// );
345///
346/// # Ok::<(), Box<dyn std::error::Error>>(())
347/// ```
348#[inline]
349pub fn parse(
350 format: impl AsRef<[u8]>,
351 input: impl AsRef<[u8]>,
352) -> Result<BrokenDownTime, Error> {
353 BrokenDownTime::parse(format, input)
354}
355
356/// Format the given broken down time using the format string given.
357///
358/// See the [module documentation](self) for details on what's supported.
359///
360/// This routine is like [`BrokenDownTime::format`], but may be more
361/// convenient to call. Also, it returns a `String` instead of accepting a
362/// [`fmt::Write`](super::Write) trait implementation to write to.
363///
364/// Note that `broken_down_time` can be _anything_ that can be converted into
365/// it. This includes, for example, [`Zoned`], [`Timestamp`], [`DateTime`],
366/// [`Date`] and [`Time`].
367///
368/// # Errors
369///
370/// This returns an error when formatting failed. Formatting can fail either
371/// because of an invalid format string, or if formatting requires a field in
372/// `BrokenDownTime` to be set that isn't. For example, trying to format a
373/// [`DateTime`] with the `%z` specifier will fail because a `DateTime` has no
374/// time zone or offset information associated with it.
375///
376/// # Example
377///
378/// This example shows how to format a `Zoned` into something resembling a RFC
379/// 2822 datetime:
380///
381/// ```
382/// use jiff::{civil::date, fmt::strtime};
383///
384/// let zdt = date(2024, 7, 15).at(16, 24, 59, 0).in_tz("America/New_York")?;
385/// let string = strtime::format("%a, %-d %b %Y %T %z", &zdt)?;
386/// assert_eq!(string, "Mon, 15 Jul 2024 16:24:59 -0400");
387///
388/// # Ok::<(), Box<dyn std::error::Error>>(())
389/// ```
390///
391/// Of course, one should prefer using the [`fmt::rfc2822`](super::rfc2822)
392/// module, which contains a dedicated RFC 2822 printer.
393///
394/// [RFC 2822]: https://datatracker.ietf.org/doc/html/rfc2822
395///
396/// # Example: `date`-like output
397///
398/// While the output of the Unix `date` command is likely locale specific,
399/// this is what it looks like on my system:
400///
401/// ```
402/// use jiff::{civil::date, fmt::strtime};
403///
404/// let zdt = date(2024, 7, 15).at(16, 24, 59, 0).in_tz("America/New_York")?;
405/// let string = strtime::format("%a %b %e %I:%M:%S %p %Z %Y", &zdt)?;
406/// assert_eq!(string, "Mon Jul 15 04:24:59 PM EDT 2024");
407///
408/// # Ok::<(), Box<dyn std::error::Error>>(())
409/// ```
410///
411/// # Example: RFC 3339 compatible output with fractional seconds
412///
413/// ```
414/// use jiff::{civil::date, fmt::strtime};
415///
416/// let zdt = date(2024, 7, 15)
417/// .at(16, 24, 59, 123_456_789)
418/// .in_tz("America/New_York")?;
419/// let string = strtime::format("%Y-%m-%dT%H:%M:%S%.f%:z", &zdt)?;
420/// assert_eq!(string, "2024-07-15T16:24:59.123456789-04:00");
421///
422/// # Ok::<(), Box<dyn std::error::Error>>(())
423/// ```
424#[cfg(any(test, feature = "alloc"))]
425#[inline]
426pub fn format(
427 format: impl AsRef<[u8]>,
428 broken_down_time: impl Into<BrokenDownTime>,
429) -> Result<alloc::string::String, Error> {
430 let broken_down_time: BrokenDownTime = broken_down_time.into();
431
432 let format = format.as_ref();
433 let mut buf = alloc::string::String::with_capacity(format.len());
434 broken_down_time.format(format, &mut buf)?;
435 Ok(buf)
436}
437
438/// Configuration for customizing the behavior of formatting or parsing.
439///
440/// One important use case enabled by this type is the ability to set a
441/// [`Custom`] trait implementation to use when calling
442/// [`BrokenDownTime::format_with_config`]
443/// or [`BrokenDownTime::to_string_with_config`].
444///
445/// It is generally expected that most callers should not need to use this.
446/// At present, the only reasons to use this are:
447///
448/// * If you specifically need to provide locale aware formatting within
449/// the context of `strtime`-style APIs. Unless you specifically need this,
450/// you should prefer using the [`icu`] crate via [`jiff-icu`] to do type
451/// conversions. More specifically, follow the examples in the `icu::datetime`
452/// module for a modern approach to datetime localization that leverages
453/// Unicode.
454/// * If you specifically need to opt into "lenient" parsing such that most
455/// errors when formatting are silently ignored.
456///
457/// # Example
458///
459/// This example shows how to use [`PosixCustom`] via `strtime` formatting:
460///
461/// ```
462/// use jiff::{civil, fmt::strtime::{BrokenDownTime, PosixCustom, Config}};
463///
464/// let config = Config::new().custom(PosixCustom::new());
465/// let dt = civil::date(2025, 7, 1).at(17, 30, 0, 0);
466/// let tm = BrokenDownTime::from(dt);
467/// assert_eq!(
468/// tm.to_string_with_config(&config, "%c")?,
469/// "Tue Jul 1 17:30:00 2025",
470/// );
471///
472/// # Ok::<(), Box<dyn std::error::Error>>(())
473/// ```
474///
475/// [`icu`]: https://docs.rs/icu
476/// [`jiff-icu`]: https://docs.rs/jiff-icu
477#[derive(Clone, Debug)]
478pub struct Config<C> {
479 custom: C,
480 lenient: bool,
481}
482
483impl Config<DefaultCustom> {
484 /// Create a new default `Config` that uses [`DefaultCustom`].
485 #[inline]
486 pub const fn new() -> Config<DefaultCustom> {
487 Config { custom: DefaultCustom::new(), lenient: false }
488 }
489}
490
491impl<C> Config<C> {
492 /// Set the implementation of [`Custom`] to use in `strtime`-style APIs
493 /// that use this configuration.
494 #[inline]
495 pub fn custom<U: Custom>(self, custom: U) -> Config<U> {
496 Config { custom, lenient: self.lenient }
497 }
498
499 /// Enable lenient formatting.
500 ///
501 /// When this is enabled, most errors that occur during formatting are
502 /// silently ignored. For example, if you try to format `%z` with a
503 /// [`BrokenDownTime`] that lacks a time zone offset, this would normally
504 /// result in an error. In contrast, when lenient mode is enabled, this
505 /// would just result in `%z` being written literally. Similarly, using
506 /// invalid UTF-8 in the format string would normally result in an error.
507 /// In lenient mode, invalid UTF-8 is automatically turned into the Unicode
508 /// replacement codepoint `U+FFFD` (which looks like this: `�`).
509 ///
510 /// Generally speaking, when this is enabled, the only error that can
511 /// occur when formatting is if a write to the underlying writer fails.
512 /// When using a writer that never errors (like `String`, unless allocation
513 /// fails), it follows that enabling lenient parsing will result in a
514 /// formatting operation that never fails (unless allocation fails).
515 ///
516 /// This currently has no effect on parsing, although this may change in
517 /// the future.
518 ///
519 /// Lenient formatting is disabled by default. It is strongly recommended
520 /// to keep it disabled in order to avoid mysterious failure modes for end
521 /// users. You should only enable this if you have strict requirements to
522 /// conform to legacy software behavior.
523 ///
524 /// # API stability
525 ///
526 /// An artifact of lenient parsing is that most error behaviors are
527 /// squashed in favor of writing the errant conversion specifier literally.
528 /// This means that if you use something like `%+`, which is currently
529 /// unrecognized, then that will result in a literal `%+` in the string
530 /// returned. But Jiff may one day add support for `%+` in a semver
531 /// compatible release.
532 ///
533 /// Stated differently, the set of unknown or error conditions is not
534 /// fixed and may decrease with time. This in turn means that the precise
535 /// conditions under which a conversion specifier gets written literally
536 /// to the resulting string may change over time in semver compatible
537 /// releases of Jiff.
538 ///
539 /// The alternative would be that Jiff could never add any new conversion
540 /// specifiers without making a semver incompatible release. The intent
541 /// of this policy is to avoid that scenario and permit reasonable
542 /// evolution of Jiff's `strtime` support.
543 ///
544 /// # Example
545 ///
546 /// This example shows how `%z` will be written literally if it would
547 /// otherwise fail:
548 ///
549 /// ```
550 /// use jiff::{civil, fmt::strtime::{BrokenDownTime, Config}};
551 ///
552 /// let tm = BrokenDownTime::from(civil::date(2025, 4, 30));
553 /// assert_eq!(
554 /// tm.to_string("%F %z").unwrap_err().to_string(),
555 /// "strftime formatting failed: %z failed: \
556 /// requires time zone offset",
557 /// );
558 ///
559 /// // Now enable lenient mode:
560 /// let config = Config::new().lenient(true);
561 /// assert_eq!(
562 /// tm.to_string_with_config(&config, "%F %z").unwrap(),
563 /// "2025-04-30 %z",
564 /// );
565 ///
566 /// // Lenient mode also applies when using an unsupported
567 /// // or unrecognized conversion specifier. This would
568 /// // normally return an error for example:
569 /// assert_eq!(
570 /// tm.to_string_with_config(&config, "%+ %0").unwrap(),
571 /// "%+ %0",
572 /// );
573 /// ```
574 #[inline]
575 pub fn lenient(self, yes: bool) -> Config<C> {
576 Config { lenient: yes, ..self }
577 }
578}
579
580/// An interface for customizing `strtime`-style parsing and formatting.
581///
582/// Each method on this trait comes with a default implementation corresponding
583/// to the behavior of [`DefaultCustom`]. More methods on this trait may be
584/// added in the future.
585///
586/// Implementers of this trait can be attached to a [`Config`] which can then
587/// be passed to [`BrokenDownTime::format_with_config`] or
588/// [`BrokenDownTime::to_string_with_config`].
589///
590/// New methods with default implementations may be added to this trait in
591/// semver compatible releases of Jiff.
592///
593/// # Motivation
594///
595/// While Jiff's API is generally locale-agnostic, this trait is meant to
596/// provide a best effort "hook" for tailoring the behavior of `strtime`
597/// routines. More specifically, for conversion specifiers in `strtime`-style
598/// APIs that are influenced by locale settings.
599///
600/// In general, a `strtime`-style API is not optimal for localization.
601/// It's both too flexible and not expressive enough. As a result, mixing
602/// localization with `strtime`-style APIs is likely not a good idea. However,
603/// this is sometimes required for legacy or convenience reasons, and that's
604/// why Jiff provides this hook.
605///
606/// If you do need to localize datetimes but don't have a requirement to
607/// have it integrate with `strtime`-style APIs, then you should use the
608/// [`icu`] crate via [`jiff-icu`] for type conversions. And then follow the
609/// examples in the `icu::datetime` API for formatting datetimes.
610///
611/// # Supported conversion specifiers
612///
613/// Currently, only formatting for the following specifiers is supported:
614///
615/// * `%c` - Formatting the date and time.
616/// * `%r` - Formatting the 12-hour clock time.
617/// * `%X` - Formatting the clock time.
618/// * `%x` - Formatting the date.
619///
620/// # Unsupported behavior
621///
622/// This trait currently does not support parsing based on locale in any way.
623///
624/// This trait also does not support locale specific behavior for `%a`/`%A`
625/// (day of the week), `%b`/`%B` (name of the month) or `%p`/`%P` (AM or PM).
626/// Supporting these is problematic with modern localization APIs, since
627/// modern APIs do not expose options to localize these things independent of
628/// anything else. Instead, they are subsumed most holistically into, e.g.,
629/// "print the long form of a date in the current locale."
630///
631/// Since the motivation for this trait is not really to provide the best way
632/// to localize datetimes, but rather, to facilitate convenience and
633/// inter-operation with legacy systems, it is plausible that the behaviors
634/// listed above could be supported by Jiff. If you need the above behaviors,
635/// please [open a new issue](https://github.com/BurntSushi/jiff/issues/new)
636/// with a proposal.
637///
638/// # Example
639///
640/// This example shows the difference between the default locale and the
641/// POSIX locale:
642///
643/// ```
644/// use jiff::{civil, fmt::strtime::{BrokenDownTime, PosixCustom, Config}};
645///
646/// let dt = civil::date(2025, 7, 1).at(17, 30, 0, 0);
647/// let tm = BrokenDownTime::from(dt);
648/// assert_eq!(
649/// tm.to_string("%c")?,
650/// "2025 M07 1, Tue 17:30:00",
651/// );
652///
653/// let config = Config::new().custom(PosixCustom::new());
654/// assert_eq!(
655/// tm.to_string_with_config(&config, "%c")?,
656/// "Tue Jul 1 17:30:00 2025",
657/// );
658///
659/// # Ok::<(), Box<dyn std::error::Error>>(())
660/// ```
661///
662/// [`icu`]: https://docs.rs/icu
663/// [`jiff-icu`]: https://docs.rs/jiff-icu
664pub trait Custom: Sized {
665 /// Called when formatting a datetime with the `%c` flag.
666 ///
667 /// This defaults to the implementation for [`DefaultCustom`].
668 fn format_datetime<W: Write>(
669 &self,
670 config: &Config<Self>,
671 ext: &Extension,
672 tm: &BrokenDownTime,
673 wtr: &mut W,
674 ) -> Result<(), Error> {
675 if matches!(ext.flag, Some(Flag::Uppercase)) {
676 tm.format_with_config(config, "%Y M%m %-d, %^a %H:%M:%S", wtr)
677 } else {
678 tm.format_with_config(config, "%Y M%m %-d, %a %H:%M:%S", wtr)
679 }
680 }
681
682 /// Called when formatting a datetime with the `%x` flag.
683 ///
684 /// This defaults to the implementation for [`DefaultCustom`].
685 fn format_date<W: Write>(
686 &self,
687 config: &Config<Self>,
688 _ext: &Extension,
689 tm: &BrokenDownTime,
690 wtr: &mut W,
691 ) -> Result<(), Error> {
692 // 2025 M04 27
693 tm.format_with_config(config, "%Y M%m %-d", wtr)
694 }
695
696 /// Called when formatting a datetime with the `%X` flag.
697 ///
698 /// This defaults to the implementation for [`DefaultCustom`].
699 fn format_time<W: Write>(
700 &self,
701 config: &Config<Self>,
702 _ext: &Extension,
703 tm: &BrokenDownTime,
704 wtr: &mut W,
705 ) -> Result<(), Error> {
706 tm.format_with_config(config, "%H:%M:%S", wtr)
707 }
708
709 /// Called when formatting a datetime with the `%r` flag.
710 ///
711 /// This defaults to the implementation for [`DefaultCustom`].
712 fn format_12hour_time<W: Write>(
713 &self,
714 config: &Config<Self>,
715 ext: &Extension,
716 tm: &BrokenDownTime,
717 wtr: &mut W,
718 ) -> Result<(), Error> {
719 if matches!(ext.flag, Some(Flag::Uppercase)) {
720 tm.format_with_config(config, "%-I:%M:%S %^p", wtr)
721 } else {
722 tm.format_with_config(config, "%-I:%M:%S %p", wtr)
723 }
724 }
725}
726
727/// The default trait implementation of [`Custom`].
728///
729/// Whenever one uses the formatting or parsing routines in this module
730/// without providing a configuration, then this customization is the one
731/// that gets used.
732///
733/// The behavior of the locale formatting of this type is meant to match that
734/// of Unicode's `und` locale.
735///
736/// # Example
737///
738/// This example shows how to explicitly use [`DefaultCustom`] via `strtime`
739/// formatting:
740///
741/// ```
742/// use jiff::{civil, fmt::strtime::{BrokenDownTime, DefaultCustom, Config}};
743///
744/// let config = Config::new().custom(DefaultCustom::new());
745/// let dt = civil::date(2025, 7, 1).at(17, 30, 0, 0);
746/// let tm = BrokenDownTime::from(dt);
747/// assert_eq!(
748/// tm.to_string_with_config(&config, "%c")?,
749/// "2025 M07 1, Tue 17:30:00",
750/// );
751///
752/// # Ok::<(), Box<dyn std::error::Error>>(())
753/// ```
754#[derive(Clone, Debug, Default)]
755pub struct DefaultCustom(());
756
757impl DefaultCustom {
758 /// Create a new instance of this default customization.
759 pub const fn new() -> DefaultCustom {
760 DefaultCustom(())
761 }
762}
763
764impl Custom for DefaultCustom {}
765
766/// A POSIX locale implementation of [`Custom`].
767///
768/// The behavior of the locale formatting of this type is meant to match that
769/// of POSIX's `C` locale.
770///
771/// # Example
772///
773/// This example shows how to use [`PosixCustom`] via `strtime` formatting:
774///
775/// ```
776/// use jiff::{civil, fmt::strtime::{BrokenDownTime, PosixCustom, Config}};
777///
778/// let config = Config::new().custom(PosixCustom::new());
779/// let dt = civil::date(2025, 7, 1).at(17, 30, 0, 0);
780/// let tm = BrokenDownTime::from(dt);
781/// assert_eq!(
782/// tm.to_string_with_config(&config, "%c")?,
783/// "Tue Jul 1 17:30:00 2025",
784/// );
785///
786/// # Ok::<(), Box<dyn std::error::Error>>(())
787/// ```
788#[derive(Clone, Debug, Default)]
789pub struct PosixCustom(());
790
791impl PosixCustom {
792 /// Create a new instance of this POSIX customization.
793 pub const fn new() -> PosixCustom {
794 PosixCustom(())
795 }
796}
797
798impl Custom for PosixCustom {
799 fn format_datetime<W: Write>(
800 &self,
801 config: &Config<Self>,
802 ext: &Extension,
803 tm: &BrokenDownTime,
804 wtr: &mut W,
805 ) -> Result<(), Error> {
806 if matches!(ext.flag, Some(Flag::Uppercase)) {
807 tm.format_with_config(config, "%^a %^b %e %H:%M:%S %Y", wtr)
808 } else {
809 tm.format_with_config(config, "%a %b %e %H:%M:%S %Y", wtr)
810 }
811 }
812
813 fn format_date<W: Write>(
814 &self,
815 config: &Config<Self>,
816 _ext: &Extension,
817 tm: &BrokenDownTime,
818 wtr: &mut W,
819 ) -> Result<(), Error> {
820 tm.format_with_config(config, "%m/%d/%y", wtr)
821 }
822
823 fn format_time<W: Write>(
824 &self,
825 config: &Config<Self>,
826 _ext: &Extension,
827 tm: &BrokenDownTime,
828 wtr: &mut W,
829 ) -> Result<(), Error> {
830 tm.format_with_config(config, "%H:%M:%S", wtr)
831 }
832
833 fn format_12hour_time<W: Write>(
834 &self,
835 config: &Config<Self>,
836 ext: &Extension,
837 tm: &BrokenDownTime,
838 wtr: &mut W,
839 ) -> Result<(), Error> {
840 if matches!(ext.flag, Some(Flag::Uppercase)) {
841 tm.format_with_config(config, "%I:%M:%S %^p", wtr)
842 } else {
843 tm.format_with_config(config, "%I:%M:%S %p", wtr)
844 }
845 }
846}
847
848/// The "broken down time" used by parsing and formatting.
849///
850/// This is a lower level aspect of the `strptime` and `strftime` APIs that you
851/// probably won't need to use directly. The main use case is if you want to
852/// observe formatting errors or if you want to format a datetime to something
853/// other than a `String` via the [`fmt::Write`](super::Write) trait.
854///
855/// Otherwise, typical use of this module happens indirectly via APIs like
856/// [`Zoned::strptime`] and [`Zoned::strftime`].
857///
858/// # Design
859///
860/// This is the type that parsing writes to and formatting reads from. That
861/// is, parsing proceeds by writing individual parsed fields to this type, and
862/// then converting the fields to datetime types like [`Zoned`] only after
863/// parsing is complete. Similarly, formatting always begins by converting
864/// datetime types like `Zoned` into a `BrokenDownTime`, and then formatting
865/// the individual fields from there.
866// Design:
867//
868// This is meant to be very similar to libc's `struct tm` in that it
869// represents civil time, although may have an offset attached to it, in which
870// case it represents an absolute time. The main difference is that each field
871// is explicitly optional, where as in C, there's no way to tell whether a
872// field is "set" or not. In C, this isn't so much a problem, because the
873// caller needs to explicitly pass in a pointer to a `struct tm`, and so the
874// API makes it clear that it's going to mutate the time.
875//
876// But in Rust, we really just want to accept a format string, an input and
877// return a fresh datetime. (Nevermind the fact that we don't provide a way
878// to mutate datetimes in place.) We could just use "default" units like you
879// might in C, but it would be very surprising if `%m-%d` just decided to fill
880// in the year for you with some default value. So we track which pieces have
881// been set individually and return errors when requesting, e.g., a `Date`
882// when no `year` has been parsed.
883//
884// We do permit time units to be filled in by default, as-is consistent with
885// the rest of Jiff's API. e.g., If a `DateTime` is requested but the format
886// string has no directives for time, we'll happy default to midnight. The
887// only catch is that you can't omit time units bigger than any present time
888// unit. For example, only `%M` doesn't fly. If you want to parse minutes, you
889// also have to parse hours.
890#[derive(Debug, Default)]
891#[cfg_attr(feature = "defmt", derive(defmt::Format))]
892pub struct BrokenDownTime {
893 year: Option<i16>,
894 month: Option<i8>,
895 day: Option<i8>,
896 day_of_year: Option<i16>,
897 iso_week_year: Option<i16>,
898 iso_week: Option<i8>,
899 week_sun: Option<i8>,
900 week_mon: Option<i8>,
901 hour: Option<i8>,
902 minute: Option<i8>,
903 second: Option<i8>,
904 subsec: Option<i32>,
905 offset: Option<Offset>,
906 // Used to confirm that it is consistent
907 // with the date given. It usually isn't
908 // used to pick a date on its own, but can
909 // be for week dates.
910 weekday: Option<Weekday>,
911 // Only generally useful with %I. But can still
912 // be used with, say, %H. In that case, AM will
913 // turn 13 o'clock to 1 o'clock.
914 meridiem: Option<Meridiem>,
915 // A timestamp. Set when converting from
916 // a `Zoned` or `Timestamp`, or when parsing `%s`.
917 timestamp: Option<Timestamp>,
918 // The time zone. Currently used only when
919 // formatting a `Zoned`.
920 tz: Option<TimeZone>,
921 // The IANA time zone identifier. Used only when
922 // formatting a `Zoned`.
923 #[cfg(feature = "alloc")]
924 iana: Option<alloc::string::String>,
925}
926
927impl BrokenDownTime {
928 /// Parse the given `input` according to the given `format` string.
929 ///
930 /// See the [module documentation](self) for details on what's supported.
931 ///
932 /// This routine is the same as the module level free function
933 /// [`strtime::parse`](parse()).
934 ///
935 /// # Errors
936 ///
937 /// This returns an error when parsing failed. This might happen because
938 /// the format string itself was invalid, or because the input didn't match
939 /// the format string.
940 ///
941 /// # Example
942 ///
943 /// ```
944 /// use jiff::{civil, fmt::strtime::BrokenDownTime};
945 ///
946 /// let tm = BrokenDownTime::parse("%m/%d/%y", "7/14/24")?;
947 /// let date = tm.to_date()?;
948 /// assert_eq!(date, civil::date(2024, 7, 14));
949 ///
950 /// # Ok::<(), Box<dyn std::error::Error>>(())
951 /// ```
952 #[inline]
953 pub fn parse(
954 format: impl AsRef<[u8]>,
955 input: impl AsRef<[u8]>,
956 ) -> Result<BrokenDownTime, Error> {
957 BrokenDownTime::parse_mono(format.as_ref(), input.as_ref())
958 }
959
960 #[inline]
961 fn parse_mono(fmt: &[u8], inp: &[u8]) -> Result<BrokenDownTime, Error> {
962 let mut pieces = BrokenDownTime::default();
963 let mut p = Parser { fmt, inp, tm: &mut pieces };
964 p.parse().context(E::FailedStrptime)?;
965 if !p.inp.is_empty() {
966 return Err(Error::from(E::unconsumed(p.inp)));
967 }
968 Ok(pieces)
969 }
970
971 /// Parse a prefix of the given `input` according to the given `format`
972 /// string. The offset returned corresponds to the number of bytes parsed.
973 /// That is, the length of the prefix (which may be the length of the
974 /// entire input if there are no unparsed bytes remaining).
975 ///
976 /// See the [module documentation](self) for details on what's supported.
977 ///
978 /// This is like [`BrokenDownTime::parse`], but it won't return an error
979 /// if there is input remaining after parsing the format directives.
980 ///
981 /// # Errors
982 ///
983 /// This returns an error when parsing failed. This might happen because
984 /// the format string itself was invalid, or because the input didn't match
985 /// the format string.
986 ///
987 /// # Example
988 ///
989 /// ```
990 /// use jiff::{civil, fmt::strtime::BrokenDownTime};
991 ///
992 /// // %y only parses two-digit years, so the 99 following
993 /// // 24 is unparsed!
994 /// let input = "7/14/2499";
995 /// let (tm, offset) = BrokenDownTime::parse_prefix("%m/%d/%y", input)?;
996 /// let date = tm.to_date()?;
997 /// assert_eq!(date, civil::date(2024, 7, 14));
998 /// assert_eq!(offset, 7);
999 /// assert_eq!(&input[offset..], "99");
1000 ///
1001 /// # Ok::<(), Box<dyn std::error::Error>>(())
1002 /// ```
1003 ///
1004 /// If the entire input is parsed, then the offset is the length of the
1005 /// input:
1006 ///
1007 /// ```
1008 /// use jiff::{civil, fmt::strtime::BrokenDownTime};
1009 ///
1010 /// let (tm, offset) = BrokenDownTime::parse_prefix(
1011 /// "%m/%d/%y", "7/14/24",
1012 /// )?;
1013 /// let date = tm.to_date()?;
1014 /// assert_eq!(date, civil::date(2024, 7, 14));
1015 /// assert_eq!(offset, 7);
1016 ///
1017 /// # Ok::<(), Box<dyn std::error::Error>>(())
1018 /// ```
1019 ///
1020 /// # Example: how to parse only a part of a timestamp
1021 ///
1022 /// If you only need, for example, the date from a timestamp, then you
1023 /// can parse it as a prefix:
1024 ///
1025 /// ```
1026 /// use jiff::{civil, fmt::strtime::BrokenDownTime};
1027 ///
1028 /// let input = "2024-01-20T17:55Z";
1029 /// let (tm, offset) = BrokenDownTime::parse_prefix("%Y-%m-%d", input)?;
1030 /// let date = tm.to_date()?;
1031 /// assert_eq!(date, civil::date(2024, 1, 20));
1032 /// assert_eq!(offset, 10);
1033 /// assert_eq!(&input[offset..], "T17:55Z");
1034 ///
1035 /// # Ok::<(), Box<dyn std::error::Error>>(())
1036 /// ```
1037 ///
1038 /// Note though that Jiff's default parsing functions are already quite
1039 /// flexible, and one can just parse a civil date directly from a timestamp
1040 /// automatically:
1041 ///
1042 /// ```
1043 /// use jiff::civil;
1044 ///
1045 /// let input = "2024-01-20T17:55-05";
1046 /// let date: civil::Date = input.parse()?;
1047 /// assert_eq!(date, civil::date(2024, 1, 20));
1048 ///
1049 /// # Ok::<(), Box<dyn std::error::Error>>(())
1050 /// ```
1051 ///
1052 /// Although in this case, you don't get the length of the prefix parsed.
1053 #[inline]
1054 pub fn parse_prefix(
1055 format: impl AsRef<[u8]>,
1056 input: impl AsRef<[u8]>,
1057 ) -> Result<(BrokenDownTime, usize), Error> {
1058 BrokenDownTime::parse_prefix_mono(format.as_ref(), input.as_ref())
1059 }
1060
1061 #[inline]
1062 fn parse_prefix_mono(
1063 fmt: &[u8],
1064 inp: &[u8],
1065 ) -> Result<(BrokenDownTime, usize), Error> {
1066 let mkoffset = util::parse::offseter(inp);
1067 let mut pieces = BrokenDownTime::default();
1068 let mut p = Parser { fmt, inp, tm: &mut pieces };
1069 p.parse().context(E::FailedStrptime)?;
1070 let remainder = mkoffset(p.inp);
1071 Ok((pieces, remainder))
1072 }
1073
1074 /// Format this broken down time using the format string given.
1075 ///
1076 /// See the [module documentation](self) for details on what's supported.
1077 ///
1078 /// This routine is like the module level free function
1079 /// [`strtime::format`](parse()), except it takes a
1080 /// [`fmt::Write`](super::Write) trait implementations instead of assuming
1081 /// you want a `String`.
1082 ///
1083 /// # Errors
1084 ///
1085 /// This returns an error when formatting failed. Formatting can fail
1086 /// either because of an invalid format string, or if formatting requires
1087 /// a field in `BrokenDownTime` to be set that isn't. For example, trying
1088 /// to format a [`DateTime`] with the `%z` specifier will fail because a
1089 /// `DateTime` has no time zone or offset information associated with it.
1090 ///
1091 /// Formatting also fails if writing to the given writer fails.
1092 ///
1093 /// # Example
1094 ///
1095 /// This example shows a formatting option, `%Z`, that isn't available
1096 /// during parsing. Namely, `%Z` inserts a time zone abbreviation. This
1097 /// is generally only intended for display purposes, since it can be
1098 /// ambiguous when parsing.
1099 ///
1100 /// ```
1101 /// use jiff::{civil::date, fmt::strtime::BrokenDownTime};
1102 ///
1103 /// let zdt = date(2024, 7, 9).at(16, 24, 0, 0).in_tz("America/New_York")?;
1104 /// let tm = BrokenDownTime::from(&zdt);
1105 ///
1106 /// let mut buf = String::new();
1107 /// tm.format("%a %b %e %I:%M:%S %p %Z %Y", &mut buf)?;
1108 ///
1109 /// assert_eq!(buf, "Tue Jul 9 04:24:00 PM EDT 2024");
1110 ///
1111 /// # Ok::<(), Box<dyn std::error::Error>>(())
1112 /// ```
1113 #[inline]
1114 pub fn format<W: Write>(
1115 &self,
1116 format: impl AsRef<[u8]>,
1117 mut wtr: W,
1118 ) -> Result<(), Error> {
1119 self.format_with_config(&Config::new(), format, &mut wtr)
1120 }
1121
1122 /// Format this broken down time with a specific configuration using the
1123 /// format string given.
1124 ///
1125 /// See the [module documentation](self) for details on what's supported.
1126 ///
1127 /// This routine is like [`BrokenDownTime::format`], except that it
1128 /// permits callers to provide their own configuration instead of using
1129 /// the default. This routine also accepts a `&mut W` instead of a `W`,
1130 /// which may be more flexible in some situations.
1131 ///
1132 /// # Errors
1133 ///
1134 /// This returns an error when formatting failed. Formatting can fail
1135 /// either because of an invalid format string, or if formatting requires
1136 /// a field in `BrokenDownTime` to be set that isn't. For example, trying
1137 /// to format a [`DateTime`] with the `%z` specifier will fail because a
1138 /// `DateTime` has no time zone or offset information associated with it.
1139 ///
1140 /// Formatting also fails if writing to the given writer fails.
1141 ///
1142 /// # Example
1143 ///
1144 /// This example shows how to use [`PosixCustom`] to get formatting
1145 /// for conversion specifiers like `%c` in the POSIX locale:
1146 ///
1147 /// ```
1148 /// use jiff::{civil, fmt::strtime::{BrokenDownTime, PosixCustom, Config}};
1149 ///
1150 /// let mut buf = String::new();
1151 /// let dt = civil::date(2025, 7, 1).at(17, 30, 0, 0);
1152 /// let tm = BrokenDownTime::from(dt);
1153 /// tm.format("%c", &mut buf)?;
1154 /// assert_eq!(buf, "2025 M07 1, Tue 17:30:00");
1155 ///
1156 /// let config = Config::new().custom(PosixCustom::new());
1157 /// buf.clear();
1158 /// tm.format_with_config(&config, "%c", &mut buf)?;
1159 /// assert_eq!(buf, "Tue Jul 1 17:30:00 2025");
1160 ///
1161 /// # Ok::<(), Box<dyn std::error::Error>>(())
1162 /// ```
1163 #[inline]
1164 pub fn format_with_config<W: Write, L: Custom>(
1165 &self,
1166 config: &Config<L>,
1167 format: impl AsRef<[u8]>,
1168 wtr: &mut W,
1169 ) -> Result<(), Error> {
1170 let fmt = format.as_ref();
1171 let mut buf = ArrayBuffer::<100>::default();
1172 let mut bbuf = buf.as_borrowed();
1173 let mut wtr = BorrowedWriter::new(&mut bbuf, wtr);
1174 let mut formatter = Formatter { config, fmt, tm: self, wtr: &mut wtr };
1175 formatter.format().context(E::FailedStrftime)?;
1176 wtr.finish()
1177 }
1178
1179 /// Format this broken down time using the format string given into a new
1180 /// `String`.
1181 ///
1182 /// See the [module documentation](self) for details on what's supported.
1183 ///
1184 /// This is like [`BrokenDownTime::format`], but always uses a `String` to
1185 /// format the time into. If you need to reuse allocations or write a
1186 /// formatted time into a different type, then you should use
1187 /// [`BrokenDownTime::format`] instead.
1188 ///
1189 /// # Errors
1190 ///
1191 /// This returns an error when formatting failed. Formatting can fail
1192 /// either because of an invalid format string, or if formatting requires
1193 /// a field in `BrokenDownTime` to be set that isn't. For example, trying
1194 /// to format a [`DateTime`] with the `%z` specifier will fail because a
1195 /// `DateTime` has no time zone or offset information associated with it.
1196 ///
1197 /// # Example
1198 ///
1199 /// This example shows a formatting option, `%Z`, that isn't available
1200 /// during parsing. Namely, `%Z` inserts a time zone abbreviation. This
1201 /// is generally only intended for display purposes, since it can be
1202 /// ambiguous when parsing.
1203 ///
1204 /// ```
1205 /// use jiff::{civil::date, fmt::strtime::BrokenDownTime};
1206 ///
1207 /// let zdt = date(2024, 7, 9).at(16, 24, 0, 0).in_tz("America/New_York")?;
1208 /// let tm = BrokenDownTime::from(&zdt);
1209 /// let string = tm.to_string("%a %b %e %I:%M:%S %p %Z %Y")?;
1210 /// assert_eq!(string, "Tue Jul 9 04:24:00 PM EDT 2024");
1211 ///
1212 /// # Ok::<(), Box<dyn std::error::Error>>(())
1213 /// ```
1214 #[cfg(feature = "alloc")]
1215 #[inline]
1216 pub fn to_string(
1217 &self,
1218 format: impl AsRef<[u8]>,
1219 ) -> Result<alloc::string::String, Error> {
1220 let format = format.as_ref();
1221 let mut buf = alloc::string::String::with_capacity(format.len());
1222 self.format(format, &mut buf)?;
1223 Ok(buf)
1224 }
1225
1226 /// Format this broken down time with a specific configuration using the
1227 /// format string given into a new `String`.
1228 ///
1229 /// See the [module documentation](self) for details on what's supported.
1230 ///
1231 /// This routine is like [`BrokenDownTime::to_string`], except that it
1232 /// permits callers to provide their own configuration instead of using
1233 /// the default.
1234 ///
1235 /// # Errors
1236 ///
1237 /// This returns an error when formatting failed. Formatting can fail
1238 /// either because of an invalid format string, or if formatting requires
1239 /// a field in `BrokenDownTime` to be set that isn't. For example, trying
1240 /// to format a [`DateTime`] with the `%z` specifier will fail because a
1241 /// `DateTime` has no time zone or offset information associated with it.
1242 ///
1243 /// # Example
1244 ///
1245 /// This example shows how to use [`PosixCustom`] to get formatting
1246 /// for conversion specifiers like `%c` in the POSIX locale:
1247 ///
1248 /// ```
1249 /// use jiff::{civil, fmt::strtime::{BrokenDownTime, PosixCustom, Config}};
1250 ///
1251 /// let dt = civil::date(2025, 7, 1).at(17, 30, 0, 0);
1252 /// let tm = BrokenDownTime::from(dt);
1253 /// assert_eq!(
1254 /// tm.to_string("%c")?,
1255 /// "2025 M07 1, Tue 17:30:00",
1256 /// );
1257 ///
1258 /// let config = Config::new().custom(PosixCustom::new());
1259 /// assert_eq!(
1260 /// tm.to_string_with_config(&config, "%c")?,
1261 /// "Tue Jul 1 17:30:00 2025",
1262 /// );
1263 ///
1264 /// # Ok::<(), Box<dyn std::error::Error>>(())
1265 /// ```
1266 #[cfg(feature = "alloc")]
1267 #[inline]
1268 pub fn to_string_with_config<L: Custom>(
1269 &self,
1270 config: &Config<L>,
1271 format: impl AsRef<[u8]>,
1272 ) -> Result<alloc::string::String, Error> {
1273 let format = format.as_ref();
1274 let mut buf = alloc::string::String::with_capacity(format.len());
1275 self.format_with_config(config, format, &mut buf)?;
1276 Ok(buf)
1277 }
1278
1279 /// Extracts a zoned datetime from this broken down time.
1280 ///
1281 /// When an IANA time zone identifier is
1282 /// present but an offset is not, then the
1283 /// [`Disambiguation::Compatible`](crate::tz::Disambiguation::Compatible)
1284 /// strategy is used if the parsed datetime is ambiguous in the time zone.
1285 ///
1286 /// If you need to use a custom time zone database for doing IANA time
1287 /// zone identifier lookups (via the `%Q` directive), then use
1288 /// [`BrokenDownTime::to_zoned_with`].
1289 ///
1290 /// This always prefers an explicitly set timestamp over other components
1291 /// of this `BrokenDownTime`. An explicit timestamp is set via
1292 /// [`BrokenDownTime::set_timestamp`]. This most commonly occurs by parsing
1293 /// a `%s` conversion specifier. When an explicit timestamp is not present,
1294 /// then the instant is derived from a civil datetime with a UTC offset
1295 /// and/or a time zone.
1296 ///
1297 /// # Warning
1298 ///
1299 /// The `strtime` module APIs do not require an IANA time zone identifier
1300 /// to parse a `Zoned`. If one is not used, then if you format a zoned
1301 /// datetime in a time zone like `America/New_York` and then parse it back
1302 /// again, the zoned datetime you get back will be a "fixed offset" zoned
1303 /// datetime. This in turn means it will not perform daylight saving time
1304 /// safe arithmetic.
1305 ///
1306 /// However, the `%Q` directive may be used to both format and parse an
1307 /// IANA time zone identifier. It is strongly recommended to use this
1308 /// directive whenever one is formatting or parsing `Zoned` values since
1309 /// it permits correctly round-tripping `Zoned` values.
1310 ///
1311 /// # Errors
1312 ///
1313 /// This returns an error if there weren't enough components to construct
1314 /// an instant with a time zone. This requires an IANA time zone identifier
1315 /// or a UTC offset, as well as either an explicitly set timestamp (via
1316 /// [`BrokenDownTime::set_timestamp`]) or enough data set to form a civil
1317 /// datetime.
1318 ///
1319 /// When both a UTC offset and an IANA time zone identifier are found, then
1320 /// an error is returned if they are inconsistent with one another for the
1321 /// parsed timestamp.
1322 ///
1323 /// # Example
1324 ///
1325 /// This example shows how to parse a zoned datetime:
1326 ///
1327 /// ```
1328 /// use jiff::fmt::strtime;
1329 ///
1330 /// let zdt = strtime::parse(
1331 /// "%F %H:%M %:z %:Q",
1332 /// "2024-07-14 21:14 -04:00 US/Eastern",
1333 /// )?.to_zoned()?;
1334 /// assert_eq!(zdt.to_string(), "2024-07-14T21:14:00-04:00[US/Eastern]");
1335 ///
1336 /// # Ok::<(), Box<dyn std::error::Error>>(())
1337 /// ```
1338 ///
1339 /// # Example: time zone inconsistent with offset
1340 ///
1341 /// This shows that an error is returned when the offset is inconsistent
1342 /// with the time zone. For example, `US/Eastern` is in daylight saving
1343 /// time in July 2024:
1344 ///
1345 /// ```
1346 /// use jiff::fmt::strtime;
1347 ///
1348 /// let result = strtime::parse(
1349 /// "%F %H:%M %:z %:Q",
1350 /// "2024-07-14 21:14 -05:00 US/Eastern",
1351 /// )?.to_zoned();
1352 /// assert_eq!(
1353 /// result.unwrap_err().to_string(),
1354 /// "datetime could not resolve to a timestamp since `reject` \
1355 /// conflict resolution was chosen, and because \
1356 /// datetime has offset `-05`, \
1357 /// but the time zone `US/Eastern` for the given datetime \
1358 /// unambiguously has offset `-04`",
1359 /// );
1360 ///
1361 /// # Ok::<(), Box<dyn std::error::Error>>(())
1362 /// ```
1363 ///
1364 /// # Example: timestamp without offset
1365 ///
1366 /// If a timestamp has been parsed but there is no offset or IANA time
1367 /// zone identifier, then the zoned datetime will be in UTC via the
1368 /// `Etc/Unknown` time zone:
1369 ///
1370 /// ```
1371 /// use jiff::fmt::strtime;
1372 ///
1373 /// let zdt = strtime::parse("%s", "1760813400")?.to_zoned()?;
1374 /// assert_eq!(zdt.to_string(), "2025-10-18T18:50:00Z[Etc/Unknown]");
1375 ///
1376 /// # Ok::<(), Box<dyn std::error::Error>>(())
1377 /// ```
1378 #[inline]
1379 pub fn to_zoned(&self) -> Result<Zoned, Error> {
1380 self.to_zoned_with(crate::tz::db())
1381 }
1382
1383 /// Extracts a zoned datetime from this broken down time and uses the time
1384 /// zone database given for any IANA time zone identifier lookups.
1385 ///
1386 /// An IANA time zone identifier lookup is only performed when this
1387 /// `BrokenDownTime` contains an IANA time zone identifier. An IANA time
1388 /// zone identifier can be parsed with the `%Q` directive.
1389 ///
1390 /// When an IANA time zone identifier is
1391 /// present but an offset is not, then the
1392 /// [`Disambiguation::Compatible`](crate::tz::Disambiguation::Compatible)
1393 /// strategy is used if the parsed datetime is ambiguous in the time zone.
1394 ///
1395 /// This always prefers an explicitly set timestamp over other components
1396 /// of this `BrokenDownTime`. An explicit timestamp is set via
1397 /// [`BrokenDownTime::set_timestamp`]. This most commonly occurs by parsing
1398 /// a `%s` conversion specifier. When an explicit timestamp is not present,
1399 /// then the instant is derived from a civil datetime with a UTC offset
1400 /// and/or a time zone.
1401 ///
1402 /// # Warning
1403 ///
1404 /// The `strtime` module APIs do not require an IANA time zone identifier
1405 /// to parse a `Zoned`. If one is not used, then if you format a zoned
1406 /// datetime in a time zone like `America/New_York` and then parse it back
1407 /// again, the zoned datetime you get back will be a "fixed offset" zoned
1408 /// datetime. This in turn means it will not perform daylight saving time
1409 /// safe arithmetic.
1410 ///
1411 /// However, the `%Q` directive may be used to both format and parse an
1412 /// IANA time zone identifier. It is strongly recommended to use this
1413 /// directive whenever one is formatting or parsing `Zoned` values since
1414 /// it permits correctly round-tripping `Zoned` values.
1415 ///
1416 /// # Errors
1417 ///
1418 /// This returns an error if there weren't enough components to construct
1419 /// an instant with a time zone. This requires an IANA time zone identifier
1420 /// or a UTC offset, as well as either an explicitly set timestamp (via
1421 /// [`BrokenDownTime::set_timestamp`]) or enough data set to form a civil
1422 /// datetime.
1423 ///
1424 /// When both a UTC offset and an IANA time zone identifier are found, then
1425 /// an error is returned if they are inconsistent with one another for the
1426 /// parsed timestamp.
1427 ///
1428 /// # Example
1429 ///
1430 /// This example shows how to parse a zoned datetime:
1431 ///
1432 /// ```
1433 /// use jiff::fmt::strtime;
1434 ///
1435 /// let zdt = strtime::parse(
1436 /// "%F %H:%M %:z %:Q",
1437 /// "2024-07-14 21:14 -04:00 US/Eastern",
1438 /// )?.to_zoned_with(jiff::tz::db())?;
1439 /// assert_eq!(zdt.to_string(), "2024-07-14T21:14:00-04:00[US/Eastern]");
1440 ///
1441 /// # Ok::<(), Box<dyn std::error::Error>>(())
1442 /// ```
1443 #[inline]
1444 pub fn to_zoned_with(
1445 &self,
1446 db: &TimeZoneDatabase,
1447 ) -> Result<Zoned, Error> {
1448 match (self.offset, self.iana_time_zone()) {
1449 (None, None) => {
1450 if let Some(ts) = self.timestamp {
1451 return Ok(ts.to_zoned(TimeZone::unknown()));
1452 }
1453 Err(Error::from(E::ZonedOffsetOrTz))
1454 }
1455 (Some(offset), None) => {
1456 let ts = match self.timestamp {
1457 Some(ts) => ts,
1458 None => {
1459 let dt = self
1460 .to_datetime()
1461 .context(E::RequiredDateTimeForZoned)?;
1462 let ts = offset
1463 .to_timestamp(dt)
1464 .context(E::RangeTimestamp)?;
1465 ts
1466 }
1467 };
1468 Ok(ts.to_zoned(TimeZone::fixed(offset)))
1469 }
1470 (None, Some(iana)) => {
1471 let tz = db.get(iana)?;
1472 match self.timestamp {
1473 Some(ts) => Ok(ts.to_zoned(tz)),
1474 None => {
1475 let dt = self
1476 .to_datetime()
1477 .context(E::RequiredDateTimeForZoned)?;
1478 Ok(tz.to_zoned(dt)?)
1479 }
1480 }
1481 }
1482 (Some(offset), Some(iana)) => {
1483 let tz = db.get(iana)?;
1484 match self.timestamp {
1485 Some(ts) => {
1486 let zdt = ts.to_zoned(tz);
1487 if zdt.offset() != offset {
1488 return Err(Error::from(E::MismatchOffset {
1489 parsed: offset,
1490 got: zdt.offset(),
1491 }));
1492 }
1493 Ok(zdt)
1494 }
1495 None => {
1496 let dt = self
1497 .to_datetime()
1498 .context(E::RequiredDateTimeForZoned)?;
1499 let azdt =
1500 OffsetConflict::Reject.resolve(dt, offset, tz)?;
1501 // Guaranteed that if OffsetConflict::Reject doesn't
1502 // reject, then we get back an unambiguous zoned
1503 // datetime.
1504 let zdt = azdt.unambiguous().unwrap();
1505 Ok(zdt)
1506 }
1507 }
1508 }
1509 }
1510 }
1511
1512 /// Extracts a timestamp from this broken down time.
1513 ///
1514 /// This always prefers an explicitly set timestamp over other components
1515 /// of this `BrokenDownTime`. An explicit timestamp is set via
1516 /// [`BrokenDownTime::set_timestamp`]. This most commonly occurs by parsing
1517 /// a `%s` conversion specifier. When an explicit timestamp is not present,
1518 /// then the instant is derived from a civil datetime with a UTC offset.
1519 ///
1520 /// # Errors
1521 ///
1522 /// This returns an error if there weren't enough components to construct
1523 /// an instant. This requires either an explicitly set timestamp (via
1524 /// [`BrokenDownTime::set_timestamp`]) or enough data set to form a civil
1525 /// datetime _and_ a UTC offset.
1526 ///
1527 /// # Example
1528 ///
1529 /// This example shows how to parse a timestamp from a broken down time:
1530 ///
1531 /// ```
1532 /// use jiff::fmt::strtime;
1533 ///
1534 /// let ts = strtime::parse(
1535 /// "%F %H:%M %:z",
1536 /// "2024-07-14 21:14 -04:00",
1537 /// )?.to_timestamp()?;
1538 /// assert_eq!(ts.to_string(), "2024-07-15T01:14:00Z");
1539 ///
1540 /// # Ok::<(), Box<dyn std::error::Error>>(())
1541 /// ```
1542 ///
1543 /// # Example: conflicting data
1544 ///
1545 /// It is possible to parse both a timestamp and a civil datetime with an
1546 /// offset in the same string. This means there could be two potentially
1547 /// different ways to derive a timestamp from the parsed data. When that
1548 /// happens, any explicitly parsed timestamp (via `%s`) takes precedence
1549 /// for this method:
1550 ///
1551 /// ```
1552 /// use jiff::fmt::strtime;
1553 ///
1554 /// // The `%s` parse wins:
1555 /// let ts = strtime::parse(
1556 /// "%F %H:%M %:z and also %s",
1557 /// "2024-07-14 21:14 -04:00 and also 1760377242",
1558 /// )?.to_timestamp()?;
1559 /// assert_eq!(ts.to_string(), "2025-10-13T17:40:42Z");
1560 ///
1561 /// // Even when it is parsed first:
1562 /// let ts = strtime::parse(
1563 /// "%s and also %F %H:%M %:z",
1564 /// "1760377242 and also 2024-07-14 21:14 -04:00",
1565 /// )?.to_timestamp()?;
1566 /// assert_eq!(ts.to_string(), "2025-10-13T17:40:42Z");
1567 ///
1568 /// # Ok::<(), Box<dyn std::error::Error>>(())
1569 /// ```
1570 ///
1571 /// If you need access to the instant parsed by a civil datetime with an
1572 /// offset, then that is still available:
1573 ///
1574 /// ```
1575 /// use jiff::fmt::strtime;
1576 ///
1577 /// let tm = strtime::parse(
1578 /// "%F %H:%M %:z and also %s",
1579 /// "2024-07-14 21:14 -04:00 and also 1760377242",
1580 /// )?;
1581 /// assert_eq!(tm.to_timestamp()?.to_string(), "2025-10-13T17:40:42Z");
1582 ///
1583 /// let dt = tm.to_datetime()?;
1584 /// let offset = tm.offset().ok_or_else(|| "missing offset")?;
1585 /// let instant = offset.to_timestamp(dt)?;
1586 /// assert_eq!(instant.to_string(), "2024-07-15T01:14:00Z");
1587 ///
1588 /// # Ok::<(), Box<dyn std::error::Error>>(())
1589 /// ```
1590 #[inline]
1591 pub fn to_timestamp(&self) -> Result<Timestamp, Error> {
1592 // Previously, I had used this as the "fast path" and
1593 // put the conversion code below into a cold unlineable
1594 // function. But this "fast path" is actually the unusual
1595 // case. It's rare to parse a timestamp (as an integer
1596 // number of seconds since the Unix epoch) directly.
1597 // So the code below, while bigger, is the common case.
1598 // So it probably makes sense to keep it inlined.
1599 if let Some(timestamp) = self.timestamp() {
1600 return Ok(timestamp);
1601 }
1602 let dt =
1603 self.to_datetime().context(E::RequiredDateTimeForTimestamp)?;
1604 let offset = self.offset.ok_or(E::RequiredOffsetForTimestamp)?;
1605 offset.to_timestamp(dt).context(E::RangeTimestamp)
1606 }
1607
1608 /// Extracts a civil datetime from this broken down time.
1609 ///
1610 /// # Errors
1611 ///
1612 /// This returns an error if there weren't enough components to construct
1613 /// a civil datetime. This means there must be at least a year, month and
1614 /// day.
1615 ///
1616 /// It's okay if there are more units than are needed to construct a civil
1617 /// datetime. For example, if this broken down time contains an offset,
1618 /// then it won't prevent a conversion to a civil datetime.
1619 ///
1620 /// # Example
1621 ///
1622 /// This example shows how to parse a civil datetime from a broken down
1623 /// time:
1624 ///
1625 /// ```
1626 /// use jiff::fmt::strtime;
1627 ///
1628 /// let dt = strtime::parse("%F %H:%M", "2024-07-14 21:14")?.to_datetime()?;
1629 /// assert_eq!(dt.to_string(), "2024-07-14T21:14:00");
1630 ///
1631 /// # Ok::<(), Box<dyn std::error::Error>>(())
1632 /// ```
1633 #[inline]
1634 pub fn to_datetime(&self) -> Result<DateTime, Error> {
1635 let date = self.to_date().context(E::RequiredDateForDateTime)?;
1636 let time = self.to_time().context(E::RequiredTimeForDateTime)?;
1637 Ok(DateTime::from_parts(date, time))
1638 }
1639
1640 /// Extracts a civil date from this broken down time.
1641 ///
1642 /// This requires that the year (Gregorian or ISO 8601 week date year)
1643 /// is set along with a way to identify the day
1644 /// in the year. Typically identifying the day is done by setting the
1645 /// month and day, but this can also be done via a number of other means:
1646 ///
1647 /// * Via an ISO week date.
1648 /// * Via the day of the year.
1649 /// * Via a week date with Sunday as the start of the week.
1650 /// * Via a week date with Monday as the start of the week.
1651 ///
1652 /// # Errors
1653 ///
1654 /// This returns an error if there weren't enough components to construct
1655 /// a civil date, or if the components don't form into a valid date. This
1656 /// means there must be at least a year and a way to determine the day of
1657 /// the year.
1658 ///
1659 /// This will also return an error when there is a weekday component
1660 /// set to a value inconsistent with the date returned.
1661 ///
1662 /// It's okay if there are more units than are needed to construct a civil
1663 /// datetime. For example, if this broken down time contains a civil time,
1664 /// then it won't prevent a conversion to a civil date.
1665 ///
1666 /// # Example
1667 ///
1668 /// This example shows how to parse a civil date from a broken down time:
1669 ///
1670 /// ```
1671 /// use jiff::fmt::strtime;
1672 ///
1673 /// let date = strtime::parse("%m/%d/%y", "7/14/24")?.to_date()?;
1674 /// assert_eq!(date.to_string(), "2024-07-14");
1675 ///
1676 /// # Ok::<(), Box<dyn std::error::Error>>(())
1677 /// ```
1678 #[inline]
1679 pub fn to_date(&self) -> Result<Date, Error> {
1680 #[cold]
1681 #[inline(never)]
1682 fn to_date(tm: &BrokenDownTime) -> Result<Date, Error> {
1683 let Some(year) = tm.year else {
1684 // The Gregorian year and ISO week year may be parsed
1685 // separately. That is, they are two different fields. So if
1686 // the Gregorian year is absent, we might still have an ISO
1687 // 8601 week date.
1688 if let Some(date) = tm.to_date_from_iso()? {
1689 return Ok(date);
1690 }
1691 return Err(Error::from(E::RequiredYearForDate));
1692 };
1693 let mut date = tm.to_date_from_gregorian(year)?;
1694 if date.is_none() {
1695 date = tm.to_date_from_iso()?;
1696 }
1697 if date.is_none() {
1698 date = tm.to_date_from_day_of_year(year)?;
1699 }
1700 if date.is_none() {
1701 date = tm.to_date_from_week_sun(year)?;
1702 }
1703 if date.is_none() {
1704 date = tm.to_date_from_week_mon(year)?;
1705 }
1706 let Some(date) = date else {
1707 return Err(Error::from(E::RequiredSomeDayForDate));
1708 };
1709 if let Some(weekday) = tm.weekday {
1710 if weekday != date.weekday() {
1711 return Err(Error::from(E::MismatchWeekday {
1712 parsed: weekday,
1713 got: date.weekday(),
1714 }));
1715 }
1716 }
1717 Ok(date)
1718 }
1719
1720 // The common case is a simple Gregorian date.
1721 // We put the rest behind a non-inlineable function
1722 // to avoid code bloat for very uncommon cases.
1723 let (Some(year), Some(month), Some(day)) =
1724 (self.year, self.month, self.day)
1725 else {
1726 return to_date(self);
1727 };
1728 let date = Date::new(year, month, day).context(E::InvalidDate)?;
1729 if let Some(weekday) = self.weekday {
1730 if weekday != date.weekday() {
1731 return Err(Error::from(E::MismatchWeekday {
1732 parsed: weekday,
1733 got: date.weekday(),
1734 }));
1735 }
1736 }
1737 Ok(date)
1738 }
1739
1740 #[inline]
1741 fn to_date_from_gregorian(
1742 &self,
1743 year: i16,
1744 ) -> Result<Option<Date>, Error> {
1745 let (Some(month), Some(day)) = (self.month, self.day) else {
1746 return Ok(None);
1747 };
1748 Ok(Some(Date::new(year, month, day).context(E::InvalidDate)?))
1749 }
1750
1751 #[inline]
1752 fn to_date_from_day_of_year(
1753 &self,
1754 year: i16,
1755 ) -> Result<Option<Date>, Error> {
1756 let Some(doy) = self.day_of_year else { return Ok(None) };
1757 Ok(Some({
1758 let first = Date::new(year, 1, 1).unwrap();
1759 first.with().day_of_year(doy).build().context(E::InvalidDate)?
1760 }))
1761 }
1762
1763 #[inline]
1764 fn to_date_from_iso(&self) -> Result<Option<Date>, Error> {
1765 let (Some(y), Some(w), Some(d)) =
1766 (self.iso_week_year, self.iso_week, self.weekday)
1767 else {
1768 return Ok(None);
1769 };
1770 let wd = ISOWeekDate::new(y, w, d).context(E::InvalidISOWeekDate)?;
1771 Ok(Some(wd.date()))
1772 }
1773
1774 #[inline]
1775 fn to_date_from_week_sun(&self, year: i16) -> Result<Option<Date>, Error> {
1776 let (Some(week), Some(weekday)) = (self.week_sun, self.weekday) else {
1777 return Ok(None);
1778 };
1779 let week = i16::from(week);
1780 let wday = i16::from(weekday.to_sunday_zero_offset());
1781 let first_of_year = Date::new(year, 1, 1).context(E::InvalidDate)?;
1782 let first_sunday = first_of_year
1783 .nth_weekday_of_month(1, Weekday::Sunday)
1784 .map(|d| d.day_of_year())
1785 .context(E::InvalidDate)?;
1786 let doy = if week == 0 {
1787 let days_before_first_sunday = 7 - wday;
1788 let doy = first_sunday
1789 .checked_sub(days_before_first_sunday)
1790 .ok_or(E::InvalidWeekdaySunday { got: weekday })?;
1791 if doy == 0 {
1792 return Err(Error::from(E::InvalidWeekdaySunday {
1793 got: weekday,
1794 }));
1795 }
1796 doy
1797 } else {
1798 let days_since_first_sunday = (week - 1) * 7 + wday;
1799 let doy = first_sunday + days_since_first_sunday;
1800 doy
1801 };
1802 let date = first_of_year
1803 .with()
1804 .day_of_year(doy)
1805 .build()
1806 .context(E::InvalidDate)?;
1807 Ok(Some(date))
1808 }
1809
1810 #[inline]
1811 fn to_date_from_week_mon(&self, year: i16) -> Result<Option<Date>, Error> {
1812 let (Some(week), Some(weekday)) = (self.week_mon, self.weekday) else {
1813 return Ok(None);
1814 };
1815 let week = i16::from(week);
1816 let wday = i16::from(weekday.to_monday_zero_offset());
1817 let first_of_year = Date::new(year, 1, 1).context(E::InvalidDate)?;
1818 let first_monday = first_of_year
1819 .nth_weekday_of_month(1, Weekday::Monday)
1820 .map(|d| d.day_of_year())
1821 .context(E::InvalidDate)?;
1822 let doy = if week == 0 {
1823 let days_before_first_monday = 7 - wday;
1824 let doy = first_monday
1825 .checked_sub(days_before_first_monday)
1826 .ok_or(E::InvalidWeekdayMonday { got: weekday })?;
1827 if doy == 0 {
1828 return Err(Error::from(E::InvalidWeekdayMonday {
1829 got: weekday,
1830 }));
1831 }
1832 doy
1833 } else {
1834 let days_since_first_monday = (week - 1) * 7 + wday;
1835 let doy = first_monday + days_since_first_monday;
1836 doy
1837 };
1838 let date = first_of_year
1839 .with()
1840 .day_of_year(doy)
1841 .build()
1842 .context(E::InvalidDate)?;
1843 Ok(Some(date))
1844 }
1845
1846 /// Extracts a civil time from this broken down time.
1847 ///
1848 /// # Errors
1849 ///
1850 /// This returns an error if there weren't enough components to construct
1851 /// a civil time. Interestingly, this succeeds if there are no time units,
1852 /// since this will assume an absent time is midnight. However, this can
1853 /// still error when, for example, there are minutes but no hours.
1854 ///
1855 /// It's okay if there are more units than are needed to construct a civil
1856 /// time. For example, if this broken down time contains a date, then it
1857 /// won't prevent a conversion to a civil time.
1858 ///
1859 /// # Example
1860 ///
1861 /// This example shows how to parse a civil time from a broken down
1862 /// time:
1863 ///
1864 /// ```
1865 /// use jiff::fmt::strtime;
1866 ///
1867 /// let time = strtime::parse("%H:%M:%S", "21:14:59")?.to_time()?;
1868 /// assert_eq!(time.to_string(), "21:14:59");
1869 ///
1870 /// # Ok::<(), Box<dyn std::error::Error>>(())
1871 /// ```
1872 ///
1873 /// # Example: time defaults to midnight
1874 ///
1875 /// Since time defaults to midnight, one can parse an empty input string
1876 /// with an empty format string and still extract a `Time`:
1877 ///
1878 /// ```
1879 /// use jiff::fmt::strtime;
1880 ///
1881 /// let time = strtime::parse("", "")?.to_time()?;
1882 /// assert_eq!(time.to_string(), "00:00:00");
1883 ///
1884 /// # Ok::<(), Box<dyn std::error::Error>>(())
1885 /// ```
1886 ///
1887 /// # Example: invalid time
1888 ///
1889 /// Other than using illegal values (like `24` for hours), if lower units
1890 /// are parsed without higher units, then this results in an error:
1891 ///
1892 /// ```
1893 /// use jiff::fmt::strtime;
1894 ///
1895 /// assert!(strtime::parse("%M:%S", "15:36")?.to_time().is_err());
1896 ///
1897 /// # Ok::<(), Box<dyn std::error::Error>>(())
1898 /// ```
1899 ///
1900 /// # Example: invalid date
1901 ///
1902 /// Since validation of a date is only done when a date is requested, it is
1903 /// actually possible to parse an invalid date and extract the time without
1904 /// an error occurring:
1905 ///
1906 /// ```
1907 /// use jiff::fmt::strtime;
1908 ///
1909 /// // 31 is a legal day value, but not for June. However, this is
1910 /// // not validated unless you ask for a `Date` from the parsed
1911 /// // `BrokenDownTime`. Most other higher level accessors on this
1912 /// // type need to create a date, but this routine does not. So
1913 /// // asking for only a `time` will circumvent date validation!
1914 /// let tm = strtime::parse("%Y-%m-%d %H:%M:%S", "2024-06-31 21:14:59")?;
1915 /// let time = tm.to_time()?;
1916 /// assert_eq!(time.to_string(), "21:14:59");
1917 ///
1918 /// # Ok::<(), Box<dyn std::error::Error>>(())
1919 /// ```
1920 #[inline]
1921 pub fn to_time(&self) -> Result<Time, Error> {
1922 let Some(hour) = self.hour() else {
1923 if self.minute.is_some() {
1924 return Err(Error::from(E::MissingTimeHourForMinute));
1925 }
1926 if self.second.is_some() {
1927 return Err(Error::from(E::MissingTimeHourForSecond));
1928 }
1929 if self.subsec.is_some() {
1930 return Err(Error::from(E::MissingTimeHourForFractional));
1931 }
1932 return Ok(Time::midnight());
1933 };
1934 let Some(minute) = self.minute() else {
1935 if self.second.is_some() {
1936 return Err(Error::from(E::MissingTimeMinuteForSecond));
1937 }
1938 if self.subsec.is_some() {
1939 return Err(Error::from(E::MissingTimeMinuteForFractional));
1940 }
1941 return Time::new(hour, 0, 0, 0);
1942 };
1943 let Some(second) = self.second() else {
1944 if self.subsec.is_some() {
1945 return Err(Error::from(E::MissingTimeSecondForFractional));
1946 }
1947 return Time::new(hour, minute, 0, 0);
1948 };
1949 let Some(subsec) = self.subsec else {
1950 return Time::new(hour, minute, second, 0);
1951 };
1952 Time::new(hour, minute, second, subsec)
1953 }
1954
1955 /// Returns the parsed year, if available.
1956 ///
1957 /// This is also set when a 2 digit year is parsed. (But that's limited to
1958 /// the years 1969 to 2068, inclusive.)
1959 ///
1960 /// # Example
1961 ///
1962 /// This shows how to parse just a year:
1963 ///
1964 /// ```
1965 /// use jiff::fmt::strtime::BrokenDownTime;
1966 ///
1967 /// let tm = BrokenDownTime::parse("%Y", "2024")?;
1968 /// assert_eq!(tm.year(), Some(2024));
1969 ///
1970 /// # Ok::<(), Box<dyn std::error::Error>>(())
1971 /// ```
1972 ///
1973 /// And 2-digit years are supported too:
1974 ///
1975 /// ```
1976 /// use jiff::fmt::strtime::BrokenDownTime;
1977 ///
1978 /// let tm = BrokenDownTime::parse("%y", "24")?;
1979 /// assert_eq!(tm.year(), Some(2024));
1980 /// let tm = BrokenDownTime::parse("%y", "00")?;
1981 /// assert_eq!(tm.year(), Some(2000));
1982 /// let tm = BrokenDownTime::parse("%y", "69")?;
1983 /// assert_eq!(tm.year(), Some(1969));
1984 ///
1985 /// // 2-digit years have limited range. They must
1986 /// // be in the range 0-99.
1987 /// assert!(BrokenDownTime::parse("%y", "2024").is_err());
1988 ///
1989 /// # Ok::<(), Box<dyn std::error::Error>>(())
1990 /// ```
1991 #[inline]
1992 pub fn year(&self) -> Option<i16> {
1993 self.year
1994 }
1995
1996 /// Returns the parsed month, if available.
1997 ///
1998 /// # Example
1999 ///
2000 /// This shows a few different ways of parsing just a month:
2001 ///
2002 /// ```
2003 /// use jiff::fmt::strtime::BrokenDownTime;
2004 ///
2005 /// let tm = BrokenDownTime::parse("%m", "12")?;
2006 /// assert_eq!(tm.month(), Some(12));
2007 ///
2008 /// let tm = BrokenDownTime::parse("%B", "December")?;
2009 /// assert_eq!(tm.month(), Some(12));
2010 ///
2011 /// let tm = BrokenDownTime::parse("%b", "Dec")?;
2012 /// assert_eq!(tm.month(), Some(12));
2013 ///
2014 /// # Ok::<(), Box<dyn std::error::Error>>(())
2015 /// ```
2016 #[inline]
2017 pub fn month(&self) -> Option<i8> {
2018 self.month
2019 }
2020
2021 /// Returns the parsed day, if available.
2022 ///
2023 /// # Example
2024 ///
2025 /// This shows how to parse the day of the month:
2026 ///
2027 /// ```
2028 /// use jiff::fmt::strtime::BrokenDownTime;
2029 ///
2030 /// let tm = BrokenDownTime::parse("%d", "5")?;
2031 /// assert_eq!(tm.day(), Some(5));
2032 ///
2033 /// let tm = BrokenDownTime::parse("%d", "05")?;
2034 /// assert_eq!(tm.day(), Some(5));
2035 ///
2036 /// let tm = BrokenDownTime::parse("%03d", "005")?;
2037 /// assert_eq!(tm.day(), Some(5));
2038 ///
2039 /// // Parsing a day only works for all possible legal
2040 /// // values, even if, e.g., 31 isn't valid for all
2041 /// // possible year/month combinations.
2042 /// let tm = BrokenDownTime::parse("%d", "31")?;
2043 /// assert_eq!(tm.day(), Some(31));
2044 /// // This is true even if you're parsing a full date:
2045 /// let tm = BrokenDownTime::parse("%Y-%m-%d", "2024-04-31")?;
2046 /// assert_eq!(tm.day(), Some(31));
2047 /// // An error only occurs when you try to extract a date:
2048 /// assert!(tm.to_date().is_err());
2049 /// // But parsing a value that is always illegal will
2050 /// // result in an error:
2051 /// assert!(BrokenDownTime::parse("%d", "32").is_err());
2052 ///
2053 /// # Ok::<(), Box<dyn std::error::Error>>(())
2054 /// ```
2055 #[inline]
2056 pub fn day(&self) -> Option<i8> {
2057 self.day
2058 }
2059
2060 /// Returns the parsed day of the year (1-366), if available.
2061 ///
2062 /// # Example
2063 ///
2064 /// This shows how to parse the day of the year:
2065 ///
2066 /// ```
2067 /// use jiff::fmt::strtime::BrokenDownTime;
2068 ///
2069 /// let tm = BrokenDownTime::parse("%j", "5")?;
2070 /// assert_eq!(tm.day_of_year(), Some(5));
2071 /// assert_eq!(tm.to_string("%j")?, "005");
2072 /// assert_eq!(tm.to_string("%-j")?, "5");
2073 ///
2074 /// // Parsing the day of the year works for all possible legal
2075 /// // values, even if, e.g., 366 isn't valid for all possible
2076 /// // year/month combinations.
2077 /// let tm = BrokenDownTime::parse("%j", "366")?;
2078 /// assert_eq!(tm.day_of_year(), Some(366));
2079 /// // This is true even if you're parsing a year:
2080 /// let tm = BrokenDownTime::parse("%Y/%j", "2023/366")?;
2081 /// assert_eq!(tm.day_of_year(), Some(366));
2082 /// // An error only occurs when you try to extract a date:
2083 /// assert_eq!(
2084 /// tm.to_date().unwrap_err().to_string(),
2085 /// "invalid date: number of days for `2023` is invalid, \
2086 /// must be in range `1..=365`",
2087 /// );
2088 /// // But parsing a value that is always illegal will
2089 /// // result in an error:
2090 /// assert!(BrokenDownTime::parse("%j", "0").is_err());
2091 /// assert!(BrokenDownTime::parse("%j", "367").is_err());
2092 ///
2093 /// # Ok::<(), Box<dyn std::error::Error>>(())
2094 /// ```
2095 ///
2096 /// # Example: extract a [`Date`]
2097 ///
2098 /// This example shows how parsing a year and a day of the year enables
2099 /// the extraction of a date:
2100 ///
2101 /// ```
2102 /// use jiff::{civil::date, fmt::strtime::BrokenDownTime};
2103 ///
2104 /// let tm = BrokenDownTime::parse("%Y-%j", "2024-60")?;
2105 /// assert_eq!(tm.to_date()?, date(2024, 2, 29));
2106 ///
2107 /// # Ok::<(), Box<dyn std::error::Error>>(())
2108 /// ```
2109 ///
2110 /// When all of `%m`, `%d` and `%j` are used, then `%m` and `%d` take
2111 /// priority over `%j` when extracting a `Date` from a `BrokenDownTime`.
2112 /// However, `%j` is still parsed and accessible:
2113 ///
2114 /// ```
2115 /// use jiff::{civil::date, fmt::strtime::BrokenDownTime};
2116 ///
2117 /// let tm = BrokenDownTime::parse(
2118 /// "%Y-%m-%d (day of year: %j)",
2119 /// "2024-02-29 (day of year: 1)",
2120 /// )?;
2121 /// assert_eq!(tm.to_date()?, date(2024, 2, 29));
2122 /// assert_eq!(tm.day_of_year(), Some(1));
2123 ///
2124 /// # Ok::<(), Box<dyn std::error::Error>>(())
2125 /// ```
2126 #[inline]
2127 pub fn day_of_year(&self) -> Option<i16> {
2128 self.day_of_year
2129 }
2130
2131 /// Returns the parsed ISO 8601 week-based year, if available.
2132 ///
2133 /// This is also set when a 2 digit ISO 8601 week-based year is parsed.
2134 /// (But that's limited to the years 1969 to 2068, inclusive.)
2135 ///
2136 /// # Example
2137 ///
2138 /// This shows how to parse just an ISO 8601 week-based year:
2139 ///
2140 /// ```
2141 /// use jiff::fmt::strtime::BrokenDownTime;
2142 ///
2143 /// let tm = BrokenDownTime::parse("%G", "2024")?;
2144 /// assert_eq!(tm.iso_week_year(), Some(2024));
2145 ///
2146 /// # Ok::<(), Box<dyn std::error::Error>>(())
2147 /// ```
2148 ///
2149 /// And 2-digit years are supported too:
2150 ///
2151 /// ```
2152 /// use jiff::fmt::strtime::BrokenDownTime;
2153 ///
2154 /// let tm = BrokenDownTime::parse("%g", "24")?;
2155 /// assert_eq!(tm.iso_week_year(), Some(2024));
2156 /// let tm = BrokenDownTime::parse("%g", "00")?;
2157 /// assert_eq!(tm.iso_week_year(), Some(2000));
2158 /// let tm = BrokenDownTime::parse("%g", "69")?;
2159 /// assert_eq!(tm.iso_week_year(), Some(1969));
2160 ///
2161 /// // 2-digit years have limited range. They must
2162 /// // be in the range 0-99.
2163 /// assert!(BrokenDownTime::parse("%g", "2024").is_err());
2164 ///
2165 /// # Ok::<(), Box<dyn std::error::Error>>(())
2166 /// ```
2167 #[inline]
2168 pub fn iso_week_year(&self) -> Option<i16> {
2169 self.iso_week_year
2170 }
2171
2172 /// Returns the parsed ISO 8601 week-based number, if available.
2173 ///
2174 /// The week number is guaranteed to be in the range `1..53`. Week `1` is
2175 /// the first week of the year to contain 4 days.
2176 ///
2177 ///
2178 /// # Example
2179 ///
2180 /// This shows how to parse just an ISO 8601 week-based dates:
2181 ///
2182 /// ```
2183 /// use jiff::{civil::{Weekday, date}, fmt::strtime::BrokenDownTime};
2184 ///
2185 /// let tm = BrokenDownTime::parse("%G-W%V-%u", "2020-W01-1")?;
2186 /// assert_eq!(tm.iso_week_year(), Some(2020));
2187 /// assert_eq!(tm.iso_week(), Some(1));
2188 /// assert_eq!(tm.weekday(), Some(Weekday::Monday));
2189 /// assert_eq!(tm.to_date()?, date(2019, 12, 30));
2190 ///
2191 /// # Ok::<(), Box<dyn std::error::Error>>(())
2192 /// ```
2193 #[inline]
2194 pub fn iso_week(&self) -> Option<i8> {
2195 self.iso_week
2196 }
2197
2198 /// Returns the Sunday based week number.
2199 ///
2200 /// The week number returned is always in the range `0..=53`. Week `1`
2201 /// begins on the first Sunday of the year. Any days in the year prior to
2202 /// week `1` are in week `0`.
2203 ///
2204 /// # Example
2205 ///
2206 /// ```
2207 /// use jiff::{civil::{Weekday, date}, fmt::strtime::BrokenDownTime};
2208 ///
2209 /// let tm = BrokenDownTime::parse("%Y-%U-%w", "2025-01-0")?;
2210 /// assert_eq!(tm.year(), Some(2025));
2211 /// assert_eq!(tm.sunday_based_week(), Some(1));
2212 /// assert_eq!(tm.weekday(), Some(Weekday::Sunday));
2213 /// assert_eq!(tm.to_date()?, date(2025, 1, 5));
2214 ///
2215 /// # Ok::<(), Box<dyn std::error::Error>>(())
2216 /// ```
2217 #[inline]
2218 pub fn sunday_based_week(&self) -> Option<i8> {
2219 self.week_sun
2220 }
2221
2222 /// Returns the Monday based week number.
2223 ///
2224 /// The week number returned is always in the range `0..=53`. Week `1`
2225 /// begins on the first Monday of the year. Any days in the year prior to
2226 /// week `1` are in week `0`.
2227 ///
2228 /// # Example
2229 ///
2230 /// ```
2231 /// use jiff::{civil::{Weekday, date}, fmt::strtime::BrokenDownTime};
2232 ///
2233 /// let tm = BrokenDownTime::parse("%Y-%U-%w", "2025-01-1")?;
2234 /// assert_eq!(tm.year(), Some(2025));
2235 /// assert_eq!(tm.sunday_based_week(), Some(1));
2236 /// assert_eq!(tm.weekday(), Some(Weekday::Monday));
2237 /// assert_eq!(tm.to_date()?, date(2025, 1, 6));
2238 ///
2239 /// # Ok::<(), Box<dyn std::error::Error>>(())
2240 /// ```
2241 #[inline]
2242 pub fn monday_based_week(&self) -> Option<i8> {
2243 self.week_mon
2244 }
2245
2246 /// Returns the parsed hour, if available.
2247 ///
2248 /// The hour returned incorporates [`BrokenDownTime::meridiem`] if it's
2249 /// set. That is, if the actual parsed hour value is `1` but the meridiem
2250 /// is `PM`, then the hour returned by this method will be `13`.
2251 ///
2252 /// # Example
2253 ///
2254 /// This shows a how to parse an hour:
2255 ///
2256 /// ```
2257 /// use jiff::fmt::strtime::BrokenDownTime;
2258 ///
2259 /// let tm = BrokenDownTime::parse("%H", "13")?;
2260 /// assert_eq!(tm.hour(), Some(13));
2261 ///
2262 /// // When parsing a 12-hour clock without a
2263 /// // meridiem, the hour value is as parsed.
2264 /// let tm = BrokenDownTime::parse("%I", "1")?;
2265 /// assert_eq!(tm.hour(), Some(1));
2266 ///
2267 /// // If a meridiem is parsed, then it is used
2268 /// // to calculate the correct hour value.
2269 /// let tm = BrokenDownTime::parse("%I%P", "1pm")?;
2270 /// assert_eq!(tm.hour(), Some(13));
2271 ///
2272 /// // This works even if the hour and meridiem are
2273 /// // inconsistent with each other:
2274 /// let tm = BrokenDownTime::parse("%H%P", "13am")?;
2275 /// assert_eq!(tm.hour(), Some(1));
2276 ///
2277 /// # Ok::<(), Box<dyn std::error::Error>>(())
2278 /// ```
2279 #[inline]
2280 pub fn hour(&self) -> Option<i8> {
2281 self.hour
2282 }
2283
2284 /// Returns the parsed minute, if available.
2285 ///
2286 /// # Example
2287 ///
2288 /// This shows how to parse the minute:
2289 ///
2290 /// ```
2291 /// use jiff::fmt::strtime::BrokenDownTime;
2292 ///
2293 /// let tm = BrokenDownTime::parse("%M", "5")?;
2294 /// assert_eq!(tm.minute(), Some(5));
2295 ///
2296 /// # Ok::<(), Box<dyn std::error::Error>>(())
2297 /// ```
2298 #[inline]
2299 pub fn minute(&self) -> Option<i8> {
2300 self.minute
2301 }
2302
2303 /// Returns the parsed second, if available.
2304 ///
2305 /// # Example
2306 ///
2307 /// This shows how to parse the second:
2308 ///
2309 /// ```
2310 /// use jiff::fmt::strtime::BrokenDownTime;
2311 ///
2312 /// let tm = BrokenDownTime::parse("%S", "5")?;
2313 /// assert_eq!(tm.second(), Some(5));
2314 ///
2315 /// # Ok::<(), Box<dyn std::error::Error>>(())
2316 /// ```
2317 #[inline]
2318 pub fn second(&self) -> Option<i8> {
2319 self.second
2320 }
2321
2322 /// Returns the parsed subsecond nanosecond, if available.
2323 ///
2324 /// # Example
2325 ///
2326 /// This shows how to parse fractional seconds:
2327 ///
2328 /// ```
2329 /// use jiff::fmt::strtime::BrokenDownTime;
2330 ///
2331 /// let tm = BrokenDownTime::parse("%f", "123456")?;
2332 /// assert_eq!(tm.subsec_nanosecond(), Some(123_456_000));
2333 ///
2334 /// # Ok::<(), Box<dyn std::error::Error>>(())
2335 /// ```
2336 ///
2337 /// Note that when using `%.f`, the fractional component is optional!
2338 ///
2339 /// ```
2340 /// use jiff::fmt::strtime::BrokenDownTime;
2341 ///
2342 /// let tm = BrokenDownTime::parse("%S%.f", "1")?;
2343 /// assert_eq!(tm.second(), Some(1));
2344 /// assert_eq!(tm.subsec_nanosecond(), None);
2345 ///
2346 /// let tm = BrokenDownTime::parse("%S%.f", "1.789")?;
2347 /// assert_eq!(tm.second(), Some(1));
2348 /// assert_eq!(tm.subsec_nanosecond(), Some(789_000_000));
2349 ///
2350 /// # Ok::<(), Box<dyn std::error::Error>>(())
2351 /// ```
2352 #[inline]
2353 pub fn subsec_nanosecond(&self) -> Option<i32> {
2354 self.subsec
2355 }
2356
2357 /// Returns the parsed offset, if available.
2358 ///
2359 /// # Example
2360 ///
2361 /// This shows how to parse the offset:
2362 ///
2363 /// ```
2364 /// use jiff::{fmt::strtime::BrokenDownTime, tz::Offset};
2365 ///
2366 /// let tm = BrokenDownTime::parse("%z", "-0430")?;
2367 /// assert_eq!(
2368 /// tm.offset(),
2369 /// Some(Offset::from_seconds(-4 * 60 * 60 - 30 * 60).unwrap()),
2370 /// );
2371 /// let tm = BrokenDownTime::parse("%z", "-043059")?;
2372 /// assert_eq!(
2373 /// tm.offset(),
2374 /// Some(Offset::from_seconds(-4 * 60 * 60 - 30 * 60 - 59).unwrap()),
2375 /// );
2376 ///
2377 /// // Or, if you want colons:
2378 /// let tm = BrokenDownTime::parse("%:z", "-04:30")?;
2379 /// assert_eq!(
2380 /// tm.offset(),
2381 /// Some(Offset::from_seconds(-4 * 60 * 60 - 30 * 60).unwrap()),
2382 /// );
2383 ///
2384 /// # Ok::<(), Box<dyn std::error::Error>>(())
2385 /// ```
2386 #[inline]
2387 pub fn offset(&self) -> Option<Offset> {
2388 self.offset
2389 }
2390
2391 /// Returns the time zone IANA identifier, if available.
2392 ///
2393 /// Note that when `alloc` is disabled, this always returns `None`. (And
2394 /// there is no way to set it.)
2395 ///
2396 /// # Example
2397 ///
2398 /// This shows how to parse an IANA time zone identifier:
2399 ///
2400 /// ```
2401 /// use jiff::{fmt::strtime::BrokenDownTime, tz};
2402 ///
2403 /// let tm = BrokenDownTime::parse("%Q", "US/Eastern")?;
2404 /// assert_eq!(tm.iana_time_zone(), Some("US/Eastern"));
2405 /// assert_eq!(tm.offset(), None);
2406 ///
2407 /// // Note that %Q (and %:Q) also support parsing an offset
2408 /// // as a fallback. If that occurs, an IANA time zone
2409 /// // identifier is not available.
2410 /// let tm = BrokenDownTime::parse("%Q", "-0400")?;
2411 /// assert_eq!(tm.iana_time_zone(), None);
2412 /// assert_eq!(tm.offset(), Some(tz::offset(-4)));
2413 ///
2414 /// # Ok::<(), Box<dyn std::error::Error>>(())
2415 /// ```
2416 #[inline]
2417 pub fn iana_time_zone(&self) -> Option<&str> {
2418 #[cfg(feature = "alloc")]
2419 {
2420 self.iana.as_deref()
2421 }
2422 #[cfg(not(feature = "alloc"))]
2423 {
2424 None
2425 }
2426 }
2427
2428 /// Returns the parsed weekday, if available.
2429 ///
2430 /// # Example
2431 ///
2432 /// This shows a few different ways of parsing just a weekday:
2433 ///
2434 /// ```
2435 /// use jiff::{civil::Weekday, fmt::strtime::BrokenDownTime};
2436 ///
2437 /// let tm = BrokenDownTime::parse("%A", "Saturday")?;
2438 /// assert_eq!(tm.weekday(), Some(Weekday::Saturday));
2439 ///
2440 /// let tm = BrokenDownTime::parse("%a", "Sat")?;
2441 /// assert_eq!(tm.weekday(), Some(Weekday::Saturday));
2442 ///
2443 /// // A weekday is only available if it is explicitly parsed!
2444 /// let tm = BrokenDownTime::parse("%F", "2024-07-27")?;
2445 /// assert_eq!(tm.weekday(), None);
2446 /// // If you need a weekday derived from a parsed date, then:
2447 /// assert_eq!(tm.to_date()?.weekday(), Weekday::Saturday);
2448 ///
2449 /// # Ok::<(), Box<dyn std::error::Error>>(())
2450 /// ```
2451 ///
2452 /// Note that this will return the parsed weekday even if
2453 /// it's inconsistent with a parsed date:
2454 ///
2455 /// ```
2456 /// use jiff::{civil::{Weekday, date}, fmt::strtime::BrokenDownTime};
2457 ///
2458 /// let mut tm = BrokenDownTime::parse("%a, %F", "Wed, 2024-07-27")?;
2459 /// // 2024-07-27 is a Saturday, but Wednesday was parsed:
2460 /// assert_eq!(tm.weekday(), Some(Weekday::Wednesday));
2461 /// // An error only occurs when extracting a date:
2462 /// assert!(tm.to_date().is_err());
2463 /// // To skip the weekday, error checking, zero it out first:
2464 /// tm.set_weekday(None);
2465 /// assert_eq!(tm.to_date()?, date(2024, 7, 27));
2466 ///
2467 /// # Ok::<(), Box<dyn std::error::Error>>(())
2468 /// ```
2469 #[inline]
2470 pub fn weekday(&self) -> Option<Weekday> {
2471 self.weekday
2472 }
2473
2474 /// Returns the parsed meridiem, if available.
2475 ///
2476 /// When there is a conflict between the meridiem and the hour value, the
2477 /// meridiem takes precedence.
2478 ///
2479 /// # Example
2480 ///
2481 /// This shows a how to parse the meridiem:
2482 ///
2483 /// ```
2484 /// use jiff::fmt::strtime::{BrokenDownTime, Meridiem};
2485 ///
2486 /// let tm = BrokenDownTime::parse("%p", "AM")?;
2487 /// assert_eq!(tm.meridiem(), Some(Meridiem::AM));
2488 /// let tm = BrokenDownTime::parse("%P", "pm")?;
2489 /// assert_eq!(tm.meridiem(), Some(Meridiem::PM));
2490 ///
2491 /// // A meridiem takes precedence.
2492 /// let tm = BrokenDownTime::parse("%H%P", "13am")?;
2493 /// assert_eq!(tm.hour(), Some(1));
2494 /// assert_eq!(tm.meridiem(), Some(Meridiem::AM));
2495 ///
2496 /// # Ok::<(), Box<dyn std::error::Error>>(())
2497 /// ```
2498 #[inline]
2499 pub fn meridiem(&self) -> Option<Meridiem> {
2500 self.meridiem
2501 }
2502
2503 /// Returns the parsed timestamp, if available.
2504 ///
2505 /// Unlike [`BrokenDownTime::to_timestamp`], this only returns a timestamp
2506 /// that has been set explicitly via [`BrokenDownTime::set_timestamp`].
2507 /// For example, this occurs when parsing a `%s` conversion specifier.
2508 ///
2509 /// # Example
2510 ///
2511 /// This shows a how to parse the timestamp:
2512 ///
2513 /// ```
2514 /// use jiff::{fmt::strtime::BrokenDownTime, Timestamp};
2515 ///
2516 /// let tm = BrokenDownTime::parse("%s", "1760723100")?;
2517 /// assert_eq!(tm.timestamp(), Some(Timestamp::constant(1760723100, 0)));
2518 ///
2519 /// # Ok::<(), Box<dyn std::error::Error>>(())
2520 /// ```
2521 ///
2522 /// # Example: difference between `timestamp` and `to_timestamp`
2523 ///
2524 /// This shows how [`BrokenDownTime::to_timestamp`] will try to return
2525 /// a timestamp when one could be formed from other data, while
2526 /// [`BrokenDownTime::timestamp`] only returns a timestamp that has been
2527 /// explicitly set.
2528 ///
2529 /// ```
2530 /// use jiff::{fmt::strtime::BrokenDownTime, tz, Timestamp};
2531 ///
2532 /// let mut tm = BrokenDownTime::default();
2533 /// tm.set_year(Some(2025))?;
2534 /// tm.set_month(Some(10))?;
2535 /// tm.set_day(Some(17))?;
2536 /// tm.set_hour(Some(13))?;
2537 /// tm.set_minute(Some(45))?;
2538 /// tm.set_offset(Some(tz::offset(-4)));
2539 /// assert_eq!(tm.to_timestamp()?, Timestamp::constant(1760723100, 0));
2540 /// // No timestamp set!
2541 /// assert_eq!(tm.timestamp(), None);
2542 /// // A timestamp can be set, and it may not be consistent
2543 /// // with other data in `BrokenDownTime`.
2544 /// tm.set_timestamp(Some(Timestamp::UNIX_EPOCH));
2545 /// assert_eq!(tm.timestamp(), Some(Timestamp::UNIX_EPOCH));
2546 /// // And note that `BrokenDownTime::to_timestamp` will prefer
2547 /// // an explicitly set timestamp whenever possible.
2548 /// assert_eq!(tm.to_timestamp()?, Timestamp::UNIX_EPOCH);
2549 ///
2550 /// # Ok::<(), Box<dyn std::error::Error>>(())
2551 /// ```
2552 #[inline]
2553 pub fn timestamp(&self) -> Option<Timestamp> {
2554 self.timestamp
2555 }
2556
2557 /// Set the year on this broken down time.
2558 ///
2559 /// # Errors
2560 ///
2561 /// This returns an error if the given year is out of range.
2562 ///
2563 /// # Example
2564 ///
2565 /// ```
2566 /// use jiff::fmt::strtime::BrokenDownTime;
2567 ///
2568 /// let mut tm = BrokenDownTime::default();
2569 /// // out of range
2570 /// assert!(tm.set_year(Some(10_000)).is_err());
2571 /// tm.set_year(Some(2024))?;
2572 /// assert_eq!(tm.to_string("%Y")?, "2024");
2573 ///
2574 /// # Ok::<(), Box<dyn std::error::Error>>(())
2575 /// ```
2576 #[inline]
2577 pub fn set_year(&mut self, year: Option<i16>) -> Result<(), Error> {
2578 self.year = year.map(b::Year::check).transpose()?;
2579 Ok(())
2580 }
2581
2582 /// Set the month on this broken down time.
2583 ///
2584 /// # Errors
2585 ///
2586 /// This returns an error if the given month is out of range.
2587 ///
2588 /// # Example
2589 ///
2590 /// ```
2591 /// use jiff::fmt::strtime::BrokenDownTime;
2592 ///
2593 /// let mut tm = BrokenDownTime::default();
2594 /// // out of range
2595 /// assert!(tm.set_month(Some(0)).is_err());
2596 /// tm.set_month(Some(12))?;
2597 /// assert_eq!(tm.to_string("%B")?, "December");
2598 ///
2599 /// # Ok::<(), Box<dyn std::error::Error>>(())
2600 /// ```
2601 #[inline]
2602 pub fn set_month(&mut self, month: Option<i8>) -> Result<(), Error> {
2603 self.month = month.map(b::Month::check).transpose()?;
2604 Ok(())
2605 }
2606
2607 /// Set the day on this broken down time.
2608 ///
2609 /// # Errors
2610 ///
2611 /// This returns an error if the given day is out of range.
2612 ///
2613 /// Note that setting a day to a value that is legal in any context is
2614 /// always valid, even if it isn't valid for the year and month
2615 /// components already set.
2616 ///
2617 /// # Example
2618 ///
2619 /// ```
2620 /// use jiff::fmt::strtime::BrokenDownTime;
2621 ///
2622 /// let mut tm = BrokenDownTime::default();
2623 /// // out of range
2624 /// assert!(tm.set_day(Some(32)).is_err());
2625 /// tm.set_day(Some(31))?;
2626 /// assert_eq!(tm.to_string("%d")?, "31");
2627 ///
2628 /// // Works even if the resulting date is invalid.
2629 /// let mut tm = BrokenDownTime::default();
2630 /// tm.set_year(Some(2024))?;
2631 /// tm.set_month(Some(4))?;
2632 /// tm.set_day(Some(31))?; // April has 30 days, not 31
2633 /// assert_eq!(tm.to_string("%F")?, "2024-04-31");
2634 ///
2635 /// # Ok::<(), Box<dyn std::error::Error>>(())
2636 /// ```
2637 #[inline]
2638 pub fn set_day(&mut self, day: Option<i8>) -> Result<(), Error> {
2639 self.day = day.map(b::Day::check).transpose()?;
2640 Ok(())
2641 }
2642
2643 /// Set the day of year on this broken down time.
2644 ///
2645 /// # Errors
2646 ///
2647 /// This returns an error if the given day is out of range.
2648 ///
2649 /// Note that setting a day to a value that is legal in any context
2650 /// is always valid, even if it isn't valid for the year, month and
2651 /// day-of-month components already set.
2652 ///
2653 /// # Example
2654 ///
2655 /// ```
2656 /// use jiff::fmt::strtime::BrokenDownTime;
2657 ///
2658 /// let mut tm = BrokenDownTime::default();
2659 /// // out of range
2660 /// assert!(tm.set_day_of_year(Some(367)).is_err());
2661 /// tm.set_day_of_year(Some(31))?;
2662 /// assert_eq!(tm.to_string("%j")?, "031");
2663 ///
2664 /// // Works even if the resulting date is invalid.
2665 /// let mut tm = BrokenDownTime::default();
2666 /// tm.set_year(Some(2023))?;
2667 /// tm.set_day_of_year(Some(366))?; // 2023 wasn't a leap year
2668 /// assert_eq!(tm.to_string("%Y/%j")?, "2023/366");
2669 ///
2670 /// # Ok::<(), Box<dyn std::error::Error>>(())
2671 /// ```
2672 #[inline]
2673 pub fn set_day_of_year(&mut self, day: Option<i16>) -> Result<(), Error> {
2674 self.day_of_year = day.map(b::DayOfYear::check).transpose()?;
2675 Ok(())
2676 }
2677
2678 /// Set the ISO 8601 week-based year on this broken down time.
2679 ///
2680 /// # Errors
2681 ///
2682 /// This returns an error if the given year is out of range.
2683 ///
2684 /// # Example
2685 ///
2686 /// ```
2687 /// use jiff::fmt::strtime::BrokenDownTime;
2688 ///
2689 /// let mut tm = BrokenDownTime::default();
2690 /// // out of range
2691 /// assert!(tm.set_iso_week_year(Some(10_000)).is_err());
2692 /// tm.set_iso_week_year(Some(2024))?;
2693 /// assert_eq!(tm.to_string("%G")?, "2024");
2694 ///
2695 /// # Ok::<(), Box<dyn std::error::Error>>(())
2696 /// ```
2697 #[inline]
2698 pub fn set_iso_week_year(
2699 &mut self,
2700 year: Option<i16>,
2701 ) -> Result<(), Error> {
2702 self.iso_week_year = year.map(b::ISOYear::check).transpose()?;
2703 Ok(())
2704 }
2705
2706 /// Set the ISO 8601 week-based number on this broken down time.
2707 ///
2708 /// The week number must be in the range `1..53`. Week `1` is
2709 /// the first week of the year to contain 4 days.
2710 ///
2711 /// # Errors
2712 ///
2713 /// This returns an error if the given week number is out of range.
2714 ///
2715 /// # Example
2716 ///
2717 /// ```
2718 /// use jiff::{civil::Weekday, fmt::strtime::BrokenDownTime};
2719 ///
2720 /// let mut tm = BrokenDownTime::default();
2721 /// // out of range
2722 /// assert!(tm.set_iso_week(Some(0)).is_err());
2723 /// // out of range
2724 /// assert!(tm.set_iso_week(Some(54)).is_err());
2725 ///
2726 /// tm.set_iso_week_year(Some(2020))?;
2727 /// tm.set_iso_week(Some(1))?;
2728 /// tm.set_weekday(Some(Weekday::Monday));
2729 /// assert_eq!(tm.to_string("%G-W%V-%u")?, "2020-W01-1");
2730 /// assert_eq!(tm.to_string("%F")?, "2019-12-30");
2731 ///
2732 /// # Ok::<(), Box<dyn std::error::Error>>(())
2733 /// ```
2734 #[inline]
2735 pub fn set_iso_week(
2736 &mut self,
2737 week_number: Option<i8>,
2738 ) -> Result<(), Error> {
2739 self.iso_week = week_number.map(b::ISOWeek::check).transpose()?;
2740 Ok(())
2741 }
2742
2743 /// Set the Sunday based week number.
2744 ///
2745 /// The week number returned is always in the range `0..=53`. Week `1`
2746 /// begins on the first Sunday of the year. Any days in the year prior to
2747 /// week `1` are in week `0`.
2748 ///
2749 /// # Example
2750 ///
2751 /// ```
2752 /// use jiff::fmt::strtime::BrokenDownTime;
2753 ///
2754 /// let mut tm = BrokenDownTime::default();
2755 /// // out of range
2756 /// assert!(tm.set_sunday_based_week(Some(56)).is_err());
2757 /// tm.set_sunday_based_week(Some(9))?;
2758 /// assert_eq!(tm.to_string("%U")?, "09");
2759 ///
2760 /// # Ok::<(), Box<dyn std::error::Error>>(())
2761 /// ```
2762 #[inline]
2763 pub fn set_sunday_based_week(
2764 &mut self,
2765 week_number: Option<i8>,
2766 ) -> Result<(), Error> {
2767 self.week_sun = week_number.map(b::WeekNum::check).transpose()?;
2768 Ok(())
2769 }
2770
2771 /// Set the Monday based week number.
2772 ///
2773 /// The week number returned is always in the range `0..=53`. Week `1`
2774 /// begins on the first Monday of the year. Any days in the year prior to
2775 /// week `1` are in week `0`.
2776 ///
2777 /// # Example
2778 ///
2779 /// ```
2780 /// use jiff::fmt::strtime::BrokenDownTime;
2781 ///
2782 /// let mut tm = BrokenDownTime::default();
2783 /// // out of range
2784 /// assert!(tm.set_monday_based_week(Some(56)).is_err());
2785 /// tm.set_monday_based_week(Some(9))?;
2786 /// assert_eq!(tm.to_string("%W")?, "09");
2787 ///
2788 /// # Ok::<(), Box<dyn std::error::Error>>(())
2789 /// ```
2790 #[inline]
2791 pub fn set_monday_based_week(
2792 &mut self,
2793 week_number: Option<i8>,
2794 ) -> Result<(), Error> {
2795 self.week_mon = week_number.map(b::WeekNum::check).transpose()?;
2796 Ok(())
2797 }
2798
2799 /// Set the hour on this broken down time.
2800 ///
2801 /// # Errors
2802 ///
2803 /// This returns an error if the given hour is out of range.
2804 ///
2805 /// # Example
2806 ///
2807 /// ```
2808 /// use jiff::fmt::strtime::BrokenDownTime;
2809 ///
2810 /// let mut tm = BrokenDownTime::default();
2811 /// // out of range
2812 /// assert!(tm.set_hour(Some(24)).is_err());
2813 /// tm.set_hour(Some(0))?;
2814 /// assert_eq!(tm.to_string("%H")?, "00");
2815 /// assert_eq!(tm.to_string("%-H")?, "0");
2816 ///
2817 /// # Ok::<(), Box<dyn std::error::Error>>(())
2818 /// ```
2819 #[inline]
2820 pub fn set_hour(&mut self, hour: Option<i8>) -> Result<(), Error> {
2821 self.hour = hour.map(b::Hour::check).transpose()?;
2822 if let Some(meridiem) = self.meridiem {
2823 self.hour = self.hour.map(|hour| meridiem.adjust_hour(hour));
2824 }
2825 Ok(())
2826 }
2827
2828 /// Set the minute on this broken down time.
2829 ///
2830 /// # Errors
2831 ///
2832 /// This returns an error if the given minute is out of range.
2833 ///
2834 /// # Example
2835 ///
2836 /// ```
2837 /// use jiff::fmt::strtime::BrokenDownTime;
2838 ///
2839 /// let mut tm = BrokenDownTime::default();
2840 /// // out of range
2841 /// assert!(tm.set_minute(Some(60)).is_err());
2842 /// tm.set_minute(Some(59))?;
2843 /// assert_eq!(tm.to_string("%M")?, "59");
2844 /// assert_eq!(tm.to_string("%03M")?, "059");
2845 /// assert_eq!(tm.to_string("%_3M")?, " 59");
2846 ///
2847 /// # Ok::<(), Box<dyn std::error::Error>>(())
2848 /// ```
2849 #[inline]
2850 pub fn set_minute(&mut self, minute: Option<i8>) -> Result<(), Error> {
2851 self.minute = minute.map(b::Minute::check).transpose()?;
2852 Ok(())
2853 }
2854
2855 /// Set the second on this broken down time.
2856 ///
2857 /// # Errors
2858 ///
2859 /// This returns an error if the given second is out of range.
2860 ///
2861 /// Jiff does not support leap seconds, so the range of valid seconds is
2862 /// `0` to `59`, inclusive. Note though that when parsing, a parsed value
2863 /// of `60` is automatically constrained to `59`.
2864 ///
2865 /// # Example
2866 ///
2867 /// ```
2868 /// use jiff::fmt::strtime::BrokenDownTime;
2869 ///
2870 /// let mut tm = BrokenDownTime::default();
2871 /// // out of range
2872 /// assert!(tm.set_second(Some(60)).is_err());
2873 /// tm.set_second(Some(59))?;
2874 /// assert_eq!(tm.to_string("%S")?, "59");
2875 ///
2876 /// # Ok::<(), Box<dyn std::error::Error>>(())
2877 /// ```
2878 #[inline]
2879 pub fn set_second(&mut self, second: Option<i8>) -> Result<(), Error> {
2880 self.second = second.map(b::Second::check).transpose()?;
2881 Ok(())
2882 }
2883
2884 /// Set the subsecond nanosecond on this broken down time.
2885 ///
2886 /// # Errors
2887 ///
2888 /// This returns an error if the given number of nanoseconds is out of
2889 /// range. It must be non-negative and less than 1 whole second.
2890 ///
2891 /// # Example
2892 ///
2893 /// ```
2894 /// use jiff::fmt::strtime::BrokenDownTime;
2895 ///
2896 /// let mut tm = BrokenDownTime::default();
2897 /// // out of range
2898 /// assert!(tm.set_subsec_nanosecond(Some(1_000_000_000)).is_err());
2899 /// tm.set_subsec_nanosecond(Some(123_000_000))?;
2900 /// assert_eq!(tm.to_string("%f")?, "123");
2901 /// assert_eq!(tm.to_string("%.6f")?, ".123000");
2902 ///
2903 /// # Ok::<(), Box<dyn std::error::Error>>(())
2904 /// ```
2905 #[inline]
2906 pub fn set_subsec_nanosecond(
2907 &mut self,
2908 subsec_nanosecond: Option<i32>,
2909 ) -> Result<(), Error> {
2910 self.subsec =
2911 subsec_nanosecond.map(b::SubsecNanosecond::check).transpose()?;
2912 Ok(())
2913 }
2914
2915 /// Set the time zone offset on this broken down time.
2916 ///
2917 /// This can be useful for setting the offset after parsing if the offset
2918 /// is known from the context or from some out-of-band information.
2919 ///
2920 /// Note that one can set any legal offset value, regardless of whether
2921 /// it's consistent with the IANA time zone identifier on this broken down
2922 /// time (if it's set). Similarly, setting the offset does not actually
2923 /// change any other value in this broken down time.
2924 ///
2925 /// # Example: setting the offset after parsing
2926 ///
2927 /// One use case for this routine is when parsing a datetime _without_
2928 /// an offset, but where one wants to set an offset based on the context.
2929 /// For example, while it's usually not correct to assume a datetime is
2930 /// in UTC, if you know it is, then you can parse it into a [`Timestamp`]
2931 /// like so:
2932 ///
2933 /// ```
2934 /// use jiff::{fmt::strtime::BrokenDownTime, tz::Offset};
2935 ///
2936 /// let mut tm = BrokenDownTime::parse(
2937 /// "%Y-%m-%d at %H:%M:%S",
2938 /// "1970-01-01 at 01:00:00",
2939 /// )?;
2940 /// tm.set_offset(Some(Offset::UTC));
2941 /// // Normally this would fail since the parse
2942 /// // itself doesn't include an offset. It only
2943 /// // works here because we explicitly set the
2944 /// // offset after parsing.
2945 /// assert_eq!(tm.to_timestamp()?.to_string(), "1970-01-01T01:00:00Z");
2946 ///
2947 /// # Ok::<(), Box<dyn std::error::Error>>(())
2948 /// ```
2949 ///
2950 /// # Example: setting the offset is not "smart"
2951 ///
2952 /// This example shows how setting the offset on an existing broken down
2953 /// time does not impact any other field, even if the result printed is
2954 /// non-sensical:
2955 ///
2956 /// ```
2957 /// use jiff::{civil::date, fmt::strtime::BrokenDownTime, tz};
2958 ///
2959 /// let zdt = date(2024, 8, 28).at(14, 56, 0, 0).in_tz("US/Eastern")?;
2960 /// let mut tm = BrokenDownTime::from(&zdt);
2961 /// tm.set_offset(Some(tz::offset(12)));
2962 /// assert_eq!(
2963 /// tm.to_string("%Y-%m-%d at %H:%M:%S in %Q %:z")?,
2964 /// "2024-08-28 at 14:56:00 in US/Eastern +12:00",
2965 /// );
2966 ///
2967 /// # Ok::<(), Box<dyn std::error::Error>>(())
2968 /// ```
2969 #[inline]
2970 pub fn set_offset(&mut self, offset: Option<Offset>) {
2971 self.offset = offset;
2972 }
2973
2974 /// Set the IANA time zone identifier on this broken down time.
2975 ///
2976 /// This can be useful for setting the time zone after parsing if the time
2977 /// zone is known from the context or from some out-of-band information.
2978 ///
2979 /// Note that one can set any string value, regardless of whether it's
2980 /// consistent with the offset on this broken down time (if it's set).
2981 /// Similarly, setting the IANA time zone identifier does not actually
2982 /// change any other value in this broken down time.
2983 ///
2984 /// # Example: setting the IANA time zone identifier after parsing
2985 ///
2986 /// One use case for this routine is when parsing a datetime _without_ a
2987 /// time zone, but where one wants to set a time zone based on the context.
2988 ///
2989 /// ```
2990 /// use jiff::{fmt::strtime::BrokenDownTime};
2991 ///
2992 /// let mut tm = BrokenDownTime::parse(
2993 /// "%Y-%m-%d at %H:%M:%S",
2994 /// "1970-01-01 at 01:00:00",
2995 /// )?;
2996 /// tm.set_iana_time_zone(Some(String::from("US/Eastern")));
2997 /// // Normally this would fail since the parse
2998 /// // itself doesn't include an offset or a time
2999 /// // zone. It only works here because we
3000 /// // explicitly set the time zone after parsing.
3001 /// assert_eq!(
3002 /// tm.to_zoned()?.to_string(),
3003 /// "1970-01-01T01:00:00-05:00[US/Eastern]",
3004 /// );
3005 ///
3006 /// # Ok::<(), Box<dyn std::error::Error>>(())
3007 /// ```
3008 ///
3009 /// # Example: setting the IANA time zone identifier is not "smart"
3010 ///
3011 /// This example shows how setting the IANA time zone identifier on an
3012 /// existing broken down time does not impact any other field, even if the
3013 /// result printed is non-sensical:
3014 ///
3015 /// ```
3016 /// use jiff::{civil::date, fmt::strtime::BrokenDownTime};
3017 ///
3018 /// let zdt = date(2024, 8, 28).at(14, 56, 0, 0).in_tz("US/Eastern")?;
3019 /// let mut tm = BrokenDownTime::from(&zdt);
3020 /// tm.set_iana_time_zone(Some(String::from("Australia/Tasmania")));
3021 /// assert_eq!(
3022 /// tm.to_string("%Y-%m-%d at %H:%M:%S in %Q %:z")?,
3023 /// "2024-08-28 at 14:56:00 in Australia/Tasmania -04:00",
3024 /// );
3025 ///
3026 /// // In fact, it's not even required that the string
3027 /// // given be a valid IANA time zone identifier!
3028 /// let mut tm = BrokenDownTime::from(&zdt);
3029 /// tm.set_iana_time_zone(Some(String::from("Clearly/Invalid")));
3030 /// assert_eq!(
3031 /// tm.to_string("%Y-%m-%d at %H:%M:%S in %Q %:z")?,
3032 /// "2024-08-28 at 14:56:00 in Clearly/Invalid -04:00",
3033 /// );
3034 ///
3035 /// # Ok::<(), Box<dyn std::error::Error>>(())
3036 /// ```
3037 #[cfg(feature = "alloc")]
3038 #[inline]
3039 pub fn set_iana_time_zone(&mut self, id: Option<alloc::string::String>) {
3040 self.iana = id;
3041 }
3042
3043 /// Set the weekday on this broken down time.
3044 ///
3045 /// # Example
3046 ///
3047 /// ```
3048 /// use jiff::{civil::Weekday, fmt::strtime::BrokenDownTime};
3049 ///
3050 /// let mut tm = BrokenDownTime::default();
3051 /// tm.set_weekday(Some(Weekday::Saturday));
3052 /// assert_eq!(tm.to_string("%A")?, "Saturday");
3053 /// assert_eq!(tm.to_string("%a")?, "Sat");
3054 /// assert_eq!(tm.to_string("%^a")?, "SAT");
3055 ///
3056 /// # Ok::<(), Box<dyn std::error::Error>>(())
3057 /// ```
3058 ///
3059 /// Note that one use case for this routine is to enable parsing of
3060 /// weekdays in datetime, but skip checking that the weekday is valid for
3061 /// the parsed date.
3062 ///
3063 /// ```
3064 /// use jiff::{civil::date, fmt::strtime::BrokenDownTime};
3065 ///
3066 /// let mut tm = BrokenDownTime::parse("%a, %F", "Wed, 2024-07-27")?;
3067 /// // 2024-07-27 was a Saturday, so asking for a date fails:
3068 /// assert!(tm.to_date().is_err());
3069 /// // But we can remove the weekday from our broken down time:
3070 /// tm.set_weekday(None);
3071 /// assert_eq!(tm.to_date()?, date(2024, 7, 27));
3072 ///
3073 /// # Ok::<(), Box<dyn std::error::Error>>(())
3074 /// ```
3075 ///
3076 /// The advantage of this approach is that it still ensures the parsed
3077 /// weekday is a valid weekday (for example, `Wat` will cause parsing to
3078 /// fail), but doesn't require it to be consistent with the date. This
3079 /// is useful for interacting with systems that don't do strict error
3080 /// checking.
3081 #[inline]
3082 pub fn set_weekday(&mut self, weekday: Option<Weekday>) {
3083 self.weekday = weekday;
3084 }
3085
3086 /// Set the meridiem (AM/PM). This is most useful when doing custom
3087 /// parsing that involves 12-hour time.
3088 ///
3089 /// When there is a conflict between the meridiem and the hour value, the
3090 /// meridiem takes precedence.
3091 ///
3092 /// # Example
3093 ///
3094 /// This shows how to set a meridiem and its impact on the hour value:
3095 ///
3096 /// ```
3097 /// use jiff::{fmt::strtime::{BrokenDownTime, Meridiem}};
3098 ///
3099 /// let mut tm = BrokenDownTime::default();
3100 /// tm.set_hour(Some(3))?;
3101 /// tm.set_meridiem(Some(Meridiem::PM));
3102 /// let time = tm.to_time()?;
3103 /// assert_eq!(time.hour(), 15); // 3:00 PM = 15:00 in 24-hour time
3104 ///
3105 /// # Ok::<(), Box<dyn std::error::Error>>(())
3106 /// ```
3107 ///
3108 /// This shows how setting a meridiem influences formatting:
3109 ///
3110 /// ```
3111 /// use jiff::{fmt::strtime::{BrokenDownTime, Meridiem}};
3112 ///
3113 /// let mut tm = BrokenDownTime::default();
3114 /// tm.set_hour(Some(3))?;
3115 /// tm.set_minute(Some(4))?;
3116 /// tm.set_second(Some(5))?;
3117 /// tm.set_meridiem(Some(Meridiem::PM));
3118 /// assert_eq!(tm.to_string("%T")?, "15:04:05");
3119 ///
3120 /// # Ok::<(), Box<dyn std::error::Error>>(())
3121 /// ```
3122 ///
3123 /// And this shows how a conflict between the hour and meridiem is
3124 /// handled. Notably, the set meridiem still applies.
3125 ///
3126 /// ```
3127 /// use jiff::{fmt::strtime::{BrokenDownTime, Meridiem}};
3128 ///
3129 /// let mut tm = BrokenDownTime::default();
3130 /// tm.set_hour(Some(13))?;
3131 /// tm.set_minute(Some(4))?;
3132 /// tm.set_second(Some(5))?;
3133 /// tm.set_meridiem(Some(Meridiem::AM));
3134 /// assert_eq!(tm.to_string("%T")?, "01:04:05");
3135 ///
3136 /// # Ok::<(), Box<dyn std::error::Error>>(())
3137 /// ```
3138 #[inline]
3139 pub fn set_meridiem(&mut self, meridiem: Option<Meridiem>) {
3140 if let Some(meridiem) = meridiem {
3141 self.hour = self.hour.map(|hour| meridiem.adjust_hour(hour));
3142 }
3143 self.meridiem = meridiem;
3144 }
3145
3146 /// Set an explicit timestamp for this `BrokenDownTime`.
3147 ///
3148 /// An explicitly set timestamp takes precedence when using higher
3149 /// level convenience accessors such as [`BrokenDownTime::to_timestamp`]
3150 /// and [`BrokenDownTime::to_zoned`].
3151 ///
3152 /// # Example
3153 ///
3154 /// This shows how [`BrokenDownTime::to_timestamp`] will try to return
3155 /// a timestamp when one could be formed from other data, while
3156 /// [`BrokenDownTime::timestamp`] only returns a timestamp that has been
3157 /// explicitly set.
3158 ///
3159 /// ```
3160 /// use jiff::{fmt::strtime::BrokenDownTime, tz, Timestamp};
3161 ///
3162 /// let mut tm = BrokenDownTime::default();
3163 /// tm.set_year(Some(2025))?;
3164 /// tm.set_month(Some(10))?;
3165 /// tm.set_day(Some(17))?;
3166 /// tm.set_hour(Some(13))?;
3167 /// tm.set_minute(Some(45))?;
3168 /// tm.set_offset(Some(tz::offset(-4)));
3169 /// assert_eq!(tm.to_timestamp()?, Timestamp::constant(1760723100, 0));
3170 /// // No timestamp set!
3171 /// assert_eq!(tm.timestamp(), None);
3172 /// // A timestamp can be set, and it may not be consistent
3173 /// // with other data in `BrokenDownTime`.
3174 /// tm.set_timestamp(Some(Timestamp::UNIX_EPOCH));
3175 /// assert_eq!(tm.timestamp(), Some(Timestamp::UNIX_EPOCH));
3176 /// // And note that `BrokenDownTime::to_timestamp` will prefer
3177 /// // an explicitly set timestamp whenever possible.
3178 /// assert_eq!(tm.to_timestamp()?, Timestamp::UNIX_EPOCH);
3179 ///
3180 /// # Ok::<(), Box<dyn std::error::Error>>(())
3181 /// ```
3182 #[inline]
3183 pub fn set_timestamp(&mut self, timestamp: Option<Timestamp>) {
3184 self.timestamp = timestamp;
3185 }
3186}
3187
3188impl<'a> From<&'a Zoned> for BrokenDownTime {
3189 fn from(zdt: &'a Zoned) -> BrokenDownTime {
3190 // let offset_info = zdt.time_zone().to_offset_info(zdt.timestamp());
3191 #[cfg(feature = "alloc")]
3192 let iana = {
3193 use alloc::string::ToString;
3194 zdt.time_zone().iana_name().map(|s| s.to_string())
3195 };
3196 BrokenDownTime {
3197 offset: Some(zdt.offset()),
3198 timestamp: Some(zdt.timestamp()),
3199 tz: Some(zdt.time_zone().clone()),
3200 #[cfg(feature = "alloc")]
3201 iana,
3202 ..BrokenDownTime::from(zdt.datetime())
3203 }
3204 }
3205}
3206
3207impl From<Timestamp> for BrokenDownTime {
3208 fn from(ts: Timestamp) -> BrokenDownTime {
3209 let dt = Offset::UTC.to_datetime(ts);
3210 BrokenDownTime {
3211 offset: Some(Offset::UTC),
3212 timestamp: Some(ts),
3213 ..BrokenDownTime::from(dt)
3214 }
3215 }
3216}
3217
3218impl From<DateTime> for BrokenDownTime {
3219 fn from(dt: DateTime) -> BrokenDownTime {
3220 let (d, t) = (dt.date(), dt.time());
3221 BrokenDownTime {
3222 year: Some(d.year()),
3223 month: Some(d.month()),
3224 day: Some(d.day()),
3225 hour: Some(t.hour()),
3226 minute: Some(t.minute()),
3227 second: Some(t.second()),
3228 subsec: Some(t.subsec_nanosecond()),
3229 meridiem: Some(Meridiem::from(t)),
3230 ..BrokenDownTime::default()
3231 }
3232 }
3233}
3234
3235impl From<Date> for BrokenDownTime {
3236 fn from(d: Date) -> BrokenDownTime {
3237 BrokenDownTime {
3238 year: Some(d.year()),
3239 month: Some(d.month()),
3240 day: Some(d.day()),
3241 ..BrokenDownTime::default()
3242 }
3243 }
3244}
3245
3246impl From<ISOWeekDate> for BrokenDownTime {
3247 fn from(wd: ISOWeekDate) -> BrokenDownTime {
3248 BrokenDownTime {
3249 iso_week_year: Some(wd.year()),
3250 iso_week: Some(wd.week()),
3251 weekday: Some(wd.weekday()),
3252 ..BrokenDownTime::default()
3253 }
3254 }
3255}
3256
3257impl From<Time> for BrokenDownTime {
3258 fn from(t: Time) -> BrokenDownTime {
3259 BrokenDownTime {
3260 hour: Some(t.hour()),
3261 minute: Some(t.minute()),
3262 second: Some(t.second()),
3263 subsec: Some(t.subsec_nanosecond()),
3264 meridiem: Some(Meridiem::from(t)),
3265 ..BrokenDownTime::default()
3266 }
3267 }
3268}
3269
3270/// A "lazy" implementation of `std::fmt::Display` for `strftime`.
3271///
3272/// Values of this type are created by the `strftime` methods on the various
3273/// datetime types in this crate. For example, [`Zoned::strftime`].
3274///
3275/// A `Display` captures the information needed from the datetime and waits to
3276/// do the actual formatting when this type's `std::fmt::Display` trait
3277/// implementation is actually used.
3278///
3279/// # Errors and panics
3280///
3281/// This trait implementation configures formatting to use
3282/// [lenient mode](Config::lenient). This avoids panics occuring when using
3283/// APIs like [`Timestamp::strftime`] with unsupported or invalid formatting
3284/// directives. Without lenient mode, whenever formatting would fail, this
3285/// would surface as a panic when converting this display implementation to
3286/// a string.
3287///
3288/// # Example
3289///
3290/// This example shows how to format a zoned datetime using
3291/// [`Zoned::strftime`]:
3292///
3293/// ```
3294/// use jiff::civil::date;
3295///
3296/// let zdt = date(2024, 7, 15).at(16, 24, 59, 0).in_tz("America/New_York")?;
3297/// let string = zdt.strftime("%a, %-d %b %Y %T %z").to_string();
3298/// assert_eq!(string, "Mon, 15 Jul 2024 16:24:59 -0400");
3299///
3300/// # Ok::<(), Box<dyn std::error::Error>>(())
3301/// ```
3302///
3303/// Or use it directly when writing to something:
3304///
3305/// ```
3306/// use jiff::{civil::date, fmt::strtime};
3307///
3308/// let zdt = date(2024, 7, 15).at(16, 24, 59, 0).in_tz("America/New_York")?;
3309///
3310/// let string = format!("the date is: {}", zdt.strftime("%-m/%-d/%-Y"));
3311/// assert_eq!(string, "the date is: 7/15/2024");
3312///
3313/// # Ok::<(), Box<dyn std::error::Error>>(())
3314/// ```
3315///
3316/// # Example: errors are silently ignored
3317///
3318/// If the formatting string is malformed in some way, then it is silently
3319/// ignored. For example, when using an invalid formatting directive:
3320///
3321/// ```
3322/// use jiff::Zoned;
3323///
3324/// let zdt = Zoned::UNIX_EPOCH;
3325/// let string = zdt.strftime("%Y %").to_string();
3326/// assert_eq!(string, "1970 %");
3327/// ```
3328///
3329/// If one wants to surface errors from a formatting string, use a lower
3330/// level API:
3331///
3332/// ```
3333/// use jiff::Zoned;
3334///
3335/// let zdt = Zoned::UNIX_EPOCH;
3336/// assert_eq!(
3337/// jiff::fmt::strtime::format("%Y %", &zdt).unwrap_err().to_string(),
3338/// "strftime formatting failed: invalid format string, \
3339/// expected byte after `%`, but found end of format string",
3340/// );
3341/// ```
3342pub struct Display<'f> {
3343 pub(crate) fmt: &'f [u8],
3344 pub(crate) tm: BrokenDownTime,
3345}
3346
3347impl<'f> core::fmt::Display for Display<'f> {
3348 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
3349 use crate::fmt::StdFmtWrite;
3350
3351 self.tm
3352 .format_with_config(
3353 &Config::new().lenient(true),
3354 self.fmt,
3355 &mut StdFmtWrite(f),
3356 )
3357 .map_err(|_| core::fmt::Error)
3358 }
3359}
3360
3361impl<'f> core::fmt::Debug for Display<'f> {
3362 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
3363 f.debug_struct("Display")
3364 .field("fmt", &escape::Bytes(self.fmt))
3365 .field("tm", &self.tm)
3366 .finish()
3367 }
3368}
3369
3370/// A label to disambiguate hours on a 12-hour clock.
3371///
3372/// This can be accessed on a [`BrokenDownTime`] via
3373/// [`BrokenDownTime::meridiem`].
3374#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
3375#[cfg_attr(feature = "defmt", derive(defmt::Format))]
3376pub enum Meridiem {
3377 /// "ante meridiem" or "before midday."
3378 ///
3379 /// Specifically, this describes hours less than 12 on a 24-hour clock.
3380 AM,
3381 /// "post meridiem" or "after midday."
3382 ///
3383 /// Specifically, this describes hours greater than 11 on a 24-hour clock.
3384 PM,
3385}
3386
3387impl Meridiem {
3388 /// Adjusts 12-hour to 24-hour based on meridiem.
3389 fn adjust_hour(self, hour: i8) -> i8 {
3390 match self {
3391 Meridiem::AM => hour % 12,
3392 Meridiem::PM => (hour % 12) + 12,
3393 }
3394 }
3395}
3396
3397impl From<Time> for Meridiem {
3398 fn from(t: Time) -> Meridiem {
3399 if t.hour() < 12 {
3400 Meridiem::AM
3401 } else {
3402 Meridiem::PM
3403 }
3404 }
3405}
3406
3407/// These are "extensions" to the standard `strftime` conversion specifiers.
3408///
3409/// This type represents which flags and/or padding were provided with a
3410/// specifier. For example, `%_3d` uses 3 spaces of padding.
3411///
3412/// Currently, this type provides no structured introspection facilities. It
3413/// is exported and available only via implementations of the [`Custom`] trait
3414/// for reasons of semver compatible API evolution. If you have use cases for
3415/// introspecting this type, please open an issue.
3416#[derive(Clone, Debug)]
3417pub struct Extension {
3418 flag: Option<Flag>,
3419 width: Option<u8>,
3420 colons: u8,
3421}
3422
3423impl Extension {
3424 /// Parses an optional directive flag from the beginning of `fmt`. This
3425 /// assumes `fmt` is not empty and guarantees that the return unconsumed
3426 /// slice is also non-empty.
3427 #[cfg_attr(feature = "perf-inline", inline(always))]
3428 fn parse_flag<'i>(
3429 fmt: &'i [u8],
3430 ) -> Result<(Option<Flag>, &'i [u8]), Error> {
3431 let (&byte, tail) = fmt.split_first().unwrap();
3432 let flag = match byte {
3433 b'_' => Flag::PadSpace,
3434 b'0' => Flag::PadZero,
3435 b'-' => Flag::NoPad,
3436 b'^' => Flag::Uppercase,
3437 b'#' => Flag::Swapcase,
3438 _ => return Ok((None, fmt)),
3439 };
3440 if tail.is_empty() {
3441 return Err(Error::from(E::ExpectedDirectiveAfterFlag {
3442 flag: byte,
3443 }));
3444 }
3445 Ok((Some(flag), tail))
3446 }
3447
3448 /// Parses an optional width that comes after a (possibly absent) flag and
3449 /// before the specifier directive itself. And if a width is parsed, the
3450 /// slice returned does not contain it. (If that slice is empty, then an
3451 /// error is returned.)
3452 ///
3453 /// Note that this is also used to parse precision settings for `%f`
3454 /// and `%.f`. In the former case, the width is just re-interpreted as
3455 /// a precision setting. In the latter case, something like `%5.9f` is
3456 /// technically valid, but the `5` is ignored.
3457 #[cfg_attr(feature = "perf-inline", inline(always))]
3458 fn parse_width<'i>(
3459 fmt: &'i [u8],
3460 ) -> Result<(Option<u8>, &'i [u8]), Error> {
3461 let mut digits = 0;
3462 while digits < fmt.len() && fmt[digits].is_ascii_digit() {
3463 digits += 1;
3464 }
3465 if digits == 0 {
3466 return Ok((None, fmt));
3467 }
3468 let (digits, fmt) = fmt.split_at(digits);
3469 let width = util::parse::i64(digits).context(E::FailedWidth)?;
3470 let width = u8::try_from(width).map_err(|_| E::RangeWidth)?;
3471 if fmt.is_empty() {
3472 return Err(Error::from(E::ExpectedDirectiveAfterWidth));
3473 }
3474 Ok((Some(width), fmt))
3475 }
3476
3477 /// Parses an optional number of colons.
3478 ///
3479 /// This is meant to be used immediately before the conversion specifier
3480 /// (after the flag and width has been parsed).
3481 ///
3482 /// This supports parsing up to 3 colons. The colons are used in some cases
3483 /// for alternate specifiers. e.g., `%:Q` or `%:::z`.
3484 #[cfg_attr(feature = "perf-inline", inline(always))]
3485 fn parse_colons<'i>(fmt: &'i [u8]) -> Result<(u8, &'i [u8]), Error> {
3486 let mut colons = 0;
3487 while colons < 3 && colons < fmt.len() && fmt[colons] == b':' {
3488 colons += 1;
3489 }
3490 let fmt = &fmt[usize::from(colons)..];
3491 if colons > 0 && fmt.is_empty() {
3492 return Err(Error::from(E::ExpectedDirectiveAfterColons));
3493 }
3494 Ok((u8::try_from(colons).unwrap(), fmt))
3495 }
3496}
3497
3498/// The different flags one can set. They are mutually exclusive.
3499#[derive(Clone, Copy, Debug)]
3500enum Flag {
3501 PadSpace,
3502 PadZero,
3503 NoPad,
3504 Uppercase,
3505 Swapcase,
3506}
3507
3508/// Returns the "full" weekday name.
3509#[cfg_attr(feature = "perf-inline", inline(always))]
3510fn weekday_name_full(wd: Weekday) -> &'static str {
3511 match wd {
3512 Weekday::Sunday => "Sunday",
3513 Weekday::Monday => "Monday",
3514 Weekday::Tuesday => "Tuesday",
3515 Weekday::Wednesday => "Wednesday",
3516 Weekday::Thursday => "Thursday",
3517 Weekday::Friday => "Friday",
3518 Weekday::Saturday => "Saturday",
3519 }
3520}
3521
3522/// Returns an abbreviated weekday name.
3523#[cfg_attr(feature = "perf-inline", inline(always))]
3524fn weekday_name_abbrev(wd: Weekday) -> &'static str {
3525 match wd {
3526 Weekday::Sunday => "Sun",
3527 Weekday::Monday => "Mon",
3528 Weekday::Tuesday => "Tue",
3529 Weekday::Wednesday => "Wed",
3530 Weekday::Thursday => "Thu",
3531 Weekday::Friday => "Fri",
3532 Weekday::Saturday => "Sat",
3533 }
3534}
3535
3536/// Returns the "full" month name.
3537///
3538/// # Panics
3539///
3540/// When the given value is not in the range `1..=12`.
3541#[cfg_attr(feature = "perf-inline", inline(always))]
3542fn month_name_full(month: i8) -> &'static str {
3543 match month {
3544 1 => "January",
3545 2 => "February",
3546 3 => "March",
3547 4 => "April",
3548 5 => "May",
3549 6 => "June",
3550 7 => "July",
3551 8 => "August",
3552 9 => "September",
3553 10 => "October",
3554 11 => "November",
3555 12 => "December",
3556 unk => unreachable!("invalid month {unk}"),
3557 }
3558}
3559
3560/// Returns the abbreviated month name.
3561///
3562/// # Panics
3563///
3564/// When the given value is not in the range `1..=12`.
3565#[cfg_attr(feature = "perf-inline", inline(always))]
3566fn month_name_abbrev(month: i8) -> &'static str {
3567 match month {
3568 1 => "Jan",
3569 2 => "Feb",
3570 3 => "Mar",
3571 4 => "Apr",
3572 5 => "May",
3573 6 => "Jun",
3574 7 => "Jul",
3575 8 => "Aug",
3576 9 => "Sep",
3577 10 => "Oct",
3578 11 => "Nov",
3579 12 => "Dec",
3580 unk => unreachable!("invalid month {unk}"),
3581 }
3582}
3583
3584#[cfg(test)]
3585mod tests {
3586 use super::*;
3587
3588 // See: https://github.com/BurntSushi/jiff/issues/62
3589 #[test]
3590 fn parse_non_delimited() {
3591 insta::assert_snapshot!(
3592 Timestamp::strptime("%Y%m%d-%H%M%S%z", "20240730-005625+0400").unwrap(),
3593 @"2024-07-29T20:56:25Z",
3594 );
3595 insta::assert_snapshot!(
3596 Zoned::strptime("%Y%m%d-%H%M%S%z", "20240730-005625+0400").unwrap(),
3597 @"2024-07-30T00:56:25+04:00[+04:00]",
3598 );
3599 }
3600
3601 // Regression test for format strings with non-ASCII in them.
3602 //
3603 // We initially didn't support non-ASCII because I had thought it wouldn't
3604 // be used. i.e., If someone wanted to do something with non-ASCII, then
3605 // I thought they'd want to be using something more sophisticated that took
3606 // locale into account. But apparently not.
3607 //
3608 // See: https://github.com/BurntSushi/jiff/issues/155
3609 #[test]
3610 fn ok_non_ascii() {
3611 let fmt = "%Y年%m月%d日,%H时%M分%S秒";
3612 let dt = crate::civil::date(2022, 2, 4).at(3, 58, 59, 0);
3613 insta::assert_snapshot!(
3614 dt.strftime(fmt),
3615 @"2022年02月04日,03时58分59秒",
3616 );
3617 insta::assert_debug_snapshot!(
3618 DateTime::strptime(fmt, "2022年02月04日,03时58分59秒").unwrap(),
3619 @"2022-02-04T03:58:59",
3620 );
3621 }
3622}