Skip to content

Commit 01e3691

Browse files
authored
Custom Error Types, Serde Support, Improved #![no_std] Support (#10)
* 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
1 parent 68796d7 commit 01e3691

File tree

11 files changed

+792
-183
lines changed

11 files changed

+792
-183
lines changed

.github/workflows/rust.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ jobs:
5454
steps:
5555
- uses: actions/checkout@v3
5656
- name: Install Rust
57-
run: rustup update stable
57+
run: rustup update nightly
5858
- name: Install cargo-llvm-cov
5959
uses: taiki-e/install-action@cargo-llvm-cov
6060
- name: Generate code coverage
61-
run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info
61+
run: cargo +nightly llvm-cov --all-features --workspace --lcov --output-path lcov.info
6262
- name: Upload coverage to Codecov
6363
uses: codecov/codecov-action@v3
6464
with:

Cargo.toml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "utc-dt"
3-
version = "0.2.1"
3+
version = "0.3.0"
44
authors = ["Reece Kibble <[email protected]>"]
55
categories = ["date-and-time", "no-std", "parsing"]
66
keywords = ["time", "datetime", "date", "utc", "epoch"]
@@ -17,7 +17,16 @@ exclude = [".git*"]
1717

1818
[features]
1919
default = ["std"]
20-
std = ["anyhow/std"]
20+
std = [
21+
"alloc",
22+
"serde/std",
23+
]
24+
alloc = ["serde/alloc"]
25+
serde = ["dep:serde"]
26+
nightly = []
2127

2228
[dependencies]
23-
anyhow = { version = "1", default-features = false }
29+
serde = { version = "1.0", default-features = false, optional = true, features = ["derive"] }
30+
31+
[dev-dependencies]
32+
serde_json = "1.0"

README.md

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
[![crates.io](https://img.shields.io/crates/v/utc-dt?style=flat-square&logo=rust)](https://crates.io/crates/utc-dt)
44
[![license](https://img.shields.io/badge/license-Apache--2.0_OR_MIT-blue?style=flat-square)](#license)
5+
[![docs](https://img.shields.io/docsrs/utc-dt/latest)](https://docs.rs/utc-dt)
56
[![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)
67
[![codecov](https://codecov.io/gh/uniciant/utc-datetime/branch/main/graph/badge.svg?token=XTOHZ187TY)](https://codecov.io/gh/uniciant/utc-datetime)
78

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

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

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

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

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

131139
// UTC Datetime from date and time-of-day components
132140
let utc_datetime = UTCDatetime::from_components(utc_date, utc_tod);
133141
// Get date and time-of-day components
134142
let (utc_date, time_of_day_ns) = (utc_datetime.as_date(), utc_datetime.as_tod()); // OR
135143
let (utc_date, time_of_day_ns) = utc_datetime.as_components();
136144
// Parse a UTC Datetime from an ISO 8601 datetime string `(YYYY-MM-DDThh:mm:ssZ)`
137-
// Not available for #![no_std]
138145
let utc_datetime = UTCDatetime::try_from_iso_datetime("2023-06-15T10:18:08.903Z").unwrap();
139146
// Get UTC datetime string formatted according to ISO 8601 `(YYYY-MM-DDThh:mm:ssZ)`
140-
// Not available for #![no_std]
141-
let precision = None;
142-
let iso_datetime = utc_datetime.as_iso_datetime(precision);
147+
const PRECISION_SECONDS: usize = 0;
148+
let iso_datetime = utc_datetime.as_iso_datetime(PRECISION_SECONDS);
143149
assert_eq!(iso_datetime, "2023-06-15T10:18:08Z");
150+
// Write ISO 8601 datetime str to a stack buffer
151+
let mut buf = [0; UTCDatetime::iso_datetime_len(PRECISION_SECONDS)];
152+
let _bytes_written = utc_datetime.write_iso_datetime(&mut buf, PRECISION_SECONDS).unwrap();
153+
let iso_datetime_str = core::str::from_utf8(&buf).unwrap();
154+
assert_eq!(iso_datetime_str, "2023-06-15T10:18:08Z");
144155

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

198+
## Feature flags
199+
The [`std`, `alloc`] feature flags are enabled by default.
200+
- `std`: Enables methods that use the system clock via `std::time::SystemTime`. Enables `alloc`.
201+
- `alloc`: Enables methods that use allocated strings.
202+
- `serde`: Derives `serde::Serialize` and `serde::Deserialize` for all internal non-error types.
203+
- `nightly`: Enables the unstable [`error_in_core`](https://github.com/rust-lang/rust/issues/103765) feature for improved `#[no_std]` error handling.
204+
187205
## References
188206
- [(Howard Hinnant, 2021) `chrono`-Compatible Low-Level Date Algorithms](http://howardhinnant.github.io/date_algorithms.html)
189207
- [(W3C, 1997) ISO 8601 Standard for Date and Time Formats](https://www.w3.org/TR/NOTE-datetime)

src/date.rs

Lines changed: 110 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@
44
//! proleptic Gregorian Calendar (the *civil* calendar),
55
//! to create UTC dates.
66
7-
use core::{
8-
fmt::{Display, Formatter},
9-
time::Duration,
10-
};
7+
use crate::time::{UTCDay, UTCTimestamp, UTCTransformations};
8+
use crate::util::StrWriter;
9+
use core::fmt::{Display, Formatter, Write};
10+
use core::num::ParseIntError;
11+
use core::time::Duration;
1112

12-
use anyhow::{bail, Result};
13+
#[cfg(feature = "alloc")]
14+
use alloc::{format, string::String};
1315

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

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

116+
/// The length of an ISO date (in characters)
117+
pub const ISO_DATE_LEN: usize = 10;
118+
104119
/// Unchecked method to create a UTC Date from provided year, month and day.
105120
///
106121
/// ## Safety
@@ -120,21 +135,21 @@ impl UTCDate {
120135
}
121136

122137
/// Try to create a UTC Date from provided year, month and day.
123-
pub fn try_from_components(year: u64, month: u8, day: u8) -> Result<Self> {
138+
pub fn try_from_components(year: u64, month: u8, day: u8) -> Result<Self, UTCDateError> {
124139
if !(Self::MIN_YEAR..=Self::MAX_YEAR).contains(&year) {
125-
bail!("Year out of range! (year: {:04})", year);
140+
return Err(UTCDateError::YearOutOfRange(year));
126141
}
127142
if month == 0 || month > 12 {
128-
bail!("Month out of range! (month: {:02})", month);
143+
return Err(UTCDateError::MonthOutOfRange(month));
129144
}
130-
// force create
145+
// SAFETY: we have checked year and month are within range
131146
let date = unsafe { Self::from_components_unchecked(year, month, day) };
132-
// then check
147+
// Then check days
133148
if date.day == 0 || date.day > date.days_in_month() {
134-
bail!("Day out of range! (date: {date}");
149+
return Err(UTCDateError::DayOutOfRange(date));
135150
}
136151
if date > UTCDate::MAX {
137-
bail!("Date out of range! (date: {date}");
152+
return Err(UTCDateError::DateOutOfRange(date));
138153
}
139154
Ok(date)
140155
}
@@ -176,6 +191,7 @@ impl UTCDate {
176191
let doy = ((153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5) + d - 1;
177192
let doe = (yoe * 365) + (yoe / 4) - (yoe / 100) + doy as u32;
178193
let days = (era as u64 * 146097) + doe as u64 - 719468;
194+
// SAFETY: days is not exceeding UTCDay::MAX
179195
unsafe { UTCDay::from_u64_unchecked(days) }
180196
}
181197

@@ -223,13 +239,16 @@ impl UTCDate {
223239
}
224240
}
225241

226-
/// Try parse date from string in the format:
242+
/// Try parse date from str in the format:
227243
/// * `YYYY-MM-DD`
228244
///
229245
/// Conforms to ISO 8601:
230246
/// <https://www.w3.org/TR/NOTE-datetime>
231-
#[cfg(feature = "std")]
232-
pub fn try_from_iso_date(iso: &str) -> Result<Self> {
247+
pub fn try_from_iso_date(iso: &str) -> Result<Self, UTCDateError> {
248+
let len = iso.len();
249+
if len != Self::ISO_DATE_LEN {
250+
return Err(UTCDateError::InvalidStrLen(len));
251+
}
233252
// handle slice
234253
let (year_str, rem) = iso.split_at(4); // remainder = "-MM-DD"
235254
let (month_str, rem) = rem[1..].split_at(2); // remainder = "-DD"
@@ -246,10 +265,38 @@ impl UTCDate {
246265
///
247266
/// Conforms to ISO 8601:
248267
/// <https://www.w3.org/TR/NOTE-datetime>
249-
#[cfg(feature = "std")]
268+
#[cfg(feature = "alloc")]
250269
pub fn as_iso_date(&self) -> String {
251270
format!("{self}")
252271
}
272+
273+
/// Internal truncated buffer write
274+
#[inline]
275+
pub(crate) fn _write_iso_date_trunc(&self, w: &mut StrWriter) {
276+
// unwrap infallible
277+
write!(w, "{self}").unwrap();
278+
}
279+
280+
/// Write an ISO date to a buffer in the format:
281+
/// * `YYYY-MM-DD`
282+
///
283+
/// The buffer should have minimum length of [UTCDate::ISO_DATE_LEN] (10).
284+
///
285+
/// A buffer of insufficient length will error ([UTCDateError::InvalidStrLen]).
286+
///
287+
/// Returns number of UTF8 characters (bytes) written
288+
///
289+
/// Conforms to ISO 8601:
290+
/// <https://www.w3.org/TR/NOTE-datetime>
291+
pub fn write_iso_date(&self, buf: &mut [u8]) -> Result<usize, UTCDateError> {
292+
let write_len = Self::ISO_DATE_LEN;
293+
if write_len > buf.len() {
294+
return Err(UTCDateError::InvalidStrLen(buf.len()));
295+
}
296+
let mut writer = StrWriter::new(&mut buf[..write_len]);
297+
self._write_iso_date_trunc(&mut writer);
298+
Ok(writer.written)
299+
}
253300
}
254301

255302
impl UTCTransformations for UTCDate {
@@ -316,3 +363,49 @@ impl From<UTCDay> for UTCDate {
316363
Self::from_day(utc_day)
317364
}
318365
}
366+
367+
/// Error type for UTCDate methods
368+
#[derive(Debug, Clone)]
369+
pub enum UTCDateError {
370+
/// Error raised parsing int to string
371+
ParseErr(ParseIntError),
372+
/// Error raised due to out of range year
373+
YearOutOfRange(u64),
374+
/// Error raised due to out of range month
375+
MonthOutOfRange(u8),
376+
/// Error raised due to out of range day
377+
DayOutOfRange(UTCDate),
378+
/// Error raised due to out of range date
379+
DateOutOfRange(UTCDate),
380+
/// Error raised due to invalid ISO date length
381+
InvalidStrLen(usize),
382+
}
383+
384+
impl Display for UTCDateError {
385+
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
386+
match self {
387+
Self::ParseErr(e) => e.fmt(f),
388+
Self::YearOutOfRange(y) => write!(f, "year ({y}) out of range!"),
389+
Self::MonthOutOfRange(m) => write!(f, "month ({m}) out of range!"),
390+
Self::DayOutOfRange(d) => write!(f, "day ({d}) out of range!"),
391+
Self::DateOutOfRange(date) => write!(f, "date ({date}) out of range!"),
392+
Self::InvalidStrLen(l) => write!(f, "invalid ISO date str length ({l}), 10 required"),
393+
}
394+
}
395+
}
396+
397+
#[cfg(any(feature = "std", feature = "nightly"))]
398+
impl Error for UTCDateError {
399+
fn source(&self) -> Option<&(dyn Error + 'static)> {
400+
match self {
401+
Self::ParseErr(e) => e.source(),
402+
_ => None,
403+
}
404+
}
405+
}
406+
407+
impl From<ParseIntError> for UTCDateError {
408+
fn from(value: ParseIntError) -> Self {
409+
Self::ParseErr(value)
410+
}
411+
}

0 commit comments

Comments
 (0)