#![no_std]
#![forbid(unsafe_code)]
extern crate alloc;
pub mod cron;
pub mod tz;
mod zones;
use alloc::format;
use alloc::string::String;
pub use cron::{Cron, CronError};
pub use tz::{LocalResult, Zone};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default, Hash)]
pub struct Timestamp {
pub secs: i64,
pub nanos: u32,
}
impl Timestamp {
pub const fn from_secs(secs: i64) -> Timestamp {
Timestamp { secs, nanos: 0 }
}
pub const fn from_nanos(ns: i64) -> Timestamp {
Timestamp { secs: ns.div_euclid(1_000_000_000), nanos: ns.rem_euclid(1_000_000_000) as u32 }
}
pub const fn from_millis(ms: i64) -> Timestamp {
Timestamp { secs: ms.div_euclid(1000), nanos: (ms.rem_euclid(1000) * 1_000_000) as u32 }
}
pub const fn as_millis(&self) -> i64 {
self.secs.saturating_mul(1000).saturating_add((self.nanos / 1_000_000) as i64)
}
pub const fn add_secs(&self, s: i64) -> Timestamp {
Timestamp { secs: self.secs.saturating_add(s), nanos: self.nanos }
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default, Hash)]
pub struct Civil {
pub year: i32,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
pub nanos: u32,
}
impl Civil {
pub const fn new(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Civil {
Civil { year, month, day, hour, minute, second, nanos: 0 }
}
pub const fn date(year: i32, month: u8, day: u8) -> Civil {
Civil::new(year, month, day, 0, 0, 0)
}
pub fn from_unix(secs: i64) -> Civil {
let days = secs.div_euclid(86_400);
let rem = secs.rem_euclid(86_400);
let (y, m, d) = civil_from_days(days);
Civil::new(y, m, d, (rem / 3600) as u8, (rem % 3600 / 60) as u8, (rem % 60) as u8)
}
pub fn to_unix(&self) -> i64 {
days_from_civil(self.year, self.month, self.day) * 86_400
+ self.hour as i64 * 3600
+ self.minute as i64 * 60
+ self.second as i64
}
pub fn weekday(&self) -> u8 {
weekday(self.year, self.month, self.day)
}
pub fn is_valid(&self) -> bool {
(1..=12).contains(&self.month)
&& self.day >= 1
&& self.day <= days_in_month(self.year, self.month)
&& self.hour < 24
&& self.minute < 60
&& self.second < 61
}
pub fn add_days(&self, days: i64) -> Civil {
let (y, m, d) = civil_from_days(days_from_civil(self.year, self.month, self.day) + days);
Civil { year: y, month: m, day: d, ..*self }
}
}
pub fn is_leap(y: i32) -> bool {
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}
pub fn days_in_month(y: i32, m: u8) -> u8 {
match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if is_leap(y) => 29,
2 => 28,
_ => 0,
}
}
pub fn days_from_civil(y: i32, m: u8, d: u8) -> i64 {
let y = if m <= 2 { y as i64 - 1 } else { y as i64 };
let era = y.div_euclid(400);
let yoe = y - era * 400;
let mp = (m as i64 + 9) % 12;
let doy = (153 * mp + 2) / 5 + d as i64 - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
pub fn civil_from_days(z: i64) -> (i32, u8, u8) {
let z = z + 719_468;
let era = z.div_euclid(146_097);
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u8;
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u8;
((if m <= 2 { y + 1 } else { y }) as i32, m, d)
}
pub fn weekday(y: i32, m: u8, d: u8) -> u8 {
(days_from_civil(y, m, d) + 3).rem_euclid(7) as u8
}
fn two(s: &[u8], i: usize) -> Option<u32> {
let a = s.get(i)?.wrapping_sub(b'0');
let b = s.get(i + 1)?.wrapping_sub(b'0');
if a > 9 || b > 9 {
return None;
}
Some(a as u32 * 10 + b as u32)
}
pub fn rfc3339(ts: Timestamp) -> String {
rfc3339_offset(ts, 0)
}
pub fn rfc3339_offset(ts: Timestamp, offset_secs: i32) -> String {
let lo = days_from_civil(0, 1, 1) * 86_400;
let hi = days_from_civil(9999, 12, 31) * 86_400 + 86_399;
let c = Civil::from_unix(ts.secs.saturating_add(offset_secs as i64).clamp(lo, hi));
let ms = ts.nanos / 1_000_000;
let frac = if ms != 0 { format!(".{:03}", ms) } else { String::new() };
let zone = if offset_secs == 0 {
String::from("Z")
} else {
let a = offset_secs.unsigned_abs();
format!("{}{:02}:{:02}", if offset_secs < 0 { '-' } else { '+' }, a / 3600, a % 3600 / 60)
};
format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{}", c.year, c.month, c.day, c.hour, c.minute, c.second, frac, zone)
}
pub fn parse_rfc3339(s: &str) -> Option<Timestamp> {
let b = s.trim().as_bytes();
if b.len() < 20 {
return None;
}
let y = two(b, 0)? * 100 + two(b, 2)?;
if b[4] != b'-' || b[7] != b'-' || !matches!(b[10], b'T' | b't' | b' ') || b[13] != b':' || b[16] != b':' {
return None;
}
let c = Civil::new(y as i32, two(b, 5)? as u8, two(b, 8)? as u8, two(b, 11)? as u8, two(b, 14)? as u8, two(b, 17)? as u8);
if !c.is_valid() {
return None;
}
let mut i = 19;
let mut nanos = 0u32;
if b.get(i) == Some(&b'.') {
i += 1;
let mut digits = 0;
while let Some(&d) = b.get(i) {
if !d.is_ascii_digit() {
break;
}
if digits < 9 {
nanos = nanos * 10 + (d - b'0') as u32;
digits += 1;
}
i += 1;
}
while digits < 9 {
nanos *= 10;
digits += 1;
}
}
let off = match b.get(i)? {
b'Z' | b'z' => {
i += 1;
0
}
&sign @ (b'+' | b'-') => {
let h = two(b, i + 1)? as i64;
if b.get(i + 3) != Some(&b':') {
return None;
}
let m = two(b, i + 4)? as i64;
if h > 23 || m > 59 {
return None;
}
i += 6;
let o = h * 3600 + m * 60;
if sign == b'-' { -o } else { o }
}
_ => return None,
};
if i != b.len() {
return None;
}
Some(Timestamp { secs: c.to_unix() - off, nanos })
}
pub fn format_date_time(ts: Timestamp, zone: &Zone, en: bool) -> String {
let c = zone.to_local(ts);
if en {
format!("{:04}-{:02}-{:02} {:02}:{:02}", c.year, c.month, c.day, c.hour, c.minute)
} else {
format!("{:02}.{:02}.{:04} {:02}:{:02}", c.day, c.month, c.year, c.hour, c.minute)
}
}
pub fn format_date(ts: Timestamp, zone: &Zone, en: bool) -> String {
let c = zone.to_local(ts);
if en {
format!("{:04}-{:02}-{:02}", c.year, c.month, c.day)
} else {
format!("{:02}.{:02}.{:04}", c.day, c.month, c.year)
}
}
pub fn format_time(ts: Timestamp, zone: &Zone) -> String {
let c = zone.to_local(ts);
format!("{:02}:{:02}", c.hour, c.minute)
}
pub fn format_duration(secs: i64, en: bool) -> String {
let s = secs.max(0);
let d = if en { "d" } else { "T" };
if s < 60 {
format!("{} s", s)
} else if s < 3600 {
format!("{} min {:02} s", s / 60, s % 60)
} else if s < 86_400 {
format!("{} h {:02} min", s / 3600, s % 3600 / 60)
} else {
format!("{} {} {} h", s / 86_400, d, s % 86_400 / 3600)
}
}
pub fn format_relative(now: i64, then: i64, en: bool) -> String {
let d = then - now;
let a = d.unsigned_abs() as i64;
let v = if a < 60 {
return String::from(if en { "just now" } else { "gerade eben" });
} else if a < 3600 {
format!("{} min", a / 60)
} else if a < 86_400 {
format!("{} h", a / 3600)
} else {
let n = a / 86_400;
format!("{} {}", n, match (en, n == 1) {
(true, true) => "day",
(true, false) => "days",
(false, true) => "Tag",
(false, false) => "Tagen",
})
};
match (d < 0, en) {
(true, true) => format!("{} ago", v),
(true, false) => format!("vor {}", v),
(false, true) => format!("in {}", v),
(false, false) => format!("in {}", v),
}
}