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