use std::fmt;
use std::str::FromStr;
use jiff::SignedDuration;
use jiff::civil::Time;
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct SecondOfDay(pub u32);
impl SecondOfDay {
pub const ZERO: SecondOfDay = SecondOfDay(0);
pub const MAX: SecondOfDay = SecondOfDay(u32::MAX);
pub const fn from_secs(s: u32) -> Self {
SecondOfDay(s)
}
pub const fn hms(h: u32, m: u32, s: u32) -> Self {
SecondOfDay(h * 3600 + m * 60 + s)
}
pub const fn as_secs(self) -> u32 {
self.0
}
pub const fn as_hms(self) -> (u32, u32, u32) {
(self.0 / 3600, (self.0 / 60) % 60, self.0 % 60)
}
pub fn every(
start: SecondOfDay,
end: SecondOfDay,
step: u32,
) -> impl Iterator<Item = SecondOfDay> + Clone {
(start.0..end.0)
.step_by(step.max(1) as usize)
.map(SecondOfDay)
}
}
impl From<u32> for SecondOfDay {
fn from(s: u32) -> Self {
SecondOfDay(s)
}
}
impl From<Time> for SecondOfDay {
fn from(t: Time) -> Self {
SecondOfDay((t.hour() as u32) * 3600 + (t.minute() as u32) * 60 + (t.second() as u32))
}
}
impl TryFrom<SecondOfDay> for Time {
type Error = jiff::Error;
fn try_from(s: SecondOfDay) -> Result<Self, Self::Error> {
let (h, m, sec) = s.as_hms();
Time::new(h as i8, m as i8, sec as i8, 0)
}
}
impl fmt::Display for SecondOfDay {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (h, m, s) = self.as_hms();
write!(f, "{h:02}:{m:02}:{s:02}")
}
}
impl FromStr for SecondOfDay {
type Err = ParseSecondOfDayError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.split(':');
let h_str = parts
.next()
.ok_or_else(|| ParseSecondOfDayError::BadFormat(s.to_string()))?;
let m_str = parts
.next()
.ok_or_else(|| ParseSecondOfDayError::BadFormat(s.to_string()))?;
let sec_str = parts.next();
if parts.next().is_some() {
return Err(ParseSecondOfDayError::BadFormat(s.to_string()));
}
let h: u32 = h_str
.parse()
.map_err(|_| ParseSecondOfDayError::BadField(h_str.to_string()))?;
let m: u32 = m_str
.parse()
.map_err(|_| ParseSecondOfDayError::BadField(m_str.to_string()))?;
if m > 59 {
return Err(ParseSecondOfDayError::OutOfRange(m));
}
let sec: u32 = match sec_str {
None => 0,
Some(s) => {
let v: u32 = s
.parse()
.map_err(|_| ParseSecondOfDayError::BadField(s.to_string()))?;
if v > 59 {
return Err(ParseSecondOfDayError::OutOfRange(v));
}
v
}
};
Ok(SecondOfDay::hms(h, m, sec))
}
}
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum ParseSecondOfDayError {
#[error("expected HH:MM[:SS], got `{0}`")]
BadFormat(String),
#[error("invalid time field: `{0}`")]
BadField(String),
#[error("time field out of range: {0} (must be 0..=59)")]
OutOfRange(u32),
}
impl std::ops::Add<Duration> for SecondOfDay {
type Output = SecondOfDay;
fn add(self, d: Duration) -> SecondOfDay {
SecondOfDay(self.0.saturating_add(d.0))
}
}
impl std::ops::Sub<SecondOfDay> for SecondOfDay {
type Output = Duration;
fn sub(self, other: SecondOfDay) -> Duration {
Duration(self.0.saturating_sub(other.0))
}
}
impl std::ops::Sub<Duration> for SecondOfDay {
type Output = SecondOfDay;
fn sub(self, d: Duration) -> SecondOfDay {
SecondOfDay(self.0.saturating_sub(d.0))
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Duration(pub u32);
impl Duration {
pub const ZERO: Duration = Duration(0);
pub const MAX: Duration = Duration(u32::MAX);
pub const fn from_secs(s: u32) -> Self {
Duration(s)
}
pub const fn as_secs(self) -> u32 {
self.0
}
}
impl From<u32> for Duration {
fn from(s: u32) -> Self {
Duration(s)
}
}
impl From<Duration> for SignedDuration {
fn from(d: Duration) -> Self {
SignedDuration::from_secs(d.0 as i64)
}
}
impl TryFrom<SignedDuration> for Duration {
type Error = TryFromSignedDurationError;
fn try_from(d: SignedDuration) -> Result<Self, Self::Error> {
let secs = d.as_secs();
if secs < 0 {
return Err(TryFromSignedDurationError::Negative);
}
u32::try_from(secs)
.map(Duration)
.map_err(|_| TryFromSignedDurationError::Overflow)
}
}
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum TryFromSignedDurationError {
#[error("negative duration cannot be represented as vulture::Duration")]
Negative,
#[error("duration overflows u32 seconds")]
Overflow,
}
impl fmt::Display for Duration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl std::ops::Add<Duration> for Duration {
type Output = Duration;
fn add(self, o: Duration) -> Duration {
Duration(self.0.saturating_add(o.0))
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Transfers(pub u8);
impl Transfers {
pub const ZERO: Transfers = Transfers(0);
pub const MAX: Transfers = Transfers(u8::MAX);
pub const fn new(n: u8) -> Self {
Transfers(n)
}
pub const fn get(self) -> u8 {
self.0
}
}
impl From<u8> for Transfers {
fn from(n: u8) -> Self {
Transfers(n)
}
}
impl fmt::Display for Transfers {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn second_of_day_display_is_hms() {
assert_eq!(SecondOfDay::hms(9, 30, 0).to_string(), "09:30:00");
assert_eq!(SecondOfDay::hms(0, 0, 0).to_string(), "00:00:00");
assert_eq!(SecondOfDay::hms(25, 30, 5).to_string(), "25:30:05");
}
#[test]
fn second_of_day_from_str_round_trips() {
for s in ["00:00:00", "09:30:00", "23:59:59", "25:30:05"] {
let parsed: SecondOfDay = s.parse().expect("parses");
assert_eq!(parsed.to_string(), s, "round-trip {s}");
}
}
#[test]
fn second_of_day_from_str_accepts_hh_mm() {
let s: SecondOfDay = "09:30".parse().expect("parses");
assert_eq!(s, SecondOfDay::hms(9, 30, 0));
}
#[test]
fn second_of_day_from_str_rejects_garbage() {
assert!(matches!(
"9:30:".parse::<SecondOfDay>(),
Err(ParseSecondOfDayError::BadField(_))
));
assert!(matches!(
"abc".parse::<SecondOfDay>(),
Err(ParseSecondOfDayError::BadFormat(_))
));
assert!(matches!(
"09:30:00:00".parse::<SecondOfDay>(),
Err(ParseSecondOfDayError::BadFormat(_))
));
assert!(matches!(
"09:60:00".parse::<SecondOfDay>(),
Err(ParseSecondOfDayError::OutOfRange(60))
));
assert!(matches!(
"09:30:99".parse::<SecondOfDay>(),
Err(ParseSecondOfDayError::OutOfRange(99))
));
}
#[test]
fn second_of_day_jiff_time_round_trip() {
let t = Time::new(9, 30, 5, 0).unwrap();
let s: SecondOfDay = t.into();
assert_eq!(s, SecondOfDay::hms(9, 30, 5));
let back: Time = s.try_into().unwrap();
assert_eq!(back, t);
}
#[test]
fn jiff_time_subseconds_truncated() {
let t = Time::new(9, 30, 5, 999_999_999).unwrap();
let s: SecondOfDay = t.into();
assert_eq!(s, SecondOfDay::hms(9, 30, 5));
}
#[test]
fn second_of_day_after_midnight_is_unrepresentable_as_jiff_time() {
let s = SecondOfDay::hms(25, 30, 0);
assert!(Time::try_from(s).is_err());
}
#[test]
fn duration_to_signed_duration_total() {
let d = Duration::from_secs(60);
let sd: SignedDuration = d.into();
assert_eq!(sd.as_secs(), 60);
}
#[test]
fn signed_duration_to_duration_truncates_subseconds() {
let sd = SignedDuration::new(60, 999_999_999);
let d: Duration = sd.try_into().unwrap();
assert_eq!(d, Duration::from_secs(60));
}
#[test]
fn signed_duration_negative_rejected() {
let sd = SignedDuration::from_secs(-1);
assert_eq!(
Duration::try_from(sd),
Err(TryFromSignedDurationError::Negative)
);
}
#[test]
fn signed_duration_overflow_rejected() {
let sd = SignedDuration::from_secs(i64::from(u32::MAX) + 1);
assert_eq!(
Duration::try_from(sd),
Err(TryFromSignedDurationError::Overflow)
);
}
}