use super::contains::Contains;
use crate::{Error, F32Bw0and1};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fmt::Debug;
use std::num::NonZeroU64;
use std::ops::RangeInclusive;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Serialize, Deserialize)]
pub struct OrdPair<T: Clone + Copy + Debug + PartialEq + PartialOrd> {
low: T,
high: T,
}
impl Eq for OrdPair<u8> {}
impl Eq for OrdPair<u64> {}
impl Eq for OrdPair<NonZeroU64> {}
impl Eq for OrdPair<F32Bw0and1> {}
impl<T: Clone + Copy + Debug + Default + Ord> Default for OrdPair<T> {
fn default() -> Self {
OrdPair {
low: T::default(),
high: T::default(),
}
}
}
impl<T: Clone + Copy + Debug + PartialEq + PartialOrd> OrdPair<T> {
pub fn new(low: T, high: T) -> Result<Self, Error> {
if low <= high {
Ok(OrdPair { low, high })
} else {
Err(Error::WrongOrder(
"wrong order in OrdPair creation".to_owned(),
))
}
}
pub fn low(&self) -> T {
self.low
}
pub fn high(&self) -> T {
self.high
}
pub fn update_low(&mut self, value: T) -> Result<(), Error> {
if value <= self.high() {
self.low = value;
Ok(())
} else {
Err(Error::WrongOrder(
"wrong order in OrdPair `update_low`".to_owned(),
))
}
}
pub fn update_high(&mut self, value: T) -> Result<(), Error> {
if value >= self.low() {
self.high = value;
Ok(())
} else {
Err(Error::WrongOrder(
"wrong order in OrdPair `update_high`".to_owned(),
))
}
}
}
impl OrdPair<u64> {
#[expect(
clippy::missing_panics_doc,
reason = "This function does not panic. The internal `.expect()` \
calls are only used after verifying the length of the parts vector, ensuring safe access."
)]
pub fn from_interval(interval_str: &str) -> Result<Self, Error> {
let parts: Vec<&str> = interval_str.split('-').collect();
match parts.len() {
2 => {
let start = parts
.first()
.expect("parts has exactly 2 elements")
.trim()
.parse::<u64>()
.map_err(|_err| {
Error::OrdPairConversion(
"Invalid start coordinate in interval!".to_string(),
)
})?;
let end = if parts
.get(1)
.expect("parts has exactly 2 elements")
.trim()
.is_empty()
{
u64::MAX
} else {
parts
.get(1)
.expect("parts has exactly 2 elements")
.trim()
.parse::<u64>()
.map_err(|_err| {
Error::OrdPairConversion(
"Invalid end coordinate in interval!".to_string(),
)
})?
};
if start < end {
Ok(OrdPair {
low: start,
high: end,
})
} else {
Err(Error::OrdPairConversion(
"Genomic intervals require start < end (strict inequality)".to_string(),
))
}
}
_ => Err(Error::OrdPairConversion(
"Invalid interval format! Expected 'start-end' or 'start-'".to_string(),
)),
}
}
}
impl<T: Clone + Copy + Debug + PartialEq + PartialOrd + FromStr> FromStr for OrdPair<T> {
type Err = Error;
fn from_str(val_str: &str) -> Result<Self, Self::Err> {
macro_rules! parse_error {
() => {
Err(Error::OrdPairConversion(
"Bad ordered pair inputs!".to_string(),
))
};
}
let v: Vec<&str> = val_str.split(',').map(str::trim).collect();
match v.len() {
2 => {
let Ok(low) = T::from_str(v.first().expect("v has exactly 2 elements")) else {
return parse_error!();
};
let Ok(high) = T::from_str(v.get(1).expect("v has exactly 2 elements")) else {
return parse_error!();
};
OrdPair::<T>::new(low, high)
}
_ => parse_error!(),
}
}
}
impl<T: Clone + Copy + Debug + PartialEq + PartialOrd> From<OrdPair<T>> for RangeInclusive<T> {
fn from(value: OrdPair<T>) -> Self {
RangeInclusive::<T>::new(value.low(), value.high())
}
}
impl<T: Clone + Copy + Debug + PartialEq + PartialOrd> Contains<T> for OrdPair<T> {
fn contains(&self, val: &T) -> bool {
RangeInclusive::<T>::from(*self).contains(val)
}
}
impl<T: Clone + Copy + Debug + fmt::Display + PartialEq + PartialOrd> fmt::Display for OrdPair<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}, {}", self.low(), self.high())
}
}
impl TryFrom<(u64, u64)> for OrdPair<NonZeroU64> {
type Error = Error;
fn try_from(value: (u64, u64)) -> Result<Self, Self::Error> {
OrdPair::new(
NonZeroU64::new(value.0).ok_or(Error::InvalidState(String::from(
"did you pass positive integers < 2^64?",
)))?,
NonZeroU64::new(value.1).ok_or(Error::InvalidState(String::from(
"did you pass positive integers < 2^64?",
)))?,
)
}
}
impl TryFrom<(f32, f32)> for OrdPair<F32Bw0and1> {
type Error = Error;
fn try_from(value: (f32, f32)) -> Result<Self, Self::Error> {
OrdPair::new(F32Bw0and1::new(value.0)?, F32Bw0and1::new(value.1)?)
}
}
impl<T: Clone + Copy + Debug + PartialEq + PartialOrd> TryFrom<(T, T)> for OrdPair<T> {
type Error = Error;
fn try_from(value: (T, T)) -> Result<Self, Self::Error> {
OrdPair::new(value.0, value.1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ord_pair_default_from_u8() {
assert_eq!(
OrdPair::<u8>::default(),
OrdPair::<u8>::new(0u8, 0u8).unwrap()
);
}
#[test]
#[expect(
clippy::float_cmp,
reason = "we expect perfect float conversion for the two examples below"
)]
fn ord_pair_from_str_f32() {
assert_eq!(
OrdPair::<f32>::from_str("1.0,2.0")
.expect("no failure")
.low(),
1.0
);
assert_eq!(
OrdPair::<f32>::from_str("1.0,2.0")
.expect("no failure")
.high(),
2.0
);
}
#[test]
fn ord_pair_from_str_u8() {
assert_eq!(
OrdPair::<u8>::from_str("1, 2").expect("no failure").low(),
1
);
assert_eq!(
OrdPair::<u8>::from_str("1, 2").expect("no failure").high(),
2
);
}
#[test]
#[should_panic(expected = "OrdPairConversion")]
fn ord_pair_from_str_empty_first_value_panics() {
let _: OrdPair<u8> = OrdPair::<u8>::from_str(",2").unwrap();
}
#[test]
#[should_panic(expected = "WrongOrder")]
fn ord_pair_from_str_wrong_order_panics() {
let _: OrdPair<u8> = OrdPair::<u8>::from_str("2,1").unwrap();
}
#[test]
fn ord_pair_to_range() {
assert_eq!(
(3..=5),
RangeInclusive::from(OrdPair::new(3, 5).expect("no failure"))
);
}
#[expect(
clippy::shadow_unrelated,
reason = "repetition is fine; each block is clearly separated"
)]
#[test]
fn ord_pair_from_interval() {
let interval = OrdPair::<u64>::from_interval("1000-2000").expect("should parse");
assert_eq!(interval.low(), 1000);
assert_eq!(interval.high(), 2000);
let interval = OrdPair::<u64>::from_interval("1000-").expect("should parse");
assert_eq!(interval.low(), 1000);
assert_eq!(interval.high(), u64::MAX);
}
#[test]
#[should_panic(expected = "OrdPairConversion")]
fn ord_pair_from_interval_equal_start_end_panics() {
let _: OrdPair<u64> = OrdPair::<u64>::from_interval("1000-1000").unwrap();
}
#[test]
#[should_panic(expected = "OrdPairConversion")]
fn ord_pair_from_interval_start_greater_than_end_panics() {
let _: OrdPair<u64> = OrdPair::<u64>::from_interval("2000-1000").unwrap();
}
#[test]
#[should_panic(expected = "OrdPairConversion")]
fn ord_pair_from_interval_no_dash_panics() {
let _: OrdPair<u64> = OrdPair::<u64>::from_interval("1000").unwrap();
}
#[test]
#[should_panic(expected = "OrdPairConversion")]
fn ord_pair_from_interval_too_many_dashes_panics() {
let _: OrdPair<u64> = OrdPair::<u64>::from_interval("1000-2000-3000").unwrap();
}
#[test]
#[should_panic(expected = "OrdPairConversion")]
fn ord_pair_from_interval_invalid_start_panics() {
let _: OrdPair<u64> = OrdPair::<u64>::from_interval("abc-2000").unwrap();
}
#[test]
#[should_panic(expected = "OrdPairConversion")]
fn ord_pair_from_interval_invalid_end_panics() {
let _: OrdPair<u64> = OrdPair::<u64>::from_interval("1000-xyz").unwrap();
}
#[test]
#[should_panic(expected = "OrdPairConversion")]
fn ord_pair_from_interval_empty_string_panics() {
let _: OrdPair<u64> = OrdPair::<u64>::from_interval("").unwrap();
}
#[test]
#[should_panic(expected = "OrdPairConversion")]
fn ord_pair_from_interval_just_dash_panics() {
let _: OrdPair<u64> = OrdPair::<u64>::from_interval("-").unwrap();
}
#[test]
#[should_panic(expected = "OrdPairConversion")]
fn ord_pair_from_interval_negative_numbers_panics() {
let _: OrdPair<u64> = OrdPair::<u64>::from_interval("-100-200").unwrap();
}
#[test]
fn ord_pair_contains() {
let pair = OrdPair::new(10, 20).expect("should create");
assert!(pair.contains(&15));
assert!(pair.contains(&10)); assert!(pair.contains(&20)); assert!(!pair.contains(&5));
assert!(!pair.contains(&25));
}
#[test]
fn ord_pair_display() {
let pair = OrdPair::new(10, 20).expect("should create");
assert_eq!(format!("{pair}"), "10, 20");
let float_pair = OrdPair::new(1.5, 2.5).expect("should create");
assert_eq!(format!("{float_pair}"), "1.5, 2.5");
}
#[test]
#[should_panic(expected = "OrdPairConversion")]
fn ord_pair_from_str_empty_string_panics() {
let _: OrdPair<i32> = OrdPair::<i32>::from_str("").unwrap();
}
#[test]
#[should_panic(expected = "OrdPairConversion")]
fn ord_pair_from_str_single_value_panics() {
let _: OrdPair<i32> = OrdPair::<i32>::from_str("1").unwrap();
}
#[test]
#[should_panic(expected = "OrdPairConversion")]
fn ord_pair_from_str_too_many_values_panics() {
let _: OrdPair<i32> = OrdPair::<i32>::from_str("1,2,3").unwrap();
}
#[test]
#[should_panic(expected = "OrdPairConversion")]
fn ord_pair_from_str_non_numeric_panics() {
let _: OrdPair<i32> = OrdPair::<i32>::from_str("abc,def").unwrap();
}
#[test]
fn ord_pair_update_low_success() {
let mut x = OrdPair::<u8>::new(10, 11).expect("no failure");
x.update_low(9).unwrap();
assert_eq!(x.low(), 9);
assert_eq!(x.high(), 11);
}
#[test]
#[should_panic(expected = "WrongOrder")]
fn ord_pair_update_low_violates_order_panics() {
let mut x = OrdPair::<u8>::new(10, 11).expect("no failure");
x.update_low(12).unwrap();
}
#[test]
fn ord_pair_update_high_success() {
let mut x = OrdPair::<u8>::new(10, 11).expect("no failure");
x.update_high(20).unwrap();
assert_eq!(x.low(), 10);
assert_eq!(x.high(), 20);
}
#[test]
#[should_panic(expected = "WrongOrder")]
fn ord_pair_update_high_violates_order_panics() {
let mut x = OrdPair::<u8>::new(10, 11).expect("no failure");
x.update_high(9).unwrap();
}
}