#![allow(clippy::tabs_in_doc_comments)]
use std::fmt::{Display, Formatter};
use std::hash::Hash;
use std::iter::Sum;
use std::ops::{Add, Div, Mul, Sub};
use std::str::FromStr;
use std::time;
use chrono::{DateTime, Duration, TimeZone};
#[cfg(feature = "clap")]
use clap::builder::OsStr;
use once_cell::sync::Lazy;
use regex::{Match, Regex};
#[cfg(feature = "serde")]
use serde::de::{Error, Unexpected, Visitor};
#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
const SECS_PER_MINUTES: i64 = 60;
const SECS_PER_HOUR: i64 = 60 * SECS_PER_MINUTES;
const SECS_PER_DAY: i64 = 24 * SECS_PER_HOUR;
const SECS_PER_WEEK: i64 = 7 * SECS_PER_DAY;
const SECS_PER_YEAR: i64 = 365 * SECS_PER_DAY;
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum DurationFlexError {
InvalidFormat,
OutOfRange,
}
impl Display for DurationFlexError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
DurationFlexError::InvalidFormat => write!(f, "invalid duration format"),
DurationFlexError::OutOfRange => write!(f, "duration value is out of range"),
}
}
}
impl std::error::Error for DurationFlexError {}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "utoipa", schema(as = String, example = "1h30m"))]
pub struct DurationFlex {
secs: i64,
nanos: i32,
}
#[cfg(feature = "validator")]
impl validator::ValidateRange<DurationFlex> for DurationFlex {
fn greater_than(&self, max: DurationFlex) -> Option<bool> {
Some(self > &max)
}
fn less_than(&self, min: DurationFlex) -> Option<bool> {
Some(self < &min)
}
}
#[cfg(feature = "validator")]
impl validator::ValidateRange<&str> for DurationFlex {
fn greater_than(&self, max: &str) -> Option<bool> {
let max = DurationFlex::try_from(max).expect("invalid duration string in validator bounds");
Some(self > &max)
}
fn less_than(&self, min: &str) -> Option<bool> {
let min = DurationFlex::try_from(min).expect("invalid duration string in validator bounds");
Some(self < &min)
}
}
#[cfg(feature = "validator")]
impl validator::ValidateRange<i64> for DurationFlex {
fn greater_than(&self, max: i64) -> Option<bool> {
Some(self.secs > max)
}
fn less_than(&self, min: i64) -> Option<bool> {
Some(self.secs < min)
}
}
static REGEX_STR: &str = r"^((?P<years>\d+)y)?((?P<weeks>\d+)w)?((?P<days>\d+)d)?((?P<hours>\d+)h)?((?P<minutes>\d+)m)?((?P<seconds>\d+)s)?$";
static REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(REGEX_STR).unwrap());
impl DurationFlex {
pub const ZERO: DurationFlex = DurationFlex { secs: 0, nanos: 0 };
pub const fn new(secs: i64, nanos: i32) -> Self {
DurationFlex { secs, nanos }
}
pub const fn from_secs(secs: i64) -> Self {
DurationFlex { secs, nanos: 0 }
}
pub const fn from_millis(millis: i64) -> Self {
let secs = millis / 1000;
let extra_millis = millis % 1000;
DurationFlex { secs, nanos: (extra_millis * 1_000_000) as i32 }
}
pub const fn from_minutes(minutes: i64) -> Self {
DurationFlex { secs: minutes * SECS_PER_MINUTES, nanos: 0 }
}
pub const fn from_hours(hours: i64) -> Self {
DurationFlex { secs: hours * SECS_PER_HOUR, nanos: 0 }
}
pub const fn from_days(days: i64) -> Self {
DurationFlex { secs: days * SECS_PER_DAY, nanos: 0 }
}
pub const fn from_weeks(weeks: i64) -> Self {
DurationFlex { secs: weeks * SECS_PER_WEEK, nanos: 0 }
}
pub const fn from_years(years: i64) -> Self {
DurationFlex { secs: years * SECS_PER_YEAR, nanos: 0 }
}
pub const fn is_zero(&self) -> bool {
self.secs == 0 && self.nanos == 0
}
pub const fn is_positive(&self) -> bool {
self.secs > 0 || (self.secs == 0 && self.nanos > 0)
}
pub const fn is_negative(&self) -> bool {
self.secs < 0 || (self.secs == 0 && self.nanos < 0)
}
pub fn to_std(&self) -> Option<time::Duration> {
if self.secs < 0 || (self.secs == 0 && self.nanos < 0) {
None
} else {
Some(time::Duration::new(self.secs as u64, self.nanos as u32))
}
}
pub fn to_chrono(&self) -> Duration {
Duration::from(*self)
}
pub fn secs(&self) -> i64 {
self.secs
}
pub fn nanos(&self) -> i32 {
self.nanos
}
fn de_component(r#match: Match) -> i64 {
r#match.as_str().parse().unwrap()
}
fn ser_component(secs: &mut i64, component: &str, component_secs: i64, f: &mut Formatter<'_>) -> std::fmt::Result {
let value = *secs / component_secs;
*secs -= value * component_secs;
if value == 0 {
Ok(())
} else {
write!(f, "{}{}", value, component)
}
}
}
impl Sub<Duration> for DurationFlex {
type Output = Duration;
fn sub(self, rhs: Duration) -> Self::Output {
Duration::from(self) - rhs
}
}
impl Add<Duration> for DurationFlex {
type Output = Duration;
fn add(self, rhs: Duration) -> Self::Output {
Duration::from(self) + rhs
}
}
impl Sub<DurationFlex> for DurationFlex {
type Output = DurationFlex;
fn sub(self, rhs: DurationFlex) -> Self::Output {
DurationFlex { secs: self.secs - rhs.secs, nanos: self.nanos - rhs.nanos }
}
}
impl Add<DurationFlex> for DurationFlex {
type Output = DurationFlex;
fn add(self, rhs: DurationFlex) -> Self::Output {
DurationFlex { secs: self.secs + rhs.secs, nanos: self.nanos + rhs.nanos }
}
}
impl Mul<u32> for DurationFlex {
type Output = DurationFlex;
fn mul(self, rhs: u32) -> Self::Output {
DurationFlex { secs: self.secs * rhs as i64, nanos: self.nanos * rhs as i32 }
}
}
impl Mul<i64> for DurationFlex {
type Output = DurationFlex;
fn mul(self, rhs: i64) -> Self::Output {
DurationFlex { secs: self.secs * rhs, nanos: (self.nanos as i64 * rhs) as i32 }
}
}
impl Div<u32> for DurationFlex {
type Output = DurationFlex;
fn div(self, rhs: u32) -> Self::Output {
DurationFlex { secs: self.secs / rhs as i64, nanos: self.nanos / rhs as i32 }
}
}
impl Div<i64> for DurationFlex {
type Output = DurationFlex;
fn div(self, rhs: i64) -> Self::Output {
DurationFlex { secs: self.secs / rhs, nanos: (self.nanos as i64 / rhs) as i32 }
}
}
impl Sum for DurationFlex {
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
iter.fold(DurationFlex::default(), |acc, x| acc + x)
}
}
impl<'a> Sum<&'a DurationFlex> for DurationFlex {
fn sum<I: Iterator<Item = &'a Self>>(iter: I) -> Self {
iter.fold(DurationFlex::default(), |acc, x| acc + *x)
}
}
impl<T> Sub<DurationFlex> for DateTime<T>
where
T: TimeZone,
{
type Output = DateTime<T>;
fn sub(self, rhs: DurationFlex) -> Self::Output {
self - Duration::from(rhs)
}
}
impl<T> Add<DateTime<T>> for DurationFlex
where
T: TimeZone,
{
type Output = DateTime<T>;
fn add(self, rhs: DateTime<T>) -> Self::Output {
rhs + Duration::from(self)
}
}
impl<T> Add<DurationFlex> for DateTime<T>
where
T: TimeZone,
{
type Output = DateTime<T>;
fn add(self, rhs: DurationFlex) -> Self::Output {
self + Duration::from(rhs)
}
}
impl TryFrom<&str> for DurationFlex {
type Error = DurationFlexError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let captures = REGEX.captures(value).ok_or(DurationFlexError::InvalidFormat)?;
let years = Duration::try_days(captures.name("years").map_or(0i64, Self::de_component) * 365)
.ok_or(DurationFlexError::OutOfRange)?;
let weeks = Duration::try_weeks(captures.name("weeks").map_or(0i64, Self::de_component))
.ok_or(DurationFlexError::OutOfRange)?;
let days = Duration::try_days(captures.name("days").map_or(0i64, Self::de_component))
.ok_or(DurationFlexError::OutOfRange)?;
let hours = Duration::try_hours(captures.name("hours").map_or(0i64, Self::de_component))
.ok_or(DurationFlexError::OutOfRange)?;
let minutes = Duration::try_minutes(captures.name("minutes").map_or(0i64, Self::de_component))
.ok_or(DurationFlexError::OutOfRange)?;
let seconds = Duration::try_seconds(captures.name("seconds").map_or(0i64, Self::de_component))
.ok_or(DurationFlexError::OutOfRange)?;
let duration = years + weeks + days + hours + minutes + seconds;
Ok(DurationFlex { secs: duration.num_seconds(), nanos: 0i32 })
}
}
impl From<String> for DurationFlex {
fn from(value: String) -> Self {
DurationFlex::try_from(value.as_str()).unwrap()
}
}
impl From<Duration> for DurationFlex {
fn from(value: Duration) -> Self {
DurationFlex { secs: value.num_seconds(), nanos: 0i32 }
}
}
impl From<DurationFlex> for Duration {
fn from(value: DurationFlex) -> Self {
Duration::try_seconds(value.secs()).unwrap() + Duration::nanoseconds(value.nanos() as i64)
}
}
impl From<time::Duration> for DurationFlex {
fn from(value: time::Duration) -> Self {
DurationFlex { secs: value.as_secs() as i64, nanos: 0i32 }
}
}
impl From<DurationFlex> for time::Duration {
fn from(value: DurationFlex) -> Self {
time::Duration::from_secs(value.secs as u64).add(time::Duration::from_nanos(value.nanos as u64))
}
}
impl Display for DurationFlex {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut secs = self.secs;
Self::ser_component(&mut secs, "y", SECS_PER_YEAR, f)?;
Self::ser_component(&mut secs, "w", SECS_PER_WEEK, f)?;
Self::ser_component(&mut secs, "d", SECS_PER_DAY, f)?;
Self::ser_component(&mut secs, "h", SECS_PER_HOUR, f)?;
Self::ser_component(&mut secs, "m", SECS_PER_MINUTES, f)?;
Self::ser_component(&mut secs, "s", 1, f)
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for DurationFlex {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
static REGEX_MSG: &str = "a String with the format years (y), weeks (w), days (d), hours (h), minutes (m) \
and/or seconds (s), in order";
struct DurationFlexVisitor;
impl<'de> Visitor<'de> for DurationFlexVisitor {
type Value = DurationFlex;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
formatter.write_str(REGEX_MSG)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
match DurationFlex::try_from(v) {
Ok(value) => Ok(value),
Err(DurationFlexError::InvalidFormat) => Err(Error::invalid_value(Unexpected::Str(v), &self)),
Err(DurationFlexError::OutOfRange) => Err(Error::invalid_value(Unexpected::Str(v), &self)),
}
}
fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
where
E: Error,
{
self.visit_str(v)
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: Error,
{
match DurationFlex::try_from(v.as_str()) {
Ok(value) => Ok(value),
Err(DurationFlexError::InvalidFormat) => {
Err(Error::invalid_value(Unexpected::Str(v.as_str()), &self))
},
Err(DurationFlexError::OutOfRange) => Err(Error::invalid_value(Unexpected::Str(v.as_str()), &self)),
}
}
}
deserializer.deserialize_string(DurationFlexVisitor)
}
}
#[cfg(feature = "serde")]
impl Serialize for DurationFlex {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(format!("{}", self).as_str())
}
}
#[cfg(feature = "clap")]
impl From<OsStr> for DurationFlex {
fn from(value: OsStr) -> Self {
DurationFlex::try_from(value.to_str().unwrap()).unwrap()
}
}
#[cfg(feature = "clap")]
impl From<DurationFlex> for OsStr {
fn from(value: DurationFlex) -> Self {
format!("{}", value).into()
}
}
impl FromStr for DurationFlex {
type Err = DurationFlexError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
DurationFlex::try_from(s)
}
}
#[cfg(test)]
mod test {
use serde::{Deserialize, Serialize};
use serde_test::{assert_de_tokens, assert_ser_tokens, Token};
use super::*;
#[test]
fn de_string() {
let value = DurationFlex::try_from("1y").unwrap();
assert_eq!(value.secs(), SECS_PER_YEAR);
assert_eq!(value.nanos(), 0);
let value = DurationFlex::try_from("1y2w3d4h5m6s").unwrap();
assert_eq!(
value.secs(),
SECS_PER_YEAR + 2 * SECS_PER_WEEK + 3 * SECS_PER_DAY + 4 * SECS_PER_HOUR + 5 * SECS_PER_MINUTES + 6
);
assert_eq!(value.nanos(), 0);
let value = DurationFlex::try_from("1w2d").unwrap();
assert_eq!(value.secs(), 9 * SECS_PER_DAY);
assert_eq!(value.nanos(), 0);
let value = DurationFlex::try_from("1w2d3h4m5s").unwrap();
assert_eq!(value.secs(), 9 * SECS_PER_DAY + 3 * SECS_PER_HOUR + 4 * SECS_PER_MINUTES + 5);
assert_eq!(value.nanos(), 0);
let value = DurationFlex::try_from("5s").unwrap();
assert_eq!(value.secs(), 5);
assert_eq!(value.nanos(), 0);
let value = DurationFlex::try_from("5s5d");
assert!(value.is_err());
let value = DurationFlex::try_from("1w1y");
assert!(value.is_err());
}
#[test]
fn ser_string() {
let value = DurationFlex::try_from("1y").unwrap().to_string();
assert_eq!(value, "1y");
let value = DurationFlex::try_from("1y2w3d4h5m6s").unwrap().to_string();
assert_eq!(value, "1y2w3d4h5m6s");
let value = DurationFlex::try_from("365d").unwrap().to_string();
assert_eq!(value, "1y");
let value = DurationFlex::try_from("372d").unwrap().to_string();
assert_eq!(value, "1y1w");
let value = DurationFlex::try_from("53w").unwrap().to_string();
assert_eq!(value, "1y6d");
let value = DurationFlex::try_from("1w2d").unwrap().to_string();
assert_eq!(value, "1w2d");
let value = DurationFlex::try_from("1w2d3h4m5s").unwrap().to_string();
assert_eq!(value, "1w2d3h4m5s");
let value = DurationFlex::try_from("5s").unwrap().to_string();
assert_eq!(value, "5s");
let value = DurationFlex::try_from("1w8d3h4m5s").unwrap().to_string();
assert_eq!(value, "2w1d3h4m5s");
let value = DurationFlex::try_from("1w8d3h4m3605s").unwrap().to_string();
assert_eq!(value, "2w1d4h4m5s");
}
#[test]
fn deserialize_nums() {
let value = DurationFlex::try_from("1y").unwrap();
assert_de_tokens(&value, &[Token::Str("1y")]);
let value = DurationFlex::try_from("1y2w3d4h5m6s").unwrap();
assert_de_tokens(&value, &[Token::Str("1y2w3d4h5m6s")]);
let value = DurationFlex::try_from("1w2d").unwrap();
assert_de_tokens(&value, &[Token::Str("1w2d")]);
let value = DurationFlex::try_from("1w2d3h4m5s").unwrap();
assert_de_tokens(&value, &[Token::Str("1w2d3h4m5s")]);
let value = DurationFlex::try_from("5s").unwrap();
assert_de_tokens(&value, &[Token::Str("5s")]);
let value = DurationFlex::try_from("1w8d3h4m5s").unwrap();
assert_de_tokens(&value, &[Token::Str("2w1d3h4m5s")]);
let value = DurationFlex::try_from("1w8d3h4m3605s").unwrap();
assert_de_tokens(&value, &[Token::Str("2w1d4h4m5s")]);
}
#[test]
fn serialize() {
let value = DurationFlex::try_from("1y").unwrap();
assert_ser_tokens(&value, &[Token::Str("1y")]);
let value = DurationFlex::try_from("1y2w3d4h5m6s").unwrap();
assert_ser_tokens(&value, &[Token::Str("1y2w3d4h5m6s")]);
let value = DurationFlex::try_from("1w2d").unwrap();
assert_ser_tokens(&value, &[Token::Str("1w2d")]);
let value = DurationFlex::try_from("1w2d3h4m5s").unwrap();
assert_ser_tokens(&value, &[Token::Str("1w2d3h4m5s")]);
let value = DurationFlex::try_from("5s").unwrap();
assert_ser_tokens(&value, &[Token::Str("5s")]);
let value = DurationFlex::try_from("1w8d3h4m5s").unwrap();
assert_ser_tokens(&value, &[Token::Str("2w1d3h4m5s")]);
let value = DurationFlex::try_from("1w8d3h4m3605s").unwrap();
assert_ser_tokens(&value, &[Token::Str("2w1d4h4m5s")]);
}
#[test]
fn in_struct() {
#[derive(Serialize, Deserialize)]
struct SomeStruct {
duration: DurationFlex,
}
let value = SomeStruct { duration: Duration::try_weeks(1).unwrap().into() };
assert_ser_tokens(
&value,
&[Token::Struct { name: "SomeStruct", len: 1 }, Token::Str("duration"), Token::Str("1w"), Token::StructEnd],
);
let value_year = SomeStruct { duration: DurationFlex::try_from("1y").unwrap() };
assert_ser_tokens(
&value_year,
&[Token::Struct { name: "SomeStruct", len: 1 }, Token::Str("duration"), Token::Str("1y"), Token::StructEnd],
);
}
#[cfg(feature = "validator")]
#[test]
fn validator() {
use validator::Validate;
#[derive(Validate)]
struct SomeStruct {
#[validate(range(
min = "DurationFlex::try_from(\"1h\").unwrap()",
max = "DurationFlex::try_from(\"2h\").unwrap()"
))]
duration: DurationFlex,
}
let value = SomeStruct { duration: DurationFlex::try_from("1h30m").unwrap() };
assert!(value.validate().is_ok());
let value = SomeStruct { duration: DurationFlex::try_from("30m").unwrap() };
assert!(value.validate().is_err());
let value = SomeStruct { duration: DurationFlex::try_from("2h30m").unwrap() };
assert!(value.validate().is_err());
#[derive(Validate)]
struct YearStruct {
#[validate(range(
min = "DurationFlex::try_from(\"1y\").unwrap()",
max = "DurationFlex::try_from(\"2y\").unwrap()"
))]
duration: DurationFlex,
}
let value = YearStruct { duration: DurationFlex::try_from("1y6w").unwrap() };
assert!(value.validate().is_ok());
let value = YearStruct { duration: DurationFlex::try_from("300d").unwrap() };
assert!(value.validate().is_err());
let value = YearStruct { duration: DurationFlex::try_from("2y1d").unwrap() };
assert!(value.validate().is_err());
}
#[cfg(feature = "validator")]
#[test]
fn validator_str() {
use validator::Validate;
#[derive(Validate)]
struct SomeStruct {
#[validate(range(min = "\"1h\"", max = "\"2h\""))]
duration: DurationFlex,
}
let value = SomeStruct { duration: DurationFlex::try_from("1h30m").unwrap() };
assert!(value.validate().is_ok());
let value = SomeStruct { duration: DurationFlex::try_from("30m").unwrap() };
assert!(value.validate().is_err());
let value = SomeStruct { duration: DurationFlex::try_from("2h30m").unwrap() };
assert!(value.validate().is_err());
#[derive(Validate)]
struct YearStruct {
#[validate(range(min = "\"1y\"", max = "\"2y\""))]
duration: DurationFlex,
}
let value = YearStruct { duration: DurationFlex::try_from("1y6w").unwrap() };
assert!(value.validate().is_ok());
let value = YearStruct { duration: DurationFlex::try_from("300d").unwrap() };
assert!(value.validate().is_err());
let value = YearStruct { duration: DurationFlex::try_from("2y1d").unwrap() };
assert!(value.validate().is_err());
}
#[cfg(feature = "validator")]
#[test]
fn validator_int() {
use validator::Validate;
#[derive(Validate)]
struct SomeStruct {
#[validate(range(min = 3600, max = 7200))]
duration: DurationFlex,
}
let value = SomeStruct { duration: DurationFlex::try_from("1h30m").unwrap() };
assert!(value.validate().is_ok());
let value = SomeStruct { duration: DurationFlex::try_from("30m").unwrap() };
assert!(value.validate().is_err());
let value = SomeStruct { duration: DurationFlex::try_from("2h30m").unwrap() };
assert!(value.validate().is_err());
}
#[test]
fn default_and_hash() {
use std::collections::HashSet;
let default_val = DurationFlex::default();
assert_eq!(default_val.secs(), 0);
assert_eq!(default_val.nanos(), 0);
let mut set = HashSet::new();
set.insert(DurationFlex::try_from("1h").unwrap());
set.insert(DurationFlex::try_from("60m").unwrap());
assert_eq!(set.len(), 1);
let mut err_set = HashSet::new();
err_set.insert(DurationFlexError::InvalidFormat);
err_set.insert(DurationFlexError::OutOfRange);
assert_eq!(err_set.len(), 2);
}
#[test]
fn error_traits() {
use std::error::Error;
let err = DurationFlexError::InvalidFormat;
assert_eq!(err.to_string(), "invalid duration format");
let err_source: &dyn Error = &err;
assert!(err_source.source().is_none());
let err_oor = DurationFlexError::OutOfRange;
assert_eq!(err_oor.to_string(), "duration value is out of range");
}
#[test]
fn arithmetic_operations() {
let a = DurationFlex::try_from("1h").unwrap();
let b = DurationFlex::try_from("30m").unwrap();
assert_eq!((a + b).secs(), 90 * 60);
assert_eq!((a - b).secs(), 30 * 60);
assert_eq!((b * 2u32).secs(), 3600);
assert_eq!((b * 3i64).secs(), 5400);
assert_eq!((a / 2u32).secs(), 1800);
assert_eq!((a / 4i64).secs(), 900);
let list = vec![
DurationFlex::try_from("1h").unwrap(),
DurationFlex::try_from("30m").unwrap(),
DurationFlex::try_from("15m").unwrap(),
];
let total: DurationFlex = list.iter().sum();
assert_eq!(total.secs(), 105 * 60);
let total_owned: DurationFlex = list.into_iter().sum();
assert_eq!(total_owned.secs(), 105 * 60);
}
#[test]
fn datetime_subtraction() {
use chrono::Utc;
let now = Utc::now();
let duration = DurationFlex::try_from("1h").unwrap();
let earlier = now - duration;
assert_eq!(earlier + duration, now);
}
#[test]
fn constructors_and_inspectors() {
assert_eq!(DurationFlex::ZERO, DurationFlex::new(0, 0));
assert!(DurationFlex::ZERO.is_zero());
assert!(!DurationFlex::ZERO.is_positive());
assert!(!DurationFlex::ZERO.is_negative());
let s = DurationFlex::from_secs(10);
assert_eq!(s.secs(), 10);
assert_eq!(s.nanos(), 0);
assert!(s.is_positive());
assert!(!s.is_negative());
let ms = DurationFlex::from_millis(1500);
assert_eq!(ms.secs(), 1);
assert_eq!(ms.nanos(), 500_000_000);
let m = DurationFlex::from_minutes(2);
assert_eq!(m.secs(), 120);
let h = DurationFlex::from_hours(3);
assert_eq!(h.secs(), 3 * 3600);
let d = DurationFlex::from_days(4);
assert_eq!(d.secs(), 4 * 86400);
let w = DurationFlex::from_weeks(2);
assert_eq!(w.secs(), 14 * 86400);
let y = DurationFlex::from_years(1);
assert_eq!(y.secs(), 365 * 86400);
let neg = DurationFlex::new(-5, 0);
assert!(neg.is_negative());
assert!(!neg.is_positive());
assert_eq!(neg.to_std(), None);
let pos = DurationFlex::from_secs(5);
assert_eq!(pos.to_std(), Some(time::Duration::from_secs(5)));
assert_eq!(pos.to_chrono(), Duration::try_seconds(5).unwrap());
}
}