strftime/
lib.rs

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