(pattern-format)=
# Pattern format
```{eval-rst}
.. currentmodule:: whenever
```
Custom format and parse patterns allow you to format datetime values into
strings and parse strings into datetime values, using a pattern string
that describes the expected format.
## Quick example
```python
>>> from whenever import Date, Time, OffsetDateTime, hours
>>> Date(2024, 3, 15).format("YYYY/MM/DD")
'2024/03/15'
>>> Date.parse("2024/03/15", format="YYYY/MM/DD")
Date("2024-03-15")
>>> OffsetDateTime(2024, 3, 15, 14, 30, offset=+2).format(
... "EEE, DD MMM YYYY hh:mm:ssxxx"
... )
'Fri, 15 Mar 2024 14:30:00+02:00'
```
## Specifiers
Each pattern is a string containing specifiers and literal text.
Specifiers are sequences of the same letter that are replaced by
the corresponding value.
### Date specifiers
| Symbol | Meaning | Pattern | Example output |
|:---------|:---------------------------|:---------------|:--------------|
| `Y` | year | `YY` [^1]
`YYYY` | `24`
`2024` |
| `M` | month | `M`
`MM`
`MMM`
`MMMM` | `3`
`03`
`Mar`
`March` |
| `D` | day of month | `D`
`DD` | `5`
`05` |
| `E` | day of week [^2] | `EEE`
`EEEE` | `Fri`
`Friday` |
### Time specifiers
| Symbol | Meaning | Pattern | Example output |
|:---------|:---------------------------|:---------------|:--------------|
| `h` | hour | `h`
`hh` | `4`
`04` |
| `i` | hour (12-hour) | `i`
`ii` | `4`
`04` |
| `m` | minute | `m`
`mm` | `5`
`05` |
| `s` | second | `s`
`ss` | `5`
`05` |
| `S` | second, optional [^3] | `SS` | `05`, (omitted) |
| `f` | fractional seconds, exact digits | `f`
`ff`
`fff`
...
`fffffffff` | `1`
`12`, `00`
`123`, `400`
...
`123456789`, `374930000` |
| `F` | fractional seconds, trimmed [^4] | `F`
`FF`
`FFF`
...
`FFFFFFFFF` | `1`
`12`, (omitted)
`123`, `4`
...
`123456789`, `37493` |
| `a` | AM/PM [^5] | `a`
`aa` | `P`
`PM` |
:::{admonition} Optional seconds
:class: hint
`SS` omits the seconds component entirely when **both** seconds *and*
nanoseconds are zero, allowing compact times like `14:30` alongside full
times like `14:30:05` in the same format string.
- When seconds *or* nanoseconds are non-zero, `SS` writes two zero-padded
digits
- When **both are zero**, nothing is written. Any preceeding colon disappears as well.
```python
>>> Time(14, 30, 0).format("hh:mm:SS")
'14:30'
>>> Time(14, 30, 5).format("hh:mm:SS")
'14:30:05'
>>> Time(14, 30, 0, nanosecond=500_000_000).format("hh:mm:SS")
'14:30:00'
>>> Time(14, 30, 0).format("hh:mm:SS.FFF")
'14:30'
>>> Time(14, 30, 0, nanosecond=500_000_000).format("hh:mm:SS.FFF")
'14:30:00.5'
```
:::
### Offset and timezone specifiers
See {ref}`timezones-explained` for background on timezones, offsets, and abbreviations.
| Symbol | Meaning | Pattern | Example output |
|:---------|:---------------------------|:---------------|:--------------|
| `x` | Offset hours and minutes | `x`
`xx`
`xxx`
`xxxx`
`xxxxx` | `+02`
`+0230`
`+02:30`
`+023045`
`+02:30:45` |
| `X` | Offset hours and minutes, with `Z` for zero offset | `X`
`XX`
`XXX`
`XXXX`
`XXXXX` | `+02`
`+0230`
`+02:30`
`+023045`
`+02:30:45` or `Z` when zero |
| `V` | IANA timezone ID | `VV` | `Europe/Paris` |
| `z` | Timezone abbreviation [^6] | `zz` | `CET`, `CEST` |
```{admonition} Choosing between x and X
:class: hint
Use uppercase `X` when you want `Z` for zero offset
(e.g. {class}`Instant` formatting).
Use lowercase `x` when you always want a numeric offset
(e.g. {class}`OffsetDateTime` formatting).
```
```python
>>> ZonedDateTime(2024, 7, 15, 14, 30, tz="Europe/Paris").format(
... "YYYY-MM-DD hh:mm zz"
... )
'2024-07-15 14:30 CEST'
>>> ZonedDateTime.parse(
... "2024-07-15 14:30+02:00[Europe/Paris]",
... format="YYYY-MM-DD hh:mmxxx'['VV']'",
... )
ZonedDateTime("2024-07-15 14:30:00+02:00[Europe/Paris]")
```
### Supported specifiers per type
| Type | Date | Time | `x`/`X` | `VV`/`zz` |
|:------------------|:----:|:----:|:--------:|:----:|
| {class}`Date` | ✅ | ❌ | ❌ | ❌ |
| {class}`Time` | ❌ | ✅ | ❌ | ❌ |
| {class}`PlainDateTime` | ✅ | ✅ | ❌ | ❌ |
| {class}`OffsetDateTime`| ✅ | ✅ | ✅ | ❌ |
| {class}`ZonedDateTime` | ✅ | ✅ | ✅ | ✅ |
| {class}`Instant` | ✅ | ✅ | ✅ | ❌ |
## Literal text
Common non-letter characters (`:`, `-`, `/`, `.`, `,`, `;`,
`_`, `(`, `)`, digits, spaces, and other ASCII
punctuation) are treated as literals by default:
```python
>>> Date(2024, 3, 15).format("YYYY/MM/DD")
'2024/03/15'
```
**Letters must be quoted** with single quotes to be used as literals.
This prevents accidental use of reserved characters and keeps options
open for future specifiers:
```python
>>> Date(2024, 3, 15).format("YYYY'xx'MM")
'2024xx03'
```
To include a literal single quote, use `''`:
```python
>>> Date(2024, 3, 15).format("YYYY''MM")
"2024'03"
```
### Restrictions
- **ASCII-only**: Pattern strings must contain only ASCII characters.
Non-ASCII characters raise ``ValueError``.
- **Reserved characters**: `<`, `>`, `[`, `]`, `{`, `}`, and `#`
are reserved for future use and cannot appear unquoted.
- **No duplicate fields**: A pattern cannot contain two specifiers that
set the same value. For example, `MM` and `MMM` both set the month,
so `"DD MM MMM YYYY"` is invalid.
## Parsing requirements
Some types require specific fields in the parse pattern:
- {meth}`OffsetDateTime.parse() ` requires an offset (`x`/`X`)
- {meth}`ZonedDateTime.parse() ` requires `VV` (timezone ID).
An offset (`x`/`X`) is optional but recommended for DST disambiguation.
- {meth}`Instant.parse() ` requires an offset (`x`/`X`)
All types that include date fields require `YYYY`, `MM`, and `DD`.
A second value of ``60`` (leap second) is accepted and normalized to ``59``.
See [](faq-leap-seconds) for details.
## Comparison with strftime
The {meth}`~OffsetDateTime.parse_strptime` methods on {class}`OffsetDateTime` and
{class}`PlainDateTime` are deprecated in favor of
{meth}`~OffsetDateTime.parse`. Here's a migration guide:
| strftime | Pattern | Notes |
|:---------|:--------|:------|
| `%Y` | `YYYY`| |
| `%y` | `YY` | Format only |
| `%m` | `MM` | |
| `%b` | `MMM` | |
| `%B` | `MMMM`| |
| `%d` | `DD` | |
| `%a` | `EEE` | |
| `%A` | `EEEE`| |
| `%H` | `hh` | Note: `hh` = 24-hour |
| `%I` | `ii` | Note: `ii` = 12-hour |
| `%M` | `mm` | |
| `%S` | `ss` | |
| `%f` | `ffffff`| microseconds (6 digits) |
| `%p` | `aa` | |
| `%z` | `xxxx` | `XXXX` for Z-style |
| `%:z` | `xxxxx` | `XXXXX` for Z-style |
| `%Z` | — | Abbreviations are not supported for parsing. See {ref}`timezones-explained`. |
[^1]: `YY` is only supported for formatting. When parsing, use `YYYY` to avoid ambiguity.
[^2]: During parsing, weekday names are validated against the parsed date. A mismatch raises ``ValueError``.
[^3]: Omitted when both seconds and nanoseconds are zero.
[^4]: Omitted when the value is zero, with preceding `.` also omitted.
[^5]: AM/PM is determined by the hour value. Using `i`/`ii` without `a`/`aa` emits a warning about ambiguity.
[^6]: Timezone abbreviations are ambiguous and not supported for parsing. Use `VV` (IANA timezone ID) instead. See {ref}`timezones-explained` for details.