2023-10-22 22:44:59 +02:00
|
|
|
use anyhow::bail;
|
|
|
|
use std::{
|
|
|
|
fmt::{self, Display, Write as _},
|
2023-10-30 21:26:17 +01:00
|
|
|
ops::{Add, Sub},
|
2023-10-22 22:44:59 +02:00
|
|
|
str::FromStr
|
|
|
|
};
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
pub struct Date {
|
2023-10-28 23:38:17 +02:00
|
|
|
pub year: u16,
|
|
|
|
pub month: u8,
|
|
|
|
pub day: u8
|
2023-10-22 22:44:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for Date {
|
|
|
|
type Err = anyhow::Error;
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> anyhow::Result<Self> {
|
|
|
|
parse_date(s)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Display for Date {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
f.write_str(&format_date(*self))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Parse a date in YYMMDD format.
|
|
|
|
pub fn parse_date(s: &str) -> anyhow::Result<Date> {
|
|
|
|
let int: u32 = s.parse()?;
|
|
|
|
|
|
|
|
let year = int / 10000 + 2000;
|
|
|
|
if year / 10 != 202 {
|
|
|
|
bail!("Expected a date in 202X");
|
|
|
|
}
|
|
|
|
|
|
|
|
let month = int / 100 % 100;
|
|
|
|
if month < 1 || month > 12 {
|
|
|
|
bail!("Invalid month: {month}");
|
|
|
|
}
|
|
|
|
|
|
|
|
let day = int % 100;
|
|
|
|
if day < 1 || day > 31 {
|
|
|
|
bail!("Invalid day: {day}");
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(Date {
|
|
|
|
year: year as _,
|
|
|
|
month: month as _,
|
|
|
|
day: day as _
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Format a date in YYMMDD format.
|
|
|
|
pub fn format_date(d: Date) -> String {
|
|
|
|
format!("{:02}{:02}{:02}", d.year % 100, d.month, d.day)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Format a date in DD. MMMM YYYY format.
|
|
|
|
pub fn format_date_long(d: Date) -> String {
|
|
|
|
let month = match d.month {
|
|
|
|
1 => "Januar",
|
|
|
|
2 => "Februar",
|
|
|
|
3 => "März",
|
|
|
|
4 => "April",
|
|
|
|
5 => "Mai",
|
|
|
|
6 => "Juni",
|
|
|
|
7 => "Juli",
|
|
|
|
8 => "August",
|
|
|
|
9 => "September",
|
|
|
|
10 => "Oktober",
|
|
|
|
11 => "November",
|
|
|
|
12 => "Dezember",
|
|
|
|
_ => unreachable!()
|
|
|
|
};
|
|
|
|
format!("{:02}. {month} {:04}", d.day, d.year)
|
|
|
|
}
|
|
|
|
|
2023-10-30 21:38:16 +01:00
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
|
2023-10-22 22:44:59 +02:00
|
|
|
pub struct Time {
|
2023-10-28 23:38:17 +02:00
|
|
|
pub seconds: u32,
|
|
|
|
pub micros: u32
|
2023-10-22 22:44:59 +02:00
|
|
|
}
|
|
|
|
|
2023-10-30 21:26:17 +01:00
|
|
|
impl Add for Time {
|
|
|
|
type Output = Self;
|
|
|
|
|
|
|
|
fn add(self, rhs: Self) -> Self {
|
|
|
|
let mut seconds = self.seconds + rhs.seconds;
|
|
|
|
let mut micros = self.micros + rhs.micros;
|
|
|
|
if micros >= 1_000_000 {
|
|
|
|
seconds += 1;
|
|
|
|
micros -= 1_000_000;
|
|
|
|
}
|
|
|
|
Self { seconds, micros }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Sub for Time {
|
|
|
|
type Output = Self;
|
|
|
|
|
|
|
|
fn sub(mut self, rhs: Self) -> Self {
|
|
|
|
if rhs.micros > self.micros {
|
|
|
|
self.seconds -= 1;
|
|
|
|
self.micros += 1_000_000;
|
|
|
|
}
|
|
|
|
Self {
|
|
|
|
seconds: self.seconds - rhs.seconds,
|
|
|
|
micros: self.micros - rhs.micros
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-22 22:44:59 +02:00
|
|
|
impl FromStr for Time {
|
|
|
|
type Err = anyhow::Error;
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> anyhow::Result<Self> {
|
|
|
|
parse_time(s)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Display for Time {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
2023-10-30 22:22:56 +01:00
|
|
|
write!(f, "{}", self.seconds)?;
|
|
|
|
if self.micros != 0 {
|
|
|
|
write!(f, ".{}", self.micros)?;
|
|
|
|
}
|
|
|
|
Ok(())
|
2023-10-22 22:44:59 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Parse a time in hh:mm:ss:sub or hh:mm:ss.micros format.
|
|
|
|
pub fn parse_time(s: &str) -> anyhow::Result<Time> {
|
|
|
|
let split = s.split(':');
|
|
|
|
let mut seconds = 0;
|
|
|
|
let mut micros = 0;
|
|
|
|
|
|
|
|
for (i, digits) in split.enumerate() {
|
|
|
|
// we cannot have more than 4 splits
|
|
|
|
if i > 3 {
|
|
|
|
bail!("Expected end of input, found ':'")
|
|
|
|
}
|
|
|
|
// we cannot process data after we have parsed micros
|
|
|
|
if micros != 0 {
|
|
|
|
bail!("Unexpected microsecond notation");
|
|
|
|
}
|
|
|
|
// the 4th split is subseconds, converting to micros
|
|
|
|
if i == 3 {
|
|
|
|
micros = digits.parse::<u32>()? * 1000000 / 60;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// add to seconds and potentially micros
|
|
|
|
seconds *= 60;
|
|
|
|
if let Some(idx) = digits.find('.') {
|
|
|
|
seconds += digits[0 .. idx].parse::<u32>()?;
|
|
|
|
let micros_digits = &digits[idx + 1 ..];
|
|
|
|
micros = micros_digits.parse::<u32>()?
|
|
|
|
* 10_u32.pow((6 - micros_digits.len()) as _);
|
|
|
|
} else {
|
|
|
|
seconds += digits.parse::<u32>()?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(Time { seconds, micros })
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Format a time in hh:mm:ss.micros format.
|
|
|
|
pub fn format_time(t: Time) -> String {
|
|
|
|
let h = t.seconds / 3600;
|
|
|
|
let m = (t.seconds / 60) % 60;
|
|
|
|
let s = t.seconds % 60;
|
|
|
|
|
|
|
|
let mut out = String::new();
|
|
|
|
if h != 0 {
|
|
|
|
write!(out, "{h}:").unwrap();
|
|
|
|
}
|
|
|
|
if h != 0 || m != 0 {
|
|
|
|
write!(out, "{m:02}:{s:02}").unwrap();
|
|
|
|
} else {
|
|
|
|
write!(out, "{s}").unwrap();
|
|
|
|
}
|
|
|
|
if t.micros != 0 {
|
|
|
|
write!(out, ".{:06}", t.micros).unwrap();
|
|
|
|
out.truncate(out.trim_end_matches('0').len());
|
|
|
|
}
|
|
|
|
|
|
|
|
out
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
fn test_time_parse_only(s: &str, t: Time) {
|
|
|
|
let time = parse_time(s).unwrap();
|
|
|
|
assert_eq!(time, t);
|
|
|
|
}
|
|
|
|
|
|
|
|
fn test_time(s: &str, t: Time) {
|
|
|
|
test_time_parse_only(s, t);
|
|
|
|
assert_eq!(format_time(t), s);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_time_0() {
|
|
|
|
test_time("0", Time {
|
|
|
|
seconds: 0,
|
|
|
|
micros: 0
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_time_2_3() {
|
|
|
|
test_time("02:03", Time {
|
|
|
|
seconds: 123,
|
|
|
|
micros: 0
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_time_1_2_3() {
|
|
|
|
test_time("1:02:03", Time {
|
|
|
|
seconds: 3723,
|
|
|
|
micros: 0
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_time_subsecs() {
|
|
|
|
test_time_parse_only("1:02:03:30", Time {
|
|
|
|
seconds: 3723,
|
|
|
|
micros: 500000
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_time_micros() {
|
|
|
|
test_time("1:02:03.5", Time {
|
|
|
|
seconds: 3723,
|
|
|
|
micros: 500000
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|