diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 605f8aa..e611f19 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -54,11 +54,11 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install Rust - run: rustup update stable + run: rustup update nightly - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: Generate code coverage - run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info + run: cargo +nightly llvm-cov --all-features --workspace --lcov --output-path lcov.info - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: diff --git a/Cargo.toml b/Cargo.toml index d604e1e..7678f46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "utc-dt" -version = "0.2.1" +version = "0.3.0" authors = ["Reece Kibble <reecek@uniciant.com>"] categories = ["date-and-time", "no-std", "parsing"] keywords = ["time", "datetime", "date", "utc", "epoch"] @@ -17,7 +17,16 @@ exclude = [".git*"] [features] default = ["std"] -std = ["anyhow/std"] +std = [ + "alloc", + "serde/std", +] +alloc = ["serde/alloc"] +serde = ["dep:serde"] +nightly = [] [dependencies] -anyhow = { version = "1", default-features = false } +serde = { version = "1.0", default-features = false, optional = true, features = ["derive"] } + +[dev-dependencies] +serde_json = "1.0" diff --git a/README.md b/README.md index f3517db..ff941c1 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![crates.io](https://img.shields.io/crates/v/utc-dt?style=flat-square&logo=rust)](https://crates.io/crates/utc-dt) [![license](https://img.shields.io/badge/license-Apache--2.0_OR_MIT-blue?style=flat-square)](#license) +[![docs](https://img.shields.io/docsrs/utc-dt/latest)](https://docs.rs/utc-dt) [![build status](https://img.shields.io/github/actions/workflow/status/uniciant/utc-datetime/rust.yml?branch=main&style=flat-square&logo=github)](https://github.com/uniciant/utc-datetime/actions) [![codecov](https://codecov.io/gh/uniciant/utc-datetime/branch/main/graph/badge.svg?token=XTOHZ187TY)](https://codecov.io/gh/uniciant/utc-datetime) @@ -12,7 +13,7 @@ It prioritizes being space-optimal and efficient. ```toml [dependencies] -utc-dt = "0.2" +utc-dt = "0.3" ``` For extended/niche features and local time-zone support see [`chrono`](https://github.com/chronotope/chrono) or [`time`](https://github.com/time-rs/time). @@ -33,7 +34,8 @@ See [docs.rs](https://docs.rs/utc-dt) for the API reference. - Provides constants useful for time transformations: [`utc-dt::constants`](https://docs.rs/utc-dt/latest/utc_dt/constants/index.html) - Nanosecond resolution. - Timestamps supporting standard math operators (`core::ops`) -- `#![no_std]` support. +- `#![no_std]` and optional `alloc` support. +- Optional serialization/deserialization of structures via `serde` ## Examples (exhaustive) ```rust @@ -99,13 +101,16 @@ See [docs.rs](https://docs.rs/utc-dt) for the API reference. // UTC Time of Day subsecond component (in nanoseconds) let subsec_ns = utc_tod.as_subsec_ns(); // Parse a UTC Time of Day from an ISO 8601 time string `(Thh:mm:ssZ)` - // Not available for #![no_std] let utc_tod = UTCTimeOfDay::try_from_iso_tod("T10:18:08.903Z").unwrap(); // Get a time of day string formatted according to ISO 8601 `(Thh:mm:ssZ)` - // Not available for #![no_std] - let precision = Some(6); - let iso_tod = utc_tod.as_iso_tod(precision); + const PRECISION_MICROS: usize = 6; + let iso_tod = utc_tod.as_iso_tod(PRECISION_MICROS); assert_eq!(iso_tod, "T10:18:08.903000Z"); + // Write ISO 8601 time of day str to a stack buffer + let mut buf = [0; UTCTimeOfDay::iso_tod_len(PRECISION_MICROS)]; + let _bytes_written = utc_tod.write_iso_tod(&mut buf, PRECISION_MICROS).unwrap(); + let iso_tod_str = core::str::from_utf8(&buf).unwrap(); + assert_eq!(iso_tod_str, "T10:18:08.903000Z"); // UTC Date directly from components let utc_date = UTCDate::try_from_components(2023, 6, 15).unwrap(); // OR @@ -121,12 +126,15 @@ See [docs.rs](https://docs.rs/utc-dt) for the API reference. // UTC Day from UTC Date let utc_day = utc_date.as_day(); // Parse a UTC Date from an ISO 8601 date string `(YYYY-MM-DD)` - // Not available for #![no_std] let utc_date = UTCDate::try_from_iso_date("2023-06-15").unwrap(); // Get date string formatted according to ISO 8601 `(YYYY-MM-DD)` - // Not available for #![no_std] let iso_date = utc_date.as_iso_date(); assert_eq!(iso_date, "2023-06-15"); + // Write ISO 8601 date str to a stack buffer + let mut buf = [0; UTCDate::ISO_DATE_LEN]; + let _bytes_written = utc_date.write_iso_date(&mut buf).unwrap(); + let iso_date_str = core::str::from_utf8(&buf).unwrap(); + assert_eq!(iso_date_str, "2023-06-15"); // UTC Datetime from date and time-of-day components let utc_datetime = UTCDatetime::from_components(utc_date, utc_tod); @@ -134,13 +142,16 @@ See [docs.rs](https://docs.rs/utc-dt) for the API reference. let (utc_date, time_of_day_ns) = (utc_datetime.as_date(), utc_datetime.as_tod()); // OR let (utc_date, time_of_day_ns) = utc_datetime.as_components(); // Parse a UTC Datetime from an ISO 8601 datetime string `(YYYY-MM-DDThh:mm:ssZ)` - // Not available for #![no_std] let utc_datetime = UTCDatetime::try_from_iso_datetime("2023-06-15T10:18:08.903Z").unwrap(); // Get UTC datetime string formatted according to ISO 8601 `(YYYY-MM-DDThh:mm:ssZ)` - // Not available for #![no_std] - let precision = None; - let iso_datetime = utc_datetime.as_iso_datetime(precision); + const PRECISION_SECONDS: usize = 0; + let iso_datetime = utc_datetime.as_iso_datetime(PRECISION_SECONDS); assert_eq!(iso_datetime, "2023-06-15T10:18:08Z"); + // Write ISO 8601 datetime str to a stack buffer + let mut buf = [0; UTCDatetime::iso_datetime_len(PRECISION_SECONDS)]; + let _bytes_written = utc_datetime.write_iso_datetime(&mut buf, PRECISION_SECONDS).unwrap(); + let iso_datetime_str = core::str::from_utf8(&buf).unwrap(); + assert_eq!(iso_datetime_str, "2023-06-15T10:18:08Z"); { // `UTCTransformations` can be used to create shortcuts to the desired type! @@ -184,6 +195,13 @@ See [docs.rs](https://docs.rs/utc-dt) for the API reference. } ``` +## Feature flags +The [`std`, `alloc`] feature flags are enabled by default. +- `std`: Enables methods that use the system clock via `std::time::SystemTime`. Enables `alloc`. +- `alloc`: Enables methods that use allocated strings. +- `serde`: Derives `serde::Serialize` and `serde::Deserialize` for all internal non-error types. +- `nightly`: Enables the unstable [`error_in_core`](https://github.com/rust-lang/rust/issues/103765) feature for improved `#[no_std]` error handling. + ## References - [(Howard Hinnant, 2021) `chrono`-Compatible Low-Level Date Algorithms](http://howardhinnant.github.io/date_algorithms.html) - [(W3C, 1997) ISO 8601 Standard for Date and Time Formats](https://www.w3.org/TR/NOTE-datetime) diff --git a/src/date.rs b/src/date.rs index b2b2acc..77850d7 100644 --- a/src/date.rs +++ b/src/date.rs @@ -4,14 +4,20 @@ //! proleptic Gregorian Calendar (the *civil* calendar), //! to create UTC dates. -use core::{ - fmt::{Display, Formatter}, - time::Duration, -}; +use crate::time::{UTCDay, UTCTimestamp, UTCTransformations}; +use crate::util::StrWriter; +use core::fmt::{Display, Formatter, Write}; +use core::num::ParseIntError; +use core::time::Duration; -use anyhow::{bail, Result}; +#[cfg(feature = "alloc")] +use alloc::{format, string::String}; -use crate::time::{UTCDay, UTCTimestamp, UTCTransformations}; +// TODO <https://github.com/rust-lang/rust/issues/103765> +#[cfg(feature = "nightly")] +use core::error::Error; +#[cfg(all(feature = "std", not(feature = "nightly")))] +use std::error::Error; /// UTC Date. /// @@ -45,11 +51,17 @@ use crate::time::{UTCDay, UTCTimestamp, UTCTransformations}; /// // Not available for #![no_std] /// let iso_date = utc_date.as_iso_date(); /// assert_eq!(iso_date, "2023-06-15"); +/// // Write ISO 8601 date str to a stack buffer +/// let mut buf = [0; UTCDate::ISO_DATE_LEN]; +/// let _bytes_written = utc_date.write_iso_date(&mut buf).unwrap(); +/// let iso_date_str = core::str::from_utf8(&buf).unwrap(); +/// assert_eq!(iso_date_str, "2023-06-15"); /// ``` /// /// ## Safety /// Unchecked methods are provided for use in hot paths requiring high levels of optimisation. /// These methods assume valid input. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct UTCDate { era: u32, @@ -101,6 +113,9 @@ impl UTCDate { /// The minimum year supported pub const MIN_YEAR: u64 = 1970; + /// The length of an ISO date (in characters) + pub const ISO_DATE_LEN: usize = 10; + /// Unchecked method to create a UTC Date from provided year, month and day. /// /// ## Safety @@ -120,21 +135,21 @@ impl UTCDate { } /// Try to create a UTC Date from provided year, month and day. - pub fn try_from_components(year: u64, month: u8, day: u8) -> Result<Self> { + pub fn try_from_components(year: u64, month: u8, day: u8) -> Result<Self, UTCDateError> { if !(Self::MIN_YEAR..=Self::MAX_YEAR).contains(&year) { - bail!("Year out of range! (year: {:04})", year); + return Err(UTCDateError::YearOutOfRange(year)); } if month == 0 || month > 12 { - bail!("Month out of range! (month: {:02})", month); + return Err(UTCDateError::MonthOutOfRange(month)); } - // force create + // SAFETY: we have checked year and month are within range let date = unsafe { Self::from_components_unchecked(year, month, day) }; - // then check + // Then check days if date.day == 0 || date.day > date.days_in_month() { - bail!("Day out of range! (date: {date}"); + return Err(UTCDateError::DayOutOfRange(date)); } if date > UTCDate::MAX { - bail!("Date out of range! (date: {date}"); + return Err(UTCDateError::DateOutOfRange(date)); } Ok(date) } @@ -176,6 +191,7 @@ impl UTCDate { let doy = ((153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5) + d - 1; let doe = (yoe * 365) + (yoe / 4) - (yoe / 100) + doy as u32; let days = (era as u64 * 146097) + doe as u64 - 719468; + // SAFETY: days is not exceeding UTCDay::MAX unsafe { UTCDay::from_u64_unchecked(days) } } @@ -223,13 +239,16 @@ impl UTCDate { } } - /// Try parse date from string in the format: + /// Try parse date from str in the format: /// * `YYYY-MM-DD` /// /// Conforms to ISO 8601: /// <https://www.w3.org/TR/NOTE-datetime> - #[cfg(feature = "std")] - pub fn try_from_iso_date(iso: &str) -> Result<Self> { + pub fn try_from_iso_date(iso: &str) -> Result<Self, UTCDateError> { + let len = iso.len(); + if len != Self::ISO_DATE_LEN { + return Err(UTCDateError::InvalidStrLen(len)); + } // handle slice let (year_str, rem) = iso.split_at(4); // remainder = "-MM-DD" let (month_str, rem) = rem[1..].split_at(2); // remainder = "-DD" @@ -246,10 +265,38 @@ impl UTCDate { /// /// Conforms to ISO 8601: /// <https://www.w3.org/TR/NOTE-datetime> - #[cfg(feature = "std")] + #[cfg(feature = "alloc")] pub fn as_iso_date(&self) -> String { format!("{self}") } + + /// Internal truncated buffer write + #[inline] + pub(crate) fn _write_iso_date_trunc(&self, w: &mut StrWriter) { + // unwrap infallible + write!(w, "{self}").unwrap(); + } + + /// Write an ISO date to a buffer in the format: + /// * `YYYY-MM-DD` + /// + /// The buffer should have minimum length of [UTCDate::ISO_DATE_LEN] (10). + /// + /// A buffer of insufficient length will error ([UTCDateError::InvalidStrLen]). + /// + /// Returns number of UTF8 characters (bytes) written + /// + /// Conforms to ISO 8601: + /// <https://www.w3.org/TR/NOTE-datetime> + pub fn write_iso_date(&self, buf: &mut [u8]) -> Result<usize, UTCDateError> { + let write_len = Self::ISO_DATE_LEN; + if write_len > buf.len() { + return Err(UTCDateError::InvalidStrLen(buf.len())); + } + let mut writer = StrWriter::new(&mut buf[..write_len]); + self._write_iso_date_trunc(&mut writer); + Ok(writer.written) + } } impl UTCTransformations for UTCDate { @@ -316,3 +363,49 @@ impl From<UTCDay> for UTCDate { Self::from_day(utc_day) } } + +/// Error type for UTCDate methods +#[derive(Debug, Clone)] +pub enum UTCDateError { + /// Error raised parsing int to string + ParseErr(ParseIntError), + /// Error raised due to out of range year + YearOutOfRange(u64), + /// Error raised due to out of range month + MonthOutOfRange(u8), + /// Error raised due to out of range day + DayOutOfRange(UTCDate), + /// Error raised due to out of range date + DateOutOfRange(UTCDate), + /// Error raised due to invalid ISO date length + InvalidStrLen(usize), +} + +impl Display for UTCDateError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + Self::ParseErr(e) => e.fmt(f), + Self::YearOutOfRange(y) => write!(f, "year ({y}) out of range!"), + Self::MonthOutOfRange(m) => write!(f, "month ({m}) out of range!"), + Self::DayOutOfRange(d) => write!(f, "day ({d}) out of range!"), + Self::DateOutOfRange(date) => write!(f, "date ({date}) out of range!"), + Self::InvalidStrLen(l) => write!(f, "invalid ISO date str length ({l}), 10 required"), + } + } +} + +#[cfg(any(feature = "std", feature = "nightly"))] +impl Error for UTCDateError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::ParseErr(e) => e.source(), + _ => None, + } + } +} + +impl From<ParseIntError> for UTCDateError { + fn from(value: ParseIntError) -> Self { + Self::ParseErr(value) + } +} diff --git a/src/lib.rs b/src/lib.rs index 3e261e1..d85d9b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ //! //! [![crates.io](https://img.shields.io/crates/v/utc-dt?style=flat-square&logo=rust)](https://crates.io/crates/utc-dt) //! [![license](https://img.shields.io/badge/license-Apache--2.0_OR_MIT-blue?style=flat-square)](#license) +//! [![docs](https://img.shields.io/docsrs/utc-dt/latest)](https://docs.rs/utc-dt) //! [![build status](https://img.shields.io/github/actions/workflow/status/uniciant/utc-datetime/rust.yml?branch=main&style=flat-square&logo=github)](https://github.com/uniciant/utc-datetime/actions) //! [![codecov](https://codecov.io/gh/uniciant/utc-datetime/branch/main/graph/badge.svg?token=XTOHZ187TY)](https://codecov.io/gh/uniciant/utc-datetime) //! @@ -12,7 +13,7 @@ //! //! ```rust,ignore //! [dependencies] -//! utc-dt = "0.2" +//! utc-dt = "0.3" //! ``` //! For extended/niche features and local time-zone support see [`chrono`](https://github.com/chronotope/chrono) or [`time`](https://github.com/time-rs/time). //! @@ -33,7 +34,8 @@ //! - Provides constants useful for time transformations: [`utc-dt::constants`](https://docs.rs/utc-dt/latest/utc_dt/constants/index.html) //! - Nanosecond resolution. //! - Timestamps supporting standard math operators (`core::ops`) -//! - `#![no_std]` support. +//! - `#![no_std]` and optional `alloc` support. +//! - Optional serialization/deserialization of structures via `serde` //! //! ## Examples (exhaustive) #![cfg_attr(not(feature = "std"), doc = "```rust,ignore")] @@ -100,13 +102,16 @@ //! // UTC Time of Day subsecond component (in nanoseconds) //! let subsec_ns = utc_tod.as_subsec_ns(); //! // Parse a UTC Time of Day from an ISO 8601 time string `(Thh:mm:ssZ)` -//! // Not available for #![no_std] //! let utc_tod = UTCTimeOfDay::try_from_iso_tod("T10:18:08.903Z").unwrap(); //! // Get a time of day string formatted according to ISO 8601 `(Thh:mm:ssZ)` -//! // Not available for #![no_std] -//! let precision = Some(6); -//! let iso_tod = utc_tod.as_iso_tod(precision); +//! const PRECISION_MICROS: usize = 6; +//! let iso_tod = utc_tod.as_iso_tod(PRECISION_MICROS); //! assert_eq!(iso_tod, "T10:18:08.903000Z"); +//! // Write ISO 8601 time of day str to a stack buffer +//! let mut buf = [0; UTCTimeOfDay::iso_tod_len(PRECISION_MICROS)]; +//! let _bytes_written = utc_tod.write_iso_tod(&mut buf, PRECISION_MICROS).unwrap(); +//! let iso_tod_str = core::str::from_utf8(&buf).unwrap(); +//! assert_eq!(iso_tod_str, "T10:18:08.903000Z"); //! //! // UTC Date directly from components //! let utc_date = UTCDate::try_from_components(2023, 6, 15).unwrap(); // OR @@ -122,12 +127,15 @@ //! // UTC Day from UTC Date //! let utc_day = utc_date.as_day(); //! // Parse a UTC Date from an ISO 8601 date string `(YYYY-MM-DD)` -//! // Not available for #![no_std] //! let utc_date = UTCDate::try_from_iso_date("2023-06-15").unwrap(); //! // Get date string formatted according to ISO 8601 `(YYYY-MM-DD)` -//! // Not available for #![no_std] //! let iso_date = utc_date.as_iso_date(); //! assert_eq!(iso_date, "2023-06-15"); +//! // Write ISO 8601 date str to a stack buffer +//! let mut buf = [0; UTCDate::ISO_DATE_LEN]; +//! let _bytes_written = utc_date.write_iso_date(&mut buf).unwrap(); +//! let iso_date_str = core::str::from_utf8(&buf).unwrap(); +//! assert_eq!(iso_date_str, "2023-06-15"); //! //! // UTC Datetime from date and time-of-day components //! let utc_datetime = UTCDatetime::from_components(utc_date, utc_tod); @@ -135,13 +143,16 @@ //! let (utc_date, time_of_day_ns) = (utc_datetime.as_date(), utc_datetime.as_tod()); // OR //! let (utc_date, time_of_day_ns) = utc_datetime.as_components(); //! // Parse a UTC Datetime from an ISO 8601 datetime string `(YYYY-MM-DDThh:mm:ssZ)` -//! // Not available for #![no_std] //! let utc_datetime = UTCDatetime::try_from_iso_datetime("2023-06-15T10:18:08.903Z").unwrap(); //! // Get UTC datetime string formatted according to ISO 8601 `(YYYY-MM-DDThh:mm:ssZ)` -//! // Not available for #![no_std] -//! let precision = None; -//! let iso_datetime = utc_datetime.as_iso_datetime(precision); +//! const PRECISION_SECONDS: usize = 0; +//! let iso_datetime = utc_datetime.as_iso_datetime(PRECISION_SECONDS); //! assert_eq!(iso_datetime, "2023-06-15T10:18:08Z"); +//! // Write ISO 8601 datetime str to a stack buffer +//! let mut buf = [0; UTCDatetime::iso_datetime_len(PRECISION_SECONDS)]; +//! let _bytes_written = utc_datetime.write_iso_datetime(&mut buf, PRECISION_SECONDS).unwrap(); +//! let iso_datetime_str = core::str::from_utf8(&buf).unwrap(); +//! assert_eq!(iso_datetime_str, "2023-06-15T10:18:08Z"); //! //! { //! // `UTCTransformations` can be used to create shortcuts to the desired type! @@ -185,6 +196,13 @@ //! } //! ``` //! +//! ## Feature flags +//! The [`std`, `alloc`] feature flags are enabled by default. +//! - `std`: Enables methods that use the system clock via `std::time::SystemTime`. Enables `alloc`. +//! - `alloc`: Enables methods that use allocated strings. +//! - `serde`: Derives `serde::Serialize` and `serde::Deserialize` for all internal non-error types. +//! - `nightly`: Enables the unstable [`error_in_core`](https://github.com/rust-lang/rust/issues/103765) feature for improved `#[no_std]` error handling. +//! //! ## References //! - [(Howard Hinnant, 2021) `chrono`-Compatible Low-Level Date Algorithms](http://howardhinnant.github.io/date_algorithms.html) //! - [(W3C, 1997) ISO 8601 Standard for Date and Time Formats](https://www.w3.org/TR/NOTE-datetime) @@ -199,22 +217,34 @@ #![warn(missing_debug_implementations)] #![warn(dead_code)] #![cfg_attr(not(feature = "std"), no_std)] +// TODO <https://github.com/rust-lang/rust/issues/103765> +// Releasing in stable rust 1.81.0, 5th september! +#![cfg_attr(all(not(feature = "std"), feature = "nightly"), feature(error_in_core))] + +#[cfg(feature = "alloc")] +extern crate alloc; pub mod date; pub mod time; #[rustfmt::skip] pub mod constants; +mod util; -use core::{ - fmt::{Display, Formatter}, - time::Duration, -}; +use crate::date::{UTCDate, UTCDateError}; +use crate::time::{UTCTimeOfDay, UTCTimeOfDayError, UTCTimestamp, UTCTransformations}; +use core::fmt::{Display, Formatter}; +use core::time::Duration; -#[cfg(feature = "std")] -use anyhow::Result; +#[cfg(feature = "alloc")] +use alloc::string::String; +use time::UTCDayErrOutOfRange; +use util::StrWriter; -use date::UTCDate; -use time::{UTCTimeOfDay, UTCTimestamp, UTCTransformations}; +// TODO <https://github.com/rust-lang/rust/issues/103765> +#[cfg(feature = "nightly")] +use core::error::Error; +#[cfg(all(feature = "std", not(feature = "nightly")))] +use std::error::Error; /// UTC Datetime. /// @@ -241,12 +271,17 @@ use time::{UTCTimeOfDay, UTCTimestamp, UTCTransformations}; /// // Parse a UTC Datetime from an ISO 8601 datetime string `(YYYY-MM-DDThh:mm:ssZ)` /// let utc_datetime = UTCDatetime::try_from_iso_datetime("2023-06-15T10:18:08.903Z").unwrap(); /// // Get UTC datetime string formatted according to ISO 8601 `(YYYY-MM-DDThh:mm:ssZ)` -/// // Not available for #![no_std] -/// let precision = None; -/// let iso_datetime = utc_datetime.as_iso_datetime(precision); +/// const PRECISION_SECONDS: usize = 0; +/// let iso_datetime = utc_datetime.as_iso_datetime(PRECISION_SECONDS); /// assert_eq!(iso_datetime, "2023-06-15T10:18:08Z"); +/// // Write ISO 8601 datetime str to a stack buffer +/// let mut buf = [0; UTCDatetime::iso_datetime_len(PRECISION_SECONDS)]; +/// let _bytes_written = utc_datetime.write_iso_datetime(&mut buf, PRECISION_SECONDS).unwrap(); +/// let iso_datetime_str = core::str::from_utf8(&buf).unwrap(); +/// assert_eq!(iso_datetime_str, "2023-06-15T10:18:08Z"); /// ``` /// +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct UTCDatetime { date: UTCDate, @@ -276,9 +311,13 @@ impl UTCDatetime { /// UTCDatetime can physically store dates up to `December 31, 1_717_986_918_399, T23:59:59.999999999Z` pub const MAX: UTCDatetime = Self { date: UTCDate::MAX, + // SAFETY: nanos is within NANOS_PER_DAY tod: unsafe { UTCTimeOfDay::from_nanos_unchecked(25215999999999) }, }; + /// The minimum length of an ISO datetime (in UTF8 characters) + pub const MIN_ISO_DATETIME_LEN: usize = UTCTimeOfDay::MIN_ISO_TOD_LEN + UTCDate::ISO_DATE_LEN; + /// Create a datetime frome date and time-of-day components. #[inline] pub const fn from_components(date: UTCDate, tod: UTCTimeOfDay) -> Self { @@ -313,7 +352,7 @@ impl UTCDatetime { self.tod } - /// Try parse datetime from string in the format: + /// Try parse datetime from str in the format: /// /// * `YYYY-MM-DDThh:mm:ssZ` or /// * `YYYY-MM-DDThh:mm:ss.nnnZ` @@ -322,8 +361,14 @@ impl UTCDatetime { /// /// Conforms to ISO 8601: /// <https://www.w3.org/TR/NOTE-datetime> - #[cfg(feature = "std")] - pub fn try_from_iso_datetime(iso: &str) -> Result<Self> { + pub fn try_from_iso_datetime(iso: &str) -> Result<Self, UTCDatetimeError> { + let len = iso.len(); + if len < Self::MIN_ISO_DATETIME_LEN { + return Err(UTCDatetimeError::InsufficientStrLen( + len, + Self::MIN_ISO_DATETIME_LEN, + )); + } let (date_str, tod_str) = iso.split_at(10); let date = UTCDate::try_from_iso_date(date_str)?; let tod = UTCTimeOfDay::try_from_iso_tod(tod_str)?; @@ -331,19 +376,52 @@ impl UTCDatetime { } /// Return datetime as a string in the format: - /// * Precision = `None`: `YYYY-MM-DDThh:mm:ssZ` - /// * Precision = `Some(3)`: `YYYY-MM-DDThh:mm:ss.nnnZ` + /// * Precision = `0`: `YYYY-MM-DDThh:mm:ssZ` + /// * Precision = `3`: `YYYY-MM-DDThh:mm:ss.nnnZ` /// - /// If specified, `precision` denotes the number decimal places included after the + /// If `precision` denotes the number decimal places included after the /// seconds, limited to 9 decimal places (nanosecond precision). - /// If `None`, no decimal component is included. + /// If `0`, no decimal component is included. /// /// Conforms to ISO 8601: /// <https://www.w3.org/TR/NOTE-datetime> - #[cfg(feature = "std")] - pub fn as_iso_datetime(&self, precision: Option<usize>) -> String { + #[cfg(feature = "alloc")] + pub fn as_iso_datetime(&self, precision: usize) -> String { self.date.as_iso_date() + &self.tod.as_iso_tod(precision) } + + /// Write an ISO datetime to a buffer in the format: + /// * Precision = `0`: `YYYY-MM-DDThh:mm:ssZ` + /// * Precision = `3`: `YYYY-MM-DDThh:mm:ss.nnnZ` + /// + /// The buffer should have a minimum length as given by [UTCDatetime::iso_datetime_len]. + /// + /// A buffer of insufficient length will error ([UTCDatetimeError::InsufficientStrLen]). + /// + /// Returns number of UTF8 characters (bytes) written + /// + /// Conforms to ISO 8601: + /// <https://www.w3.org/TR/NOTE-datetime> + pub fn write_iso_datetime( + &self, + buf: &mut [u8], + precision: usize, + ) -> Result<usize, UTCDatetimeError> { + let write_len = Self::iso_datetime_len(precision); + if write_len > buf.len() { + return Err(UTCDatetimeError::InsufficientStrLen(buf.len(), write_len)); + } + let mut writer = StrWriter::new(&mut buf[..write_len]); + self.date._write_iso_date_trunc(&mut writer); + self.tod._write_iso_tod_trunc(&mut writer); + Ok(writer.written) + } + + /// Calculate the number of characters in an ISO datetime str + #[inline] + pub const fn iso_datetime_len(precision: usize) -> usize { + UTCTimeOfDay::iso_tod_len(precision) + UTCDate::ISO_DATE_LEN + } } impl UTCTransformations for UTCDatetime { @@ -371,3 +449,109 @@ impl From<Duration> for UTCDatetime { Self::from_duration(duration) } } + +/// Error type for UTCDatetime methods +#[derive(Debug, Clone)] +pub enum UTCDatetimeError { + /// Error within UTC Date + UTCDate(UTCDateError), + /// Error within UTC Time of Day + UTCTimeOfDay(UTCTimeOfDayError), + /// Error raised due to insufficient length of input ISO datetime str + InsufficientStrLen(usize, usize), +} + +impl Display for UTCDatetimeError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + Self::UTCDate(e) => e.fmt(f), + Self::UTCTimeOfDay(e) => e.fmt(f), + Self::InsufficientStrLen(l, m) => { + write!(f, "insufficient ISO datetime str len ({l}), {m} required") + } + } + } +} + +#[cfg(any(feature = "std", feature = "nightly"))] +impl Error for UTCDatetimeError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::UTCDate(e) => e.source(), + Self::UTCTimeOfDay(e) => e.source(), + _ => None, + } + } +} + +impl From<UTCDateError> for UTCDatetimeError { + fn from(value: UTCDateError) -> Self { + Self::UTCDate(value) + } +} + +impl From<UTCTimeOfDayError> for UTCDatetimeError { + fn from(value: UTCTimeOfDayError) -> Self { + Self::UTCTimeOfDay(value) + } +} + +/// UTC Datetime crate level error type +#[derive(Debug, Clone)] +pub enum UTCError { + /// Error within UTC Date + UTCDate(UTCDateError), + /// Error within UTC Time of Day + UTCTimeOfDay(UTCTimeOfDayError), + /// Error within UTC Day + UTCDay(UTCDayErrOutOfRange), + /// Error within UTC Datetime + UTCDatetime(UTCDatetimeError), +} + +impl Display for UTCError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + Self::UTCDate(e) => e.fmt(f), + Self::UTCTimeOfDay(e) => e.fmt(f), + Self::UTCDay(e) => e.fmt(f), + Self::UTCDatetime(e) => e.fmt(f), + } + } +} + +#[cfg(any(feature = "std", feature = "nightly"))] +impl Error for UTCError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::UTCDate(e) => e.source(), + Self::UTCTimeOfDay(e) => e.source(), + Self::UTCDay(e) => e.source(), + Self::UTCDatetime(e) => e.source(), + } + } +} + +impl From<UTCDateError> for UTCError { + fn from(value: UTCDateError) -> Self { + Self::UTCDate(value) + } +} + +impl From<UTCTimeOfDayError> for UTCError { + fn from(value: UTCTimeOfDayError) -> Self { + Self::UTCTimeOfDay(value) + } +} + +impl From<UTCDayErrOutOfRange> for UTCError { + fn from(value: UTCDayErrOutOfRange) -> Self { + Self::UTCDay(value) + } +} + +impl From<UTCDatetimeError> for UTCError { + fn from(value: UTCDatetimeError) -> Self { + Self::UTCDatetime(value) + } +} diff --git a/src/time.rs b/src/time.rs index 5aa91b4..da15d2d 100644 --- a/src/time.rs +++ b/src/time.rs @@ -2,18 +2,24 @@ //! //! Implements core time concepts via UTC Timestamps, UTC Days and UTC Time-of-Days. -use core::{ - fmt::{Display, Formatter}, - ops::*, - time::Duration, -}; +use crate::constants::*; +use crate::util::StrWriter; +use core::fmt::{Display, Formatter, Write}; +use core::num::ParseIntError; +use core::ops::*; +use core::time::Duration; -#[cfg(feature = "std")] -use std::time::SystemTime; +#[cfg(feature = "alloc")] +use alloc::{format, string::String}; -use anyhow::{bail, Result}; +#[cfg(feature = "std")] +use std::time::{SystemTime, SystemTimeError}; -use crate::constants::*; +// TODO <https://github.com/rust-lang/rust/issues/103765> +#[cfg(feature = "nightly")] +use core::error::Error; +#[cfg(all(feature = "std", not(feature = "nightly")))] +use std::error::Error; /// UTC Timestamp. /// @@ -59,6 +65,7 @@ use crate::constants::*; /// let utc_timestamp_minus_1s = utc_timestamp.saturating_sub_secs(1); /// ``` /// +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct UTCTimestamp(Duration); @@ -90,7 +97,7 @@ impl UTCTimestamp { /// Try to create a UTC Timestamp from the local system time. #[cfg(feature = "std")] - pub fn try_from_system_time() -> Result<Self> { + pub fn try_from_system_time() -> Result<Self, SystemTimeError> { let duration = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; Ok(UTCTimestamp(duration)) } @@ -119,6 +126,7 @@ impl UTCTimestamp { pub const fn as_tod(&self) -> UTCTimeOfDay { let ns = ((self.0.as_secs() % SECONDS_PER_DAY) * NANOS_PER_SECOND) + (self.0.subsec_nanos() as u64); + // SAFETY: nanos is within NANOS_PER_DAY unsafe { UTCTimeOfDay::from_nanos_unchecked(ns) } } @@ -585,7 +593,7 @@ where /// Create from local system time #[cfg(feature = "std")] - fn try_from_system_time() -> Result<Self> { + fn try_from_system_time() -> Result<Self, SystemTimeError> { let timestamp = UTCTimestamp::try_from_system_time()?; Ok(Self::from_timestamp(timestamp)) } @@ -620,6 +628,7 @@ where /// ## Safety /// Unchecked methods are provided for use in hot paths requiring high levels of optimisation. /// These methods assume valid input. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct UTCDay(u64); @@ -644,10 +653,10 @@ impl UTCDay { } /// Try create UTC Day from integer. - pub fn try_from_u64(u: u64) -> Result<Self> { + pub fn try_from_u64(u: u64) -> Result<Self, UTCDayErrOutOfRange> { let day = unsafe { Self::from_u64_unchecked(u) }; if day > Self::MAX { - bail!("UTC Day exceeding maximum: {}", day.to_u64()); + return Err(UTCDayErrOutOfRange(day.0)); } Ok(day) } @@ -777,6 +786,19 @@ impl UTCDay { } } +/// Error type for UTCDay out of range +#[derive(Debug, Clone)] +pub struct UTCDayErrOutOfRange(u64); + +impl Display for UTCDayErrOutOfRange { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "UTC day ({}) exceeding maximum", self.0) + } +} + +#[cfg(any(feature = "std", feature = "nightly"))] +impl Error for UTCDayErrOutOfRange {} + impl UTCTransformations for UTCDay { #[inline] fn from_secs(secs: u64) -> Self { @@ -927,9 +949,9 @@ impl DivAssign<u64> for UTCDay { } impl TryFrom<u64> for UTCDay { - type Error = anyhow::Error; + type Error = UTCDayErrOutOfRange; - fn try_from(value: u64) -> core::result::Result<Self, Self::Error> { + fn try_from(value: u64) -> Result<Self, Self::Error> { Self::try_from_u64(value) } } @@ -972,15 +994,20 @@ impl From<UTCTimestamp> for UTCDay { /// // Parse a UTC Time of Day from an ISO 8601 time string `(Thh:mm:ssZ)` /// let utc_tod = UTCTimeOfDay::try_from_iso_tod("T10:18:08.903Z").unwrap(); /// // Get a time of day string formatted according to ISO 8601 `(Thh:mm:ssZ)` -/// // Not available for #![no_std] -/// let precision = Some(6); -/// let iso_tod = utc_tod.as_iso_tod(precision); +/// const PRECISION_MICROS: usize = 6; +/// let iso_tod = utc_tod.as_iso_tod(PRECISION_MICROS); /// assert_eq!(iso_tod, "T10:18:08.903000Z"); +/// // Write ISO 8601 time of day str to a stack buffer +/// let mut buf = [0; UTCTimeOfDay::iso_tod_len(PRECISION_MICROS)]; +/// let _bytes_written = utc_tod.write_iso_tod(&mut buf, PRECISION_MICROS).unwrap(); +/// let iso_tod_str = core::str::from_utf8(&buf).unwrap(); +/// assert_eq!(iso_tod_str, "T10:18:08.903000Z"); /// ``` /// /// ## Safety /// Unchecked methods are provided for use in hot paths requiring high levels of optimisation. /// These methods assume valid input. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct UTCTimeOfDay(u64); @@ -1007,6 +1034,12 @@ impl UTCTimeOfDay { /// Equal to the number of nanoseconds in a day. pub const MAX: Self = Self(NANOS_PER_DAY - 1); + /// The minimum length of an ISO time (in UTF8 characters) + pub const MIN_ISO_TOD_LEN: usize = 10; + + /// The maximum supported subsecond precision of an ISO time + pub const MAX_ISO_TOD_PRECISION: usize = 9; + /// Unchecked method to create time of day from nanoseconds /// /// ### Safety @@ -1065,37 +1098,41 @@ impl UTCTimeOfDay { } /// Try to create UTC time of day from nanoseconds - pub fn try_from_nanos(nanos: u64) -> Result<Self> { + pub fn try_from_nanos(nanos: u64) -> Result<Self, UTCTimeOfDayError> { + // SAFETY: we immediately check that nanos was within NANOS_PER_DAY (tod does not exceed UTCTimeOfDay::MAX) let tod = unsafe { Self::from_nanos_unchecked(nanos) }; if tod > Self::MAX { - bail!("Nanoseconds not within a day! (nanos: {})", nanos); + return Err(UTCTimeOfDayError::ExcessNanos(nanos)); } Ok(tod) } /// Try to create UTC time of day from microseconds - pub fn try_from_micros(micros: u64) -> Result<Self> { + pub fn try_from_micros(micros: u64) -> Result<Self, UTCTimeOfDayError> { + // SAFETY: we immediately check that micros was within MICROS_PER_DAY (tod does not exceed UTCTimeOfDay::MAX) let tod = unsafe { Self::from_micros_unchecked(micros) }; if tod > Self::MAX { - bail!("Microseconds not within a day! (micros: {})", micros); + return Err(UTCTimeOfDayError::ExcessMicros(micros)); } Ok(tod) } /// Try to create UTC time of day from milliseconds - pub fn try_from_millis(millis: u32) -> Result<Self> { + pub fn try_from_millis(millis: u32) -> Result<Self, UTCTimeOfDayError> { + // SAFETY: we immediately check that millis was within MILLIS_PER_DAY (tod does not exceed UTCTimeOfDay::MAX) let tod = unsafe { Self::from_millis_unchecked(millis) }; if tod > Self::MAX { - bail!("Milliseconds not within a day! (millis: {})", millis); + return Err(UTCTimeOfDayError::ExcessMillis(millis)); } Ok(tod) } /// Try to create UTC time of day from seconds - pub fn try_from_secs(secs: u32) -> Result<Self> { + pub fn try_from_secs(secs: u32) -> Result<Self, UTCTimeOfDayError> { + // SAFETY: we immediately check that secs was within SECONDS_PER_DAY (tod does not exceed UTCTimeOfDay::MAX) let tod = unsafe { Self::from_secs_unchecked(secs) }; if tod > Self::MAX { - bail!("Seconds not within a day! (secs: {})", secs); + return Err(UTCTimeOfDayError::ExcessSeconds(secs)); } Ok(tod) } @@ -1104,7 +1141,12 @@ impl UTCTimeOfDay { /// /// Inputs are not limited by divisions. eg. 61 minutes is valid input, 61 seconds, etc. /// The time described must not exceed the number of nanoseconds in a day. - pub fn try_from_hhmmss(hrs: u8, mins: u8, secs: u8, subsec_ns: u32) -> Result<Self> { + pub fn try_from_hhmmss( + hrs: u8, + mins: u8, + secs: u8, + subsec_ns: u32, + ) -> Result<Self, UTCTimeOfDayError> { Self::try_from_nanos(Self::_ns_from_hhmmss(hrs, mins, secs, subsec_ns)) } @@ -1159,61 +1201,156 @@ impl UTCTimeOfDay { timestamp.as_tod() } - /// Try parse time-of-day from string in the format: + /// Try parse time-of-day from an ISO str in the format: /// * `Thh:mm:ssZ` /// * `Thh:mm:ss.nnnZ` (up to 9 decimal places) /// /// Conforms to ISO 8601: /// <https://www.w3.org/TR/NOTE-datetime> - #[cfg(feature = "std")] - pub fn try_from_iso_tod(iso: &str) -> Result<Self> { + pub fn try_from_iso_tod(iso: &str) -> Result<Self, UTCTimeOfDayError> { + let len = iso.len(); + if len < Self::MIN_ISO_TOD_LEN { + return Err(UTCTimeOfDayError::InsufficientStrLen( + len, + Self::MIN_ISO_TOD_LEN, + )); + } let (hour_str, rem) = iso[1..].split_at(2); // remainder = ":mm:ss.nnnZ" let (minute_str, rem) = rem[1..].split_at(2); // remainder = ":ss.nnnZ" let (second_str, rem) = rem[1..].split_at(2); // remainder = ".nnnZ" - let hrs: u8 = hour_str.parse()?; let mins: u8 = minute_str.parse()?; let secs: u8 = second_str.parse()?; - + // calculate subseconds let rem_len = rem.len(); let subsec_ns: u32 = if rem_len > 1 { let subsec_str = &rem[1..(rem_len - 1)]; // "nnn" let precision: u32 = subsec_str.len() as u32; - if precision > 9 { - bail!( - "Cannot parse ISO time-of-day: Precision ({}) exceeds maximum of 9", - precision - ); + if precision > Self::MAX_ISO_TOD_PRECISION as u32 { + return Err(UTCTimeOfDayError::ExcessPrecision(precision)); } if precision == 0 { 0 } else { let subsec: u32 = subsec_str.parse()?; - subsec * 10u32.pow(9 - precision) + subsec * 10u32.pow(Self::MAX_ISO_TOD_PRECISION as u32 - precision) } } else { 0 }; - Self::try_from_hhmmss(hrs, mins, secs, subsec_ns) } /// Return time-of-day as a string in the format: - /// * Precision = `None`: `Thh:mm:ssZ` - /// * Precision = `Some(3)`: `Thh:mm:ss.nnnZ` + /// * Precision = `0`: `Thh:mm:ssZ` + /// * Precision = `3`: `Thh:mm:ss.nnnZ` /// /// Conforms to ISO 8601: /// <https://www.w3.org/TR/NOTE-datetime> - #[cfg(feature = "std")] - pub fn as_iso_tod(&self, precision: Option<usize>) -> String { + #[cfg(feature = "alloc")] + pub fn as_iso_tod(&self, precision: usize) -> String { + let len = Self::iso_tod_len(precision); let mut s = format!("{self}"); - let len = if let Some(p) = precision { - 10 + p.min(9) - } else { - 9 - }; - s.truncate(len); + s.truncate(len - 1); s.push('Z'); s } + + /// Internal truncated buffer write + #[inline] + pub(crate) fn _write_iso_tod_trunc(&self, w: &mut StrWriter) { + // unwrap infallible + write!(w, "{self}").unwrap(); + w.buf[w.written - 1] = b'Z'; + } + + /// Write time-of-day to a buffer in the format: + /// * Precision = `0`: `Thh:mm:ssZ` + /// * Precision = `3`: `Thh:mm:ss.nnnZ` + /// + /// The buffer should have a minimum length as given by [UTCTimeOfDay::iso_tod_len]. + /// + /// A buffer of insufficient length will error ([UTCTimeOfDayError::InsufficientStrLen]). + /// + /// Returns number of UTF8 characters (bytes) written + /// + /// Conforms to ISO 8601: + /// <https://www.w3.org/TR/NOTE-datetime> + pub fn write_iso_tod( + &self, + buf: &mut [u8], + precision: usize, + ) -> Result<usize, UTCTimeOfDayError> { + let write_len = Self::iso_tod_len(precision); + if write_len > buf.len() { + return Err(UTCTimeOfDayError::InsufficientStrLen(buf.len(), write_len)); + } + let mut writer = StrWriter::new(&mut buf[..write_len]); + self._write_iso_tod_trunc(&mut writer); + Ok(writer.written) + } + + /// Calculate the number of characters in an ISO time-of-day str + #[inline] + pub const fn iso_tod_len(precision: usize) -> usize { + if precision == 0 { + Self::MIN_ISO_TOD_LEN + } else if precision < Self::MAX_ISO_TOD_PRECISION { + Self::MIN_ISO_TOD_LEN + precision + 1 + } else { + // clamp to precision to max + Self::MIN_ISO_TOD_LEN + Self::MAX_ISO_TOD_PRECISION + 1 + } + } +} + +/// Error type for UTCTimeOfDay methods +#[derive(Debug, Clone)] +pub enum UTCTimeOfDayError { + /// Error raised parsing int to string + ParseErr(ParseIntError), + /// Error raised due to excessive ISO precision + ExcessPrecision(u32), + /// Error raised due to nanos exceeding nanos in a day + ExcessNanos(u64), + /// Error raised due to micros exceeding micros in a day + ExcessMicros(u64), + /// Error raised due to millis exceeding millis in a day + ExcessMillis(u32), + /// Error raised due to seconds exceeding seconds in a day + ExcessSeconds(u32), + /// Error raised due to insufficient length of input ISO time-of-day str + InsufficientStrLen(usize, usize), +} + +impl Display for UTCTimeOfDayError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + Self::ParseErr(e) => e.fmt(f), + Self::ExcessPrecision(p) => write!(f, "ISO precision ({p}) exceeds maximum of 9"), + Self::ExcessNanos(n) => write!(f, "nanoseconds ({n}) not within a day"), + Self::ExcessMicros(u) => write!(f, "microseconds ({u}) not within a day"), + Self::ExcessMillis(m) => write!(f, "milliseconds ({m}) not within a day"), + Self::ExcessSeconds(s) => write!(f, "seconds ({s}) not within a day"), + Self::InsufficientStrLen(l, m) => { + write!(f, "insufficient ISO time str len ({l}), {m} required") + } + } + } +} + +#[cfg(any(feature = "std", feature = "nightly"))] +impl Error for UTCTimeOfDayError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::ParseErr(e) => e.source(), + _ => None, + } + } +} + +impl From<ParseIntError> for UTCTimeOfDayError { + fn from(value: ParseIntError) -> Self { + Self::ParseErr(value) + } } diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..108582c --- /dev/null +++ b/src/util.rs @@ -0,0 +1,27 @@ +//! Internal utilities + +/// Utility for no-alloc str writing to a buffer via `core::fmt` +pub struct StrWriter<'a> { + pub buf: &'a mut [u8], + pub written: usize, +} + +impl<'a> StrWriter<'a> { + #[inline] + pub fn new(buf: &'a mut [u8]) -> Self { + let written = 0; + Self { buf, written } + } +} + +impl<'a> core::fmt::Write for StrWriter<'a> { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + let remaining = self.buf.len() - self.written; + let write_len = remaining.min(s.len()); + let write_bytes = &s.as_bytes()[..write_len]; + // infallible truncating write + self.buf[self.written..][..write_len].copy_from_slice(write_bytes); + self.written += write_len; + Ok(()) + } +} diff --git a/tests/date.rs b/tests/date.rs index 7f778de..a3e4ae4 100644 --- a/tests/date.rs +++ b/tests/date.rs @@ -1,15 +1,14 @@ use std::collections::HashSet; -use anyhow::Result; - use utc_dt::{ constants::{MICROS_PER_DAY, MILLIS_PER_DAY, NANOS_PER_DAY, SECONDS_PER_DAY}, date::UTCDate, time::{UTCDay, UTCTimestamp, UTCTransformations}, + UTCError, }; #[test] -fn test_date_from_components() -> Result<()> { +fn test_date_from_components() { let test_cases = [ (2023, 6, 14, true, false, 30), // valid recent date (1970, 1, 1, true, false, 31), // valid epoch date @@ -38,12 +37,10 @@ fn test_date_from_components() -> Result<()> { } } } - - Ok(()) } #[test] -fn test_date_from_day() -> Result<()> { +fn test_date_from_day() -> Result<(), UTCError> { let test_cases = [ (UTCDay::ZERO, 1970, 1, 1), (UTCDay::try_from_u64(30)?, 1970, 1, 31), @@ -66,8 +63,7 @@ fn test_date_from_day() -> Result<()> { } #[test] -#[cfg(feature = "std")] -fn test_date_iso_conversions() -> Result<()> { +fn test_date_iso_conversions() -> Result<(), UTCError> { let test_cases = [ (2023, 6, 14, true, "2023-06-14"), // valid recent date (1970, 1, 1, true, "1970-01-01"), // valid epoch date @@ -82,7 +78,9 @@ fn test_date_iso_conversions() -> Result<()> { (2023, 9, 0, false, "202a-09-00"), // invalid date, year not integer (2023, 9, 0, false, "2023-0a-00"), // invalid date, month not integer (2023, 9, 0, false, "2023-09-0a"), // invalid date, day not integer + (2023, 9, 1, false, "2023-09-1"), // invalid date, incorrect formatting ]; + let mut buf = [0; UTCDate::ISO_DATE_LEN]; for (year, month, day, case_is_valid, iso_date) in test_cases { match UTCDate::try_from_iso_date(iso_date) { @@ -90,7 +88,14 @@ fn test_date_iso_conversions() -> Result<()> { assert!(case_is_valid); let date_from_comp = UTCDate::try_from_components(year, month, day)?; assert_eq!(date_from_comp, date_from_iso); + #[cfg(feature = "alloc")] assert_eq!(iso_date, date_from_comp.as_iso_date()); + let written = date_from_comp.write_iso_date(&mut buf)?; + assert_eq!(iso_date.as_bytes(), &buf[..written]); + assert_eq!(iso_date, core::str::from_utf8(&buf[..written]).unwrap()); + // test invalid buf len + let mut buf = [0; 1]; + assert!(date_from_comp.write_iso_date(&mut buf).is_err()); } Err(_) => { assert!(!case_is_valid); @@ -99,26 +104,28 @@ fn test_date_iso_conversions() -> Result<()> { } // test transform from system time - let date_from_system_time = UTCDate::try_from_system_time()?; - assert!(date_from_system_time >= UTCDate::MIN); - assert!(date_from_system_time <= UTCDate::MAX); - // test debug & display - println!("{:?}:{date_from_system_time}", date_from_system_time); - // test default, clone & copy, ord - assert_eq!(UTCDate::default().clone(), UTCDate::MIN); - let date_copy = date_from_system_time; - assert_eq!(date_copy, date_from_system_time); - assert_eq!(UTCDate::MIN, date_copy.min(UTCDate::MIN)); - assert_eq!(UTCDate::MAX, date_copy.max(UTCDate::MAX)); - // test limits - assert_eq!(UTCDate::from_day(UTCDay::MAX), UTCDate::MAX); - assert_eq!(UTCDate::from_day(UTCDay::ZERO), UTCDate::MIN); - + #[cfg(feature = "std")] + { + let date_from_system_time = UTCDate::try_from_system_time().unwrap(); + assert!(date_from_system_time >= UTCDate::MIN); + assert!(date_from_system_time <= UTCDate::MAX); + // test debug & display + println!("{:?}:{date_from_system_time}", date_from_system_time); + // test default, clone & copy, ord + assert_eq!(UTCDate::default().clone(), UTCDate::MIN); + let date_copy = date_from_system_time; + assert_eq!(date_copy, date_from_system_time); + assert_eq!(UTCDate::MIN, date_copy.min(UTCDate::MIN)); + assert_eq!(UTCDate::MAX, date_copy.max(UTCDate::MAX)); + // test limits + assert_eq!(UTCDate::from_day(UTCDay::MAX), UTCDate::MAX); + assert_eq!(UTCDate::from_day(UTCDay::ZERO), UTCDate::MIN); + } Ok(()) } #[test] -fn test_date_transformations() -> Result<()> { +fn test_date_transformations() -> Result<(), UTCError> { let test_cases = [ (UTCTimestamp::from_secs(0), 1970, 1, 1), (UTCTimestamp::from_secs(2592000), 1970, 1, 31), @@ -184,3 +191,11 @@ fn test_date_transformations() -> Result<()> { Ok(()) } + +#[cfg(feature = "serde")] +#[test] +fn test_date_serde() { + let date = UTCDate::from_day(UTCDay::try_from_u64(19959).unwrap()); + let v = serde_json::to_value(&date).unwrap(); + assert_eq!(date, serde_json::from_value(v).unwrap()) +} diff --git a/tests/lib.rs b/tests/datetime.rs similarity index 70% rename from tests/lib.rs rename to tests/datetime.rs index 6ec3521..80618d1 100644 --- a/tests/lib.rs +++ b/tests/datetime.rs @@ -1,13 +1,11 @@ -use anyhow::Result; - use utc_dt::{ date::UTCDate, time::{UTCDay, UTCTimeOfDay, UTCTimestamp, UTCTransformations}, - UTCDatetime, + UTCDatetime, UTCError, }; #[test] -fn test_datetime_from_raw_components() -> Result<()> { +fn test_datetime_from_raw_components() -> Result<(), UTCError> { let test_cases = [ (1970, 1, 1, 0, 0, 0, 0, 0, UTCDay::ZERO), // thu, 00:00:00.000 ( @@ -35,7 +33,7 @@ fn test_datetime_from_raw_components() -> Result<()> { // test display & debug #[cfg(feature = "std")] - let datetime = UTCDatetime::try_from_system_time()?; + let datetime = UTCDatetime::try_from_system_time().unwrap(); #[cfg(not(feature = "std"))] let datetime = UTCDatetime::from_millis(1686824288903); // test to/as components @@ -43,7 +41,7 @@ fn test_datetime_from_raw_components() -> Result<()> { assert_eq!((date, tod), datetime.to_components()); // test from timestamp #[cfg(feature = "std")] - let timestamp = UTCTimestamp::try_from_system_time()?; + let timestamp = UTCTimestamp::try_from_system_time().unwrap(); #[cfg(not(feature = "std"))] let timestamp = UTCTimestamp::from_millis(1686824288903); let datetime_from_timestamp = UTCDatetime::from_timestamp(timestamp); @@ -95,24 +93,23 @@ fn test_datetime_from_raw_components() -> Result<()> { Ok(()) } -#[cfg(feature = "std")] #[test] -fn test_datetime_iso_conversions() -> Result<()> { +fn test_datetime_iso_conversions() -> Result<(), UTCError> { let test_cases = [ - (1970, 1, 1, 0, None, "1970-01-01T00:00:00Z"), // thu, 00:00:00 - (1970, 1, 1, 0, Some(0), "1970-01-01T00:00:00.Z"), // thu, 00:00:00. - (1970, 1, 1, 0, Some(3), "1970-01-01T00:00:00.000Z"), // thu, 00:00:00.000 - (1970, 1, 1, 0, Some(9), "1970-01-01T00:00:00.000000000Z"), // thu, 00:00:00.000000000 - (1970, 1, 1, 0, Some(11), "1970-01-01T00:00:00.000000000Z"), // thu, 00:00:00.000000000 + (1970, 1, 1, 0, 0, "1970-01-01T00:00:00Z"), // thu, 00:00:00 + (1970, 1, 1, 0, 3, "1970-01-01T00:00:00.000Z"), // thu, 00:00:00.000 + (1970, 1, 1, 0, 9, "1970-01-01T00:00:00.000000000Z"), // thu, 00:00:00.000000000 + (1970, 1, 1, 0, 11, "1970-01-01T00:00:00.000000000Z"), // thu, 00:00:00.000000000 ( 2023, 6, 14, 33_609_648_000_000, - Some(3), + 3, "2023-06-14T09:20:09.648Z", ), // wed, 09:20:09.648 ]; + let mut buf = [0; UTCDatetime::iso_datetime_len(9)]; // run iso conversion test cases for (year, month, day, tod_ns, precision, iso_datetime) in test_cases { @@ -120,20 +117,45 @@ fn test_datetime_iso_conversions() -> Result<()> { let tod = UTCTimeOfDay::try_from_nanos(tod_ns)?; let datetime_from_components = UTCDatetime::from_components(date, tod); let datetime_from_iso = UTCDatetime::try_from_iso_datetime(iso_datetime)?; + #[cfg(feature = "alloc")] assert_eq!( datetime_from_components.as_iso_datetime(precision), iso_datetime ); + let written = datetime_from_components.write_iso_datetime(&mut buf, precision)?; + let iso_raw_str = core::str::from_utf8(&buf[..written]).unwrap(); + assert_eq!(iso_raw_str.len(), UTCDatetime::iso_datetime_len(precision)); + assert_eq!(iso_datetime.as_bytes(), &buf[..written]); + assert_eq!(iso_datetime, iso_raw_str); assert_eq!(datetime_from_iso, datetime_from_components); + // test maybe-invalid buf len + let mut buf = [0; 3]; + let result = datetime_from_components.write_iso_datetime(&mut buf, precision); + if buf.len() < UTCDatetime::iso_datetime_len(precision) { + assert!(result.is_err()) + } else { + assert!(result.is_ok()) + } } // test invalid iso dates assert!(UTCDatetime::try_from_iso_datetime("197a-01-01T00:00:00Z").is_err()); assert!(UTCDatetime::try_from_iso_datetime("1970-01-01T00:a0:00Z").is_err()); + assert!(UTCDatetime::try_from_iso_datetime("1970-01-01T00:a0").is_err()); // test display & debug - let datetime = UTCDatetime::try_from_system_time()?; - println!("{:?}:{datetime}", datetime); - + #[cfg(feature = "std")] + { + let datetime = UTCDatetime::try_from_system_time().unwrap(); + println!("{:?}:{datetime}", datetime); + } Ok(()) } + +#[cfg(feature = "serde")] +#[test] +fn test_datetime_serde() { + let datetime = UTCDatetime::from_secs(1724493234); + let v = serde_json::to_value(&datetime).unwrap(); + assert_eq!(datetime, serde_json::from_value(v).unwrap()); +} diff --git a/tests/error.rs b/tests/error.rs new file mode 100644 index 0000000..7889979 --- /dev/null +++ b/tests/error.rs @@ -0,0 +1,61 @@ +use core::fmt::Display; +use utc_dt::date::{UTCDate, UTCDateError}; +use utc_dt::time::{UTCDay, UTCTimeOfDayError}; +use utc_dt::{UTCDatetimeError, UTCError}; + +#[cfg(feature = "std")] +fn check_errors<T: std::error::Error + Display>(errors: &[T]) { + for error in errors { + print!("Error Display test: {error}"); + if let Some(source) = error.source() { + print!(", caused by {source}"); + } + print!("\n"); + } +} + +#[cfg(not(feature = "std"))] +fn check_errors<T: Display>(errors: &[T]) { + for error in errors { + print!("Error Display test: {error}"); + print!("\n"); + } +} + +#[test] +fn test_errors() { + let utc_date_errors = [ + UTCDateError::ParseErr("a".parse::<u32>().unwrap_err()), + UTCDateError::DateOutOfRange(UTCDate::MAX), + UTCDateError::DayOutOfRange(UTCDate::MIN), + UTCDateError::InvalidStrLen(30), + UTCDateError::MonthOutOfRange(13), + UTCDateError::YearOutOfRange(1969), + ]; + check_errors(&utc_date_errors); + let utc_tod_errors = [ + UTCTimeOfDayError::ParseErr("a".parse::<u32>().unwrap_err()), + UTCTimeOfDayError::ExcessMicros(0), + UTCTimeOfDayError::ExcessMillis(0), + UTCTimeOfDayError::ExcessNanos(0), + UTCTimeOfDayError::ExcessSeconds(0), + UTCTimeOfDayError::ExcessPrecision(0), + UTCTimeOfDayError::InsufficientStrLen(10, 20), + ]; + check_errors(&utc_tod_errors); + let utc_day_error = [UTCDay::try_from_u64(213_503_982_334_602).unwrap_err()]; + check_errors(&utc_day_error); + let utc_datetime_errors = [ + utc_date_errors[0].clone().into(), + utc_tod_errors[0].clone().into(), + UTCDatetimeError::InsufficientStrLen(10, 20), + ]; + check_errors(&utc_datetime_errors); + let utc_errors: [UTCError; 4] = [ + utc_date_errors[1].clone().into(), + utc_tod_errors[1].clone().into(), + utc_day_error[0].clone().into(), + utc_datetime_errors[0].clone().into(), + ]; + check_errors(&utc_errors.clone()); +} diff --git a/tests/time.rs b/tests/time.rs index dcec299..5681d84 100644 --- a/tests/time.rs +++ b/tests/time.rs @@ -1,15 +1,14 @@ use core::time::Duration; use std::collections::HashSet; -use anyhow::Result; - use utc_dt::{ constants::{MICROS_PER_DAY, MILLIS_PER_DAY, NANOS_PER_DAY, NANOS_PER_SECOND, SECONDS_PER_DAY}, time::{UTCDay, UTCTimeOfDay, UTCTimestamp, UTCTransformations}, + UTCError, }; #[test] -fn test_utc_timestamp() -> Result<()> { +fn test_utc_timestamp() -> Result<(), UTCError> { let test_cases = [ ( UTCTimestamp::from_nanos(0), @@ -96,7 +95,7 @@ fn test_utc_timestamp() -> Result<()> { // test from system time #[cfg(feature = "std")] - let timestamp = UTCTimestamp::try_from_system_time()?; + let timestamp = UTCTimestamp::try_from_system_time().unwrap(); #[cfg(not(feature = "std"))] let timestamp = UTCTimestamp::from_millis(1686824288903); assert!(timestamp <= UTCTimestamp::MAX); @@ -198,10 +197,10 @@ fn test_utc_timestamp() -> Result<()> { } #[test] -fn test_utc_day() -> Result<()> { +fn test_utc_day() -> Result<(), UTCError> { // test from system time #[cfg(feature = "std")] - let utc_day = UTCDay::try_from_system_time()?; + let utc_day = UTCDay::try_from_system_time().unwrap(); #[cfg(not(feature = "std"))] let utc_day = UTCDay::from_millis(1686824288903); assert!(utc_day <= UTCDay::MAX); @@ -290,10 +289,10 @@ fn test_utc_day() -> Result<()> { } #[test] -fn test_utc_tod() -> Result<()> { +fn test_utc_tod() -> Result<(), UTCError> { // test from system time #[cfg(feature = "std")] - let timestamp = UTCTimestamp::try_from_system_time()?; + let timestamp = UTCTimestamp::try_from_system_time().unwrap(); #[cfg(not(feature = "std"))] let timestamp = UTCTimestamp::from_millis(1686824288903); let tod_from_timestamp = UTCTimeOfDay::from_timestamp(timestamp); @@ -311,25 +310,53 @@ fn test_utc_tod() -> Result<()> { assert!(UTCTimeOfDay::try_from_hhmmss(23, 59, 59, (NANOS_PER_SECOND - 1) as u32).is_ok()); assert!(UTCTimeOfDay::try_from_hhmmss(u8::MAX, u8::MAX, u8::MAX, u32::MAX).is_err()); // test iso conversions - #[cfg(feature = "std")] - { - let iso_from_tod = tod_from_timestamp.as_iso_tod(Some(9)); - let tod_from_iso = UTCTimeOfDay::try_from_iso_tod(&iso_from_tod)?; - assert_eq!(tod_from_iso, tod_from_timestamp); - assert_eq!( - UTCTimeOfDay::try_from_iso_tod("T00:00:00Z")?, - UTCTimeOfDay::ZERO - ); - assert_eq!( - UTCTimeOfDay::try_from_iso_tod("T23:59:59.999999999Z")?, - UTCTimeOfDay::MAX - ); - assert!(UTCTimeOfDay::try_from_iso_tod("Taa:59:59.999999999Z").is_err()); // invalid hour - assert!(UTCTimeOfDay::try_from_iso_tod("T23:aa:59.999999999Z").is_err()); // invalid mins - assert!(UTCTimeOfDay::try_from_iso_tod("T23:59:aa.999999999Z").is_err()); // invalid secs - assert!(UTCTimeOfDay::try_from_iso_tod("T23:59:59.a99999999Z").is_err()); // invalid subsec - assert!(UTCTimeOfDay::try_from_iso_tod("T23:59:59.9999999990Z").is_err()); - // invalid precision + #[cfg(feature = "alloc")] + let iso_from_tod = tod_from_timestamp.as_iso_tod(9); + #[cfg(not(feature = "alloc"))] + let mut buf = [0; UTCTimeOfDay::iso_tod_len(9)]; + #[cfg(not(feature = "alloc"))] + let iso_from_tod = { + let _ = tod_from_timestamp.write_iso_tod(&mut buf, 9)?; + core::str::from_utf8(&buf).unwrap() + }; + let tod_from_iso = UTCTimeOfDay::try_from_iso_tod(&iso_from_tod)?; + assert_eq!(tod_from_iso, tod_from_timestamp); + assert_eq!( + UTCTimeOfDay::try_from_iso_tod("T00:00:00Z")?, + UTCTimeOfDay::ZERO + ); + assert_eq!( + UTCTimeOfDay::try_from_iso_tod("T00:00:00.Z")?, + UTCTimeOfDay::ZERO + ); + assert_eq!( + UTCTimeOfDay::try_from_iso_tod("T23:59:59.999999999Z")?, + UTCTimeOfDay::MAX + ); + assert!(UTCTimeOfDay::try_from_iso_tod("Taa:59:59.999999999Z").is_err()); // invalid hour + assert!(UTCTimeOfDay::try_from_iso_tod("T23:aa:59.999999999Z").is_err()); // invalid mins + assert!(UTCTimeOfDay::try_from_iso_tod("T23:59:aa.999999999Z").is_err()); // invalid secs + assert!(UTCTimeOfDay::try_from_iso_tod("T23:59:59.a99999999Z").is_err()); // invalid subsec + assert!(UTCTimeOfDay::try_from_iso_tod("T23:59:59.9999999990Z").is_err()); + assert!(UTCTimeOfDay::try_from_iso_tod("T23::59.9999999990Z").is_err()); + assert!(UTCTimeOfDay::try_from_iso_tod("T23:59.9999999990Z").is_err()); + assert!(UTCTimeOfDay::try_from_iso_tod("T23:59:59").is_err()); + // test no-alloc iso conversions + let mut buf = [0; UTCTimeOfDay::iso_tod_len(9)]; + for precision in 0..13 { + let written = tod_from_timestamp.write_iso_tod(&mut buf, precision)?; + let iso_raw_str = core::str::from_utf8(&buf[..written]).unwrap(); + assert_eq!(iso_raw_str.len(), UTCTimeOfDay::iso_tod_len(precision)); + #[cfg(feature = "alloc")] + assert_eq!(tod_from_timestamp.as_iso_tod(precision), iso_raw_str); + // test maybe-invalid buf len + let mut buf = [0; 5]; + let result = tod_from_timestamp.write_iso_tod(&mut buf, precision); + if buf.len() < UTCTimeOfDay::iso_tod_len(precision) { + assert!(result.is_err()) + } else { + assert!(result.is_ok()) + } } // test unit conversions @@ -367,3 +394,19 @@ fn test_utc_tod() -> Result<()> { ); Ok(()) } + +#[cfg(feature = "serde")] +#[test] +fn test_time_serde() { + let timestamp = UTCTimestamp::from_day(UTCDay::try_from_u64(19959).unwrap()); + let v = serde_json::to_value(×tamp).unwrap(); + assert_eq!(timestamp, serde_json::from_value(v).unwrap()); + + let day = UTCDay::try_from_u64(19959).unwrap(); + let v = serde_json::to_value(&day).unwrap(); + assert_eq!(day, serde_json::from_value(v).unwrap()); + + let time_of_day = UTCTimeOfDay::try_from_hhmmss(17, 50, 23, 0).unwrap(); + let v = serde_json::to_value(&time_of_day).unwrap(); + assert_eq!(time_of_day, serde_json::from_value(v).unwrap()); +}