Skip to main content

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