Initial commit

This commit is contained in:
Aleksandr 2025-06-22 05:46:26 +03:00
commit 75a589f235
43 changed files with 4840 additions and 0 deletions

146
src/array.rs Normal file
View file

@ -0,0 +1,146 @@
//! # Array utilities and types.
use std::{
borrow::{Borrow, Cow},
fmt,
ops::Deref,
sync::Arc,
};
use eva_macros::data;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de};
#[data(copy, error, display("too big, expected array no bigger than {LIMIT}"), crate = crate)]
pub struct TooBig<const LIMIT: usize>;
#[crate::perfect_derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct ImmutableHeap<T, const MAX: usize>(Arc<[T]>);
impl<T, const MAX: usize> Borrow<[T]> for ImmutableHeap<T, MAX> {
fn borrow(&self) -> &[T] {
self.0.as_ref()
}
}
impl<T, const MAX: usize> AsRef<[T]> for ImmutableHeap<T, MAX> {
fn as_ref(&self) -> &[T] {
self.0.as_ref()
}
}
impl<T, const MAX: usize> Deref for ImmutableHeap<T, MAX> {
type Target = [T];
fn deref(&self) -> &Self::Target {
self.0.as_ref()
}
}
impl<'a, T, const MAX: usize> IntoIterator for &'a ImmutableHeap<T, MAX> {
type IntoIter = <&'a [T] as IntoIterator>::IntoIter;
type Item = &'a T;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<T, const MAX: usize> TryFrom<Vec<T>> for ImmutableHeap<T, MAX> {
type Error = TooBig<MAX>;
fn try_from(value: Vec<T>) -> Result<Self, Self::Error> {
if value.len() > MAX {
return Err(TooBig::<MAX>);
}
Ok(Self(value.into()))
}
}
impl<T, const MAX: usize, const N: usize> TryFrom<[T; N]> for ImmutableHeap<T, MAX> {
type Error = TooBig<MAX>;
fn try_from(value: [T; N]) -> Result<Self, Self::Error> {
if N > MAX {
return Err(TooBig::<MAX>);
}
Ok(Self(Arc::new(value)))
}
}
impl<'de, T: Deserialize<'de>, const MAX: usize> Deserialize<'de> for ImmutableHeap<T, MAX> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct Visitor<T, const MAX: usize> {
v: Vec<T>,
}
impl<'d, T: Deserialize<'d>, const MAX: usize> de::Visitor<'d> for Visitor<T, MAX> {
type Value = Vec<T>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "array no bigger than {MAX}")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: de::SeqAccess<'d>,
{
let mut dst = self.v;
let mut fuel = MAX;
loop {
if fuel <= 0 {
return Err(de::Error::custom(TooBig::<MAX>));
}
let Some(element) = seq.next_element()? else {
break;
};
dst.push(element);
fuel -= 1;
}
Ok(dst)
}
}
let v = deserializer.deserialize_seq(Visitor::<T, MAX> {
v: Vec::with_capacity(MAX),
})?;
Ok(Self(v.into()))
}
}
impl<T: Serialize, const MAX: usize> Serialize for ImmutableHeap<T, MAX> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
impl<T: JsonSchema, const MAX: usize> JsonSchema for ImmutableHeap<T, MAX> {
fn schema_id() -> Cow<'static, str> {
<Vec<T> as JsonSchema>::schema_id()
}
fn schema_name() -> Cow<'static, str> {
<Vec<T> as JsonSchema>::schema_name()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "array",
"items": T::json_schema(generator),
"maxItems": MAX,
})
}
}

6
src/collections.rs Normal file
View file

@ -0,0 +1,6 @@
pub use hashbrown::{Equivalent, HashTable, hash_map, hash_table};
use std::hash::BuildHasherDefault;
pub type HashMap<K, V> = hashbrown::HashMap<K, V, BuildHasherDefault<crate::hash::Hasher>>;
pub type HashSet<V> = hashbrown::HashSet<V, BuildHasherDefault<crate::hash::Hasher>>;

1
src/encoding.rs Normal file
View file

@ -0,0 +1 @@
pub mod dict;

28
src/encoding/dict.rs Normal file
View file

@ -0,0 +1,28 @@
use crate::data;
#[data(copy, ord, not(serde), crate = crate)]
pub struct Dict<'a, T>(pub &'a [T]);
impl<'a, T> Dict<'a, T> {
pub const fn get_encoded_size(&self, of: u64) -> usize {
if of == 0 {
1
} else {
of.ilog(self.0.len() as u64) as usize
}
}
pub fn encode(&self, mut value: u64, mut put: impl FnMut(&T)) {
let base = self.0.len() as u64;
loop {
let rem = value % base;
value /= base;
let idx = rem as usize;
put(&self.0[idx]);
if value == 0 {
break;
}
}
}
}

69
src/error.rs Normal file
View file

@ -0,0 +1,69 @@
//! # Error utilities.
use std::fmt;
mod seal {
pub trait Seal {}
}
#[doc(inline)]
pub use crate::_combined as combined;
#[macro_export]
#[doc(hidden)]
macro_rules! _combined {
($($(#[$outer_meta:meta])* $vis:vis enum $name:ident {
$( $VarName:ident($ty:ty) ),* $(,)?
})*) => {$(
$(#[$outer_meta])*
$vis enum $name {$(
$VarName($ty)
),*}
impl $name {
pub fn transmogrify<T>(self) -> T
where
T: $crate::Anything $(+ std::convert::From<$ty>)*
{
match self {$(
Self::$VarName(v) => v.into()
),*}
}
}
$(
impl std::convert::From<$ty> for $name {
fn from(v: $ty) -> Self {
Self::$VarName(v)
}
}
)*
)*};
}
/// Indicate that error is highly unlikely.
#[track_caller]
pub const fn shit_happens() -> ! {
panic!("shit happens")
}
pub trait ShitHappens<T>: seal::Seal {
/// Same as [`shit_happens`], but for unwrapping errors.
fn shit_happens(self) -> T;
}
impl<O> seal::Seal for Option<O> {}
impl<T> ShitHappens<T> for Option<T> {
#[track_caller]
fn shit_happens(self) -> T {
self.unwrap_or_else(|| shit_happens())
}
}
impl<O, E> seal::Seal for Result<O, E> {}
impl<O, E: fmt::Debug> ShitHappens<O> for Result<O, E> {
#[track_caller]
fn shit_happens(self) -> O {
self.expect("shit happens")
}
}

22
src/fut.rs Normal file
View file

@ -0,0 +1,22 @@
use std::{
marker::PhantomData,
pin::Pin,
task::{Context, Poll},
};
/// Future which is never ready.
#[crate::perfect_derive(Debug, Clone, Default, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Never<T>(PhantomData<T>);
impl<T> Future for Never<T> {
type Output = T;
fn poll(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Pending
}
}
crate::trait_set! {
/// Future + Send.
pub trait Fut = Future + Send;
}

22
src/generic.rs Normal file
View file

@ -0,0 +1,22 @@
use crate::data;
pub trait Anything {}
impl<T: ?Sized> Anything for T {}
// TODO: include all possible ranges.
#[data(
copy,
error,
display("integer out of range"),
crate = crate
)]
pub struct OutOfRange;
/// Text case.
#[data(copy, crate = crate, display(name))]
pub enum Case {
Snake,
Pascal,
Kebab,
Camel,
}

13
src/handling/and_then.rs Normal file
View file

@ -0,0 +1,13 @@
use crate::data;
#[data(copy, ord, crate = crate)]
pub struct AndThen<L, R> {
pub lhs: L,
pub rhs: R,
}
impl<L, R> AndThen<L, R> {
pub const fn new(lhs: L, rhs: R) -> Self {
Self { lhs, rhs }
}
}

29
src/handling/apply.rs Normal file
View file

@ -0,0 +1,29 @@
use crate::{
data,
fut::Fut,
handling::{Endpoint, Handler},
};
#[data(copy, ord, crate = crate)]
pub struct Apply<L, R> {
pub lhs: L,
pub rhs: R,
}
impl<L, R> Apply<L, R> {
pub const fn new(lhs: L, rhs: R) -> Self {
Self { lhs, rhs }
}
}
impl<I, S, L, R, Output> Endpoint<I, S> for Apply<L, R>
where
L: for<'a> Handler<I, S, &'a R, Output = Output>,
R: Send + Sync,
{
type Output = Output;
fn call(&self, state: S, in_: I) -> impl Fut<Output = Self::Output> {
self.lhs.call(state, in_, &self.rhs)
}
}

37
src/handling/mod.rs Normal file
View file

@ -0,0 +1,37 @@
use crate::{auto_impl, fut::Fut};
pub use self::{and_then::AndThen, apply::Apply, then::Then};
mod and_then;
mod apply;
mod then;
pub trait HandlerExt: Sized {
fn then<R>(self, rhs: R) -> Then<Self, R> {
Then::new(self, rhs)
}
}
impl<T> HandlerExt for T {}
pub trait EndpointExt: Sized {
fn apply<R>(self, rhs: R) -> Apply<Self, R> {
Apply::new(self, rhs)
}
}
impl<T> EndpointExt for T {}
#[auto_impl(&, &mut, Arc, Box)]
pub trait Endpoint<I, S>: Send + Sync {
type Output;
fn call(&self, state: S, in_: I) -> impl Fut<Output = Self::Output>;
}
#[auto_impl(&, &mut, Arc, Box)]
pub trait Handler<I, S, N>: Send + Sync {
type Output;
fn call(&self, state: S, in_: I, next: N) -> impl Fut<Output = Self::Output>;
}

View file

@ -0,0 +1 @@

29
src/handling/then.rs Normal file
View file

@ -0,0 +1,29 @@
use crate::{
data,
fut::Fut,
handling::{Apply, Handler},
};
#[data(copy, ord, crate = crate)]
pub struct Then<L, R> {
pub lhs: L,
pub rhs: R,
}
impl<I, S, L, R, N, Output> Handler<I, S, N> for Then<L, R>
where
R: Send + Sync,
L: for<'a> Handler<I, S, Apply<&'a R, N>, Output = Output>,
{
type Output = Output;
fn call(&self, state: S, in_: I, next: N) -> impl Fut<Output = Self::Output> {
self.lhs.call(state, in_, Apply::new(&self.rhs, next))
}
}
impl<L, R> Then<L, R> {
pub const fn new(lhs: L, rhs: R) -> Self {
Self { lhs, rhs }
}
}

1
src/hash.rs Normal file
View file

@ -0,0 +1 @@
pub type Hasher = ahash::AHasher;

59
src/lib.rs Normal file
View file

@ -0,0 +1,59 @@
#[macro_export]
macro_rules! zst_error {
($($tt:tt)*) => {{
#[$crate::data(
copy,
crate = $crate,
not(serde, schemars),
display($($tt)*),
)]
struct E;
E
}};
}
/// The trait that is implemented for everything.
pub trait Anything {}
impl<T: ?Sized> Anything for T {}
pub use bytes;
pub use bytesize;
pub use url;
pub use eva_macros::{data, endpoint, int, str};
pub use seq_macro::seq;
pub use auto_impl::auto_impl;
pub use perfect_derive::perfect_derive;
pub use trait_set::trait_set;
pub mod array;
pub mod error;
pub mod fut;
pub mod time;
pub mod sync;
pub mod trace_id;
pub mod generic;
pub mod slab;
pub mod str;
pub mod encoding;
pub mod rand;
pub mod collections;
pub mod handling;
pub mod hash;
pub use paste::paste;
#[doc(hidden)]
pub mod _priv {
pub use schemars;
pub use serde;
pub use eva_macros::RastGawno;
}

4
src/rand.rs Normal file
View file

@ -0,0 +1,4 @@
///! # Random utilities, refer to [`::rand`] crate docs.
pub use ::rand::*;
pub use rand_xoshiro as xoshiro;

1
src/slab.rs Normal file
View file

@ -0,0 +1 @@

212
src/str.rs Normal file
View file

@ -0,0 +1,212 @@
use std::{fmt, mem::MaybeUninit, slice, str::FromStr};
pub use compact_str::{
CompactString, CompactStringExt, ToCompactString, ToCompactStringError, format_compact,
};
use crate::data;
#[macro_export]
macro_rules! single_ascii_char {
($ty:ty) => {
const _: () = {
use ::core::{result::Result, str::FromStr};
use ::std::string::String;
use $crate::str::{HasPattern, ParseError};
impl HasPattern for $ty {
#[inline]
fn pat_into(buf: &mut String) {
$crate::push_ascii_pat!(Self, buf);
}
}
impl FromStr for $ty {
type Err = ParseError;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
let [c]: [u8; 1] = s.as_bytes().try_into().map_err(|_| ParseError::Length)?;
Self::new(c).ok_or(ParseError::Char)
}
}
};
};
}
/// Simple parse error.
#[data(copy, error, display(doc), crate = crate)]
pub enum ParseError {
/// Unexpected char.
Char,
/// Invalid length of string.
Length,
}
pub trait FixedParseError {
fn length(expected: usize) -> Self;
}
impl FixedParseError for ParseError {
#[inline]
fn length(expected: usize) -> Self {
_ = expected;
Self::Length
}
}
pub trait HasPattern {
fn pat_into(buf: &mut String);
#[inline]
fn regex_pat() -> String {
let mut s = String::new();
Self::pat_into(&mut s);
s
}
#[inline]
fn regex_pat_fullmatch() -> String {
let mut s = String::with_capacity(8);
s.push('^');
Self::pat_into(&mut s);
s.push('$');
s
}
}
impl HasPattern for String {
#[inline]
fn pat_into(buf: &mut String) {
buf.push_str(".*");
}
}
impl HasPattern for CompactString {
#[inline]
fn pat_into(buf: &mut String) {
buf.push_str(".*");
}
}
/// # Safety
///
/// Implementation of this trait implies that reinterpreting the reference to the type as &[u8] and then as valid utf8 sequence
/// is sound and defined.
pub unsafe trait FixedUtf8
where
Self: Sized + Copy + FromStr<Err: FixedParseError + fmt::Display> + HasPattern,
{
}
/// Reinterpret fixed size string as a standard library string slice.
pub const fn reinterpret<'a, T: FixedUtf8>(val: &'a T) -> &'a str {
let ts = size_of::<T>();
let slice: &'a [u8] = unsafe { slice::from_raw_parts(val as *const T as *const u8, ts) };
unsafe { std::str::from_utf8_unchecked(slice) }
}
#[derive(Clone, Copy)]
union EitherUnion<L: FixedUtf8, R: FixedUtf8> {
lhs: L,
rhs: R,
}
#[derive(Clone, Copy)]
pub struct Either<L: FixedUtf8, R: FixedUtf8>(EitherUnion<L, R>);
impl<L: FixedUtf8, R: FixedUtf8> FromStr for Either<L, R> {
type Err = R::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(res) = L::from_str(s) {
Ok(Self::left(res))
} else {
R::from_str(s).map(Self::right)
}
}
}
unsafe impl<L: FixedUtf8, R: FixedUtf8> FixedUtf8 for Either<L, R> {}
impl<L: FixedUtf8, R: FixedUtf8> HasPattern for Either<L, R> {
fn pat_into(buf: &mut String) {
buf.push('(');
L::pat_into(buf);
buf.push('|');
R::pat_into(buf);
buf.push(')');
}
}
impl<L: FixedUtf8, R: FixedUtf8> Either<L, R> {
const fn new(un: EitherUnion<L, R>) -> Self {
const {
if size_of::<L>() != size_of::<R>() {
panic!("Could not make string `Either` of differently sized strings");
}
}
Self(un)
}
pub const fn left(lhs: L) -> Self {
Self::new(EitherUnion { lhs })
}
pub const fn right(rhs: R) -> Self {
Self::new(EitherUnion { rhs })
}
pub const fn as_str(&self) -> &str {
reinterpret(self)
}
}
/// Sequence of fixed size string.
#[crate::str(custom, copy, crate = crate)]
pub struct Seq<const N: usize, T: FixedUtf8>(pub [T; N]);
impl<const N: usize, T: FixedUtf8> Seq<N, T> {
pub const fn as_str(&self) -> &str {
reinterpret(self)
}
}
impl<const N: usize, T: FixedUtf8> HasPattern for Seq<N, T> {
fn pat_into(buf: &mut String) {
T::pat_into(buf);
buf.push_str("{");
buf.push_str(&N.to_string());
buf.push_str("}");
}
}
unsafe impl<const N: usize, T: FixedUtf8> FixedUtf8 for Seq<N, T> {}
impl<const N: usize, T> FromStr for Seq<N, T>
where
T: FixedUtf8,
{
type Err = T::Err;
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
let expected_size = size_of::<T>() * N;
if s.len() != expected_size {
return Err(T::Err::length(expected_size));
}
let mut arr: [MaybeUninit<T>; N] = unsafe { MaybeUninit::uninit().assume_init() };
for idx in 0..N {
let len = size_of::<T>();
let chunk = unsafe { s.get_unchecked(..len) };
let res = T::from_str(chunk)?;
unsafe { arr.get_unchecked_mut(idx) }.write(res);
s = unsafe { s.get_unchecked(len..) };
}
Ok(Self(unsafe { std::ptr::read((&raw const arr).cast()) }))
}
}
pub mod ascii;

109
src/str/ascii.rs Normal file
View file

@ -0,0 +1,109 @@
use crate::int;
pub fn push_ascii_pat(u: u8, to: &mut String) {
let push = match u {
b'\r' => r"\r",
b'\n' => r"\n",
b'-' => r"\-",
b'^' => r"\^",
b'$' => r"\$",
b'[' => r"\[",
b']' => r"\]",
b'\\' => r"\",
b'.' => r"\.",
b'*' => r"\*",
b'+' => r"\+",
b'?' => r"\?",
b'{' => r"\{",
b'}' => r"\}",
b'|' => r"\|",
b'(' => r"\(",
b')' => r"\)",
_ => {
to.push(u as char);
return;
}
};
to.push_str(push);
}
#[macro_export]
macro_rules! push_ascii_pat {
($e:ident, $to:expr) => {{
let buf = $to;
let requires_brackets = match $e::RANGES.as_slice() {
[r] => r.len() > 1,
[] => false,
_ => true,
};
if requires_brackets {
buf.push('[');
}
for range in $e::RANGES.iter().cloned() {
let start = *range.start();
let end = *range.end();
$crate::str::ascii::push_ascii_pat(start, buf);
if start != end {
buf.push('-');
$crate::str::ascii::push_ascii_pat(end, buf);
}
}
if requires_brackets {
buf.push(']');
}
}};
}
macro_rules! valid {
($e:ident) => {
$crate::single_ascii_char!($e);
unsafe impl $crate::str::FixedUtf8 for $e {}
#[allow(dead_code)]
impl $e {
pub const fn as_str(&self) -> &str {
$crate::str::reinterpret(self)
}
}
};
}
pub(crate) use valid;
#[int(u8, b'.', crate = crate)]
pub enum Dot {}
valid!(Dot);
#[int(u8, b':', crate = crate)]
pub enum Colon {}
valid!(Colon);
#[int(u8, b' ', crate = crate)]
pub enum Space {}
valid!(Space);
#[int(u8, b'0'..=b'9', crate = crate)]
pub enum Digit {}
valid!(Digit);
impl Digit {
pub const fn parse(self) -> u8 {
self as u8 - b'0'
}
}
/// A valid ASCII character.
#[int(u8, 0..=177, crate = crate)]
pub enum Char {}
valid!(Char);
/// Printable ASCII character.
#[int(u8, 32..=126, crate = crate)]
pub enum Printable {}
valid!(Printable);
/// ASCII control character.
#[int(u8, 0..=31, crate = crate)]
pub enum Control {}
valid!(Control);

1
src/sync.rs Normal file
View file

@ -0,0 +1 @@

62
src/time/clock.rs Normal file
View file

@ -0,0 +1,62 @@
use std::{
num::NonZeroU64,
sync::{Arc, atomic},
time::{Duration, SystemTime},
};
use crate::time::Timestamp;
#[crate::auto_impl(&, &mut)]
pub trait Clock {
fn get(&self) -> Timestamp;
}
#[derive(Debug, Clone)]
pub struct Mock(Arc<atomic::AtomicU64>);
impl Default for Mock {
fn default() -> Self {
Self(Arc::new(atomic::AtomicU64::new(
Timestamp::TEST_ORIGIN.as_nanos().get(),
)))
}
}
impl Mock {
pub fn advance(&mut self, dur: Duration) {
self.0
.fetch_add(dur.as_nanos() as u64, atomic::Ordering::Release);
}
pub fn set(&mut self, ts: Timestamp) {
self.0.store(ts.as_nanos().get(), atomic::Ordering::Release);
}
pub fn back(&self, dur: Duration) {
_ = dur;
todo!()
}
}
impl Clock for Mock {
fn get(&self) -> Timestamp {
let time = self.0.load(atomic::Ordering::Acquire);
Timestamp::from_nanos(time.try_into().expect("invalid time set"))
}
}
/// Real time clock.
#[derive(Debug, Clone, Copy, Default)]
pub struct RealTime(());
impl Clock for RealTime {
fn get(&self) -> Timestamp {
// TODO: get time with a fixed time zone.
let dur = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let nanos = NonZeroU64::new(dur.as_nanos() as u64).unwrap();
Timestamp::from_nanos(nanos)
}
}

526
src/time/date.rs Normal file
View file

@ -0,0 +1,526 @@
use std::{borrow::Cow, mem, str::FromStr};
use schemars::JsonSchema;
use seq_macro::seq;
use serde::{Deserialize, Serialize, de};
use crate::{
data, int,
str::{HasPattern, ParseError},
};
use super::str;
#[data(ord, copy, display(name), crate = crate)]
pub enum Leapness {
Leap = 1,
Ordinary = 0,
}
impl Leapness {
pub const fn is_leap(self) -> bool {
matches!(self, Self::Leap)
}
pub const fn is_ordinary(self) -> bool {
matches!(self, Self::Ordinary)
}
}
#[data(copy, ord, not(serde, schemars), crate = crate)]
pub struct LooseDate {
day: Day,
month: Month,
year: Year,
}
impl JsonSchema for LooseDate {
fn schema_id() -> Cow<'static, str> {
Cow::Borrowed(concat!(module_path!(), "::LooseDate"))
}
fn schema_name() -> Cow<'static, str> {
Cow::Borrowed("LooseDate")
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"pattern": str::DateStr::regex_pat_fullmatch(),
"description": "Day, month and year"
})
}
}
impl<'de> Deserialize<'de> for LooseDate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
let s = <&'de str as Deserialize<'de>>::deserialize(deserializer)?;
let date: str::DateStr = s.parse().map_err(de::Error::custom)?;
date.parse().ok_or(de::Error::custom(zst_error!(
"invalid day, month and year combination"
)))
}
}
impl Serialize for LooseDate {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.to_str().serialize(serializer)
}
}
impl LooseDate {
pub const fn from_dmy(day: Day, month: Month, year: Year) -> Option<Self> {
let last_day = month.last_day(year.leapness());
if day as u8 > last_day as u8 {
return None;
}
Some(Self { day, month, year })
}
/// Get day of month.
pub const fn day_of_month(self) -> Day {
self.day
}
/// Get current month.
pub const fn month(self) -> Month {
self.month
}
/// Get year.
pub const fn year(self) -> Year {
self.year
}
/// Convert date to more compact representation.
pub const fn compact(self) -> Date {
let year = self.year();
let leapness = year.leapness();
let first_day_of_year = year.first_day().days();
let year_offset = self.month().days_from_year_start(leapness);
let month_offset = self.day_of_month() as u16 - 1;
Date::from_days(first_day_of_year + year_offset + month_offset)
}
/// Convert date to string.
pub const fn to_str(self) -> str::DateStr {
str::DateStr::new(
self.day_of_month().to_str(),
self.month().to_str(),
self.year().to_str(),
)
}
}
#[data(copy, ord, not(serde, schemars), crate = crate)]
#[derive(Hash)]
pub struct Date(u16);
impl JsonSchema for Date {
fn schema_id() -> Cow<'static, str> {
Cow::Borrowed(concat!(module_path!(), "::Date"))
}
fn schema_name() -> Cow<'static, str> {
Cow::Borrowed("Date")
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"anyOf": [
{
"type": "integer",
"minimum": 0,
"maximum": 65535,
"description": "number of days since 1970, only in binary formats"
},
LooseDate::json_schema(generator)
]
})
}
}
impl FromStr for Date {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
str::DateStr::from_str(s)?
.parse()
.map(|d| d.compact())
.ok_or(ParseError::Char)
}
}
impl<'de> Deserialize<'de> for Date {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
if deserializer.is_human_readable() {
Ok(Date::from_days(u16::deserialize(deserializer)?))
} else {
let loose = LooseDate::deserialize(deserializer)?;
Ok(loose.compact())
}
}
}
impl Serialize for Date {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if serializer.is_human_readable() {
self.to_str().serialize(serializer)
} else {
self.days().serialize(serializer)
}
}
}
impl Date {
pub const MIN: Self = Self(0);
pub const MAX: Self = Self(u16::MAX);
pub const fn from_dmy(day: Day, month: Month, year: Year) -> Option<Self> {
let Some(loosen) = LooseDate::from_dmy(day, month, year) else {
return None;
};
Some(loosen.compact())
}
pub const fn to_secs(self) -> u64 {
let hours = (self.0 as u64) * 24;
let mins = hours * 60;
mins * 60
}
/// Create date from number of days.
pub const fn from_days(days: u16) -> Self {
Self(days)
}
/// Get number of days.
pub const fn days(self) -> u16 {
self.0
}
/// Loosen up layout to less compact one.
pub const fn loose(self) -> LooseDate {
let year = self.year();
let total_days = self.days();
let leapness = year.leapness();
let days_since_year = total_days - year.first_day().days();
let month = unsafe {
Month::from_days_since_year_start(days_since_year, leapness).unwrap_unchecked()
};
let day_of_month = days_since_year - month.days_from_year_start(leapness);
LooseDate {
// SAFETY: safe, since condition for exiting loop is falling
// into days range.
day: unsafe { Day::new_unchecked(day_of_month as u8 + 1) },
month,
year,
}
}
/// Get current year.
pub const fn year(self) -> Year {
let days = self.days();
if days == 0 {
return Year::MIN;
}
let naive_year = days / 365;
let leap_days = super::utils::leap_days_after((naive_year + Year::ORIGIN) - 1)
- Year::LEAP_DAYS_BEFORE1970;
// naive_year includes leap days from the previous years! Skewing
// the calculations! This nigger should be rape-fixed!
let fixed = days - leap_days;
debug_assert!((fixed / 365) < 179);
unsafe { Year::new_unchecked((fixed / 365) as u8) }
}
/// Get number of days since the current year. I.e. if it's
/// January 1st -> result would be 0.
pub const fn days_since_year(self) -> u16 {
self.0 - self.year().first_day().days()
}
/// Get current month.
pub const fn month(self) -> Month {
self.loose().month()
}
/// Get day of month.
pub const fn day_of_month(self) -> Day {
self.loose().day_of_month()
}
/// Convert date to string.
pub const fn to_str(self) -> str::DateStr {
self.loose().to_str()
}
}
#[int(u8, 0..179, crate = crate)]
pub enum Year {}
impl Year {
pub const MIN: Self = Self::new(0).unwrap();
pub const MAX: Self = Self::new(178).unwrap();
const LEAP_DAYS_BEFORE1970: u16 = super::utils::leap_days_after(1970 - 1);
pub const ORIGIN: u16 = 1970;
pub const fn from_abs(abs: u16) -> Option<Self> {
if matches!(abs, 0..Self::ORIGIN) {
return None;
}
let shifted = abs - Self::ORIGIN;
if shifted > Self::MAX as u16 {
None
} else {
Some(unsafe { Year::new_unchecked(shifted as u8) })
}
}
pub const fn leapness(self) -> Leapness {
if super::utils::is_leap_year(self.abs()) {
Leapness::Leap
} else {
Leapness::Ordinary
}
}
/// Get date of the first day in a year.
pub const fn first_day(self) -> Date {
let leap_days = super::utils::leap_days_after(self.abs() - 1) - Self::LEAP_DAYS_BEFORE1970;
let naive_days = (self as u16) * 365;
Date::from_days(naive_days + leap_days)
}
/// Convert relative year to absolute year.
pub const fn abs(self) -> u16 {
self as u16 + Self::ORIGIN
}
/// Convert year to fixed size string.
pub const fn to_str(self) -> str::YearStr {
let v = self.abs();
let buf = [
(v / 1000) as u8 + b'0',
(v / 100 % 10) as u8 + b'0',
(v / 10 % 10) as u8 + b'0',
(v % 10) as u8 + b'0',
];
unsafe { mem::transmute::<[u8; 4], str::YearStr>(buf) }
}
}
#[int(u8, 1..=32, crate = crate)]
pub enum Day {}
impl Day {
pub const fn first() -> Self {
Self::VARIANTS[0]
}
/// Get index inside [`Day::VARIANTS`].
pub const fn index(self) -> usize {
self as usize - 1
}
/// Convert day to fixed size string.
pub const fn to_str(self) -> str::DayStr {
let d = self as u8;
unsafe { mem::transmute::<[u8; 2], str::DayStr>([d / 10 + b'0', d % 10 + b'0']) }
}
}
#[data(copy, ord, crate = crate, display(name))]
pub enum Month {
Jan = 0,
Feb = 1,
Mar = 2,
Apr = 3,
May = 4,
Jun = 5,
Jul = 6,
Aug = 7,
Sep = 8,
Oct = 9,
Nov = 10,
Dec = 11,
}
impl Month {
pub const MIN: Self = Self::Jan;
pub const MAX: Self = Self::Dec;
pub const VARIANTS: [Self; 12] = {
use Month::*;
[Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
};
pub const fn prev(self) -> Self {
let Some(prev) = self.prev_checked() else {
return Self::MIN;
};
prev
}
pub const fn prev_checked(self) -> Option<Self> {
let Some(prev) = (self as u8).checked_sub(1) else {
return None;
};
Some(unsafe { Month::from_repr_unchecked(prev) })
}
pub const fn next(self) -> Self {
let Some(next) = self.next_checked() else {
return Self::MAX;
};
next
}
pub const fn next_checked(self) -> Option<Self> {
let repr = self as u8 + 1;
Self::from_repr(repr)
}
const fn from_days_since_year_start_impl(mut days: u16, leapness: Leapness) -> Self {
let mut month = Month::Jan;
loop {
let last_day = month.last_day(leapness);
if days < last_day as u16 {
break;
}
days -= last_day as u16;
month = month.next();
}
month
}
pub const fn from_days_since_year_start(days: u16, leapness: Leapness) -> Option<Self> {
use Leapness::*;
const ORDINARY: [Month; 365] = seq!(N in 0..365 {
[#(Month::from_days_since_year_start_impl(N, Ordinary),)*]
});
const LEAP: [Month; 366] = seq!(N in 0..366 {
[#(Month::from_days_since_year_start_impl(N, Leap),)*]
});
match leapness {
Leap => {
if days >= 366 {
None
} else {
Some(LEAP[days as usize])
}
}
Ordinary => {
if days >= 365 {
None
} else {
Some(ORDINARY[days as usize])
}
}
}
}
pub const fn days_from_year_start(self, leapness: Leapness) -> u16 {
const fn l(m: Month) -> u16 {
m.calc_days_from_year_start(Leapness::Leap)
}
const fn o(m: Month) -> u16 {
m.calc_days_from_year_start(Leapness::Ordinary)
}
const LEAP: [u16; 12] = seq!(N in 0..12 {
[#(l(Month::VARIANTS[N]),)*]
});
const ORDINARY: [u16; 12] = seq!(N in 0..12 {
[#(o(Month::VARIANTS[N]),)*]
});
match leapness {
Leapness::Leap => LEAP[self as usize],
Leapness::Ordinary => ORDINARY[self as usize],
}
}
const fn calc_days_from_year_start(self, leapness: Leapness) -> u16 {
let mut cur = Self::Jan;
let mut days = 0_u16;
// Sigh, recursion would be better.
while cur as u8 != self as u8 {
let last_day = cur.last_day(leapness);
days += last_day as u16;
cur = cur.next();
}
days
}
pub const fn last_day_naive(self) -> Day {
self.last_day(Leapness::Ordinary)
}
/// Get last day of month with respect to the leapness of year.
pub const fn last_day(self, leapness: Leapness) -> Day {
use Day::*;
const LAST_DAY: [Day; 12] = [
POS31, POS28, POS31, POS30, POS31, POS30, POS31, POS31, POS30, POS31, POS30, POS31,
];
const LAST_LEAP_DAY: [Day; 12] = {
let mut src = LAST_DAY;
src[1] = POS29;
src
};
const TBL: [[Day; 12]; 2] = [LAST_DAY, LAST_LEAP_DAY];
TBL[leapness.is_leap() as usize][self as usize]
}
pub const unsafe fn from_repr_unchecked(repr: u8) -> Self {
unsafe { mem::transmute::<u8, Self>(repr) }
}
pub const fn from_repr(repr: u8) -> Option<Self> {
match repr {
0..12 => Some(unsafe { Self::from_repr_unchecked(repr) }),
_ => None,
}
}
/// Convert month to fixed size string.
pub const fn to_str(self) -> str::MonthStr {
let v = self as u8 + 1;
unsafe { mem::transmute::<[u8; 2], str::MonthStr>([v / 10 + b'0', v % 10 + b'0']) }
}
}

49
src/time/mod.rs Normal file
View file

@ -0,0 +1,49 @@
//! # Date and time
//!
//! Contains heavily and precisely typed utilities for working
//! with types.
//!
//! # Glossary
//!
//! ### [`Clock`]
//!
//! Trait which is responsible for getting current time.
//!
//! - [`Mock`] - Mock [`Clock`], can be used for testing purposes
//! - [`RealTime`] - realtime clock
//!
//! ### Date and time types
//!
//! Main types:
//! - [`Date`] - day precise time, contains [`Day`], [`Month`] and [`Year`]
//! - [`Time`] - time during day, contains [`Hours`] and [`Mins`]
//! - [`SecsTime`] - same as [`Time`], but contains seconds
//! - [`PreciseTime`] - same as [`Time`], but contains seconds and nanoseconds
//! - [`Timestamp`] - maximum precision timestamp, nanoseconds precision
//!
//! Every timestamp here return timestamp in the UTC timezone.
//!
//! ### String representation
//!
//! Every type presented here have string representation, see [`self::str`] module.
pub use self::{
clock::{Clock, Mock, RealTime},
date::{Date, Day, Leapness, LooseDate, Month, Year},
time::{Hours, Mins, PreciseTime, Secs, SecsTime, SubsecNanos, Time},
timestamp::Timestamp,
};
pub mod ser;
pub mod str;
pub mod tz;
mod clock;
mod date;
mod time;
mod timestamp;
mod utils;
#[cfg(test)]
mod tests;

3
src/time/ser.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod tz {
pub mod msk {}
}

278
src/time/str.rs Normal file
View file

@ -0,0 +1,278 @@
//! # Strings that represent time
use crate::{
int, str,
str::{ParseError, Seq, ascii},
};
use super::{
Day, Hours, LooseDate, Mins, Month, PreciseTime, Secs, SecsTime, Time, Timestamp, Year,
time::SubsecNanos, timestamp::LooseTimestamp,
};
// == TimestampStr ==
/// Precise string timestamp representation.
///
/// format: `dd.mm.YYYY HH:MM:SS.NNNNNNNNN`.
#[str(fixed(error = ParseError), crate = crate)]
pub struct TimestampStr(DateStr, ascii::Space, PreciseTimeStr);
impl TimestampStr {
pub const fn new(date: DateStr, time: PreciseTimeStr) -> Self {
Self(date, ascii::Space::POS32, time)
}
pub const fn parse_compact(self) -> Option<Timestamp> {
let Some(loose) = self.parse() else {
return None;
};
Some(loose.compact_loose().compact())
}
pub const fn parse(self) -> Option<LooseTimestamp<LooseDate>> {
let Some(date) = self.0.parse() else {
return None;
};
let Some(time) = self.2.parse() else {
return None;
};
Some(LooseTimestamp::<_>::new(date, time))
}
}
// == PreciseTimeStr ==
/// [`TimeStr`] with seconds an nanoseconds.
///
/// format: `HH:MM:SS.NNNNNNNNN`, where N is nanosecond digit.
#[str(fixed(error = ParseError), crate = crate)]
pub struct PreciseTimeStr(SecsTimeStr, ascii::Dot, SubsecNanosStr);
impl PreciseTimeStr {
pub const fn new(secs_time: SecsTimeStr, subsec_nanos: SubsecNanosStr) -> Self {
Self(secs_time, ascii::Dot::POS46, subsec_nanos)
}
pub const fn parse(self) -> Option<PreciseTime> {
let Some(time) = self.0.parse() else {
return None;
};
let nanos = self.2.parse();
Some(PreciseTime::new(time, nanos))
}
}
// == SecsTimeStr ==
/// [`TimeStr`] with seconds part.
///
/// format: `HH:MM:SS`.
#[str(fixed(error = ParseError), crate = crate)]
pub struct SecsTimeStr(TimeStr, ascii::Colon, SecsStr);
impl SecsTimeStr {
pub const fn new(time: TimeStr, secs: SecsStr) -> Self {
Self(time, ascii::Colon::POS58, secs)
}
pub const fn parse(self) -> Option<SecsTime> {
let Some(time) = self.0.parse() else {
return None;
};
let secs = self.2.parse();
Some(SecsTime::new(time, secs))
}
}
// == DateStr ==
#[str(fixed(error = ParseError), crate = crate)]
pub struct DateStr(DayStr, ascii::Dot, MonthStr, ascii::Dot, YearStr);
impl DateStr {
pub const fn new(day: DayStr, month: MonthStr, year: YearStr) -> Self {
Self(day, ascii::Dot::POS46, month, ascii::Dot::POS46, year)
}
pub const fn parse(self) -> Option<LooseDate> {
let Some(day) = self.0.parse() else {
return None;
};
let Some(month) = self.2.parse() else {
return None;
};
let Some(year) = self.4.parse() else {
return None;
};
LooseDate::from_dmy(day, month, year)
}
}
// == TimeStr ==
/// Partly valid time string. Contains hours and minutes.
///
/// Format: `HH:MM`.
#[str(fixed(error = ParseError), crate = crate)]
pub struct TimeStr(HoursStr, ascii::Colon, MinsStr);
impl TimeStr {
pub const fn new(hours: HoursStr, minutes: MinsStr) -> Self {
Self(hours, ascii::Colon::POS58, minutes)
}
pub const fn parse(self) -> Option<Time> {
let Some(hours) = self.0.parse() else {
return None;
};
let mins = self.2.parse();
Some(Time::new(hours, mins))
}
}
// == YearStr ==
#[str(fixed(error = ParseError), crate = crate)]
pub struct YearStr(FirstYearChar, Seq<3, ascii::Digit>);
impl YearStr {
pub const fn parse(self) -> Option<Year> {
let mut num = digit(self.0 as u8) as u16 * 1000;
let i = self.1.0;
num += digit(i[0] as u8) as u16 * 100;
num += digit(i[1] as u8) as u16 * 10;
num += digit(i[2] as u8) as u16;
let Some(relative) = num.checked_sub(1970) else {
return None;
};
Year::new(relative as u8)
}
}
#[int(u8, b'1' | b'2', crate = crate)]
enum FirstYearChar {}
ascii::valid!(FirstYearChar);
// == MonthStr ==
#[str(fixed(error = ParseError), crate = crate)]
pub struct MonthStr(FirstMonthChar, ascii::Digit);
impl MonthStr {
pub const fn parse(self) -> Option<Month> {
let num = digit(self.0 as u8) * 10 + digit(self.1 as u8);
let Some(index) = num.checked_sub(1) else {
return None;
};
Month::from_repr(index)
}
}
#[int(u8, b'0'..=b'1', crate = crate)]
enum FirstMonthChar {}
ascii::valid!(FirstMonthChar);
// == DayStr ==
#[str(fixed(error = ParseError), crate = crate)]
pub struct DayStr(FirstDayChar, ascii::Digit);
impl DayStr {
pub const fn parse(self) -> Option<Day> {
let num = digit(self.0 as u8) * 10 + digit(self.1 as u8);
Day::new(num)
}
}
#[int(u8, b'0'..=b'3', crate = crate)]
enum FirstDayChar {}
ascii::valid!(FirstDayChar);
// == SubsecNanosStr ==
#[str(fixed(error = ParseError), crate = crate)]
pub struct SubsecNanosStr(Seq<9, ascii::Digit>);
impl SubsecNanosStr {
pub const fn parse(self) -> SubsecNanos {
let i = self.0.0;
let mut n = 0;
n += digit(i[8] as u8) as u32 * 1_000_000_00;
n += digit(i[7] as u8) as u32 * 1_000_000_0;
n += digit(i[6] as u8) as u32 * 1_000_000;
n += digit(i[5] as u8) as u32 * 1_000_00;
n += digit(i[4] as u8) as u32 * 1_000_0;
n += digit(i[3] as u8) as u32 * 1_000;
n += digit(i[2] as u8) as u32 * 1_00;
n += digit(i[1] as u8) as u32 * 10;
n += digit(i[0] as u8) as u32;
unsafe { SubsecNanos::new_unchecked(n) }
}
}
// == SecsStr ==
#[str(fixed(error = ParseError), crate = crate)]
pub struct SecsStr(FirstSecsChar, ascii::Digit);
impl SecsStr {
pub const fn parse(self) -> Secs {
let num = digit(self.0 as u8) * 10 + digit(self.1 as u8);
unsafe { Secs::new_unchecked(num) }
}
}
#[int(u8, b'0'..=b'5', crate = crate)]
enum FirstSecsChar {}
ascii::valid!(FirstSecsChar);
// == HoursStr ==
/// Number of hours, two ascii digits.
#[str(fixed(error = ParseError), crate = crate)]
pub struct HoursStr(FirstHoursChar, ascii::Digit);
impl HoursStr {
pub const fn parse(self) -> Option<Hours> {
let num = digit(self.0 as u8) * 10 + digit(self.1 as u8);
Hours::new(num)
}
}
#[int(u8, b'0' | b'1' | b'2', crate = crate)]
enum FirstHoursChar {}
ascii::valid!(FirstHoursChar);
// == MinsStr ==
/// Number of minutes, two ascii digits.
#[str(fixed(error = ParseError), crate = crate)]
pub struct MinsStr(FirstMinsChar, ascii::Digit);
impl MinsStr {
pub const fn parse(self) -> Mins {
unsafe { Mins::new_unchecked(digit(self.0 as u8) * 10 + digit(self.1 as u8)) }
}
}
#[int(u8, b'0'..=b'5', crate = crate)]
enum FirstMinsChar {}
ascii::valid!(FirstMinsChar);
// == Utils ==
const fn digit(n: u8) -> u8 {
n - b'0'
}

306
src/time/tests.rs Normal file
View file

@ -0,0 +1,306 @@
use super::{
Date, Day, Hours, Leapness, LooseDate, Mins, Month, PreciseTime, Secs, SecsTime, SubsecNanos,
Time, Timestamp, Year, str,
};
#[track_caller]
fn each_date(mut f: impl FnMut(LooseDate)) {
for year in Year::VARIANTS {
for month in Month::VARIANTS {
let last_day = month.last_day(year.leapness());
for day in Day::VARIANTS.into_iter().take(last_day as usize) {
f(LooseDate::from_dmy(day, month, year).unwrap());
}
}
}
}
#[track_caller]
#[allow(dead_code)]
fn each_time(mut f: impl FnMut(Time)) {
for hours in Hours::VARIANTS {
for mins in Mins::VARIANTS {
let time = Time::new(hours, mins);
f(time);
}
}
}
mod timestamp {
use super::*;
#[cfg(feature = "get_time_test")]
#[test]
fn get_time() {
each_date(|date| {
let date = date.compact();
each_time(|time| {
let ts = date.to_secs() + time.to_secs();
if ts == 0 {
return;
}
let ts = Timestamp::from_secs_checked(ts).unwrap();
let actual_date = ts.date();
let actual_time = ts.time().secs_time().time();
assert_eq!(actual_date, date);
assert_eq!(time, actual_time);
});
});
}
#[test]
fn from_date_and_back() {
each_date(|date| {
let date = date.compact();
let secs = date.to_secs();
let ts = Timestamp::from_secs(secs);
assert_eq!(ts.date(), date);
});
}
}
mod time {
use super::*;
#[track_caller]
fn time(hours: u8, mins: u8) -> Time {
Time::new(Hours::new(hours).unwrap(), Mins::new(mins).unwrap())
}
#[test]
fn format_precise() {
#[track_caller]
fn case(res: &str, t: Time, subsecs: u8, nanos: u32) {
let expected = PreciseTime::new(
SecsTime::new(t, Secs::new(subsecs).unwrap()),
SubsecNanos::new(nanos).unwrap(),
);
assert_eq!(expected.to_str().as_str(), res);
}
case("15:00:00.000000000", time(15, 00), 0, 0);
case("15:00:00.148814880", time(15, 00), 0, 148814880);
}
#[test]
fn format_secs() {
#[track_caller]
fn case(res: &str, t: Time, subsecs: u8) {
let expected = SecsTime::new(t, Secs::new(subsecs).unwrap());
assert_eq!(expected.to_str().as_str(), res);
}
case("15:00:00", time(15, 00), 00);
case("15:15:30", time(15, 15), 30);
}
#[test]
fn format_basic() {
#[track_caller]
fn case(res: &str, t: Time) {
let actual = t.to_str();
let expected: Time = res.parse().unwrap();
assert_eq!(expected.to_str(), actual);
}
case("15:00", time(15, 00));
case("23:59", time(23, 59));
case("00:00", time(00, 00));
}
}
mod date {
use super::*;
#[track_caller]
fn year(abs: u16) -> Year {
Year::from_abs(abs).unwrap()
}
#[test]
fn year_parse() {
let year: str::YearStr = "2025".parse().unwrap();
let parsed = year.parse().unwrap();
assert_eq!(parsed.abs(), 2025);
}
#[test]
fn first_day_of_year() {
for y in Year::VARIANTS {
let date = y.first_day();
assert_eq!(date.day_of_month(), 1);
assert_eq!(date.month(), Month::Jan);
assert_eq!(date.year(), y);
}
}
#[test]
fn leapness_works_well() {
const fn is_leap(y: u16) -> bool {
(y % 400 == 0) || (y % 100 != 0 && y % 4 == 0)
}
for y in Year::VARIANTS {
let expected = is_leap(y.abs());
let actual = y.leapness().is_leap();
assert_eq!(expected, actual, "{y} wrongly calculated leapness");
}
}
#[test]
fn zero_date() {
let date = Date::from_days(0);
let y = year(1970);
assert_eq!(date.to_str().as_str(), "01.01.1970");
assert_eq!(y.first_day(), date);
}
#[test]
fn loose_and_compact_identity() {
for y in Year::VARIANTS {
let leapness = y.leapness();
for month in Month::VARIANTS {
let last_day = month.last_day(leapness);
for day in Day::VARIANTS
.into_iter()
.take_while(|d| *d as u8 <= last_day as u8)
{
eprintln!("{}.{}.{}", day, month, y.abs());
let dmy = Date::from_dmy(day, month, y).unwrap();
assert_eq!(dmy, dmy.loose().compact());
}
}
}
}
#[test]
fn days_from_year_start() {
// TODO: extend tests.
assert_eq!(Month::Jan.last_day(Leapness::Ordinary), 31);
assert_eq!(Month::Feb.days_from_year_start(Leapness::Ordinary), 31);
}
// Some problematic cases.
#[test]
fn from_dmy_28feb_1970() {
let y = year(1970);
let m = Month::Feb;
let d = Day::new(1).unwrap();
let date = Date::from_dmy(d, m, y).unwrap();
assert_eq!(date.year(), y);
assert_eq!(date.month(), m);
assert_eq!(date.day_of_month(), d);
}
#[test]
fn from_dmy_1jan_1970() {
let y = year(1970);
let m = Month::Jan;
let d = Day::new(1).unwrap();
let date = Date::from_dmy(d, m, y).unwrap();
assert_eq!(date.year(), y);
assert_eq!(date.month(), m);
assert_eq!(date.day_of_month(), d);
}
#[test]
fn from_dmy_31dec_1972() {
let y = year(1972);
let m = Month::Dec;
let d = Day::new(31).unwrap();
{
let date = Date::from_dmy(Day::first(), Month::Jan, year(1972)).unwrap();
assert_eq!(date.year().abs(), 1972);
}
let date = Date::from_dmy(d, m, y).unwrap();
assert_eq!(date.year().abs(), y.abs());
assert_eq!(date.month(), m);
assert_eq!(date.day_of_month(), d);
}
#[test]
fn from_dmy_31dec_2000() {
let y = year(2000);
let m = Month::Dec;
let d = Day::new(31).unwrap();
let date = Date::from_dmy(d, m, y).unwrap();
eprintln!(
"{}.{}.{} = {}.{}.{}",
date.day_of_month(),
date.month(),
date.year().abs(),
d,
m,
y.abs()
);
assert_eq!(date.year(), y);
assert_eq!(date.month(), m);
assert_eq!(date.day_of_month(), d);
}
#[test]
fn from_dmy() {
for y in Year::VARIANTS {
for month in Month::VARIANTS {
let last_day = month.last_day(y.leapness());
for day in Day::VARIANTS
.into_iter()
.take_while(|d| *d as u8 >= last_day as u8)
{
let date = Date::from_dmy(day, month, y).expect("got invalid date");
let real_dom = date.day_of_month();
let real_year = date.year();
let real_month = date.month();
eprintln!(
"{real_dom} {real_month} {} = {day}.{month}.{}",
real_year.abs(),
y.abs()
);
assert_eq!(real_dom, day);
assert_eq!(real_month, month);
assert_eq!(real_year.abs(), y.abs());
}
}
}
}
#[test]
fn format() {
use Month::*;
#[track_caller]
fn case(fmt: &str, dmy: (u8, Month, u16)) {
let (d, m, y) = dmy;
let d = Day::new(d).unwrap();
let y = year(y);
let date = Date::from_dmy(d, m, y).unwrap();
assert_eq!(date.to_str().as_str(), fmt);
}
case("01.01.1970", (1, Jan, 1970));
case("01.01.2024", (1, Jan, 2024));
case("29.09.2005", (29, Sep, 2005));
}
}

234
src/time/time.rs Normal file
View file

@ -0,0 +1,234 @@
use std::{mem, str::FromStr};
use crate::{data, int, str::ParseError, time::str};
// == PreciseTime ==
#[data(copy, ord, not(serde, schemars), crate = crate)]
pub struct PreciseTime {
time: SecsTime,
nanos: SubsecNanos,
}
impl FromStr for PreciseTime {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
str::PreciseTimeStr::from_str(s).and_then(|s| s.parse().ok_or(ParseError::Char))
}
}
impl PreciseTime {
pub const fn new(time: SecsTime, nanos: SubsecNanos) -> Self {
Self { time, nanos }
}
pub const fn to_nanos(self) -> u64 {
let secs = self.time.to_secs();
self.nanos.into_inner() as u64 + secs * 1_000_000_000
}
pub const fn secs(self) -> Secs {
self.secs_time().secs()
}
pub const fn mins(self) -> Mins {
self.secs_time().mins()
}
pub const fn hours(self) -> Hours {
self.secs_time().hours()
}
pub const fn subsecs_nanos(self) -> SubsecNanos {
self.nanos
}
pub const fn secs_time(self) -> SecsTime {
self.time
}
pub const fn to_str(self) -> str::PreciseTimeStr {
str::PreciseTimeStr::new(self.time.to_str(), self.nanos.to_str())
}
}
// == SecsTime ==
#[data(copy, ord, not(serde, schemars), crate = crate)]
#[derive(Hash)]
pub struct SecsTime {
time: Time,
secs: Secs,
}
impl FromStr for SecsTime {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
str::SecsTimeStr::from_str(s).and_then(|s| s.parse().ok_or(ParseError::Char))
}
}
impl SecsTime {
pub const fn to_secs(self) -> u64 {
self.time.to_secs() + self.secs.into_inner() as u64
}
}
impl SecsTime {
pub const fn new(time: Time, secs: Secs) -> Self {
Self { time, secs }
}
pub const fn hours(self) -> Hours {
self.time().hours()
}
pub const fn mins(self) -> Mins {
self.time().mins()
}
pub const fn secs(self) -> Secs {
self.secs
}
pub const fn time(self) -> Time {
self.time
}
pub const fn to_str(self) -> str::SecsTimeStr {
str::SecsTimeStr::new(self.time.to_str(), self.secs.to_str())
}
}
// == Time ==
#[data(copy, ord, not(serde, schemars), crate = crate)]
#[derive(Hash)]
pub struct Time {
hours: Hours,
mins: Mins,
}
impl FromStr for Time {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
str::TimeStr::from_str(s).and_then(|s| s.parse().ok_or(ParseError::Char))
}
}
impl Time {
pub const fn new(hours: Hours, mins: Mins) -> Self {
Self { hours, mins }
}
pub const fn to_secs(self) -> u64 {
(self.hours as u64 * 60 + self.mins as u64) * 60
}
pub const fn hours(self) -> Hours {
self.hours
}
pub const fn mins(self) -> Mins {
self.mins
}
pub const fn to_str(self) -> str::TimeStr {
let hours = self.hours().to_str();
let mins = self.mins().to_str();
str::TimeStr::new(hours, mins)
}
}
/// Hours.
#[int(u8, 0..24, crate = crate)]
#[derive(Hash)]
pub enum Hours {}
impl Hours {
pub const fn to_str(self) -> str::HoursStr {
let last = (self as u8) % 10;
let first = (self as u8) / 10;
unsafe { mem::transmute::<[u8; 2], str::HoursStr>([b'0' + first, b'0' + last]) }
}
}
/// Minutes.
#[int(u8, 0..60, crate = crate)]
#[derive(Hash)]
pub enum Mins {}
impl Mins {
pub const fn to_str(self) -> str::MinsStr {
let first = (self as u8) / 10;
let last = (self as u8) % 10;
unsafe { mem::transmute::<[u8; 2], str::MinsStr>([b'0' + first, b'0' + last]) }
}
}
/// Seconds.
#[int(u8, 0..60, crate = crate)]
#[derive(Hash)]
pub enum Secs {}
impl Secs {
pub const fn to_str(self) -> str::SecsStr {
let first = (self as u8) / 10;
let last = (self as u8) % 10;
unsafe { mem::transmute::<[u8; 2], str::SecsStr>([b'0' + first, b'0' + last]) }
}
}
#[data(copy, ord, not(serde), crate = crate)]
#[derive(Hash, Default)]
pub struct SubsecNanos(#[schemars(range(min = Self::MIN.0, max = Self::MAX.0))] u32);
impl SubsecNanos {
pub const MIN: Self = Self(0);
pub const MAX: Self = Self(999999999);
pub const fn into_inner(self) -> u32 {
self.0
}
pub const fn to_str(self) -> str::SubsecNanosStr {
const fn digit(v: u32) -> u8 {
(v % 10) as u8 + b'0'
}
let val = self.0;
let arr = [
digit(val / 1_000_000_00),
digit(val / 1_000_000_0),
digit(val / 1_000_000),
digit(val / 1_000_00),
digit(val / 1_000_0),
digit(val / 1_000),
digit(val / 100),
digit(val / 10),
digit(val),
];
unsafe { mem::transmute::<[u8; 9], str::SubsecNanosStr>(arr) }
}
/// Create subsecs without checking the range.
pub const unsafe fn new_unchecked(value: u32) -> Self {
Self(value)
}
pub const fn new(value: u32) -> Option<Self> {
if value > Self::MAX.0 {
None
} else {
Some(Self(value))
}
}
}

321
src/time/timestamp.rs Normal file
View file

@ -0,0 +1,321 @@
use std::{borrow::Cow, num::NonZeroU64, ops::Add, str::FromStr, time::Duration};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de};
use crate::{
data,
str::{HasPattern, ParseError},
time::{Hours, Mins, Secs, SecsTime, SubsecNanos, Time},
zst_error,
};
use super::{Date, LooseDate, PreciseTime, str, utils::divmod};
#[data(copy, ord, not(serde, schemars), crate = crate)]
pub struct LooseTimestamp<D = Date> {
date: D,
time: PreciseTime,
}
impl<D> JsonSchema for LooseTimestamp<D> {
fn schema_id() -> Cow<'static, str> {
Cow::Borrowed(concat!(module_path!(), "::LooseTimestamp"))
}
fn schema_name() -> Cow<'static, str> {
Cow::Borrowed("LooseTimestamp")
}
fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"pattern": str::TimestampStr::regex_pat_fullmatch(),
"description": "precise timestamp representation (UTC)"
})
}
}
impl Serialize for LooseTimestamp<LooseDate> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.to_str().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for LooseTimestamp<LooseDate> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = str::TimestampStr::deserialize(deserializer)?;
let ts = s
.parse()
.ok_or(de::Error::custom(zst_error!("malformed timestamp")))?;
Ok(ts)
}
}
impl Serialize for LooseTimestamp {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.loose().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for LooseTimestamp {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let loose = LooseTimestamp::<LooseDate>::deserialize(deserializer)?;
Ok(loose.compact_loose())
}
}
impl LooseTimestamp<LooseDate> {
pub const fn to_str(self) -> str::TimestampStr {
let date = self.date.to_str();
let time = self.time.to_str();
str::TimestampStr::new(date, time)
}
pub const fn compact_loose(self) -> LooseTimestamp {
LooseTimestamp {
date: self.date.compact(),
time: self.time,
}
}
}
impl LooseTimestamp {
pub const fn compact(self) -> Timestamp {
let secs = self.date().to_secs() as u64;
let nanos =
unsafe { NonZeroU64::new_unchecked(self.time().to_nanos() + secs * 1_000_000_000) };
Timestamp::from_nanos(nanos)
}
pub const fn loose(self) -> LooseTimestamp<LooseDate> {
LooseTimestamp {
date: self.date.loose(),
time: self.time,
}
}
}
impl<D: Copy> LooseTimestamp<D> {
pub const fn new(date: D, time: PreciseTime) -> Self {
Self { date, time }
}
pub const fn date(self) -> D {
self.date
}
pub const fn time(self) -> PreciseTime {
self.time
}
}
/// Timestamp in nanoseconds, since UTC.
#[data(copy, ord, not(serde, schemars), display("{}", self.to_str()), crate = crate)]
#[derive(Hash)]
pub struct Timestamp(NonZeroU64);
impl FromStr for Timestamp {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
str::TimestampStr::from_str(s).and_then(|t| {
let loose = t.parse().ok_or(ParseError::Char)?;
Ok(loose.compact_loose().compact())
})
}
}
impl JsonSchema for Timestamp {
fn schema_id() -> Cow<'static, str> {
Cow::Borrowed(concat!(module_path!(), "::Timestamp"))
}
fn schema_name() -> Cow<'static, str> {
Cow::Borrowed("Timestamp")
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"anyOf": [
{
"type": "integer",
"minimum": 1,
"description": "Number of nanoseconds since UTC unix epoch, only in binary formats"
},
LooseTimestamp::<LooseDate>::json_schema(generator)
]
})
}
}
impl Serialize for Timestamp {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if serializer.is_human_readable() {
self.loose().serialize(serializer)
} else {
self.0.serialize(serializer)
}
}
}
impl<'de> Deserialize<'de> for Timestamp {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
if deserializer.is_human_readable() {
Ok(LooseTimestamp::deserialize(deserializer)?.compact())
} else {
let nanos = NonZeroU64::deserialize(deserializer)?;
Ok(Self::from_nanos(nanos))
}
}
}
impl Add<Duration> for Timestamp {
type Output = Timestamp;
fn add(self, rhs: Duration) -> Self::Output {
Self(
self.0
.checked_add(rhs.as_nanos() as u64)
.expect("duration addition overflown"),
)
}
}
#[rustfmt::skip]
macro_rules! checked {
($lhs:expr) => {'ret: {
let Some(value) = NonZeroU64::new($lhs) else {
break 'ret None;
};
Some(Timestamp::from_nanos(value))
}};
}
macro_rules! saturated {
($lhs:expr) => {
if let Some(val) = checked!($lhs) {
val
} else {
Timestamp::MIN
}
};
}
impl Timestamp {
pub const fn from_nanos(nanos: NonZeroU64) -> Self {
Self(nanos)
}
pub const fn from_secs(secs: u64) -> Self {
saturated!(secs * 1_000_000_000)
}
pub const fn from_secs_checked(secs: u64) -> Option<Self> {
checked!(secs * 1_000_000_000)
}
pub const fn from_millis(millis: u64) -> Self {
saturated!(millis * 1_000_000)
}
pub const fn from_millis_checked(millis: u64) -> Option<Self> {
checked!(millis * 1_000_000)
}
pub fn loose(self) -> LooseTimestamp<LooseDate> {
let date = self.date();
let time = {
let nanos = self.0.get();
let (secs, subsec_nanos) = divmod(nanos, 1_000_000_000);
let (mins, subsecs) = divmod(secs, 60);
let (hours, submins) = divmod(mins, 60);
let subhours = hours % 24;
let time = Time::new(unsafe { Hours::new_unchecked(subhours as u8) }, unsafe {
Mins::new_unchecked(submins as u8)
});
let secs_time = SecsTime::new(time, unsafe { Secs::new_unchecked(subsecs as u8) });
let subsec_nanos = unsafe { SubsecNanos::new_unchecked(subsec_nanos as u32) };
PreciseTime::new(secs_time, subsec_nanos)
};
LooseTimestamp { date, time }.loose()
}
/// Convert timestamp to string.
pub fn to_str(self) -> str::TimestampStr {
self.loose().to_str()
}
}
impl Timestamp {
/// Get date-precision time.
pub const fn date(self) -> Date {
let days = self.as_hours() / 24;
Date::from_days(days as u16)
}
/// Get time of current day.
pub fn time(self) -> PreciseTime {
self.loose().time()
}
/// Get time in hours.
pub const fn as_hours(self) -> u32 {
(self.as_mins() / 60) as u32
}
/// Get time in minutes.
pub const fn as_mins(self) -> u64 {
self.as_secs() / 60
}
/// Get time in seconds.
pub const fn as_secs(self) -> u64 {
self.as_millis() / 1_000
}
/// Get time in milliseconds.
pub const fn as_millis(self) -> u64 {
self.as_nanos().get() / 1_000_000
}
/// Get time in nanoseconds.
pub const fn as_nanos(self) -> NonZeroU64 {
self.0
}
}
impl Timestamp {
// 2025.01.01 00:00.00
pub const TEST_ORIGIN: Self = Self(NonZeroU64::new(1735689600 * 1_000_000_000).unwrap());
/// Minimum time that can be represented.
pub const MIN: Self = Self(NonZeroU64::MIN);
/// Maximum time that can be represented.
pub const MAX: Self = Self(NonZeroU64::MAX);
}

23
src/time/tz.rs Normal file
View file

@ -0,0 +1,23 @@
//! # Timezones
//!
//! Timezone utilities.
use crate::{data, time::Hours};
#[data(ord, copy, crate = crate)]
pub struct FixedOffset(Hours);
impl FixedOffset {
/// UTC time offset. Basically no offset.
pub const UTC: Self = Self(Hours::new(0).unwrap());
/// Make offset from hours.
pub const fn from_hours(hours: Hours) -> Self {
Self(hours)
}
/// Get offset in hours.
pub const fn hours(self) -> Hours {
self.0
}
}

25
src/time/utils.rs Normal file
View file

@ -0,0 +1,25 @@
pub const fn is_leap_year(year: u16) -> bool {
let ndiv_by4 = (year & 3) != 0;
let cycle = ((year & 15) != 0) && (year % 25 == 0);
!(ndiv_by4 || cycle)
}
pub const fn divmod(lhs: u64, rhs: u64) -> (u64, u64) {
let div = lhs / rhs;
let mod_ = lhs - (div * rhs);
(div, mod_)
}
/// Returns number of additional days in `year`.
pub const fn leap_days_after(year: u16) -> u16 {
let first_criteria = year / 400;
let every4 = year >> 2;
let every100 = year / 100;
let second_criteria = every4 - every100;
first_criteria + second_criteria
}

13
src/trace_id.rs Normal file
View file

@ -0,0 +1,13 @@
use crate::data;
#[data(copy, ord, display("{_0:X}"), crate = crate)]
#[derive(Hash)]
pub struct TraceId(u128);
impl TraceId {
pub const fn from_parts(millis: u64, random: u128) -> Self {
let mut result = random & ((1 << 80) - 1);
result |= (millis as u128) << 80;
Self(result)
}
}