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