#![doc(html_logo_url = "https://codeberg.org/pezcore/compact-reltime/raw/branch/main/icon.svg")]
#![doc = include_str!("../README.md")]
#![doc = include_str!("../usage.md")]
use std::fmt::{Display, Formatter, Result};
use std::io::Write;
pub const MAX_DURATION: u64 = 99 * Units::Years as u64 + 11 * Units::Months as u64;
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Reltime(u64);
#[repr(u64)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Units {
Seconds = 1,
Minutes = 60,
Hours = 60 * Self::Minutes as u64,
Days = 24 * Self::Hours as u64,
Weeks = 7 * Self::Days as u64,
Months = 31 * Self::Days as u64,
Years = 365 * Self::Days as u64,
}
impl From<Units> for char {
fn from(u: Units) -> Self {
match u {
Units::Seconds => 's',
Units::Minutes => 'm',
Units::Hours => 'h',
Units::Days => 'd',
Units::Weeks => 'w',
Units::Months => 'M',
Units::Years => 'y',
}
}
}
impl Display for Units {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}", char::from(*self))
}
}
#[derive(Debug, Copy, Clone)]
struct Part {
amount: u8,
units: Units,
}
impl Display for Part {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match self.amount {
0 => Ok(()),
n => write!(f, "{}{}", n, self.units),
}
}
}
#[derive(Debug, Copy, Clone)]
pub struct FormattedReltime(Part, Part);
impl Display for FormattedReltime {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let mut buf = [0u8; 8];
write!(buf.as_mut_slice(), "{}{}", self.0, self.1).unwrap();
let len = buf.iter().position(|&x| x == 0).unwrap_or(8);
f.pad(str::from_utf8(&buf[..len]).unwrap())
}
}
impl Display for Reltime {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
const MAP: [(u64, Units, Units); 6] = [
(90, Units::Seconds, Units::Seconds),
(90 * Units::Minutes as u64, Units::Minutes, Units::Seconds),
(72 * Units::Hours as u64, Units::Hours, Units::Minutes),
(10 * Units::Days as u64, Units::Days, Units::Hours),
(8 * Units::Weeks as u64, Units::Weeks, Units::Days),
(18 * Units::Months as u64, Units::Months, Units::Weeks),
];
self.format_with_timetable(&MAP).fmt(f)
}
}
pub type ThreshSpec = (u64, Units, Units);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TooLong;
impl Reltime {
pub fn new(seconds: u64) -> std::result::Result<Self, TooLong> {
if seconds > MAX_DURATION { Err(TooLong) } else { Ok(Self(seconds)) }
}
pub fn seconds(self) -> u64 {
self.0
}
#[inline]
fn decomp(self, u_big: Units, u_smol: Units) -> FormattedReltime {
let (q_big, rem) = (self.0 / u_big as u64, self.0 % u_big as u64);
let q_smol = rem / u_smol as u64;
FormattedReltime(
Part { amount: q_big as _, units: u_big },
Part { amount: q_smol as _, units: u_smol },
)
}
pub fn format(self, f: impl FnOnce(u64) -> (Units, Units)) -> FormattedReltime {
let (u_big, u_smol) = f(self.0);
self.decomp(u_big, u_smol)
}
pub fn format_with_timetable(self, threshmap: &[ThreshSpec]) -> FormattedReltime {
const HIGH_UNITS: (u64, Units, Units) = (0, Units::Years, Units::Months);
let (Ok(idx) | Err(idx)) = threshmap.binary_search_by(|x| x.0.cmp(&self.0));
let &(_, u_big, u_smol) = threshmap.get(idx).unwrap_or(&HIGH_UNITS);
self.decomp(u_big, u_smol)
}
}
impl TryFrom<std::time::Duration> for Reltime {
type Error = TooLong;
fn try_from(value: std::time::Duration) -> std::result::Result<Self, Self::Error> {
Self::new(value.as_secs())
}
}
impl From<Reltime> for std::time::Duration {
fn from(r: Reltime) -> Self {
std::time::Duration::from_secs(r.seconds())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one() {
let reltime = Reltime(135243);
assert_eq!(reltime.to_string(), "37h34m");
let reltime = Reltime(3204898);
assert_eq!(reltime.to_string(), "5w2d");
}
#[test]
fn error_too_long() {
let reltime = Reltime::new(999_999_999_999);
assert_eq!(reltime, Err(TooLong));
}
}