use crate::error::ParseError;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(
from = "IntervalWire<T>",
into = "IntervalWire<T>",
bound = "T: Clone + PartialOrd + Serialize + serde::de::DeserializeOwned"
)]
pub struct Interval<T> {
lower: Option<T>,
upper: Option<T>,
lower_included: Option<bool>,
upper_included: Option<bool>,
}
impl<T: PartialOrd> Interval<T> {
pub fn new(
lower: Option<T>,
upper: Option<T>,
lower_included: Option<bool>,
upper_included: Option<bool>,
) -> Result<Self, ParseError> {
if lower.is_none() && upper.is_none() {
return Err(ParseError::new("INTERVAL", "both bounds are absent", ""));
}
if lower.is_none() && lower_included.is_some() {
return Err(ParseError::new(
"INTERVAL",
"lower_included given for an unbounded lower end",
"",
));
}
if upper.is_none() && upper_included.is_some() {
return Err(ParseError::new(
"INTERVAL",
"upper_included given for an unbounded upper end",
"",
));
}
if let (Some(lo), Some(hi)) = (&lower, &upper) {
if !matches!(
lo.partial_cmp(hi),
Some(core::cmp::Ordering::Less | core::cmp::Ordering::Equal)
) {
return Err(ParseError::invariant("DV_INTERVAL", "Limits_consistent"));
}
}
Ok(Self {
lower,
upper,
lower_included,
upper_included,
})
}
pub fn closed(lower: T, upper: T) -> Result<Self, ParseError> {
Self::new(Some(lower), Some(upper), Some(true), Some(true))
}
pub fn open(lower: T, upper: T) -> Result<Self, ParseError> {
Self::new(Some(lower), Some(upper), Some(false), Some(false))
}
pub fn at_least(lower: T) -> Result<Self, ParseError> {
Self::new(Some(lower), None, Some(true), None)
}
pub fn greater_than(lower: T) -> Result<Self, ParseError> {
Self::new(Some(lower), None, Some(false), None)
}
pub fn at_most(upper: T) -> Result<Self, ParseError> {
Self::new(None, Some(upper), None, Some(true))
}
pub fn less_than(upper: T) -> Result<Self, ParseError> {
Self::new(None, Some(upper), None, Some(false))
}
pub fn point(value: T) -> Result<Self, ParseError>
where
T: Clone,
{
Self::closed(value.clone(), value)
}
#[must_use]
pub fn contains(&self, value: &T) -> bool {
let above_lower = match &self.lower {
None => true,
Some(lo) if self.lower_included.unwrap_or(true) => value >= lo,
Some(lo) => value > lo,
};
let below_upper = match &self.upper {
None => true,
Some(hi) if self.upper_included.unwrap_or(true) => value <= hi,
Some(hi) => value < hi,
};
above_lower && below_upper
}
}
impl<T> Interval<T> {
#[must_use]
pub fn lower(&self) -> Option<&T> {
self.lower.as_ref()
}
#[must_use]
pub fn upper(&self) -> Option<&T> {
self.upper.as_ref()
}
#[must_use]
pub fn lower_unbounded(&self) -> bool {
self.lower.is_none()
}
#[must_use]
pub fn upper_unbounded(&self) -> bool {
self.upper.is_none()
}
#[must_use]
pub fn lower_included(&self) -> Option<bool> {
self.lower_included
}
#[must_use]
pub fn upper_included(&self) -> Option<bool> {
self.upper_included
}
}
#[derive(Serialize, Deserialize)]
struct IntervalWire<T> {
#[serde(skip_serializing_if = "Option::is_none", default = "none")]
lower: Option<T>,
#[serde(skip_serializing_if = "Option::is_none", default = "none")]
upper: Option<T>,
lower_unbounded: bool,
upper_unbounded: bool,
#[serde(skip_serializing_if = "Option::is_none", default = "none")]
lower_included: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none", default = "none")]
upper_included: Option<bool>,
}
fn none<T>() -> Option<T> {
None
}
impl<T> From<Interval<T>> for IntervalWire<T> {
fn from(v: Interval<T>) -> Self {
Self {
lower_unbounded: v.lower.is_none(),
upper_unbounded: v.upper.is_none(),
lower: v.lower,
upper: v.upper,
lower_included: v.lower_included,
upper_included: v.upper_included,
}
}
}
impl<T> From<IntervalWire<T>> for Interval<T> {
fn from(v: IntervalWire<T>) -> Self {
Self {
lower: v.lower,
upper: v.upper,
lower_included: v.lower_included,
upper_included: v.upper_included,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unbounded_flags_are_derived_from_the_bounds() {
let i = Interval::at_least(1_i32).unwrap();
assert!(!i.lower_unbounded());
assert!(i.upper_unbounded());
}
#[test]
fn a_self_contradictory_payload_trusts_the_bound() {
let json = r#"{"lower":5,"lower_unbounded":true,"upper_unbounded":true}"#;
let i: Interval<i32> = serde_json::from_str(json).unwrap();
assert_eq!(i.lower(), Some(&5));
assert!(!i.lower_unbounded());
let out = serde_json::to_string(&i).unwrap();
assert!(out.contains(r#""lower_unbounded":false"#), "{out}");
}
#[test]
fn inverted_bounds_are_refused() {
assert!(Interval::closed(100_i32, 60).is_err());
}
#[test]
fn open_and_closed_ends_differ_at_the_boundary() {
assert!(Interval::closed(60_i32, 100).unwrap().contains(&100));
assert!(!Interval::open(60_i32, 100).unwrap().contains(&100));
}
#[test]
fn contains_uses_the_right_comparison_on_both_sides_of_an_open_bound() {
let open = Interval::open(60_i32, 100).unwrap();
assert!(open.contains(&61));
assert!(open.contains(&99));
assert!(!open.contains(&60), "the excluded lower bound was included");
assert!(!open.contains(&100), "the excluded upper bound was included");
assert!(!open.contains(&30), "below the range was reported inside it");
assert!(!open.contains(&130), "above the range was reported inside it");
let closed = Interval::closed(60_i32, 100).unwrap();
assert!(closed.contains(&60));
assert!(closed.contains(&100));
assert!(closed.contains(&80));
assert!(!closed.contains(&59));
assert!(!closed.contains(&101));
let at_least = Interval::at_least(10_i32).unwrap();
assert!(at_least.contains(&10));
assert!(at_least.contains(&1_000_000));
assert!(!at_least.contains(&9));
let at_most = Interval::at_most(10_i32).unwrap();
assert!(at_most.contains(&10));
assert!(at_most.contains(&-1_000_000));
assert!(!at_most.contains(&11));
}
#[test]
fn unbounded_and_included_are_reported_for_each_shape() {
let closed = Interval::closed(60_i32, 100).unwrap();
assert!(!closed.lower_unbounded());
assert!(!closed.upper_unbounded());
assert_eq!(closed.lower_included(), Some(true));
assert_eq!(closed.upper_included(), Some(true));
let open = Interval::open(60_i32, 100).unwrap();
assert_eq!(open.lower_included(), Some(false));
assert_eq!(open.upper_included(), Some(false));
let at_least = Interval::at_least(10_i32).unwrap();
assert!(!at_least.lower_unbounded());
assert!(at_least.upper_unbounded(), "an open-ended range was reported bounded");
assert_eq!(at_least.upper(), None);
let at_most = Interval::at_most(10_i32).unwrap();
assert!(at_most.lower_unbounded(), "an open-started range was reported bounded");
assert!(!at_most.upper_unbounded());
assert_eq!(at_most.lower(), None);
}
}