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