mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-20 01:08:55 +00:00
171 lines
5.7 KiB
Rust
171 lines
5.7 KiB
Rust
pub use duration::Duration;
|
|
|
|
mod duration {
|
|
use std::fmt;
|
|
use std::time::Duration as StdDuration;
|
|
use String as Error;
|
|
// Parse a single duration unit
|
|
fn parse_duration_string(s: &str) -> Result<StdDuration, Error> {
|
|
// We reject the empty case
|
|
if s == "" {
|
|
return Err(Error::from("empty strings are not valid durations"));
|
|
}
|
|
struct ParseStep {
|
|
current_value: Option<u64>,
|
|
current_duration: StdDuration,
|
|
}
|
|
s.chars()
|
|
.try_fold(
|
|
ParseStep {
|
|
current_value: None,
|
|
current_duration: StdDuration::from_secs(0),
|
|
},
|
|
|s, item| match (item, s.current_value) {
|
|
('0'..='9', v) => Ok(ParseStep {
|
|
current_value: Some(v.unwrap_or(0) * 10 + ((item as u64) - ('0' as u64))),
|
|
..s
|
|
}),
|
|
(_, None) => Err(Error::from("Not a valid duration")),
|
|
(item, Some(v)) => Ok(ParseStep {
|
|
current_value: None,
|
|
current_duration: s.current_duration
|
|
+ match item.to_ascii_lowercase() {
|
|
's' => StdDuration::from_secs(1),
|
|
'm' => StdDuration::from_secs(60),
|
|
'h' => StdDuration::from_secs(60 * 60),
|
|
'd' => StdDuration::from_secs(60 * 60 * 24),
|
|
'w' => StdDuration::from_secs(60 * 60 * 24 * 7),
|
|
_ => return Err(Error::from("Not a valid duration")),
|
|
} * (v as u32),
|
|
}),
|
|
},
|
|
)
|
|
.and_then(|v| match v.current_value {
|
|
// All values should be consumed
|
|
None => Ok(v),
|
|
_ => Err(Error::from("Not a valid duration")),
|
|
})
|
|
.map(|v| v.current_duration)
|
|
}
|
|
|
|
// Our new-orphan-type of duration.
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub struct Duration(pub StdDuration);
|
|
|
|
impl std::str::FromStr for Duration {
|
|
type Err = Error;
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
parse_duration_string(s).map(|v| Duration(v))
|
|
}
|
|
}
|
|
|
|
impl From<Duration> for StdDuration {
|
|
fn from(d: Duration) -> Self {
|
|
d.0
|
|
}
|
|
}
|
|
|
|
impl Duration {
|
|
fn num_weeks(&self) -> u64 {
|
|
self.0.as_secs() / (60 * 60 * 24 * 7)
|
|
}
|
|
fn num_days(&self) -> u64 {
|
|
self.0.as_secs() / (60 * 60 * 24)
|
|
}
|
|
fn num_hours(&self) -> u64 {
|
|
self.0.as_secs() / (60 * 60)
|
|
}
|
|
fn num_minutes(&self) -> u64 {
|
|
self.0.as_secs() / 60
|
|
}
|
|
fn num_seconds(&self) -> u64 {
|
|
self.0.as_secs()
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Duration {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let d = self;
|
|
// weeks
|
|
let weeks = d.num_weeks();
|
|
let days = d.num_days() - d.num_weeks() * 7;
|
|
let hours = d.num_hours() - d.num_days() * 24;
|
|
let minutes = d.num_minutes() - d.num_hours() * 60;
|
|
let seconds = d.num_seconds() - d.num_minutes() * 60;
|
|
let formats = [
|
|
(weeks, "week"),
|
|
(days, "day"),
|
|
(hours, "hour"),
|
|
(minutes, "minute"),
|
|
(seconds, "second"),
|
|
];
|
|
let count = f.precision().unwrap_or(formats.len());
|
|
let mut first = true;
|
|
for (val, counter) in formats.iter().skip_while(|(a, _)| *a == 0).take(count) {
|
|
if *val > 0 {
|
|
write!(
|
|
f,
|
|
"{}{} {}{}",
|
|
(if first { "" } else { " " }),
|
|
val,
|
|
counter,
|
|
(if *val == 1 { "" } else { "s" })
|
|
)?;
|
|
first = false;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::time::Duration as StdDuration;
|
|
#[test]
|
|
fn test_parse_success() {
|
|
let tests = [
|
|
(
|
|
"2D2h1m",
|
|
StdDuration::from_secs(2 * 60 * 60 * 24 + 2 * 60 * 60 + 1 * 60),
|
|
),
|
|
(
|
|
"1W2D3h4m5s",
|
|
StdDuration::from_secs(
|
|
1 * 7 * 24 * 60 * 60 + // 1W
|
|
2 * 24 * 60 * 60 + // 2D
|
|
3 * 60 * 60 + // 3h
|
|
4 * 60 + // 4m
|
|
5, // 5s
|
|
),
|
|
),
|
|
(
|
|
"1W2D3h4m5s6W",
|
|
StdDuration::from_secs(
|
|
1 * 7 * 24 * 60 * 60 + // 1W
|
|
2 * 24 * 60 * 60 + // 2D
|
|
3 * 60 * 60 + // 3h
|
|
4 * 60 + // 4m
|
|
5 + // 5s
|
|
6 * 7 * 24 * 60 * 60,
|
|
), // 6W
|
|
),
|
|
];
|
|
for (input, output) in &tests {
|
|
assert_eq!(parse_duration_string(input).unwrap(), *output);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_fail() {
|
|
let tests = ["", "1w", "-1W", "1"];
|
|
for input in &tests {
|
|
assert!(
|
|
parse_duration_string(input).is_err(),
|
|
"parsing {} succeeded",
|
|
input
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|