use crate::error::FuelError;
use regex::Regex;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use std::{convert::TryFrom, fmt, str::FromStr, time::Duration};
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct Period(Duration);
impl Serialize for Period {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'d> Deserialize<'d> for Period {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'d>,
{
let s = String::deserialize(deserializer)?;
Period::from_str(&s).map_err(|e| de::Error::custom(e.to_string()))
}
}
const YR: u64 = 31_557_600_u64;
const WK: u64 = 604_800_u64;
const DY: u64 = 86_400_u64;
const HR: u64 = 3_600_u64;
const MN: u64 = 60_u64;
impl fmt::Debug for Period {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Period({})", self)
}
}
impl fmt::Display for Period {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let secs = self.0.as_secs();
let years = secs / YR;
if years > 0 {
write!(f, "{}y", years)?
}
let y_secs = secs % YR;
let weeks = y_secs / WK;
if weeks > 0 {
write!(f, "{}w", weeks)?
}
let w_secs = y_secs % WK;
let days = w_secs / DY;
if days > 0 {
write!(f, "{}d", days)?
}
let d_secs = w_secs % DY;
let hours = d_secs / HR;
if hours > 0 {
write!(f, "{}h", hours)?
}
let h_secs = d_secs % HR;
let minutes = h_secs / MN;
if minutes > 0 {
write!(f, "{}m", minutes)?
}
let s = h_secs % MN;
let nsecs = self.0.subsec_nanos();
let is_ns = (nsecs % 1000) > 0;
let is_us = (nsecs / 1_000 % 1_000) > 0;
let is_ms = (nsecs / 1_000_000) > 0;
if is_ms && (s > 0 || is_ns) {
let ss = format!("{:0>9}", nsecs); let ss = ss.trim_end_matches('0'); write!(f, "{}.{}s", s, ss)
} else if nsecs > 0 || s > 0 {
if s > 0 {
write!(f, "{}s", s)?;
}
if is_ns {
write!(f, "{}ns", nsecs)
} else if is_us {
write!(f, "{}us", nsecs / 1_000)
} else if is_ms {
write!(f, "{}ms", nsecs / 1_000_000)
} else {
Ok(())
}
} else if nsecs == 0 && secs == 0 {
write!(f, "0s")
} else {
Ok(())
}
}
}
impl FromStr for Period {
type Err = FuelError;
fn from_str(period_str: &str) -> Result<Self, Self::Err> {
lazy_static! {
static ref PERIOD_RE: Regex = Regex::new(
r"(?xi) # whitespace-mode, case-insensitive
^
(?:\s*(?P<y>\d+)\s*y((((ea)?r)s?)?)?)? # y|yr|yrs|year|years
(?:\s*(?P<w>\d+)\s*w((((ee)?k)s?)?)?)?
(?:\s*(?P<d>\d+)\s*d((((a )?y)s?)?)?)?
(?:\s*(?P<h>\d+)\s*h((((ou)?r)s?)?)?)?
(?:\s*(?P<m>\d+)\s*m((in(ute)?)s?)?)? # m|min|minute|mins|minutes
(?:
(?:\s* # seconds mantissa (optional) + fraction (required)
(?P<s_man>\d+)?
[.,](?P<s_fra>\d+)\s* s((ec(ond)?)s?)?
)?
| (?:
(:?\s*(?P<s> \d+)\s* s((ec(ond)?)s?)?)?
(?:\s*(?P<ms>\d+)\s*(m|(milli)) s((ec(ond)?)s?)?)?
(?:\s*(?P<us>\d+)\s*(u|μ|(micro)) s((ec(ond)?)s?)?)?
(?:\s*(?P<ns>\d+)\s*(n|(nano)) s((ec(ond)?)s?)?)?
)
)
\s*
$"
)
.unwrap();
}
Ok(Period({
PERIOD_RE.captures(period_str).map_or_else(
|| {
Err(FuelError::Generic(format!(
"Failed to find Period specification in {:?}",
period_str
)))
},
|cap| {
let seconds: u64 = YR
* cap
.name("y")
.map_or("0", |y| y.as_str())
.parse::<u64>()
.map_err(|e| {
FuelError::Generic(format!(
"Invalid year(s) in period {:?}: {:?}",
period_str, e
))
})?
+ WK * cap
.name("w")
.map_or("0", |w| w.as_str())
.parse::<u64>()
.map_err(|e| {
FuelError::Generic(format!(
"Invalid week(s) in period {:?}: {:?}",
period_str, e
))
})?
+ DY * cap
.name("d")
.map_or("0", |d| d.as_str())
.parse::<u64>()
.map_err(|e| {
FuelError::Generic(format!(
"Invalid days(s) in period {:?}: {:?}",
period_str, e
))
})?
+ HR * cap
.name("h")
.map_or("0", |w| w.as_str())
.parse::<u64>()
.map_err(|e| {
FuelError::Generic(format!(
"Invalid hour(s) in period {:?}: {:?}",
period_str, e
))
})?
+ MN * cap
.name("m")
.map_or("0", |m| m.as_str())
.parse::<u64>()
.map_err(|e| {
FuelError::Generic(format!(
"Invalid minute(s) in period {:?}: {:?}",
period_str, e
))
})?
+ cap
.name("s")
.map_or_else(
|| cap.name("s_man").map_or("0", |s_man| s_man.as_str()),
|s| s.as_str(),
)
.parse::<u64>()
.map_err(|e| {
FuelError::Generic(format!(
"Invalid seconds in period {:?}: {:?}",
period_str, e
))
})?;
let nanos: u64 = cap
.name("s_fra")
.map_or(Ok(0_u64), |s_fra| {
format!("{:0<9.9}", s_fra.as_str()).parse::<u64>()
})
.map_err(|e| {
FuelError::Generic(format!(
"Invalid fractional seconds in period {:?}: {:?}",
period_str, e
))
})?
+ 1_000_000
* cap
.name("ms")
.map_or("0", |ms| ms.as_str())
.parse::<u64>()
.map_err(|e| {
FuelError::Generic(format!(
"Invalid milliseconds in period {:?}: {:?}",
period_str, e
))
})?
+ 1_000
* cap
.name("us")
.map_or("0", |us| us.as_str())
.parse::<u64>()
.map_err(|e| {
FuelError::Generic(format!(
"Invalid microseconds in period {:?}: {:?}",
period_str, e
))
})?
+ cap
.name("ns")
.map_or("0", |ns| ns.as_str())
.parse::<u64>()
.map_err(|e| {
FuelError::Generic(format!(
"Invalid nanoseconds in period {:?}: {:?}",
period_str, e
))
})?;
Ok(Duration::new(
seconds + nanos / 1_000_000_000,
(nanos % 1_000_000_000) as u32,
))
},
)?
}))
}
}
impl TryFrom<String> for Period {
type Error = FuelError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Period::from_str(&s)
}
}
impl TryFrom<&str> for Period {
type Error = FuelError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Period::from_str(s)
}
}
impl From<Period> for Duration {
fn from(p: Period) -> Self {
p.0
}
}
impl From<&Period> for Duration {
fn from(p: &Period) -> Self {
p.0.to_owned()
}
}
impl From<Duration> for Period {
fn from(d: Duration) -> Self {
Period(d)
}
}
impl From<&Duration> for Period {
fn from(d: &Duration) -> Self {
Period(d.to_owned())
}
}