Initial commit
This commit is contained in:
commit
75a589f235
43 changed files with 4840 additions and 0 deletions
146
src/array.rs
Normal file
146
src/array.rs
Normal 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
6
src/collections.rs
Normal 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
1
src/encoding.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod dict;
|
28
src/encoding/dict.rs
Normal file
28
src/encoding/dict.rs
Normal 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
69
src/error.rs
Normal 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
22
src/fut.rs
Normal 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
22
src/generic.rs
Normal 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
13
src/handling/and_then.rs
Normal 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
29
src/handling/apply.rs
Normal 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
37
src/handling/mod.rs
Normal 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>;
|
||||
}
|
1
src/handling/provide_state.rs
Normal file
1
src/handling/provide_state.rs
Normal file
|
@ -0,0 +1 @@
|
|||
|
29
src/handling/then.rs
Normal file
29
src/handling/then.rs
Normal 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
1
src/hash.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub type Hasher = ahash::AHasher;
|
59
src/lib.rs
Normal file
59
src/lib.rs
Normal 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
4
src/rand.rs
Normal 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
1
src/slab.rs
Normal file
|
@ -0,0 +1 @@
|
|||
|
212
src/str.rs
Normal file
212
src/str.rs
Normal 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
109
src/str/ascii.rs
Normal 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
1
src/sync.rs
Normal file
|
@ -0,0 +1 @@
|
|||
|
62
src/time/clock.rs
Normal file
62
src/time/clock.rs
Normal 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
526
src/time/date.rs
Normal 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
49
src/time/mod.rs
Normal 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
3
src/time/ser.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod tz {
|
||||
pub mod msk {}
|
||||
}
|
278
src/time/str.rs
Normal file
278
src/time/str.rs
Normal 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
306
src/time/tests.rs
Normal 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
234
src/time/time.rs
Normal 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
321
src/time/timestamp.rs
Normal 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
23
src/time/tz.rs
Normal 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
25
src/time/utils.rs
Normal 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
13
src/trace_id.rs
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue