1#![no_std]
18#![forbid(unsafe_code)]
19
20extern crate alloc;
21
22pub mod cron;
23pub mod tz;
24mod zones;
25
26use alloc::format;
27use alloc::string::String;
28
29pub use cron::{Cron, CronError};
30pub use tz::{LocalResult, Zone};
31
32#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default, Hash)]
34pub struct Timestamp {
35 pub secs: i64,
36 pub nanos: u32,
37}
38
39impl Timestamp {
40 pub const fn from_secs(secs: i64) -> Timestamp {
41 Timestamp { secs, nanos: 0 }
42 }
43 pub const fn from_nanos(ns: i64) -> Timestamp {
45 Timestamp { secs: ns.div_euclid(1_000_000_000), nanos: ns.rem_euclid(1_000_000_000) as u32 }
46 }
47 pub const fn from_millis(ms: i64) -> Timestamp {
48 Timestamp { secs: ms.div_euclid(1000), nanos: (ms.rem_euclid(1000) * 1_000_000) as u32 }
49 }
50 pub const fn as_millis(&self) -> i64 {
51 self.secs.saturating_mul(1000).saturating_add((self.nanos / 1_000_000) as i64)
52 }
53 pub const fn add_secs(&self, s: i64) -> Timestamp {
54 Timestamp { secs: self.secs.saturating_add(s), nanos: self.nanos }
55 }
56}
57
58#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Default, Hash)]
60pub struct Civil {
61 pub year: i32,
62 pub month: u8,
63 pub day: u8,
64 pub hour: u8,
65 pub minute: u8,
66 pub second: u8,
67 pub nanos: u32,
68}
69
70impl Civil {
71 pub const fn new(year: i32, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Civil {
72 Civil { year, month, day, hour, minute, second, nanos: 0 }
73 }
74 pub const fn date(year: i32, month: u8, day: u8) -> Civil {
75 Civil::new(year, month, day, 0, 0, 0)
76 }
77 pub fn from_unix(secs: i64) -> Civil {
79 let days = secs.div_euclid(86_400);
80 let rem = secs.rem_euclid(86_400);
81 let (y, m, d) = civil_from_days(days);
82 Civil::new(y, m, d, (rem / 3600) as u8, (rem % 3600 / 60) as u8, (rem % 60) as u8)
83 }
84 pub fn to_unix(&self) -> i64 {
86 days_from_civil(self.year, self.month, self.day) * 86_400
87 + self.hour as i64 * 3600
88 + self.minute as i64 * 60
89 + self.second as i64
90 }
91 pub fn weekday(&self) -> u8 {
93 weekday(self.year, self.month, self.day)
94 }
95 pub fn is_valid(&self) -> bool {
96 (1..=12).contains(&self.month)
97 && self.day >= 1
98 && self.day <= days_in_month(self.year, self.month)
99 && self.hour < 24
100 && self.minute < 60
101 && self.second < 61
102 }
103 pub fn add_days(&self, days: i64) -> Civil {
105 let (y, m, d) = civil_from_days(days_from_civil(self.year, self.month, self.day) + days);
106 Civil { year: y, month: m, day: d, ..*self }
107 }
108}
109
110pub fn is_leap(y: i32) -> bool {
111 (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
112}
113
114pub fn days_in_month(y: i32, m: u8) -> u8 {
116 match m {
117 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
118 4 | 6 | 9 | 11 => 30,
119 2 if is_leap(y) => 29,
120 2 => 28,
121 _ => 0,
122 }
123}
124
125pub fn days_from_civil(y: i32, m: u8, d: u8) -> i64 {
127 let y = if m <= 2 { y as i64 - 1 } else { y as i64 };
128 let era = y.div_euclid(400);
129 let yoe = y - era * 400;
130 let mp = (m as i64 + 9) % 12;
131 let doy = (153 * mp + 2) / 5 + d as i64 - 1;
132 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
133 era * 146_097 + doe - 719_468
134}
135
136pub fn civil_from_days(z: i64) -> (i32, u8, u8) {
138 let z = z + 719_468;
139 let era = z.div_euclid(146_097);
140 let doe = z - era * 146_097;
141 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
142 let y = yoe + era * 400;
143 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
144 let mp = (5 * doy + 2) / 153;
145 let d = (doy - (153 * mp + 2) / 5 + 1) as u8;
146 let m = if mp < 10 { mp + 3 } else { mp - 9 } as u8;
147 ((if m <= 2 { y + 1 } else { y }) as i32, m, d)
148}
149
150pub fn weekday(y: i32, m: u8, d: u8) -> u8 {
152 (days_from_civil(y, m, d) + 3).rem_euclid(7) as u8
153}
154
155fn two(s: &[u8], i: usize) -> Option<u32> {
156 let a = s.get(i)?.wrapping_sub(b'0');
157 let b = s.get(i + 1)?.wrapping_sub(b'0');
158 if a > 9 || b > 9 {
159 return None;
160 }
161 Some(a as u32 * 10 + b as u32)
162}
163
164pub fn rfc3339(ts: Timestamp) -> String {
166 rfc3339_offset(ts, 0)
167}
168
169pub fn rfc3339_offset(ts: Timestamp, offset_secs: i32) -> String {
171 let lo = days_from_civil(0, 1, 1) * 86_400;
173 let hi = days_from_civil(9999, 12, 31) * 86_400 + 86_399;
174 let c = Civil::from_unix(ts.secs.saturating_add(offset_secs as i64).clamp(lo, hi));
175 let ms = ts.nanos / 1_000_000;
176 let frac = if ms != 0 { format!(".{:03}", ms) } else { String::new() };
177 let zone = if offset_secs == 0 {
178 String::from("Z")
179 } else {
180 let a = offset_secs.unsigned_abs();
181 format!("{}{:02}:{:02}", if offset_secs < 0 { '-' } else { '+' }, a / 3600, a % 3600 / 60)
182 };
183 format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{}{}", c.year, c.month, c.day, c.hour, c.minute, c.second, frac, zone)
184}
185
186pub fn parse_rfc3339(s: &str) -> Option<Timestamp> {
188 let b = s.trim().as_bytes();
189 if b.len() < 20 {
190 return None;
191 }
192 let y = two(b, 0)? * 100 + two(b, 2)?;
193 if b[4] != b'-' || b[7] != b'-' || !matches!(b[10], b'T' | b't' | b' ') || b[13] != b':' || b[16] != b':' {
194 return None;
195 }
196 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);
197 if !c.is_valid() {
198 return None;
199 }
200 let mut i = 19;
201 let mut nanos = 0u32;
202 if b.get(i) == Some(&b'.') {
203 i += 1;
204 let mut digits = 0;
205 while let Some(&d) = b.get(i) {
206 if !d.is_ascii_digit() {
207 break;
208 }
209 if digits < 9 {
210 nanos = nanos * 10 + (d - b'0') as u32;
211 digits += 1;
212 }
213 i += 1;
214 }
215 while digits < 9 {
216 nanos *= 10;
217 digits += 1;
218 }
219 }
220 let off = match b.get(i)? {
221 b'Z' | b'z' => {
222 i += 1;
223 0
224 }
225 &sign @ (b'+' | b'-') => {
226 let h = two(b, i + 1)? as i64;
227 if b.get(i + 3) != Some(&b':') {
228 return None;
229 }
230 let m = two(b, i + 4)? as i64;
231 if h > 23 || m > 59 {
232 return None;
233 }
234 i += 6;
235 let o = h * 3600 + m * 60;
236 if sign == b'-' { -o } else { o }
237 }
238 _ => return None,
239 };
240 if i != b.len() {
241 return None;
242 }
243 Some(Timestamp { secs: c.to_unix() - off, nanos })
244}
245
246pub fn format_date_time(ts: Timestamp, zone: &Zone, en: bool) -> String {
248 let c = zone.to_local(ts);
249 if en {
250 format!("{:04}-{:02}-{:02} {:02}:{:02}", c.year, c.month, c.day, c.hour, c.minute)
251 } else {
252 format!("{:02}.{:02}.{:04} {:02}:{:02}", c.day, c.month, c.year, c.hour, c.minute)
253 }
254}
255
256pub fn format_date(ts: Timestamp, zone: &Zone, en: bool) -> String {
258 let c = zone.to_local(ts);
259 if en {
260 format!("{:04}-{:02}-{:02}", c.year, c.month, c.day)
261 } else {
262 format!("{:02}.{:02}.{:04}", c.day, c.month, c.year)
263 }
264}
265
266pub fn format_time(ts: Timestamp, zone: &Zone) -> String {
268 let c = zone.to_local(ts);
269 format!("{:02}:{:02}", c.hour, c.minute)
270}
271
272pub fn format_duration(secs: i64, en: bool) -> String {
274 let s = secs.max(0);
275 let d = if en { "d" } else { "T" };
276 if s < 60 {
277 format!("{} s", s)
278 } else if s < 3600 {
279 format!("{} min {:02} s", s / 60, s % 60)
280 } else if s < 86_400 {
281 format!("{} h {:02} min", s / 3600, s % 3600 / 60)
282 } else {
283 format!("{} {} {} h", s / 86_400, d, s % 86_400 / 3600)
284 }
285}
286
287pub fn format_relative(now: i64, then: i64, en: bool) -> String {
289 let d = then - now;
290 let a = d.unsigned_abs() as i64;
291 let v = if a < 60 {
292 return String::from(if en { "just now" } else { "gerade eben" });
293 } else if a < 3600 {
294 format!("{} min", a / 60)
295 } else if a < 86_400 {
296 format!("{} h", a / 3600)
297 } else {
298 let n = a / 86_400;
299 format!("{} {}", n, match (en, n == 1) {
301 (true, true) => "day",
302 (true, false) => "days",
303 (false, true) => "Tag",
304 (false, false) => "Tagen",
305 })
306 };
307 match (d < 0, en) {
308 (true, true) => format!("{} ago", v),
309 (true, false) => format!("vor {}", v),
310 (false, true) => format!("in {}", v),
311 (false, false) => format!("in {}", v),
312 }
313}