#![cfg_attr(not(test), no_std)]
#![allow(clippy::tabs_in_doc_comments)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use tinystr::TinyStr16;
#[cfg(feature = "chrono")]
use chrono::{Datelike, Timelike};
pub type Ts32Str = TinyStr16;
const ALPHABET: &[u8] = b"0123456789abcdefghjkmnpqrstvwxyz";
const ALPHABET_REV: &[u8] = &[
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 10, 11, 12, 13, 14, 15, 16, 17, 0, 18, 19, 0, 20, 21, 0,
22, 23, 24, 25, 26, 0, 27, 28, 29, 30, 31, 0, 0, 0, 0, 0,
];
const U5MAX: u32 = 0b11111;
const fn encode2(i: u32) -> [u8; 2] {
debug_assert!(i < 1024 );
[((i >> 5) & U5MAX) as u8, (i & U5MAX) as u8]
}
const fn decode2(i: &[u8]) -> u32 {
((ALPHABET_REV[i[0] as usize] as u32) << 5) + ALPHABET_REV[i[1] as usize] as u32
}
fn values_to_s(y: i32, m: u8, d: u8, h: u8, min: u8, s: u8, ms: u16) -> Ts32Str {
let full = values_to_s_full(y, m, d, h, min, s, ms);
let bytes = full.as_bytes();
let cut_head = if bytes[0] == b'0' { 1 } else { 0 };
let cut_tail = if bytes[12] == b'0' && bytes[13] == b'0' { 3 } else { 0 };
full[cut_head..full.len() - cut_tail].parse().unwrap()
}
fn values_to_s_full(y: i32, m: u8, d: u8, h: u8, min: u8, s: u8, ms: u16) -> Ts32Str {
let ya = y.unsigned_abs();
let y0 = ya % 1000;
let y1 = ya / 1000;
let y0e = encode2(y0);
let y1e = encode2(y1);
let sih = min as u16 * 60 + s as u16;
let sixth = sih / 600;
let si6 = sih % 600;
let si6e = encode2(si6.into());
let mse = if ms == 0 { [0, 0] } else { encode2(ms as u32) };
let bytes = [
b'-',
ALPHABET[y1e[0] as usize], ALPHABET[y1e[1] as usize], ALPHABET[y0e[0] as usize], ALPHABET[y0e[1] as usize],
ALPHABET[m as usize], ALPHABET[d as usize],
b':',
ALPHABET[h as usize], ALPHABET[sixth as usize], ALPHABET[si6e[0] as usize], ALPHABET[si6e[1] as usize],
b'.',
ALPHABET[mse[0] as usize], ALPHABET[mse[1] as usize],
];
let bytes = if y >= 0 { &bytes[1..] } else { &bytes };
TinyStr16::try_from_utf8(bytes).unwrap()
}
#[allow(clippy::type_complexity)]
fn values_from_s(s: &str) -> Result<(i32, u8, u8, u8, u8, u8, u16), Error> {
let neg = s.starts_with('-');
let s = if neg { &s[1..] } else { s };
let bytes = s.as_bytes();
let sep_date = s.find(':').ok_or(Error::DecodeNoDate)?;
let sep_ms = s.find('.');
let s_y = match sep_date - 2 {
1 => &[0, 0, 0, bytes[0]],
2 => &[0, 0, bytes[0], bytes[1]],
3 => &[0, bytes[0], bytes[1], bytes[2]],
4 => &bytes[..sep_date],
_ => return Err(Error::DecodeUnimplementedYearRange),
};
let y = decode2(&s_y[..2]) * 1000 + decode2(&s_y[2..]);
let y = if neg { -(y as i32) } else { y as i32 };
let m = ALPHABET_REV[bytes[sep_date - 2] as usize];
let d = ALPHABET_REV[bytes[sep_date - 1] as usize];
let h = ALPHABET_REV[bytes[sep_date + 1] as usize];
let sixth = ALPHABET_REV[bytes[sep_date + 2] as usize];
let si6 = decode2(&bytes[sep_date + 3..=sep_date + 4]);
let sih = sixth as u32 * 600 + si6;
let s = sih % 60;
let min = sih / 60;
let ms = if let Some(sep_ms) = sep_ms {
decode2(&bytes[sep_ms + 1..])
} else {
0
};
Ok((y, m, d, h, min as u8, s as u8, ms as u16))
}
pub trait Ts32: Sized {
fn to_ts32(&self) -> Ts32Str;
fn try_from_ts32(s: &str) -> Result<Self, Error>;
}
#[cfg(feature = "time")]
impl Ts32 for time::OffsetDateTime {
fn to_ts32(&self) -> Ts32Str {
let dt = self;
values_to_s(dt.year(), dt.month() as u8, dt.day(), dt.hour(), dt.minute(), dt.second(), dt.millisecond())
}
fn try_from_ts32(s: &str) -> Result<Self, Error> {
let (y, m, d, h, min, s, ms) = values_from_s(s)?;
Ok(time::OffsetDateTime::new_utc(
time::Date::from_calendar_date(y, m.try_into().unwrap(), d).map_err(|_| Error::DecodeUnsupportedDate)?,
time::Time::from_hms_milli(h, min, s, ms).map_err(|_| Error::DecodeInvalidTime)?,
))
}
}
#[cfg(feature = "chrono")]
impl Ts32 for chrono::DateTime<chrono::Utc> {
fn to_ts32(&self) -> Ts32Str {
let dt = self;
values_to_s(dt.year(), dt.month() as u8, dt.day() as u8, dt.hour() as u8, dt.minute() as u8, dt.second() as u8, dt.timestamp_subsec_millis() as u16)
}
fn try_from_ts32(s: &str) -> Result<Self, Error> {
let (y, m, d, h, min, s, ms) = values_from_s(s)?;
Ok(chrono::NaiveDate::from_ymd_opt(y, m.into(), d.into()).ok_or(Error::DecodeUnsupportedDate)?
.and_hms_milli_opt(h.into(), min.into(), s.into(), ms.into()).ok_or(Error::DecodeInvalidTime)?
.and_utc())
}
}
#[derive(Debug)]
pub enum Error {
DecodeNoDate,
DecodeUnsupportedDate,
DecodeUnimplementedYearRange,
DecodeInvalidTime,
}
#[cfg(test)]
mod test {
use super::*;
#[test]
#[cfg(feature = "time")]
fn time_negative() {
let t_negative = time::OffsetDateTime::new_utc(
time::Date::from_calendar_date(-1234, 5.try_into().unwrap(), 16).unwrap(),
time::Time::from_hms_milli(17, 18, 19, 123).unwrap(),
);
let ts32s = t_negative.to_ts32();
assert_eq!(ts32s, "-017a5g:h1fk.3v");
assert_eq!(time::OffsetDateTime::try_from_ts32(&ts32s).unwrap(), t_negative);
}
#[test]
#[cfg(feature = "chrono")]
fn chrono_negative() {
let t_negative = chrono::NaiveDate::from_ymd_opt(-1234, 5, 16).unwrap()
.and_hms_milli_opt(17, 18, 19, 123).unwrap()
.and_utc();
let ts32s = t_negative.to_ts32();
assert_eq!(ts32s, "-017a5g:h1fk.3v");
assert_eq!(chrono::DateTime::try_from_ts32(&ts32s).unwrap(), t_negative);
}
}