Skip to content

Commit

Permalink
Custom Error Types, Serde Support, Improved #![no_std] Support (#10)
Browse files Browse the repository at this point in the history
* tests/lib.rs: rename to datetime.rs

* Cargo.toml: increment minor ver, remove anyhow dep, add optional serde dep, alloc + nightly features.

* docs: Increment version, add features

* Cargo.toml: no thiserror (until error in core is stabilized)

* src*: custom error types and alloc feature

* tests/*: tests using custom error types

* src/*: fmt pass

* tests/date.rs: fmt pass

* tests/*: add serde tests

* src/time.rs: missed serde derive

* Cargo.toml: serde_json as dev dep for tests

* src/util.rs: init StrWriter util

* src/*: add `write_iso` functions for writing static buffers with ISO formatted characters

* src/time.rs: truncate extra value on string

* src/date.rs: error type renamed

* src/util.rs: fix incorrect start index on buf

* tests/*: extend for new no-alloc iso conversions

* src/*:  better docs of unsafe usage

* src/*: fmt and clippy pass

* tests/time.rs: fmt pass

* src/*: update docstrings

* README.md: update examples

* dosctring correction static -> stack

* workflows/rust.yml: codecov test on nightly

* src/*: clone on error types, consistent formatting

* tests/*: coverage

* tests/error.rs: testing error displays

* tests/error.rs: test From impls

* add docs badge
  • Loading branch information
poshcoe authored Aug 27, 2024
1 parent 68796d7 commit 01e3691
Show file tree
Hide file tree
Showing 11 changed files with 792 additions and 183 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
15 changes: 12 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "utc-dt"
version = "0.2.1"
version = "0.3.0"
authors = ["Reece Kibble <[email protected]>"]
categories = ["date-and-time", "no-std", "parsing"]
keywords = ["time", "datetime", "date", "utc", "epoch"]
Expand All @@ -17,7 +17,16 @@ exclude = [".git*"]

[features]
default = ["std"]
std = ["anyhow/std"]
std = [
"alloc",
"serde/std",
]
alloc = ["serde/alloc"]
serde = ["dep:serde"]
nightly = []

[dependencies]
anyhow = { version = "1", default-features = false }
serde = { version = "1.0", default-features = false, optional = true, features = ["derive"] }

[dev-dependencies]
serde_json = "1.0"
42 changes: 30 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -12,7 +13,7 @@ It prioritizes being space-optimal and efficient.

```toml
[dependencies]
utc-dt = "0.2"
utc-dt = "0.3"
```
For extended/niche features and local time-zone support see [`chrono`](https://github.com/chronotope/chrono) or [`time`](https://github.com/time-rs/time).

Expand All @@ -33,7 +34,8 @@ See [docs.rs](https://docs.rs/utc-dt) for the API reference.
- Provides constants useful for time transformations: [`utc-dt::constants`](https://docs.rs/utc-dt/latest/utc_dt/constants/index.html)
- Nanosecond resolution.
- Timestamps supporting standard math operators (`core::ops`)
- `#![no_std]` support.
- `#![no_std]` and optional `alloc` support.
- Optional serialization/deserialization of structures via `serde`

## Examples (exhaustive)
```rust
Expand Down Expand Up @@ -99,13 +101,16 @@ See [docs.rs](https://docs.rs/utc-dt) for the API reference.
// UTC Time of Day subsecond component (in nanoseconds)
let subsec_ns = utc_tod.as_subsec_ns();
// Parse a UTC Time of Day from an ISO 8601 time string `(Thh:mm:ssZ)`
// Not available for #![no_std]
let utc_tod = UTCTimeOfDay::try_from_iso_tod("T10:18:08.903Z").unwrap();
// Get a time of day string formatted according to ISO 8601 `(Thh:mm:ssZ)`
// Not available for #![no_std]
let precision = Some(6);
let iso_tod = utc_tod.as_iso_tod(precision);
const PRECISION_MICROS: usize = 6;
let iso_tod = utc_tod.as_iso_tod(PRECISION_MICROS);
assert_eq!(iso_tod, "T10:18:08.903000Z");
// Write ISO 8601 time of day str to a stack buffer
let mut buf = [0; UTCTimeOfDay::iso_tod_len(PRECISION_MICROS)];
let _bytes_written = utc_tod.write_iso_tod(&mut buf, PRECISION_MICROS).unwrap();
let iso_tod_str = core::str::from_utf8(&buf).unwrap();
assert_eq!(iso_tod_str, "T10:18:08.903000Z");

// UTC Date directly from components
let utc_date = UTCDate::try_from_components(2023, 6, 15).unwrap(); // OR
Expand All @@ -121,26 +126,32 @@ See [docs.rs](https://docs.rs/utc-dt) for the API reference.
// UTC Day from UTC Date
let utc_day = utc_date.as_day();
// Parse a UTC Date from an ISO 8601 date string `(YYYY-MM-DD)`
// Not available for #![no_std]
let utc_date = UTCDate::try_from_iso_date("2023-06-15").unwrap();
// Get date string formatted according to ISO 8601 `(YYYY-MM-DD)`
// Not available for #![no_std]
let iso_date = utc_date.as_iso_date();
assert_eq!(iso_date, "2023-06-15");
// Write ISO 8601 date str to a stack buffer
let mut buf = [0; UTCDate::ISO_DATE_LEN];
let _bytes_written = utc_date.write_iso_date(&mut buf).unwrap();
let iso_date_str = core::str::from_utf8(&buf).unwrap();
assert_eq!(iso_date_str, "2023-06-15");

// UTC Datetime from date and time-of-day components
let utc_datetime = UTCDatetime::from_components(utc_date, utc_tod);
// Get date and time-of-day components
let (utc_date, time_of_day_ns) = (utc_datetime.as_date(), utc_datetime.as_tod()); // OR
let (utc_date, time_of_day_ns) = utc_datetime.as_components();
// Parse a UTC Datetime from an ISO 8601 datetime string `(YYYY-MM-DDThh:mm:ssZ)`
// Not available for #![no_std]
let utc_datetime = UTCDatetime::try_from_iso_datetime("2023-06-15T10:18:08.903Z").unwrap();
// Get UTC datetime string formatted according to ISO 8601 `(YYYY-MM-DDThh:mm:ssZ)`
// Not available for #![no_std]
let precision = None;
let iso_datetime = utc_datetime.as_iso_datetime(precision);
const PRECISION_SECONDS: usize = 0;
let iso_datetime = utc_datetime.as_iso_datetime(PRECISION_SECONDS);
assert_eq!(iso_datetime, "2023-06-15T10:18:08Z");
// Write ISO 8601 datetime str to a stack buffer
let mut buf = [0; UTCDatetime::iso_datetime_len(PRECISION_SECONDS)];
let _bytes_written = utc_datetime.write_iso_datetime(&mut buf, PRECISION_SECONDS).unwrap();
let iso_datetime_str = core::str::from_utf8(&buf).unwrap();
assert_eq!(iso_datetime_str, "2023-06-15T10:18:08Z");

{
// `UTCTransformations` can be used to create shortcuts to the desired type!
Expand Down Expand Up @@ -184,6 +195,13 @@ See [docs.rs](https://docs.rs/utc-dt) for the API reference.
}
```

## Feature flags
The [`std`, `alloc`] feature flags are enabled by default.
- `std`: Enables methods that use the system clock via `std::time::SystemTime`. Enables `alloc`.
- `alloc`: Enables methods that use allocated strings.
- `serde`: Derives `serde::Serialize` and `serde::Deserialize` for all internal non-error types.
- `nightly`: Enables the unstable [`error_in_core`](https://github.com/rust-lang/rust/issues/103765) feature for improved `#[no_std]` error handling.

## References
- [(Howard Hinnant, 2021) `chrono`-Compatible Low-Level Date Algorithms](http://howardhinnant.github.io/date_algorithms.html)
- [(W3C, 1997) ISO 8601 Standard for Date and Time Formats](https://www.w3.org/TR/NOTE-datetime)
Expand Down
127 changes: 110 additions & 17 deletions src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@
//! proleptic Gregorian Calendar (the *civil* calendar),
//! to create UTC dates.
use core::{
fmt::{Display, Formatter},
time::Duration,
};
use crate::time::{UTCDay, UTCTimestamp, UTCTransformations};
use crate::util::StrWriter;
use core::fmt::{Display, Formatter, Write};
use core::num::ParseIntError;
use core::time::Duration;

use anyhow::{bail, Result};
#[cfg(feature = "alloc")]
use alloc::{format, string::String};

use crate::time::{UTCDay, UTCTimestamp, UTCTransformations};
// TODO <https://github.com/rust-lang/rust/issues/103765>
#[cfg(feature = "nightly")]
use core::error::Error;
#[cfg(all(feature = "std", not(feature = "nightly")))]
use std::error::Error;

/// UTC Date.
///
Expand Down Expand Up @@ -45,11 +51,17 @@ use crate::time::{UTCDay, UTCTimestamp, UTCTransformations};
/// // Not available for #![no_std]
/// let iso_date = utc_date.as_iso_date();
/// assert_eq!(iso_date, "2023-06-15");
/// // Write ISO 8601 date str to a stack buffer
/// let mut buf = [0; UTCDate::ISO_DATE_LEN];
/// let _bytes_written = utc_date.write_iso_date(&mut buf).unwrap();
/// let iso_date_str = core::str::from_utf8(&buf).unwrap();
/// assert_eq!(iso_date_str, "2023-06-15");
/// ```
///
/// ## Safety
/// Unchecked methods are provided for use in hot paths requiring high levels of optimisation.
/// These methods assume valid input.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct UTCDate {
era: u32,
Expand Down Expand Up @@ -101,6 +113,9 @@ impl UTCDate {
/// The minimum year supported
pub const MIN_YEAR: u64 = 1970;

/// The length of an ISO date (in characters)
pub const ISO_DATE_LEN: usize = 10;

/// Unchecked method to create a UTC Date from provided year, month and day.
///
/// ## Safety
Expand All @@ -120,21 +135,21 @@ impl UTCDate {
}

/// Try to create a UTC Date from provided year, month and day.
pub fn try_from_components(year: u64, month: u8, day: u8) -> Result<Self> {
pub fn try_from_components(year: u64, month: u8, day: u8) -> Result<Self, UTCDateError> {
if !(Self::MIN_YEAR..=Self::MAX_YEAR).contains(&year) {
bail!("Year out of range! (year: {:04})", year);
return Err(UTCDateError::YearOutOfRange(year));
}
if month == 0 || month > 12 {
bail!("Month out of range! (month: {:02})", month);
return Err(UTCDateError::MonthOutOfRange(month));
}
// force create
// SAFETY: we have checked year and month are within range
let date = unsafe { Self::from_components_unchecked(year, month, day) };
// then check
// Then check days
if date.day == 0 || date.day > date.days_in_month() {
bail!("Day out of range! (date: {date}");
return Err(UTCDateError::DayOutOfRange(date));
}
if date > UTCDate::MAX {
bail!("Date out of range! (date: {date}");
return Err(UTCDateError::DateOutOfRange(date));
}
Ok(date)
}
Expand Down Expand Up @@ -176,6 +191,7 @@ impl UTCDate {
let doy = ((153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5) + d - 1;
let doe = (yoe * 365) + (yoe / 4) - (yoe / 100) + doy as u32;
let days = (era as u64 * 146097) + doe as u64 - 719468;
// SAFETY: days is not exceeding UTCDay::MAX
unsafe { UTCDay::from_u64_unchecked(days) }
}

Expand Down Expand Up @@ -223,13 +239,16 @@ impl UTCDate {
}
}

/// Try parse date from string in the format:
/// Try parse date from str in the format:
/// * `YYYY-MM-DD`
///
/// Conforms to ISO 8601:
/// <https://www.w3.org/TR/NOTE-datetime>
#[cfg(feature = "std")]
pub fn try_from_iso_date(iso: &str) -> Result<Self> {
pub fn try_from_iso_date(iso: &str) -> Result<Self, UTCDateError> {
let len = iso.len();
if len != Self::ISO_DATE_LEN {
return Err(UTCDateError::InvalidStrLen(len));
}
// handle slice
let (year_str, rem) = iso.split_at(4); // remainder = "-MM-DD"
let (month_str, rem) = rem[1..].split_at(2); // remainder = "-DD"
Expand All @@ -246,10 +265,38 @@ impl UTCDate {
///
/// Conforms to ISO 8601:
/// <https://www.w3.org/TR/NOTE-datetime>
#[cfg(feature = "std")]
#[cfg(feature = "alloc")]
pub fn as_iso_date(&self) -> String {
format!("{self}")
}

/// Internal truncated buffer write
#[inline]
pub(crate) fn _write_iso_date_trunc(&self, w: &mut StrWriter) {
// unwrap infallible
write!(w, "{self}").unwrap();
}

/// Write an ISO date to a buffer in the format:
/// * `YYYY-MM-DD`
///
/// The buffer should have minimum length of [UTCDate::ISO_DATE_LEN] (10).
///
/// A buffer of insufficient length will error ([UTCDateError::InvalidStrLen]).
///
/// Returns number of UTF8 characters (bytes) written
///
/// Conforms to ISO 8601:
/// <https://www.w3.org/TR/NOTE-datetime>
pub fn write_iso_date(&self, buf: &mut [u8]) -> Result<usize, UTCDateError> {
let write_len = Self::ISO_DATE_LEN;
if write_len > buf.len() {
return Err(UTCDateError::InvalidStrLen(buf.len()));
}
let mut writer = StrWriter::new(&mut buf[..write_len]);
self._write_iso_date_trunc(&mut writer);
Ok(writer.written)
}
}

impl UTCTransformations for UTCDate {
Expand Down Expand Up @@ -316,3 +363,49 @@ impl From<UTCDay> for UTCDate {
Self::from_day(utc_day)
}
}

/// Error type for UTCDate methods
#[derive(Debug, Clone)]
pub enum UTCDateError {
/// Error raised parsing int to string
ParseErr(ParseIntError),
/// Error raised due to out of range year
YearOutOfRange(u64),
/// Error raised due to out of range month
MonthOutOfRange(u8),
/// Error raised due to out of range day
DayOutOfRange(UTCDate),
/// Error raised due to out of range date
DateOutOfRange(UTCDate),
/// Error raised due to invalid ISO date length
InvalidStrLen(usize),
}

impl Display for UTCDateError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match self {
Self::ParseErr(e) => e.fmt(f),
Self::YearOutOfRange(y) => write!(f, "year ({y}) out of range!"),
Self::MonthOutOfRange(m) => write!(f, "month ({m}) out of range!"),
Self::DayOutOfRange(d) => write!(f, "day ({d}) out of range!"),
Self::DateOutOfRange(date) => write!(f, "date ({date}) out of range!"),
Self::InvalidStrLen(l) => write!(f, "invalid ISO date str length ({l}), 10 required"),
}
}
}

#[cfg(any(feature = "std", feature = "nightly"))]
impl Error for UTCDateError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::ParseErr(e) => e.source(),
_ => None,
}
}
}

impl From<ParseIntError> for UTCDateError {
fn from(value: ParseIntError) -> Self {
Self::ParseErr(value)
}
}
Loading

0 comments on commit 01e3691

Please sign in to comment.