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 {}