From ce69ebbb00b196d40943a97c2a2dfee85f8195be Mon Sep 17 00:00:00 2001 From: reece Date: Sat, 24 Aug 2024 15:08:55 +0800 Subject: [PATCH 01/29] tests/lib.rs: rename to datetime.rs --- tests/{lib.rs => datetime.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{lib.rs => datetime.rs} (100%) diff --git a/tests/lib.rs b/tests/datetime.rs similarity index 100% rename from tests/lib.rs rename to tests/datetime.rs From b1f4efb740a63ed72c1315a04be44dbc3499a4f0 Mon Sep 17 00:00:00 2001 From: reece Date: Sat, 24 Aug 2024 15:14:05 +0800 Subject: [PATCH 02/29] Cargo.toml: increment minor ver, remove anyhow dep, add optional serde dep, alloc + nightly features. --- Cargo.toml | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d604e1e..6bfea94 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 "] categories = ["date-and-time", "no-std", "parsing"] keywords = ["time", "datetime", "date", "utc", "epoch"] @@ -16,8 +16,21 @@ exclude = [".git*"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["std"] -std = ["anyhow/std"] +default = ["std", "serde"] +std = [ + "alloc", + "anyhow/std", + "serde/std", +] +nightly = [] +alloc = ["serde/alloc"] +serde = ["dep:serde"] [dependencies] -anyhow = { version = "1", default-features = false } +serde = { version = "1.0", default-features = false, optional = true, features = ["derive"] } +# TODO not wrapping thiserror_core https://github.com/rust-lang/rust/issues/103765 +# Releasing in stable rust 1.81.0, 5th september! +thiserror = { version = "1.0", package = "thiserror-core", default-features = false } + +[dev-dependencies] +anyhow = { version = "1.0", default-features = false } From 852ab8c3523cf7b903621cb8a9582f912a1d62e2 Mon Sep 17 00:00:00 2001 From: reece Date: Sat, 24 Aug 2024 16:50:34 +0800 Subject: [PATCH 03/29] docs: Increment version, add features --- README.md | 18 ++++++++++-------- src/lib.rs | 18 ++++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index f3517db..1ae1d3b 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,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 +33,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,10 +100,8 @@ 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); assert_eq!(iso_tod, "T10:18:08.903000Z"); @@ -121,10 +120,8 @@ 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"); @@ -134,10 +131,8 @@ 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); assert_eq!(iso_datetime, "2023-06-15T10:18:08Z"); @@ -184,6 +179,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/lib.rs b/src/lib.rs index 3e261e1..ecf6722 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,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 +33,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,10 +101,8 @@ //! // 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); //! assert_eq!(iso_tod, "T10:18:08.903000Z"); @@ -122,10 +121,8 @@ //! // 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"); //! @@ -135,10 +132,8 @@ //! 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); //! assert_eq!(iso_datetime, "2023-06-15T10:18:08Z"); @@ -185,6 +180,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) From 9d4749789014dec12d0864cb939e4d0f819fada7 Mon Sep 17 00:00:00 2001 From: reece Date: Sat, 24 Aug 2024 16:51:21 +0800 Subject: [PATCH 04/29] Cargo.toml: no thiserror (until error in core is stabilized) --- Cargo.toml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6bfea94..04daa59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,21 +16,14 @@ exclude = [".git*"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["std", "serde"] +default = ["std"] std = [ "alloc", - "anyhow/std", "serde/std", ] -nightly = [] alloc = ["serde/alloc"] serde = ["dep:serde"] +nightly = [] [dependencies] serde = { version = "1.0", default-features = false, optional = true, features = ["derive"] } -# TODO not wrapping thiserror_core https://github.com/rust-lang/rust/issues/103765 -# Releasing in stable rust 1.81.0, 5th september! -thiserror = { version = "1.0", package = "thiserror-core", default-features = false } - -[dev-dependencies] -anyhow = { version = "1.0", default-features = false } From 3ba743d556b8b5f6b0d151735d27b759c73bddd4 Mon Sep 17 00:00:00 2001 From: reece Date: Sat, 24 Aug 2024 16:53:01 +0800 Subject: [PATCH 05/29] src*: custom error types and alloc feature --- src/date.rs | 83 +++++++++++++++++++++++++++------ src/lib.rs | 132 +++++++++++++++++++++++++++++++++++++++++++++++----- src/time.rs | 130 +++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 290 insertions(+), 55 deletions(-) diff --git a/src/date.rs b/src/date.rs index b2b2acc..9f53196 100644 --- a/src/date.rs +++ b/src/date.rs @@ -4,14 +4,22 @@ //! proleptic Gregorian Calendar (the *civil* calendar), //! to create UTC dates. -use core::{ - fmt::{Display, Formatter}, - time::Duration, +use crate::time::{UTCDay, UTCTimestamp, UTCTransformations}; +use core::fmt::{Display, Formatter}; +use core::num::ParseIntError; +use core::time::Duration; + +#[cfg(feature = "alloc")] +use alloc::{ + string::String, + format }; -use anyhow::{bail, Result}; - -use crate::time::{UTCDay, UTCTimestamp, UTCTransformations}; +// TODO +#[cfg(feature = "nightly")] +use core::error::Error; +#[cfg(all(feature ="std", not(feature = "nightly")))] +use std::error::Error; /// UTC Date. /// @@ -50,6 +58,7 @@ use crate::time::{UTCDay, UTCTimestamp, UTCTransformations}; /// ## 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, @@ -120,21 +129,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 { + pub fn try_from_components(year: u64, month: u8, day: u8) -> Result { 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 let date = unsafe { Self::from_components_unchecked(year, month, day) }; // then check 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) } @@ -228,8 +237,7 @@ impl UTCDate { /// /// Conforms to ISO 8601: /// - #[cfg(feature = "std")] - pub fn try_from_iso_date(iso: &str) -> Result { + pub fn try_from_iso_date(iso: &str) -> Result { // 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,7 +254,7 @@ impl UTCDate { /// /// Conforms to ISO 8601: /// - #[cfg(feature = "std")] + #[cfg(feature = "alloc")] pub fn as_iso_date(&self) -> String { format!("{self}") } @@ -316,3 +324,50 @@ impl From for UTCDate { Self::from_day(utc_day) } } + +/// Error type for UTCDate methods +#[derive(Debug)] +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) +} + +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!"), + } + } +} + +#[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 for UTCDateError { + fn from(value: ParseIntError) -> Self { + Self::ParseErr(value) + } +} diff --git a/src/lib.rs b/src/lib.rs index ecf6722..bb9871e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -201,22 +201,32 @@ #![warn(missing_debug_implementations)] #![warn(dead_code)] #![cfg_attr(not(feature = "std"), no_std)] +// TODO +// 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; -use core::{ - fmt::{Display, Formatter}, - time::Duration, -}; +use crate::date::{UTCDate, UTCDateError}; +use crate::time::{UTCTimeOfDay, UTCTimestamp, UTCTransformations, UTCTimeOfDayError}; +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 date::UTCDate; -use time::{UTCTimeOfDay, UTCTimestamp, UTCTransformations}; +// TODO +#[cfg(feature = "nightly")] +use core::error::Error; +#[cfg(all(feature = "std", not(feature = "nightly")))] +use std::error::Error; /// UTC Datetime. /// @@ -249,6 +259,7 @@ use time::{UTCTimeOfDay, UTCTimestamp, UTCTransformations}; /// assert_eq!(iso_datetime, "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, @@ -324,8 +335,7 @@ impl UTCDatetime { /// /// Conforms to ISO 8601: /// - #[cfg(feature = "std")] - pub fn try_from_iso_datetime(iso: &str) -> Result { + pub fn try_from_iso_datetime(iso: &str) -> Result { 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)?; @@ -342,7 +352,7 @@ impl UTCDatetime { /// /// Conforms to ISO 8601: /// - #[cfg(feature = "std")] + #[cfg(feature = "alloc")] pub fn as_iso_datetime(&self, precision: Option) -> String { self.date.as_iso_date() + &self.tod.as_iso_tod(precision) } @@ -373,3 +383,103 @@ impl From for UTCDatetime { Self::from_duration(duration) } } + +/// Error type for UTCDatetime methods +#[derive(Debug)] +pub enum UTCDatetimeError { + /// Error within UTC Date + UTCDate(UTCDateError), + /// Error within UTC Time of Day + UTCTimeOfDay(UTCTimeOfDayError), +} + +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), + } + } +} + +#[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(), + } + } +} + +impl From for UTCDatetimeError { + fn from(value: UTCDateError) -> Self { + Self::UTCDate(value) + } +} + +impl From for UTCDatetimeError { + fn from(value: UTCTimeOfDayError) -> Self { + Self::UTCTimeOfDay(value) + } +} + +/// UTC Datetime crate level error type +#[derive(Debug)] +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 for UTCError { + fn from(value: UTCDateError) -> Self { + Self::UTCDate(value) + } +} + +impl From for UTCError { + fn from(value: UTCTimeOfDayError) -> Self { + Self::UTCTimeOfDay(value) + } +} + +impl From for UTCError { + fn from(value: UTCDayErrOutOfRange) -> Self { + Self::UTCDay(value) + } +} + +impl From for UTCError { + fn from(value: UTCDatetimeError) -> Self { + Self::UTCDatetime(value) + } +} diff --git a/src/time.rs b/src/time.rs index 5aa91b4..3b370d4 100644 --- a/src/time.rs +++ b/src/time.rs @@ -2,18 +2,26 @@ //! //! 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 core::fmt::{Display, Formatter}; +use core::num::ParseIntError; +use core::ops::*; +use core::time::Duration; + +#[cfg(feature = "alloc")] +use alloc::{ + string::String, + format }; #[cfg(feature = "std")] -use std::time::SystemTime; - -use anyhow::{bail, Result}; +use std::time::{SystemTime, SystemTimeError}; -use crate::constants::*; +// TODO +#[cfg(feature = "nightly")] +use core::error::Error; +#[cfg(all(feature = "std", not(feature = "nightly")))] +use std::error::Error; /// UTC Timestamp. /// @@ -59,6 +67,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 +99,7 @@ impl UTCTimestamp { /// Try to create a UTC Timestamp from the local system time. #[cfg(feature = "std")] - pub fn try_from_system_time() -> Result { + pub fn try_from_system_time() -> Result { let duration = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; Ok(UTCTimestamp(duration)) } @@ -585,7 +594,7 @@ where /// Create from local system time #[cfg(feature = "std")] - fn try_from_system_time() -> Result { + fn try_from_system_time() -> Result { let timestamp = UTCTimestamp::try_from_system_time()?; Ok(Self::from_timestamp(timestamp)) } @@ -644,10 +653,10 @@ impl UTCDay { } /// Try create UTC Day from integer. - pub fn try_from_u64(u: u64) -> Result { + pub fn try_from_u64(u: u64) -> Result { 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)] +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 for UTCDay { } impl TryFrom for UTCDay { - type Error = anyhow::Error; + type Error = UTCDayErrOutOfRange; - fn try_from(value: u64) -> core::result::Result { + fn try_from(value: u64) -> Result { Self::try_from_u64(value) } } @@ -981,6 +1003,7 @@ impl From for UTCDay { /// ## 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); @@ -1065,37 +1088,37 @@ impl UTCTimeOfDay { } /// Try to create UTC time of day from nanoseconds - pub fn try_from_nanos(nanos: u64) -> Result { + pub fn try_from_nanos(nanos: u64) -> Result { 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 { + pub fn try_from_micros(micros: u64) -> Result { 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 { + pub fn try_from_millis(millis: u32) -> Result { 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 { + pub fn try_from_secs(secs: u32) -> Result { 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 +1127,7 @@ 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 { + pub fn try_from_hhmmss(hrs: u8, mins: u8, secs: u8, subsec_ns: u32) -> Result { Self::try_from_nanos(Self::_ns_from_hhmmss(hrs, mins, secs, subsec_ns)) } @@ -1165,8 +1188,7 @@ impl UTCTimeOfDay { /// /// Conforms to ISO 8601: /// - #[cfg(feature = "std")] - pub fn try_from_iso_tod(iso: &str) -> Result { + pub fn try_from_iso_tod(iso: &str) -> Result { 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" @@ -1180,10 +1202,7 @@ impl UTCTimeOfDay { 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 - ); + return Err(UTCTimeOfDayError::ExcessPrecision(precision)); } if precision == 0 { 0 @@ -1204,7 +1223,7 @@ impl UTCTimeOfDay { /// /// Conforms to ISO 8601: /// - #[cfg(feature = "std")] + #[cfg(feature = "alloc")] pub fn as_iso_tod(&self, precision: Option) -> String { let mut s = format!("{self}"); let len = if let Some(p) = precision { @@ -1217,3 +1236,54 @@ impl UTCTimeOfDay { s } } + +/// Error type for UTCTimeOfDay methods +#[derive(Debug)] +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), +} + +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"), + } + } +} + +#[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 for UTCTimeOfDayError { + fn from(value: ParseIntError) -> Self { + Self::ParseErr(value) + } +} From 00837ffaceae20eb39e74023217fd9e74e56fe98 Mon Sep 17 00:00:00 2001 From: reece Date: Sat, 24 Aug 2024 16:53:20 +0800 Subject: [PATCH 06/29] tests/*: tests using custom error types --- tests/date.rs | 15 ++++++--------- tests/datetime.rs | 22 +++++++++++----------- tests/time.rs | 17 ++++++++--------- 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/tests/date.rs b/tests/date.rs index 7f778de..60ac041 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), @@ -67,7 +64,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 @@ -99,7 +96,7 @@ fn test_date_iso_conversions() -> Result<()> { } // test transform from system time - let date_from_system_time = UTCDate::try_from_system_time()?; + 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 @@ -118,7 +115,7 @@ fn test_date_iso_conversions() -> Result<()> { } #[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), diff --git a/tests/datetime.rs b/tests/datetime.rs index 6ec3521..cbb9457 100644 --- a/tests/datetime.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,9 +93,9 @@ fn test_datetime_from_raw_components() -> Result<()> { Ok(()) } -#[cfg(feature = "std")] +#[cfg(feature = "alloc")] #[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. @@ -132,8 +130,10 @@ fn test_datetime_iso_conversions() -> Result<()> { assert!(UTCDatetime::try_from_iso_datetime("1970-01-01T00:a0:00Z").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(()) } diff --git a/tests/time.rs b/tests/time.rs index dcec299..9468173 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,7 +310,7 @@ 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")] + #[cfg(feature = "alloc")] { let iso_from_tod = tod_from_timestamp.as_iso_tod(Some(9)); let tod_from_iso = UTCTimeOfDay::try_from_iso_tod(&iso_from_tod)?; From b4441b24c4779b62e40b241d15485019ba45c2d3 Mon Sep 17 00:00:00 2001 From: reece Date: Sat, 24 Aug 2024 16:55:39 +0800 Subject: [PATCH 07/29] src/*: fmt pass --- src/date.rs | 23 ++++++++--------------- src/lib.rs | 2 +- src/time.rs | 29 +++++++++++++---------------- 3 files changed, 22 insertions(+), 32 deletions(-) diff --git a/src/date.rs b/src/date.rs index 9f53196..3d47543 100644 --- a/src/date.rs +++ b/src/date.rs @@ -10,15 +10,12 @@ use core::num::ParseIntError; use core::time::Duration; #[cfg(feature = "alloc")] -use alloc::{ - string::String, - format -}; +use alloc::{format, string::String}; // TODO #[cfg(feature = "nightly")] use core::error::Error; -#[cfg(all(feature ="std", not(feature = "nightly")))] +#[cfg(all(feature = "std", not(feature = "nightly")))] use std::error::Error; /// UTC Date. @@ -337,21 +334,17 @@ pub enum UTCDateError { /// Error raised due to out of range day DayOutOfRange(UTCDate), /// Error raised due to out of range date - DateOutOfRange(UTCDate) + DateOutOfRange(UTCDate), } 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::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!"), } } } @@ -361,7 +354,7 @@ impl Error for UTCDateError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { Self::ParseErr(e) => e.source(), - _ => None + _ => None, } } } diff --git a/src/lib.rs b/src/lib.rs index bb9871e..1fa0133 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -214,7 +214,7 @@ pub mod time; pub mod constants; use crate::date::{UTCDate, UTCDateError}; -use crate::time::{UTCTimeOfDay, UTCTimestamp, UTCTransformations, UTCTimeOfDayError}; +use crate::time::{UTCTimeOfDay, UTCTimeOfDayError, UTCTimestamp, UTCTransformations}; use core::fmt::{Display, Formatter}; use core::time::Duration; diff --git a/src/time.rs b/src/time.rs index 3b370d4..de15ad8 100644 --- a/src/time.rs +++ b/src/time.rs @@ -9,10 +9,7 @@ use core::ops::*; use core::time::Duration; #[cfg(feature = "alloc")] -use alloc::{ - string::String, - format -}; +use alloc::{format, string::String}; #[cfg(feature = "std")] use std::time::{SystemTime, SystemTimeError}; @@ -1127,7 +1124,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 { + pub fn try_from_hhmmss( + hrs: u8, + mins: u8, + secs: u8, + subsec_ns: u32, + ) -> Result { Self::try_from_nanos(Self::_ns_from_hhmmss(hrs, mins, secs, subsec_ns)) } @@ -1258,16 +1260,11 @@ 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::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"), } } } @@ -1277,7 +1274,7 @@ impl Error for UTCTimeOfDayError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { Self::ParseErr(e) => e.source(), - _ => None + _ => None, } } } From 07ba90847e03093757fe0afe4d50dc4a9d635569 Mon Sep 17 00:00:00 2001 From: reece Date: Sat, 24 Aug 2024 16:56:02 +0800 Subject: [PATCH 08/29] tests/date.rs: fmt pass --- tests/date.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/date.rs b/tests/date.rs index 60ac041..a2c5e1d 100644 --- a/tests/date.rs +++ b/tests/date.rs @@ -40,7 +40,7 @@ fn test_date_from_components() { } #[test] -fn test_date_from_day() -> Result<(), UTCError>{ +fn test_date_from_day() -> Result<(), UTCError> { let test_cases = [ (UTCDay::ZERO, 1970, 1, 1), (UTCDay::try_from_u64(30)?, 1970, 1, 31), From 37561d5360b0b627b3500fde1586247d538f3736 Mon Sep 17 00:00:00 2001 From: reece Date: Sat, 24 Aug 2024 18:01:20 +0800 Subject: [PATCH 09/29] tests/*: add serde tests --- tests/date.rs | 8 ++++++++ tests/datetime.rs | 8 ++++++++ tests/time.rs | 16 ++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/tests/date.rs b/tests/date.rs index a2c5e1d..c9b27f6 100644 --- a/tests/date.rs +++ b/tests/date.rs @@ -181,3 +181,11 @@ fn test_date_transformations() -> Result<(), UTCError> { 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/datetime.rs b/tests/datetime.rs index cbb9457..545be43 100644 --- a/tests/datetime.rs +++ b/tests/datetime.rs @@ -137,3 +137,11 @@ fn test_datetime_iso_conversions() -> Result<(), UTCError> { } 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/time.rs b/tests/time.rs index 9468173..efecfb9 100644 --- a/tests/time.rs +++ b/tests/time.rs @@ -366,3 +366,19 @@ fn test_utc_tod() -> Result<(), UTCError> { ); 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()); +} From 215ba2205001a38f936ca778d9c04987a1217fef Mon Sep 17 00:00:00 2001 From: reece Date: Sat, 24 Aug 2024 18:01:44 +0800 Subject: [PATCH 10/29] src/time.rs: missed serde derive --- src/time.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/time.rs b/src/time.rs index de15ad8..6d8d27e 100644 --- a/src/time.rs +++ b/src/time.rs @@ -626,6 +626,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); From f7ec0533babcaad41c2ff784a6b5b13e393880c7 Mon Sep 17 00:00:00 2001 From: reece Date: Sat, 24 Aug 2024 18:02:02 +0800 Subject: [PATCH 11/29] Cargo.toml: serde_json as dev dep for tests --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 04daa59..7678f46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,3 +27,6 @@ nightly = [] [dependencies] serde = { version = "1.0", default-features = false, optional = true, features = ["derive"] } + +[dev-dependencies] +serde_json = "1.0" From 0dc5b83e31658baf82e30a849b5dc63cae43eeac Mon Sep 17 00:00:00 2001 From: reece Date: Sun, 25 Aug 2024 16:46:07 +0800 Subject: [PATCH 12/29] src/util.rs: init StrWriter util --- src/util.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/util.rs diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..43505ae --- /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[remaining..][..write_len].copy_from_slice(&write_bytes); + self.written += write_len; + Ok(()) + } +} From 76f4c9a0d1d9244d13a59125a9b4a504ed3c9bfc Mon Sep 17 00:00:00 2001 From: reece Date: Sun, 25 Aug 2024 16:48:02 +0800 Subject: [PATCH 13/29] src/*: add `write_iso` functions for writing static buffers with ISO formatted characters --- src/date.rs | 43 ++++++++++++++++++++++++++-- src/lib.rs | 54 +++++++++++++++++++++++++++++++---- src/time.rs | 82 +++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 156 insertions(+), 23 deletions(-) diff --git a/src/date.rs b/src/date.rs index 3d47543..a4c7497 100644 --- a/src/date.rs +++ b/src/date.rs @@ -5,7 +5,8 @@ //! to create UTC dates. use crate::time::{UTCDay, UTCTimestamp, UTCTransformations}; -use core::fmt::{Display, Formatter}; +use crate::util::StrWriter; +use core::fmt::{Display, Formatter, Write}; use core::num::ParseIntError; use core::time::Duration; @@ -107,6 +108,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 @@ -229,12 +233,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: /// pub fn try_from_iso_date(iso: &str) -> Result { + let len = iso.len(); + if len != Self::ISO_DATE_LEN { + return Err(UTCDateError::InsufficientStrLen(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" @@ -255,6 +263,34 @@ impl UTCDate { 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::InsufficientStrLen]). + /// + /// Returns number of UTF8 characters (bytes) written + /// + /// Conforms to ISO 8601: + /// + pub fn write_iso_date(&self, buf: &mut [u8]) -> Result { + let write_len = Self::ISO_DATE_LEN; + if write_len > buf.len() { + return Err(UTCDateError::InsufficientStrLen(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 { @@ -335,6 +371,8 @@ pub enum UTCDateError { DayOutOfRange(UTCDate), /// Error raised due to out of range date DateOutOfRange(UTCDate), + /// Error raised due to invalid ISO date length + InsufficientStrLen(usize), } impl Display for UTCDateError { @@ -345,6 +383,7 @@ impl Display for UTCDateError { 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::InsufficientStrLen(l) => write!(f, "Insufficient ISO date str length ({l}), 10 required"), } } } diff --git a/src/lib.rs b/src/lib.rs index 1fa0133..f1028c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -212,6 +212,7 @@ pub mod date; pub mod time; #[rustfmt::skip] pub mod constants; +mod util; use crate::date::{UTCDate, UTCDateError}; use crate::time::{UTCTimeOfDay, UTCTimeOfDayError, UTCTimestamp, UTCTransformations}; @@ -221,6 +222,7 @@ use core::time::Duration; #[cfg(feature = "alloc")] use alloc::string::String; use time::UTCDayErrOutOfRange; +use util::StrWriter; // TODO #[cfg(feature = "nightly")] @@ -292,6 +294,9 @@ impl UTCDatetime { 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 { @@ -326,7 +331,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` @@ -336,6 +341,10 @@ impl UTCDatetime { /// Conforms to ISO 8601: /// pub fn try_from_iso_datetime(iso: &str) -> Result { + 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)?; @@ -343,19 +352,48 @@ 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: /// #[cfg(feature = "alloc")] - pub fn as_iso_datetime(&self, precision: Option) -> String { + 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: + /// + pub fn write_iso_datetime(&self, buf: &mut [u8], precision: usize) -> Result{ + 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 { @@ -391,6 +429,8 @@ pub enum UTCDatetimeError { 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 { @@ -398,6 +438,7 @@ impl Display for UTCDatetimeError { 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"), } } } @@ -408,6 +449,7 @@ impl Error for UTCDatetimeError { match self { Self::UTCDate(e) => e.source(), Self::UTCTimeOfDay(e) => e.source(), + _ => None, } } } diff --git a/src/time.rs b/src/time.rs index 6d8d27e..0c0ebb1 100644 --- a/src/time.rs +++ b/src/time.rs @@ -3,7 +3,8 @@ //! Implements core time concepts via UTC Timestamps, UTC Days and UTC Time-of-Days. use crate::constants::*; -use core::fmt::{Display, Formatter}; +use crate::util::StrWriter; +use core::fmt::{Display, Formatter, Write}; use core::num::ParseIntError; use core::ops::*; use core::time::Duration; @@ -1028,6 +1029,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 @@ -1185,59 +1192,101 @@ 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: /// pub fn try_from_iso_tod(iso: &str) -> Result { + 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" - + // parse 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 { + 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: /// #[cfg(feature = "alloc")] - pub fn as_iso_tod(&self, precision: Option) -> String { + 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.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: + /// + pub fn write_iso_tod(&self, buf: &mut [u8], precision: usize) -> Result { + 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 @@ -1255,6 +1304,8 @@ pub enum UTCTimeOfDayError { 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 { @@ -1266,6 +1317,7 @@ impl Display for UTCTimeOfDayError { 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"), } } } From 5ca4ffb055d2654bdff5dc9fb6843eabbdadf43c Mon Sep 17 00:00:00 2001 From: reece Date: Sun, 25 Aug 2024 17:53:57 +0800 Subject: [PATCH 14/29] src/time.rs: truncate extra value on string --- src/time.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/time.rs b/src/time.rs index 0c0ebb1..548fb15 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1240,7 +1240,7 @@ impl UTCTimeOfDay { pub fn as_iso_tod(&self, precision: usize) -> String { let len = Self::iso_tod_len(precision); let mut s = format!("{self}"); - s.truncate(len); + s.truncate(len - 1); s.push('Z'); s } From 9056ae0aa996a03cc88641176ce529157d496fac Mon Sep 17 00:00:00 2001 From: reece Date: Sun, 25 Aug 2024 17:55:43 +0800 Subject: [PATCH 15/29] src/date.rs: error type renamed --- src/date.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/date.rs b/src/date.rs index a4c7497..0ad0c11 100644 --- a/src/date.rs +++ b/src/date.rs @@ -241,7 +241,7 @@ impl UTCDate { pub fn try_from_iso_date(iso: &str) -> Result { let len = iso.len(); if len != Self::ISO_DATE_LEN { - return Err(UTCDateError::InsufficientStrLen(len)); + return Err(UTCDateError::InvalidStrLen(len)); } // handle slice let (year_str, rem) = iso.split_at(4); // remainder = "-MM-DD" @@ -285,7 +285,7 @@ impl UTCDate { pub fn write_iso_date(&self, buf: &mut [u8]) -> Result { let write_len = Self::ISO_DATE_LEN; if write_len > buf.len() { - return Err(UTCDateError::InsufficientStrLen(buf.len())); + return Err(UTCDateError::InvalidStrLen(buf.len())); } let mut writer = StrWriter::new(&mut buf[..write_len]); self._write_iso_date_trunc(&mut writer); @@ -372,7 +372,7 @@ pub enum UTCDateError { /// Error raised due to out of range date DateOutOfRange(UTCDate), /// Error raised due to invalid ISO date length - InsufficientStrLen(usize), + InvalidStrLen(usize), } impl Display for UTCDateError { @@ -383,7 +383,7 @@ impl Display for UTCDateError { 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::InsufficientStrLen(l) => write!(f, "Insufficient ISO date str length ({l}), 10 required"), + Self::InvalidStrLen(l) => write!(f, "Invalid ISO date str length ({l}), 10 required"), } } } From 38dbdc212ccff612627fa6c9ea3a275889565abf Mon Sep 17 00:00:00 2001 From: reece Date: Sun, 25 Aug 2024 17:56:16 +0800 Subject: [PATCH 16/29] src/util.rs: fix incorrect start index on buf --- src/util.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.rs b/src/util.rs index 43505ae..ef3edb6 100644 --- a/src/util.rs +++ b/src/util.rs @@ -20,7 +20,7 @@ impl<'a> core::fmt::Write for StrWriter<'a> { let write_len = remaining.min(s.len()); let write_bytes = &s.as_bytes()[..write_len]; // infallible truncating write - self.buf[remaining..][..write_len].copy_from_slice(&write_bytes); + self.buf[self.written..][..write_len].copy_from_slice(&write_bytes); self.written += write_len; Ok(()) } From 8b222b5c039d4b57c44dbd10209d2eb8774b33d0 Mon Sep 17 00:00:00 2001 From: reece Date: Sun, 25 Aug 2024 17:56:44 +0800 Subject: [PATCH 17/29] tests/*: extend for new no-alloc iso conversions --- tests/date.rs | 41 +++++++++++++++++++++++++---------------- tests/datetime.rs | 27 ++++++++++++++++++++------- tests/time.rs | 20 +++++++++++++++++++- 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/tests/date.rs b/tests/date.rs index c9b27f6..a59fb65 100644 --- a/tests/date.rs +++ b/tests/date.rs @@ -63,7 +63,6 @@ fn test_date_from_day() -> Result<(), UTCError> { } #[test] -#[cfg(feature = "std")] fn test_date_iso_conversions() -> Result<(), UTCError> { let test_cases = [ (2023, 6, 14, true, "2023-06-14"), // valid recent date @@ -80,6 +79,7 @@ fn test_date_iso_conversions() -> Result<(), UTCError> { (2023, 9, 0, false, "2023-0a-00"), // invalid date, month not integer (2023, 9, 0, false, "2023-09-0a"), // invalid date, day not integer ]; + 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) { @@ -87,7 +87,14 @@ fn test_date_iso_conversions() -> Result<(), UTCError> { 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); @@ -96,21 +103,23 @@ fn test_date_iso_conversions() -> Result<(), UTCError> { } // test transform from system time - 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); - + #[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(()) } diff --git a/tests/datetime.rs b/tests/datetime.rs index 545be43..5525e02 100644 --- a/tests/datetime.rs +++ b/tests/datetime.rs @@ -93,24 +93,23 @@ fn test_datetime_from_raw_components() -> Result<(), UTCError> { Ok(()) } -#[cfg(feature = "alloc")] #[test] 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 { @@ -118,11 +117,25 @@ fn test_datetime_iso_conversions() -> Result<(), UTCError> { 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 diff --git a/tests/time.rs b/tests/time.rs index efecfb9..27f9edc 100644 --- a/tests/time.rs +++ b/tests/time.rs @@ -312,7 +312,7 @@ fn test_utc_tod() -> Result<(), UTCError> { // test iso conversions #[cfg(feature = "alloc")] { - let iso_from_tod = tod_from_timestamp.as_iso_tod(Some(9)); + let iso_from_tod = tod_from_timestamp.as_iso_tod(9); let tod_from_iso = UTCTimeOfDay::try_from_iso_tod(&iso_from_tod)?; assert_eq!(tod_from_iso, tod_from_timestamp); assert_eq!( @@ -330,6 +330,24 @@ fn test_utc_tod() -> Result<(), UTCError> { assert!(UTCTimeOfDay::try_from_iso_tod("T23:59:59.9999999990Z").is_err()); // invalid precision } + // 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 let secs_from_tod = tod_from_timestamp.as_secs(); From 3d53b065e24a21446f1c293b76691d1bcf6a6629 Mon Sep 17 00:00:00 2001 From: reece Date: Sun, 25 Aug 2024 18:10:43 +0800 Subject: [PATCH 18/29] src/*: better docs of unsafe usage --- src/date.rs | 5 +++-- src/lib.rs | 1 + src/time.rs | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/date.rs b/src/date.rs index 0ad0c11..843171a 100644 --- a/src/date.rs +++ b/src/date.rs @@ -137,9 +137,9 @@ impl UTCDate { if month == 0 || month > 12 { 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() { return Err(UTCDateError::DayOutOfRange(date)); } @@ -186,6 +186,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) } } diff --git a/src/lib.rs b/src/lib.rs index f1028c6..5ba92c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -291,6 +291,7 @@ 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) }, }; diff --git a/src/time.rs b/src/time.rs index 548fb15..78f6c63 100644 --- a/src/time.rs +++ b/src/time.rs @@ -126,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) } } @@ -1094,6 +1095,7 @@ impl UTCTimeOfDay { /// Try to create UTC time of day from nanoseconds pub fn try_from_nanos(nanos: u64) -> Result { + // 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 { return Err(UTCTimeOfDayError::ExcessNanos(nanos)); @@ -1103,6 +1105,7 @@ impl UTCTimeOfDay { /// Try to create UTC time of day from microseconds pub fn try_from_micros(micros: u64) -> Result { + // 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 { return Err(UTCTimeOfDayError::ExcessMicros(micros)); @@ -1112,6 +1115,7 @@ impl UTCTimeOfDay { /// Try to create UTC time of day from milliseconds pub fn try_from_millis(millis: u32) -> Result { + // 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 { return Err(UTCTimeOfDayError::ExcessMillis(millis)); @@ -1121,6 +1125,7 @@ impl UTCTimeOfDay { /// Try to create UTC time of day from seconds pub fn try_from_secs(secs: u32) -> Result { + // 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 { return Err(UTCTimeOfDayError::ExcessSeconds(secs)); From c8a8aa96838548f0a2904ca8acd994cce03eda1d Mon Sep 17 00:00:00 2001 From: reece Date: Sun, 25 Aug 2024 18:12:46 +0800 Subject: [PATCH 19/29] src/*: fmt and clippy pass --- src/lib.rs | 15 ++++++++++++--- src/time.rs | 16 ++++++++++++---- src/util.rs | 4 ++-- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5ba92c1..7829c01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -344,7 +344,10 @@ impl UTCDatetime { pub fn try_from_iso_datetime(iso: &str) -> Result { let len = iso.len(); if len < Self::MIN_ISO_DATETIME_LEN { - return Err(UTCDatetimeError::InsufficientStrLen(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)?; @@ -379,7 +382,11 @@ impl UTCDatetime { /// /// Conforms to ISO 8601: /// - pub fn write_iso_datetime(&self, buf: &mut [u8], precision: usize) -> Result{ + pub fn write_iso_datetime( + &self, + buf: &mut [u8], + precision: usize, + ) -> Result { let write_len = Self::iso_datetime_len(precision); if write_len > buf.len() { return Err(UTCDatetimeError::InsufficientStrLen(buf.len(), write_len)); @@ -439,7 +446,9 @@ impl Display for UTCDatetimeError { 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"), + Self::InsufficientStrLen(l, m) => { + write!(f, "Insufficient ISO datetime str len ({l}), {m} required") + } } } } diff --git a/src/time.rs b/src/time.rs index 78f6c63..2b4b41a 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1206,12 +1206,14 @@ impl UTCTimeOfDay { pub fn try_from_iso_tod(iso: &str) -> Result { let len = iso.len(); if len < Self::MIN_ISO_TOD_LEN { - return Err(UTCTimeOfDayError::InsufficientStrLen(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" - // parse let hrs: u8 = hour_str.parse()?; let mins: u8 = minute_str.parse()?; let secs: u8 = second_str.parse()?; @@ -1270,7 +1272,11 @@ impl UTCTimeOfDay { /// /// Conforms to ISO 8601: /// - pub fn write_iso_tod(&self, buf: &mut [u8], precision: usize) -> Result { + pub fn write_iso_tod( + &self, + buf: &mut [u8], + precision: usize, + ) -> Result { let write_len = Self::iso_tod_len(precision); if write_len > buf.len() { return Err(UTCTimeOfDayError::InsufficientStrLen(buf.len(), write_len)); @@ -1322,7 +1328,9 @@ impl Display for UTCTimeOfDayError { 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"), + Self::InsufficientStrLen(l, m) => { + write!(f, "Insufficient ISO time str len ({l}), {m} required") + } } } } diff --git a/src/util.rs b/src/util.rs index ef3edb6..108582c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -3,7 +3,7 @@ /// Utility for no-alloc str writing to a buffer via `core::fmt` pub struct StrWriter<'a> { pub buf: &'a mut [u8], - pub written: usize + pub written: usize, } impl<'a> StrWriter<'a> { @@ -20,7 +20,7 @@ impl<'a> core::fmt::Write for StrWriter<'a> { 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.buf[self.written..][..write_len].copy_from_slice(write_bytes); self.written += write_len; Ok(()) } From eba99e8ed8753d6522bbe64d4478589cc3d16cd1 Mon Sep 17 00:00:00 2001 From: reece Date: Sun, 25 Aug 2024 18:13:01 +0800 Subject: [PATCH 20/29] tests/time.rs: fmt pass --- tests/time.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/time.rs b/tests/time.rs index 27f9edc..557188c 100644 --- a/tests/time.rs +++ b/tests/time.rs @@ -348,7 +348,6 @@ fn test_utc_tod() -> Result<(), UTCError> { } } - // test unit conversions let secs_from_tod = tod_from_timestamp.as_secs(); let millis_from_tod = tod_from_timestamp.as_millis(); From 71d9ff8ecd1ef636c858a8e4033ef79421c302a8 Mon Sep 17 00:00:00 2001 From: reece Date: Sun, 25 Aug 2024 18:41:04 +0800 Subject: [PATCH 21/29] src/*: update docstrings --- src/date.rs | 7 ++++++- src/lib.rs | 33 ++++++++++++++++++++++++++------- src/time.rs | 10 +++++++--- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/date.rs b/src/date.rs index 843171a..fea37e3 100644 --- a/src/date.rs +++ b/src/date.rs @@ -51,6 +51,11 @@ use std::error::Error; /// // 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 static 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 @@ -277,7 +282,7 @@ impl UTCDate { /// /// The buffer should have minimum length of [UTCDate::ISO_DATE_LEN] (10). /// - /// A buffer of insufficient length will error ([UTCDateError::InsufficientStrLen]). + /// A buffer of insufficient length will error ([UTCDateError::InvalidStrLen]). /// /// Returns number of UTF8 characters (bytes) written /// diff --git a/src/lib.rs b/src/lib.rs index 7829c01..e3bec49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -103,9 +103,14 @@ //! // 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)` -//! 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 static 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 @@ -125,6 +130,11 @@ //! // Get date string formatted according to ISO 8601 `(YYYY-MM-DD)` //! let iso_date = utc_date.as_iso_date(); //! assert_eq!(iso_date, "2023-06-15"); +//! // Write ISO 8601 date str to a static 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,9 +144,14 @@ //! // 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)` -//! 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 static 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! @@ -255,10 +270,14 @@ use std::error::Error; /// // 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 static 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))] diff --git a/src/time.rs b/src/time.rs index 2b4b41a..c7fad2a 100644 --- a/src/time.rs +++ b/src/time.rs @@ -994,10 +994,14 @@ impl From 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 static 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 From 159c5967b7db23fb865230248e1ebbc538629979 Mon Sep 17 00:00:00 2001 From: reece Date: Sun, 25 Aug 2024 18:41:22 +0800 Subject: [PATCH 22/29] README.md: update examples --- README.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1ae1d3b..2e50068 100644 --- a/README.md +++ b/README.md @@ -102,9 +102,14 @@ See [docs.rs](https://docs.rs/utc-dt) for the API reference. // 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)` - 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 static 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 @@ -124,6 +129,11 @@ See [docs.rs](https://docs.rs/utc-dt) for the API reference. // Get date string formatted according to ISO 8601 `(YYYY-MM-DD)` let iso_date = utc_date.as_iso_date(); assert_eq!(iso_date, "2023-06-15"); + // Write ISO 8601 date str to a static 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); @@ -133,9 +143,14 @@ See [docs.rs](https://docs.rs/utc-dt) for the API reference. // 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)` - 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 static 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! From b32a29f1720693001591521b7f03678cdc12c6f3 Mon Sep 17 00:00:00 2001 From: reece Date: Mon, 26 Aug 2024 12:17:41 +0800 Subject: [PATCH 23/29] dosctring correction static -> stack --- README.md | 6 +++--- src/date.rs | 2 +- src/lib.rs | 8 ++++---- src/time.rs | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2e50068..741c0ff 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ See [docs.rs](https://docs.rs/utc-dt) for the API reference. 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 static buffer + // 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(); @@ -129,7 +129,7 @@ See [docs.rs](https://docs.rs/utc-dt) for the API reference. // Get date string formatted according to ISO 8601 `(YYYY-MM-DD)` let iso_date = utc_date.as_iso_date(); assert_eq!(iso_date, "2023-06-15"); - // Write ISO 8601 date str to a static buffer + // 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(); @@ -146,7 +146,7 @@ See [docs.rs](https://docs.rs/utc-dt) for the API reference. 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 static buffer + // 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(); diff --git a/src/date.rs b/src/date.rs index fea37e3..c23d23b 100644 --- a/src/date.rs +++ b/src/date.rs @@ -51,7 +51,7 @@ use std::error::Error; /// // 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 static buffer +/// // 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(); diff --git a/src/lib.rs b/src/lib.rs index e3bec49..84a7aaa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,7 +106,7 @@ //! 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 static buffer +//! // 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(); @@ -130,7 +130,7 @@ //! // Get date string formatted according to ISO 8601 `(YYYY-MM-DD)` //! let iso_date = utc_date.as_iso_date(); //! assert_eq!(iso_date, "2023-06-15"); -//! // Write ISO 8601 date str to a static buffer +//! // 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(); @@ -147,7 +147,7 @@ //! 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 static buffer +//! // 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(); @@ -273,7 +273,7 @@ use std::error::Error; /// 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 static buffer +/// // 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(); diff --git a/src/time.rs b/src/time.rs index c7fad2a..3432bb5 100644 --- a/src/time.rs +++ b/src/time.rs @@ -997,7 +997,7 @@ impl From for UTCDay { /// 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 static buffer +/// // 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(); From 6ffe714378335a5d11c183920464e6dd2362cab2 Mon Sep 17 00:00:00 2001 From: reece Date: Mon, 26 Aug 2024 12:43:45 +0800 Subject: [PATCH 24/29] workflows/rust.yml: codecov test on nightly --- .github/workflows/rust.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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: From 86655d3feb97932587e3d965993a83119053658a Mon Sep 17 00:00:00 2001 From: reece Date: Mon, 26 Aug 2024 13:55:16 +0800 Subject: [PATCH 25/29] src/*: clone on error types, consistent formatting --- src/date.rs | 12 ++++++------ src/lib.rs | 6 +++--- src/time.rs | 14 +++++++------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/date.rs b/src/date.rs index c23d23b..77850d7 100644 --- a/src/date.rs +++ b/src/date.rs @@ -365,7 +365,7 @@ impl From for UTCDate { } /// Error type for UTCDate methods -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum UTCDateError { /// Error raised parsing int to string ParseErr(ParseIntError), @@ -385,11 +385,11 @@ 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"), + 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"), } } } diff --git a/src/lib.rs b/src/lib.rs index 84a7aaa..1ecf378 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -450,7 +450,7 @@ impl From for UTCDatetime { } /// Error type for UTCDatetime methods -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum UTCDatetimeError { /// Error within UTC Date UTCDate(UTCDateError), @@ -466,7 +466,7 @@ impl Display for UTCDatetimeError { 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") + write!(f, "insufficient ISO datetime str len ({l}), {m} required") } } } @@ -496,7 +496,7 @@ impl From for UTCDatetimeError { } /// UTC Datetime crate level error type -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum UTCError { /// Error within UTC Date UTCDate(UTCDateError), diff --git a/src/time.rs b/src/time.rs index 3432bb5..da15d2d 100644 --- a/src/time.rs +++ b/src/time.rs @@ -787,7 +787,7 @@ impl UTCDay { } /// Error type for UTCDay out of range -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct UTCDayErrOutOfRange(u64); impl Display for UTCDayErrOutOfRange { @@ -1305,7 +1305,7 @@ impl UTCTimeOfDay { } /// Error type for UTCTimeOfDay methods -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum UTCTimeOfDayError { /// Error raised parsing int to string ParseErr(ParseIntError), @@ -1328,12 +1328,12 @@ impl Display for UTCTimeOfDayError { 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::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") + write!(f, "insufficient ISO time str len ({l}), {m} required") } } } From 6226c3615bb31dec1050969194b7d2c76f3e130e Mon Sep 17 00:00:00 2001 From: reece Date: Mon, 26 Aug 2024 13:56:14 +0800 Subject: [PATCH 26/29] tests/*: coverage --- tests/date.rs | 1 + tests/datetime.rs | 1 + tests/time.rs | 49 +++++++++++++++++++++++++++++------------------ 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/tests/date.rs b/tests/date.rs index a59fb65..a3e4ae4 100644 --- a/tests/date.rs +++ b/tests/date.rs @@ -78,6 +78,7 @@ fn test_date_iso_conversions() -> Result<(), UTCError> { (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]; diff --git a/tests/datetime.rs b/tests/datetime.rs index 5525e02..80618d1 100644 --- a/tests/datetime.rs +++ b/tests/datetime.rs @@ -141,6 +141,7 @@ fn test_datetime_iso_conversions() -> Result<(), UTCError> { // 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 #[cfg(feature = "std")] diff --git a/tests/time.rs b/tests/time.rs index 557188c..5681d84 100644 --- a/tests/time.rs +++ b/tests/time.rs @@ -311,25 +311,36 @@ fn test_utc_tod() -> Result<(), UTCError> { assert!(UTCTimeOfDay::try_from_hhmmss(u8::MAX, u8::MAX, u8::MAX, u32::MAX).is_err()); // test iso conversions #[cfg(feature = "alloc")] - { - let iso_from_tod = tod_from_timestamp.as_iso_tod(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 - } + 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 { From cacc71d708d6c4cd1bec77e1b1045e32a5495339 Mon Sep 17 00:00:00 2001 From: reece Date: Mon, 26 Aug 2024 13:56:33 +0800 Subject: [PATCH 27/29] tests/error.rs: testing error displays --- tests/error.rs | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/error.rs diff --git a/tests/error.rs b/tests/error.rs new file mode 100644 index 0000000..450df6f --- /dev/null +++ b/tests/error.rs @@ -0,0 +1,53 @@ +use std::{error::Error, fmt::Display}; + +use utc_dt::date::{UTCDate, UTCDateError}; +use utc_dt::time::{UTCDay, UTCTimeOfDayError}; +use utc_dt::{UTCDatetimeError, UTCError}; + +fn check_errors(errors: &[T]) { + for error in errors { + print!("Error Display test: {error}"); + if let Some(source) = error.source() { + print!(", caused by {source}"); + } + print!("\n"); + } +} + +#[test] +fn test_errors() { + let utc_date_errors = [ + UTCDateError::ParseErr("a".parse::().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::().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 = [ + UTCDatetimeError::UTCDate(utc_date_errors[0].clone()), + UTCDatetimeError::UTCTimeOfDay(utc_tod_errors[0].clone()), + UTCDatetimeError::InsufficientStrLen(10, 20), + ]; + check_errors(&utc_datetime_errors); + let utc_errors = [ + UTCError::UTCDate(utc_date_errors[1].clone()), + UTCError::UTCTimeOfDay(utc_tod_errors[1].clone()), + UTCError::UTCDay(utc_day_error[0].clone()), + UTCError::UTCDatetime(utc_datetime_errors[0].clone()), + ]; + check_errors(&utc_errors.clone()); +} From cdf26b9716f92aece63458dc20a2c91eb596c186 Mon Sep 17 00:00:00 2001 From: reece Date: Mon, 26 Aug 2024 14:06:05 +0800 Subject: [PATCH 28/29] tests/error.rs: test From impls --- tests/error.rs | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/error.rs b/tests/error.rs index 450df6f..7889979 100644 --- a/tests/error.rs +++ b/tests/error.rs @@ -1,10 +1,10 @@ -use std::{error::Error, fmt::Display}; - +use core::fmt::Display; use utc_dt::date::{UTCDate, UTCDateError}; use utc_dt::time::{UTCDay, UTCTimeOfDayError}; use utc_dt::{UTCDatetimeError, UTCError}; -fn check_errors(errors: &[T]) { +#[cfg(feature = "std")] +fn check_errors(errors: &[T]) { for error in errors { print!("Error Display test: {error}"); if let Some(source) = error.source() { @@ -14,6 +14,14 @@ fn check_errors(errors: &[T]) { } } +#[cfg(not(feature = "std"))] +fn check_errors(errors: &[T]) { + for error in errors { + print!("Error Display test: {error}"); + print!("\n"); + } +} + #[test] fn test_errors() { let utc_date_errors = [ @@ -38,16 +46,16 @@ fn test_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 = [ - UTCDatetimeError::UTCDate(utc_date_errors[0].clone()), - UTCDatetimeError::UTCTimeOfDay(utc_tod_errors[0].clone()), + 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::UTCDate(utc_date_errors[1].clone()), - UTCError::UTCTimeOfDay(utc_tod_errors[1].clone()), - UTCError::UTCDay(utc_day_error[0].clone()), - UTCError::UTCDatetime(utc_datetime_errors[0].clone()), + 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()); } From b9756d8ab3a540a04bb7a2b9d2804bd1a0329579 Mon Sep 17 00:00:00 2001 From: reece Date: Mon, 26 Aug 2024 19:41:24 +0800 Subject: [PATCH 29/29] add docs badge --- README.md | 1 + src/lib.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 741c0ff..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) diff --git a/src/lib.rs b/src/lib.rs index 1ecf378..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) //!