Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Error Types, Serde Support, Improved #![no_std] Support #10

Merged
merged 29 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ce69ebb
tests/lib.rs: rename to datetime.rs
poshcoe Aug 24, 2024
b1f4efb
Cargo.toml: increment minor ver, remove anyhow dep, add optional serd…
poshcoe Aug 24, 2024
852ab8c
docs: Increment version, add features
poshcoe Aug 24, 2024
9d47497
Cargo.toml: no thiserror (until error in core is stabilized)
poshcoe Aug 24, 2024
3ba743d
src*: custom error types and alloc feature
poshcoe Aug 24, 2024
00837ff
tests/*: tests using custom error types
poshcoe Aug 24, 2024
b4441b2
src/*: fmt pass
poshcoe Aug 24, 2024
07ba908
tests/date.rs: fmt pass
poshcoe Aug 24, 2024
37561d5
tests/*: add serde tests
poshcoe Aug 24, 2024
215ba22
src/time.rs: missed serde derive
poshcoe Aug 24, 2024
f7ec053
Cargo.toml: serde_json as dev dep for tests
poshcoe Aug 24, 2024
0dc5b83
src/util.rs: init StrWriter util
poshcoe Aug 25, 2024
76f4c9a
src/*: add `write_iso` functions for writing static buffers with ISO …
poshcoe Aug 25, 2024
5ca4ffb
src/time.rs: truncate extra value on string
poshcoe Aug 25, 2024
9056ae0
src/date.rs: error type renamed
poshcoe Aug 25, 2024
38dbdc2
src/util.rs: fix incorrect start index on buf
poshcoe Aug 25, 2024
8b222b5
tests/*: extend for new no-alloc iso conversions
poshcoe Aug 25, 2024
3d53b06
src/*: better docs of unsafe usage
poshcoe Aug 25, 2024
c8a8aa9
src/*: fmt and clippy pass
poshcoe Aug 25, 2024
eba99e8
tests/time.rs: fmt pass
poshcoe Aug 25, 2024
71d9ff8
src/*: update docstrings
poshcoe Aug 25, 2024
159c596
README.md: update examples
poshcoe Aug 25, 2024
b32a29f
dosctring correction static -> stack
poshcoe Aug 26, 2024
6ffe714
workflows/rust.yml: codecov test on nightly
poshcoe Aug 26, 2024
86655d3
src/*: clone on error types, consistent formatting
poshcoe Aug 26, 2024
6226c36
tests/*: coverage
poshcoe Aug 26, 2024
cacc71d
tests/error.rs: testing error displays
poshcoe Aug 26, 2024
cdf26b9
tests/error.rs: test From impls
poshcoe Aug 26, 2024
b9756d8
add docs badge
poshcoe Aug 26, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <reecek@uniciant.com>"]
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
Loading