Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for human-readable duration formats #647

Merged
merged 38 commits into from
Sep 18, 2023
Merged
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f438507
Update author constant
Techassi Sep 7, 2023
1bc051a
Add humantime Duration
Techassi Sep 7, 2023
a3f3a5e
Update changelog
Techassi Sep 7, 2023
ae18cc5
Add `from_secs` and `from_millis` helper functions
Techassi Sep 8, 2023
a524ef4
Trim trailing whitespace using pre-commit hook
Techassi Sep 8, 2023
0e5658f
Fix markdownlint errors using pre-commit hook
Techassi Sep 8, 2023
794dca8
Implement first iteration of custom parsing
Techassi Sep 8, 2023
8658713
Remove humantime dependency
Techassi Sep 8, 2023
88d4677
Add `Display` impl, add various `ops` impls
Techassi Sep 13, 2023
128eff7
Add negative tests
Techassi Sep 13, 2023
7dd912f
Merge branch 'main' into feat/humantime
Techassi Sep 13, 2023
c4d22b0
Rework `Duration` implementation based on @nightkr gist
Techassi Sep 14, 2023
cad5cfe
Add documentation
Techassi Sep 14, 2023
f63ec1a
Fix module documentation links
Techassi Sep 14, 2023
2377276
Fix bug
Techassi Sep 14, 2023
9e9257d
Update parser, add context to errors, add more tests
Techassi Sep 15, 2023
96f1600
Update src/duration/mod.rs
Techassi Sep 15, 2023
58b04c3
Re-add millisecond support, switch to Snafu
Techassi Sep 15, 2023
dbf1dce
Merge branch 'main' into feat/humantime
Techassi Sep 15, 2023
3282f9e
Fix markdown link references
Techassi Sep 15, 2023
a39af93
duration::serde_impl exports nothing
nightkr Sep 15, 2023
9fdb1b3
Split duration parsing context selectors into a submodule
nightkr Sep 15, 2023
191151a
typo: "expected character" -> "unexpected character"
nightkr Sep 15, 2023
9dbcefe
Fail if duration buffer is non-empty at the end of parsing
nightkr Sep 15, 2023
705ed1f
Use Debug impls to quote strings in errors
nightkr Sep 15, 2023
fe1911e
Non-ASCII input will be caught anyway, because it's never a valid cha…
nightkr Sep 15, 2023
8ef94a7
DurationUnit doesn't need to be public
nightkr Sep 15, 2023
e7e4718
Add check for invalid unit order and duplicate units
Techassi Sep 18, 2023
2d3d32c
Revert "Non-ASCII input will be caught anyway, because it's never a v…
Techassi Sep 18, 2023
6b882e6
Fix doc comment
Techassi Sep 18, 2023
36e8035
Impl Mul and Div
sbernauer Sep 18, 2023
dcb4dd0
Impl Mul and Div 2
sbernauer Sep 18, 2023
198ae50
impl JsonSchema for Duration
sbernauer Sep 18, 2023
6737e51
Re-order `SubAssign` impl
Techassi Sep 18, 2023
29fca80
Revert "Revert "Non-ASCII input will be caught anyway, because it's n…
nightkr Sep 18, 2023
9a5c6da
Remove `impl Div<Duration> for u32`
Techassi Sep 18, 2023
98149d7
Fix scanning of multibyte chars
nightkr Sep 18, 2023
017ca87
Merge branch 'feat/humantime' of github.com:stackabletech/operator-rs…
nightkr Sep 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Re-add millisecond support, switch to Snafu
  • Loading branch information
Techassi committed Sep 15, 2023
commit 58b04c3500ea19635da664d6a0e02d168032040e
76 changes: 44 additions & 32 deletions src/duration/mod.rs
Original file line number Diff line number Diff line change
@@ -21,30 +21,31 @@ use std::{

use derivative::Derivative;
use schemars::JsonSchema;
use snafu::{OptionExt, ResultExt, Snafu};
use strum::IntoEnumIterator;
use thiserror::Error;

mod serde_impl;
pub use serde_impl::*;

#[derive(Debug, Error, PartialEq)]
#[derive(Debug, Snafu, PartialEq)]
#[snafu(context(suffix(false)))]
pub enum DurationParseError {
#[error("invalid input, either empty or contains non-ascii characters")]
#[snafu(display("invalid input, either empty or contains non-ascii characters"))]
InvalidInput,

#[error(
"expected character '{0}', the duration fragments must end with an alphabetic character"
)]
ExpectedCharacter(char),
#[snafu(display(
"expected character '{expected}', the duration fragments must end with an alphabetic character"
))]
ExpectedCharacter { expected: char },

#[error("duration fragment with value '{0} has no unit")]
NoUnit(u64),
#[snafu(display("duration fragment with value '{value}' has no unit"))]
NoUnit { value: u128 },

#[error("failed to parse fragment unit '{0}'")]
ParseUnitError(String),
#[snafu(display("failed to parse fragment unit '{unit}'"))]
ParseUnitError { unit: String },

#[error("failed to parse fragment value as integer: {0}")]
ParseIntError(#[from] ParseIntError),
#[snafu(display("failed to parse fragment value as integer"))]
ParseIntError { source: ParseIntError },
}

#[derive(Clone, Copy, Debug, Derivative, Hash, PartialEq, PartialOrd, JsonSchema)]
@@ -76,21 +77,26 @@ impl FromStr for Duration {
};

while let Some(value) = take_group(char::is_numeric) {
let value = value.parse::<u64>()?;
let value = value.parse::<u128>().context(ParseInt)?;

let Some(unit) = take_group(char::is_alphabetic) else {
if let Some(&(_, c)) = chars.peek() {
return Err(DurationParseError::ExpectedCharacter(c));
return ExpectedCharacter {expected: c}.fail();
} else {
return Err(DurationParseError::NoUnit(value));
return NoUnit { value }.fail();
}
};

let unit = unit
.parse::<DurationUnit>()
.map_err(|_| DurationParseError::ParseUnitError(unit.to_string()))?;
let unit = unit.parse::<DurationUnit>().ok().context(ParseUnit {
unit: unit.to_string(),
})?;

duration += std::time::Duration::from_secs(value * unit.secs())
// This try_into is needed, as Duration::from_millis was stabilized
// in 1.3.0 but u128 was only added in 1.26.0. See
// - https://users.rust-lang.org/t/why-duration-as-from-millis-uses-different-primitives/89302
// - https://github.com/rust-lang/rust/issues/58580
duration +=
std::time::Duration::from_millis((value * unit.millis()).try_into().unwrap())
}

Ok(Self(duration))
@@ -105,17 +111,17 @@ impl Display for Duration {
return write!(f, "0{}", DurationUnit::Seconds);
}

let mut secs = self.0.as_secs();
let mut millis = self.0.as_millis();

for unit in DurationUnit::iter() {
let whole = secs / unit.secs();
let rest = secs % unit.secs();
let whole = millis / unit.millis();
let rest = millis % unit.millis();

if whole > 0 {
write!(f, "{}{}", whole, unit)?;
}

secs = rest;
millis = rest;
}

Ok(())
@@ -195,19 +201,23 @@ pub enum DurationUnit {

#[strum(serialize = "s")]
Seconds,

#[strum(serialize = "ms")]
Milliseconds,
}

impl DurationUnit {
/// Returns the number of whole milliseconds in each supported
/// [`DurationUnit`].
pub fn secs(&self) -> u64 {
pub fn millis(&self) -> u128 {
use DurationUnit::*;

match self {
Days => 24 * Hours.secs(),
Hours => 60 * Minutes.secs(),
Minutes => 60 * Seconds.secs(),
Seconds => 1,
Days => 24 * Hours.millis(),
Hours => 60 * Minutes.millis(),
Minutes => 60 * Seconds.millis(),
Seconds => 1000,
Milliseconds => 1,
}
}
}
@@ -219,21 +229,23 @@ mod test {
use serde::{Deserialize, Serialize};

#[rstest]
#[case("15d2m2s1000ms", 1296123)]
#[case("15d2m2s600ms", 1296122)]
#[case("15d2m2s", 1296122)]
#[case("70m", 4200)]
#[case("1h", 3600)]
#[case("1m", 60)]
#[case("1s", 1)]
fn parse(#[case] input: &str, #[case] output: u64) {
fn parse_as_secs(#[case] input: &str, #[case] output: u64) {
let dur: Duration = input.parse().unwrap();
assert_eq!(dur.as_secs(), output);
}

#[rstest]
#[case("1D", DurationParseError::ParseUnitError("D".into()))]
#[case("1D", DurationParseError::ParseUnitError{unit: "D".into()})]
#[case("2d2", DurationParseError::NoUnit{value: 2})]
#[case("1ä", DurationParseError::InvalidInput)]
#[case(" ", DurationParseError::InvalidInput)]
#[case("2d2", DurationParseError::NoUnit(2))]
fn parse_invalid(#[case] input: &str, #[case] expected_err: DurationParseError) {
let err = Duration::from_str(input).unwrap_err();
assert_eq!(err, expected_err)