strftime/
lib.rs

1#![forbid(unsafe_code, reason = "this crate is not marked as `unsafe`")]
2#![warn(
3    clippy::all,
4    clippy::pedantic,
5    clippy::cargo,
6    reason = "artichoke standard clippy pragmas"
7)]
8#![allow(
9    unknown_lints,
10    clippy::cast_possible_truncation,
11    reason = "artichoke standard pragmas"
12)]
13#![warn(
14    missing_debug_implementations,
15    missing_docs,
16    rust_2018_idioms,
17    trivial_casts,
18    trivial_numeric_casts,
19    unsafe_op_in_unsafe_fn,
20    unused_qualifications,
21    variant_size_differences,
22    reason = "artichoke standard rust pragmas"
23)]
24// Enable feature callouts in generated documentation:
25// https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html
26//
27// This approach is borrowed from tokio.
28#![cfg_attr(docsrs, feature(doc_cfg))]
29#![cfg_attr(docsrs, feature(doc_alias))]
30
31//! This crate provides a Ruby 3.1.2 compatible `strftime` function, which
32//! formats time according to the directives in the given format string.
33//!
34//! The directives begin with a percent `%` character. Any text not listed as a
35//! directive will be passed through to the output string.
36//!
37//! Each directive consists of a percent `%` character, zero or more flags,
38//! optional minimum field width, optional modifier and a conversion specifier
39//! as follows:
40//!
41//! ```text
42//! %<flags><width><modifier><conversion>
43//! ```
44//!
45//! # Usage
46//!
47//! The various `strftime` functions in this crate take a generic _time_
48//! parameter that implements the [`Time`] trait.
49//!
50//! # Format Specifiers
51//!
52//! ## Flags
53//!
54//! | Flag | Description                                                                            |
55//! |------|----------------------------------------------------------------------------------------|
56//! |  `-` | Use left padding, ignoring width and removing all other padding options in most cases. |
57//! |  `_` | Use spaces for padding.                                                                |
58//! |  `0` | Use zeros for padding.                                                                 |
59//! |  `^` | Convert the resulting string to uppercase.                                             |
60//! |  `#` | Change case of the resulting string.                                                   |
61//!
62//!
63//! ## Width
64//!
65//! The minimum field width specifies the minimum width.
66//!
67//! ## Modifiers
68//!
69//! The modifiers are `E` and `O`. They are ignored.
70//!
71//! ## Specifiers
72//!
73//! | Specifier  | Example       | Description                                                                                                           |
74//! |------------|---------------|-----------------------------------------------------------------------------------------------------------------------|
75//! |    `%Y`    | `-2001`       | Year with century if provided, zero-padded to at least 4 digits plus the possible negative sign.                      |
76//! |    `%C`    | `-21`         | `Year / 100` using Euclidean division, zero-padded to at least 2 digits.                                              |
77//! |    `%y`    | `99`          | `Year % 100` in `00..=99`, using Euclidean remainder, zero-padded to 2 digits.                                        |
78//! |    `%m`    | `01`          | Month of the year in `01..=12`, zero-padded to 2 digits.                                                              |
79//! |    `%B`    | `July`        | Locale independent full month name.                                                                                   |
80//! | `%b`, `%h` | `Jul`         | Locale independent abbreviated month name, using the first 3 letters.                                                 |
81//! |    `%d`    | `01`          | Day of the month in `01..=31`, zero-padded to 2 digits.                                                               |
82//! |    `%e`    | ` 1`          | Day of the month in ` 1..=31`, blank-padded to 2 digits.                                                              |
83//! |    `%j`    | `001`         | Day of the year in `001..=366`, zero-padded to 3 digits.                                                              |
84//! |    `%H`    | `00`          | Hour of the day (24-hour clock) in `00..=23`, zero-padded to 2 digits.                                                |
85//! |    `%k`    | ` 0`          | Hour of the day (24-hour clock) in ` 0..=23`, blank-padded to 2 digits.                                               |
86//! |    `%I`    | `01`          | Hour of the day (12-hour clock) in `01..=12`, zero-padded to 2 digits.                                                |
87//! |    `%l`    | ` 1`          | Hour of the day (12-hour clock) in ` 1..=12`, blank-padded to 2 digits.                                               |
88//! |    `%P`    | `am`          | Lowercase meridian indicator (`"am"` or `"pm"`).                                                                      |
89//! |    `%p`    | `AM`          | Uppercase meridian indicator (`"AM"` or `"PM"`).                                                                      |
90//! |    `%M`    | `00`          | Minute of the hour in `00..=59`, zero-padded to 2 digits.                                                             |
91//! |    `%S`    | `00`          | Second of the minute in `00..=60`, zero-padded to 2 digits.                                                           |
92//! |    `%L`    | `123`         | Truncated fractional seconds digits, with 3 digits by default. Number of digits is specified by the width field.      |
93//! |    `%N`    | `123456789`   | Truncated fractional seconds digits, with 9 digits by default. Number of digits is specified by the width field.      |
94//! |    `%z`    | `+0200`       | Zero-padded signed time zone UTC hour and minute offsets (`+hhmm`).                                                   |
95//! |    `%:z`   | `+02:00`      | Zero-padded signed time zone UTC hour and minute offsets with colons (`+hh:mm`).                                      |
96//! |    `%::z`  | `+02:00:00`   | Zero-padded signed time zone UTC hour, minute and second offsets with colons (`+hh:mm:ss`).                           |
97//! |    `%:::z` | `+02`         | Zero-padded signed time zone UTC hour offset, with optional minute and second offsets with colons (`+hh[:mm[:ss]]`).  |
98//! |    `%Z`    | `CEST`        | Platform-dependent abbreviated time zone name.                                                                        |
99//! |    `%A`    | `Sunday`      | Locale independent full weekday name.                                                                                 |
100//! |    `%a`    | `Sun`         | Locale independent abbreviated weekday name, using the first 3 letters.                                               |
101//! |    `%u`    | `1`           | Day of the week from Monday in `1..=7`, zero-padded to 1 digit.                                                       |
102//! |    `%w`    | `0`           | Day of the week from Sunday in `0..=6`, zero-padded to 1 digit.                                                       |
103//! |    `%G`    | `-2001`       | Same as `%Y`, but using the ISO 8601 week-based year. [^1]                                                            |
104//! |    `%g`    | `99`          | Same as `%y`, but using the ISO 8601 week-based year. [^1]                                                            |
105//! |    `%V`    | `01`          | ISO 8601 week number in `01..=53`, zero-padded to 2 digits. [^1]                                                      |
106//! |    `%U`    | `00`          | Week number from Sunday in `00..=53`, zero-padded to 2 digits. The week `1` starts with the first Sunday of the year. |
107//! |    `%W`    | `00`          | Week number from Monday in `00..=53`, zero-padded to 2 digits. The week `1` starts with the first Monday of the year. |
108//! |    `%s`    | `86400`       | Number of seconds since `1970-01-01 00:00:00 UTC`, zero-padded to at least 1 digit.                                   |
109//! |    `%n`    | `\n`          | Newline character `'\n'`.                                                                                             |
110//! |    `%t`    | `\t`          | Tab character `'\t'`.                                                                                                 |
111//! |    `%%`    | `%`           | Literal `'%'` character.                                                                                              |
112//! |    `%c`    | `Sun Jul  8 00:23:45 2001` | Date and time, equivalent to `"%a %b %e %H:%M:%S %Y"`.                                                   |
113//! | `%D`, `%x` | `07/08/01`    | Date, equivalent to `"%m/%d/%y"`.                                                                                     |
114//! |    `%F`    | `2001-07-08`  | ISO 8601 date, equivalent to `"%Y-%m-%d"`.                                                                            |
115//! |    `%v`    | ` 8-JUL-2001` | VMS date, equivalent to `"%e-%^b-%4Y"`.                                                                               |
116//! |    `%r`    | `12:23:45 AM` | 12-hour time, equivalent to `"%I:%M:%S %p"`.                                                                          |
117//! |    `%R`    | `00:23`       | 24-hour time without seconds, equivalent to `"%H:%M"`.                                                                |
118//! | `%T`, `%X` | `00:23:45`    | 24-hour time, equivalent to `"%H:%M:%S"`.                                                                             |
119//!
120//! [^1]: `%G`, `%g`, `%V`: Week 1 of ISO 8601 is the first week with at least 4
121//! days in that year. The days before the first week are in the last week of
122//! the previous year.
123
124#![doc(html_root_url = "https://docs.rs/strftime-ruby/1.3.1")]
125#![no_std]
126
127#[cfg(feature = "alloc")]
128extern crate alloc;
129
130#[cfg(feature = "std")]
131extern crate std;
132
133#[cfg(feature = "alloc")]
134use alloc::collections::TryReserveError;
135use core::error;
136
137mod format;
138
139#[cfg(test)]
140mod tests;
141
142/// Error type returned by the `strftime` functions.
143#[derive(Debug)]
144#[non_exhaustive]
145#[allow(
146    missing_copy_implementations,
147    variant_size_differences,
148    reason = "when features are enabled, some variants wrap inner errors which may be larger and not Copy"
149)]
150pub enum Error {
151    /// Provided time implementation returns invalid values.
152    InvalidTime,
153    /// Provided format string is ended by an unterminated format specifier.
154    InvalidFormatString,
155    /// Formatted string is too large and could cause an out-of-memory error.
156    FormattedStringTooLarge,
157    /// Provided buffer for the [`buffered::strftime`] function is too small for
158    /// the formatted string.
159    ///
160    /// This corresponds to the [`std::io::ErrorKind::WriteZero`] variant.
161    ///
162    /// [`std::io::ErrorKind::WriteZero`]: <https://doc.rust-lang.org/std/io/enum.ErrorKind.html#variant.WriteZero>
163    WriteZero,
164    /// Formatting error, corresponding to [`core::fmt::Error`].
165    FmtError(core::fmt::Error),
166    /// An allocation failure has occurred in either [`bytes::strftime`] or
167    /// [`string::strftime`].
168    #[cfg(feature = "alloc")]
169    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
170    OutOfMemory(TryReserveError),
171    /// An I/O error has occurred in [`io::strftime`].
172    #[cfg(feature = "std")]
173    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
174    IoError(std::io::Error),
175}
176
177impl core::fmt::Display for Error {
178    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
179        match self {
180            Error::InvalidTime => f.write_str("invalid time"),
181            Error::InvalidFormatString => f.write_str("invalid format string"),
182            Error::FormattedStringTooLarge => f.write_str("formatted string too large"),
183            Error::WriteZero => f.write_str("failed to write the whole buffer"),
184            Error::FmtError(_) => f.write_str("formatter error"),
185            #[cfg(feature = "alloc")]
186            Error::OutOfMemory(_) => f.write_str("allocation failure"),
187            #[cfg(feature = "std")]
188            Error::IoError(_) => f.write_str("I/O error"),
189        }
190    }
191}
192
193impl error::Error for Error {
194    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
195        match self {
196            Self::FmtError(inner) => Some(inner),
197            #[cfg(feature = "alloc")]
198            Self::OutOfMemory(inner) => Some(inner),
199            #[cfg(feature = "std")]
200            Self::IoError(inner) => Some(inner),
201            _ => None,
202        }
203    }
204}
205
206impl From<core::fmt::Error> for Error {
207    fn from(err: core::fmt::Error) -> Self {
208        Self::FmtError(err)
209    }
210}
211
212#[cfg(feature = "alloc")]
213#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
214impl From<TryReserveError> for Error {
215    fn from(err: TryReserveError) -> Self {
216        Self::OutOfMemory(err)
217    }
218}
219
220#[cfg(feature = "std")]
221#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
222impl From<std::io::Error> for Error {
223    fn from(err: std::io::Error) -> Self {
224        Self::IoError(err)
225    }
226}
227
228/// Common methods needed for formatting _time_.
229///
230/// This should be implemented for structs representing a _time_.
231///
232/// All the `strftime` functions take as input an implementation of this trait.
233pub trait Time {
234    /// Returns the year for _time_ (including the century).
235    fn year(&self) -> i32;
236    /// Returns the month of the year in `1..=12` for _time_.
237    fn month(&self) -> u8;
238    /// Returns the day of the month in `1..=31` for _time_.
239    fn day(&self) -> u8;
240    /// Returns the hour of the day in `0..=23` for _time_.
241    fn hour(&self) -> u8;
242    /// Returns the minute of the hour in `0..=59` for _time_.
243    fn minute(&self) -> u8;
244    /// Returns the second of the minute in `0..=60` for _time_.
245    fn second(&self) -> u8;
246    /// Returns the number of nanoseconds in `0..=999_999_999` for _time_.
247    fn nanoseconds(&self) -> u32;
248    /// Returns an integer representing the day of the week in `0..=6`, with
249    /// `Sunday == 0`.
250    fn day_of_week(&self) -> u8;
251    /// Returns an integer representing the day of the year in `1..=366`.
252    fn day_of_year(&self) -> u16;
253    /// Returns the number of seconds as a signed integer since the Epoch.
254    fn to_int(&self) -> i64;
255    /// Returns true if the time zone is UTC.
256    fn is_utc(&self) -> bool;
257    /// Returns the offset in seconds between the timezone of _time_ and UTC.
258    fn utc_offset(&self) -> i32;
259    /// Returns the name of the time zone as a string.
260    fn time_zone(&self) -> &str;
261}
262
263// Check that the Time trait is object-safe
264const _: Option<&dyn Time> = None;
265
266/// Format string used by Ruby [`Time#asctime`] method.
267///
268/// [`Time#asctime`]: <https://ruby-doc.org/core-3.1.2/Time.html#method-i-asctime>
269pub const ASCTIME_FORMAT_STRING: &str = "%c";
270
271/// Provides a `strftime` implementation using a format string with arbitrary
272/// bytes, writing to a provided byte slice.
273pub mod buffered {
274    use super::{Error, Time};
275    use crate::format::TimeFormatter;
276
277    /// Format a _time_ implementation with the specified format byte string,
278    /// writing in the provided buffer and returning the written subslice.
279    ///
280    /// See the [crate-level documentation](crate) for a complete description of
281    /// possible format specifiers.
282    ///
283    /// # Allocations
284    ///
285    /// This `strftime` implementation makes no heap allocations and is usable
286    /// in a `no_std` context.
287    ///
288    /// # Examples
289    ///
290    /// ```
291    /// use strftime::buffered::strftime;
292    /// use strftime::Time;
293    ///
294    /// // Not shown: create a time implementation with the year 1970
295    /// // let time = ...;
296    /// # include!("tests/mock.rs");
297    /// # fn main() -> Result<(), strftime::Error> {
298    /// # let time = MockTime { year: 1970, ..Default::default() };
299    /// assert_eq!(time.year(), 1970);
300    ///
301    /// let mut buf = [0u8; 8];
302    /// assert_eq!(strftime(&time, b"%Y", &mut buf)?, b"1970");
303    /// assert_eq!(buf, *b"1970\0\0\0\0");
304    /// # Ok(())
305    /// # }
306    /// ```
307    ///
308    /// # Errors
309    ///
310    /// Can produce an [`Error`] when the formatting fails.
311    pub fn strftime<'a>(
312        time: &impl Time,
313        format: &[u8],
314        buf: &'a mut [u8],
315    ) -> Result<&'a mut [u8], Error> {
316        let len = buf.len();
317
318        let mut cursor = &mut buf[..];
319        TimeFormatter::new(time, format).fmt(&mut cursor)?;
320        let remaining_len = cursor.len();
321
322        Ok(&mut buf[..len - remaining_len])
323    }
324}
325
326/// Provides a `strftime` implementation using a UTF-8 format string, writing to
327/// a [`core::fmt::Write`] object.
328pub mod fmt {
329    use core::fmt::Write;
330
331    use super::{Error, Time};
332    use crate::format::{FmtWrite, TimeFormatter};
333
334    /// Format a _time_ implementation with the specified UTF-8 format string,
335    /// writing to the provided [`core::fmt::Write`] object.
336    ///
337    /// See the [crate-level documentation](crate) for a complete description of
338    /// possible format specifiers.
339    ///
340    /// # Allocations
341    ///
342    /// This `strftime` implementation makes no heap allocations on its own, but
343    /// the provided writer may allocate.
344    ///
345    /// # Examples
346    ///
347    /// ```
348    /// use strftime::fmt::strftime;
349    /// use strftime::Time;
350    ///
351    /// // Not shown: create a time implementation with the year 1970
352    /// // let time = ...;
353    /// # include!("tests/mock.rs");
354    /// # fn main() -> Result<(), strftime::Error> {
355    /// # let time = MockTime { year: 1970, ..Default::default() };
356    /// assert_eq!(time.year(), 1970);
357    ///
358    /// let mut buf = String::new();
359    /// strftime(&time, "%Y", &mut buf)?;
360    /// assert_eq!(buf, "1970");
361    /// # Ok(())
362    /// # }
363    /// ```
364    ///
365    /// # Errors
366    ///
367    /// Can produce an [`Error`] when the formatting fails.
368    pub fn strftime(time: &impl Time, format: &str, buf: &mut dyn Write) -> Result<(), Error> {
369        TimeFormatter::new(time, format).fmt(&mut FmtWrite::new(buf))
370    }
371}
372
373/// Provides a `strftime` implementation using a format string with arbitrary
374/// bytes, writing to a newly allocated [`Vec`].
375///
376/// [`Vec`]: alloc::vec::Vec
377#[cfg(feature = "alloc")]
378#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
379pub mod bytes {
380    use alloc::vec::Vec;
381
382    use super::{Error, Time};
383    use crate::format::TimeFormatter;
384
385    /// Format a _time_ implementation with the specified format byte string.
386    ///
387    /// See the [crate-level documentation](crate) for a complete description of
388    /// possible format specifiers.
389    ///
390    /// # Allocations
391    ///
392    /// This `strftime` implementation writes its output to a heap-allocated
393    /// [`Vec`]. The implementation exclusively uses fallible allocation APIs
394    /// like [`Vec::try_reserve`]. This function will return [`Error::OutOfMemory`]
395    /// if there is an allocation failure.
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// use strftime::bytes::strftime;
401    /// use strftime::Time;
402    ///
403    /// // Not shown: create a time implementation with the year 1970
404    /// // let time = ...;
405    /// # include!("tests/mock.rs");
406    /// # fn main() -> Result<(), strftime::Error> {
407    /// # let time = MockTime { year: 1970, ..Default::default() };
408    /// assert_eq!(time.year(), 1970);
409    ///
410    /// assert_eq!(strftime(&time, b"%Y")?, b"1970");
411    /// # Ok(())
412    /// # }
413    /// ```
414    ///
415    /// # Errors
416    ///
417    /// Can produce an [`Error`] when the formatting fails.
418    pub fn strftime(time: &impl Time, format: &[u8]) -> Result<Vec<u8>, Error> {
419        let mut buf = Vec::new();
420        TimeFormatter::new(time, format).fmt(&mut buf)?;
421        Ok(buf)
422    }
423}
424
425/// Provides a `strftime` implementation using a UTF-8 format string, writing to
426/// a newly allocated [`String`].
427///
428/// [`String`]: alloc::string::String
429#[cfg(feature = "alloc")]
430#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
431pub mod string {
432    use alloc::string::String;
433    use alloc::vec::Vec;
434
435    use super::{Error, Time};
436    use crate::format::TimeFormatter;
437
438    /// Format a _time_ implementation with the specified UTF-8 format string.
439    ///
440    /// See the [crate-level documentation](crate) for a complete description of
441    /// possible format specifiers.
442    ///
443    /// # Allocations
444    ///
445    /// This `strftime` implementation writes its output to a heap-allocated
446    /// [`Vec`]. The implementation exclusively uses fallible allocation APIs
447    /// like [`Vec::try_reserve`]. This function will return [`Error::OutOfMemory`]
448    /// if there is an allocation failure.
449    ///
450    /// # Examples
451    ///
452    /// ```
453    /// use strftime::string::strftime;
454    /// use strftime::Time;
455    ///
456    /// // Not shown: create a time implementation with the year 1970
457    /// // let time = ...;
458    /// # include!("tests/mock.rs");
459    /// # fn main() -> Result<(), strftime::Error> {
460    /// # let time = MockTime { year: 1970, ..Default::default() };
461    /// assert_eq!(time.year(), 1970);
462    ///
463    /// assert_eq!(strftime(&time, "%Y")?, "1970");
464    /// # Ok(())
465    /// # }
466    /// ```
467    ///
468    /// # Errors
469    ///
470    /// Can produce an [`Error`] when the formatting fails.
471    #[expect(
472        clippy::missing_panics_doc,
473        reason = "formatted string should be valid UTF-8"
474    )]
475    pub fn strftime(time: &impl Time, format: &str) -> Result<String, Error> {
476        let mut buf = Vec::new();
477        TimeFormatter::new(time, format).fmt(&mut buf)?;
478        Ok(String::from_utf8(buf).expect("formatted string should be valid UTF-8"))
479    }
480}
481
482/// Provides a `strftime` implementation using a format string with arbitrary
483/// bytes, writing to a [`std::io::Write`] object.
484#[cfg(feature = "std")]
485#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
486pub mod io {
487    use std::io::Write;
488
489    use super::{Error, Time};
490    use crate::format::{IoWrite, TimeFormatter};
491
492    /// Format a _time_ implementation with the specified format byte string,
493    /// writing to the provided [`std::io::Write`] object.
494    ///
495    /// See the [crate-level documentation](crate) for a complete description of
496    /// possible format specifiers.
497    ///
498    /// # Allocations
499    ///
500    /// This `strftime` implementation makes no heap allocations on its own, but
501    /// the provided writer may allocate.
502    ///
503    /// # Examples
504    ///
505    /// ```
506    /// use strftime::io::strftime;
507    /// use strftime::Time;
508    ///
509    /// // Not shown: create a time implementation with the year 1970
510    /// // let time = ...;
511    /// # include!("tests/mock.rs");
512    /// # fn main() -> Result<(), strftime::Error> {
513    /// # let time = MockTime { year: 1970, ..Default::default() };
514    /// assert_eq!(time.year(), 1970);
515    ///
516    /// let mut buf = Vec::new();
517    /// strftime(&time, b"%Y", &mut buf)?;
518    /// assert_eq!(buf, *b"1970");
519    /// # Ok(())
520    /// # }
521    /// ```
522    ///
523    /// # Errors
524    ///
525    /// Can produce an [`Error`] when the formatting fails.
526    pub fn strftime(time: &impl Time, format: &[u8], buf: &mut dyn Write) -> Result<(), Error> {
527        TimeFormatter::new(time, format).fmt(&mut IoWrite::new(buf))
528    }
529}
530
531// Ensure code blocks in `README.md` compile.
532//
533// This module declaration should be kept at the end of the file, in order to
534// not interfere with code coverage.
535#[cfg(all(doctest, feature = "std"))]
536#[doc = include_str!("../README.md")]
537mod readme {}