Skip to content

Commit

Permalink
Merge pull request OpenLEADR#39 from tweedegolf/iso-duration
Browse files Browse the repository at this point in the history
use the `iso8601_duration` crate to parse durations
  • Loading branch information
folkertdev authored Jul 16, 2024
2 parents 10acbcf + e8aed65 commit 82c7d17
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 15 deletions.
48 changes: 48 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ regex = "1.10.4"
chrono = "0.4.38"
url = "2.5.0"
tower-http = { version = "0.5.2" , features = ["trace"]}
iso8601-duration = "0.2.0"

[dev-dependencies]
quickcheck = "1.0.3"

[lib]
name = "openadr"
Expand Down
8 changes: 4 additions & 4 deletions src/wire/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,15 +350,15 @@ mod tests {
payload_descriptors: None,
interval_period: Some(IntervalPeriod {
start: "2023-06-15T09:30:00Z".parse().unwrap(),
duration: Some(Duration("PT1H".into())),
randomize_start: Some(Duration("PT1H".into())),
duration: Some(Duration::hour()),
randomize_start: Some(Duration::hour()),
}),
intervals: vec![EventInterval {
id: 0,
interval_period: Some(IntervalPeriod {
start: "2023-06-15T09:30:00Z".parse().unwrap(),
duration: Some(Duration("PT1H".into())),
randomize_start: Some(Duration("PT1H".into())),
duration: Some(Duration::hour()),
randomize_start: Some(Duration::hour()),
}),
payloads: vec![EventValuesMap {
value_type: EventType::Price,
Expand Down
101 changes: 97 additions & 4 deletions src/wire/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
//! Most types are originally generated from the OpenAPI specification of OpenADR
//! and manually modified to be more idiomatic.
use std::fmt::Display;

use serde::{Deserialize, Serialize};

pub use event::Event;
Expand Down Expand Up @@ -49,10 +51,73 @@ mod serde_rfc3339 {
}
}

// TODO: Replace with real ISO8601 type
/// A ISO 8601 formatted duration
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Duration(String);
/// An ISO 8601 formatted duration
#[derive(Clone, Debug, PartialEq)]
pub struct Duration(::iso8601_duration::Duration);

impl<'de> Deserialize<'de> for Duration {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
let duration = raw
.parse::<::iso8601_duration::Duration>()
.map_err(|_| "iso8601_duration::ParseDurationError")
.map_err(serde::de::Error::custom)?;

Ok(Self(duration))
}
}

impl Serialize for Duration {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.to_string().serialize(serializer)
}
}

impl Duration {
pub const fn hour() -> Self {
Self(::iso8601_duration::Duration {
year: 0.0,
month: 0.0,
day: 0.0,
hour: 1.0,
minute: 0.0,
second: 0.0,
})
}
}

impl std::str::FromStr for Duration {
type Err = ::iso8601_duration::ParseDurationError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let duration = s.parse::<::iso8601_duration::Duration>()?;
Ok(Self(duration))
}
}

impl Display for Duration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let ::iso8601_duration::Duration {
year,
month,
day,
hour,
minute,
second,
} = self.0;

f.write_fmt(format_args!(
"P{}Y{}M{}DT{}H{}M{}S",
year, month, day, hour, minute, second
))
}
}

#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
Expand Down Expand Up @@ -232,4 +297,32 @@ mod tests {
Unit::Private(String::from("something else"))
);
}

impl quickcheck::Arbitrary for super::Duration {
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
// the iso8601_duration library uses an f32 to store the values, which starts losing
// precision at 24-bit integers.
super::Duration(::iso8601_duration::Duration {
year: (<u32 as quickcheck::Arbitrary>::arbitrary(g) & 0x00FF_FFFF) as f32,
month: (<u32 as quickcheck::Arbitrary>::arbitrary(g) & 0x00FF_FFFF) as f32,
day: (<u32 as quickcheck::Arbitrary>::arbitrary(g) & 0x00FF_FFFF) as f32,
hour: (<u32 as quickcheck::Arbitrary>::arbitrary(g) & 0x00FF_FFFF) as f32,
minute: (<u32 as quickcheck::Arbitrary>::arbitrary(g) & 0x00FF_FFFF) as f32,
second: (<u32 as quickcheck::Arbitrary>::arbitrary(g) & 0x00FF_FFFF) as f32,
})
}
}

#[test]
fn duration_to_string_from_str_roundtrip() {
quickcheck::quickcheck(test as fn(_) -> bool);

fn test(input: super::Duration) -> bool {
let roundtrip = input.to_string().parse::<super::Duration>().unwrap();

assert_eq!(input.0, roundtrip.0);

input.0 == roundtrip.0
}
}
}
6 changes: 3 additions & 3 deletions src/wire/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,11 @@ mod tests {
program_type: Some("PRICING_TARIFF".into()),
country: Some("US".into()),
principal_subdivision: Some("CO".into()),
time_zone_offset: Some(Duration("PT1H".into())),
time_zone_offset: Some(Duration::hour()),
interval_period: Some(IntervalPeriod {
start: "2023-06-15T09:30:00Z".parse().unwrap(),
duration: Some(Duration("PT1H".into())),
randomize_start: Some(Duration("PT1H".into())),
duration: Some(Duration::hour()),
randomize_start: Some(Duration::hour()),
}),
program_descriptions: None,
binding_events: Some(false),
Expand Down
8 changes: 4 additions & 4 deletions src/wire/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,15 +449,15 @@ mod tests {
resource_name: ResourceName::Private("RESOURCE-999".into()),
interval_period: Some(IntervalPeriod {
start: "2023-06-15T09:30:00Z".parse().unwrap(),
duration: Some(Duration("PT1H".into())),
randomize_start: Some(Duration("PT1H".into())),
duration: Some(Duration::hour()),
randomize_start: Some(Duration::hour()),
}),
intervals: vec![Interval {
id: 0,
interval_period: Some(IntervalPeriod {
start: "2023-06-15T09:30:00Z".parse().unwrap(),
duration: Some(Duration("PT1H".into())),
randomize_start: Some(Duration("PT1H".into())),
duration: Some(Duration::hour()),
randomize_start: Some(Duration::hour()),
}),
payloads: vec![ValuesMap {
value_type: ValueType("PRICE".into()),
Expand Down

0 comments on commit 82c7d17

Please sign in to comment.