use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ZeroBasedPos(u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct OneBasedPos(u64);
impl ZeroBasedPos {
#[inline]
pub const fn new(pos: u64) -> Self {
Self(pos)
}
#[inline]
pub const fn value(self) -> u64 {
self.0
}
#[inline]
pub const fn to_one_based(self) -> OneBasedPos {
OneBasedPos(self.0 + 1)
}
#[inline]
pub const fn as_index(self) -> usize {
self.0 as usize
}
#[inline]
pub const fn checked_add(self, offset: u64) -> Option<Self> {
match self.0.checked_add(offset) {
Some(v) => Some(Self(v)),
None => None,
}
}
#[inline]
pub const fn checked_sub(self, offset: u64) -> Option<Self> {
match self.0.checked_sub(offset) {
Some(v) => Some(Self(v)),
None => None,
}
}
#[inline]
pub const fn saturating_sub(self, offset: u64) -> Self {
Self(self.0.saturating_sub(offset))
}
}
impl OneBasedPos {
#[inline]
pub fn new(pos: u64) -> Self {
assert!(pos > 0, "1-based position cannot be 0");
Self(pos)
}
#[inline]
pub const fn new_unchecked(pos: u64) -> Self {
Self(pos)
}
#[inline]
pub const fn try_new(pos: u64) -> Option<Self> {
if pos > 0 {
Some(Self(pos))
} else {
None
}
}
#[inline]
pub const fn value(self) -> u64 {
self.0
}
#[inline]
pub const fn to_zero_based(self) -> ZeroBasedPos {
ZeroBasedPos(self.0 - 1)
}
#[inline]
pub const fn checked_add(self, offset: u64) -> Option<Self> {
match self.0.checked_add(offset) {
Some(v) => Some(Self(v)),
None => None,
}
}
#[inline]
pub const fn checked_sub(self, offset: u64) -> Option<Self> {
match self.0.checked_sub(offset) {
Some(v) if v > 0 => Some(Self(v)),
_ => None,
}
}
}
impl fmt::Display for ZeroBasedPos {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl fmt::Display for OneBasedPos {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<ZeroBasedPos> for u64 {
fn from(pos: ZeroBasedPos) -> Self {
pos.0
}
}
impl From<OneBasedPos> for u64 {
fn from(pos: OneBasedPos) -> Self {
pos.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Interval<P> {
pub start: P,
pub end: P,
}
pub type ZeroBasedInterval = Interval<ZeroBasedPos>;
pub type OneBasedInterval = Interval<OneBasedPos>;
impl ZeroBasedInterval {
pub const fn new(start: ZeroBasedPos, end: ZeroBasedPos) -> Self {
Self { start, end }
}
pub fn to_one_based_closed(self) -> OneBasedInterval {
assert!(
self.end.value() > self.start.value(),
"Cannot convert empty interval to 1-based closed"
);
Interval {
start: self.start.to_one_based(),
end: ZeroBasedPos::new(self.end.value() - 1).to_one_based(),
}
}
#[inline]
pub const fn len(&self) -> u64 {
self.end.value() - self.start.value()
}
#[inline]
pub const fn is_empty(&self) -> bool {
self.end.value() <= self.start.value()
}
}
impl OneBasedInterval {
pub fn new(start: OneBasedPos, end: OneBasedPos) -> Self {
Self { start, end }
}
pub fn to_zero_based_half_open(self) -> ZeroBasedInterval {
Interval {
start: self.start.to_zero_based(),
end: ZeroBasedPos::new(self.end.value()), }
}
#[inline]
pub const fn len(&self) -> u64 {
self.end.value() - self.start.value() + 1
}
#[inline]
pub const fn is_empty(&self) -> bool {
self.end.value() < self.start.value()
}
}
#[inline]
pub const fn hgvs_pos_to_index(pos: u64) -> usize {
(pos - 1) as usize
}
#[inline]
pub const fn index_to_hgvs_pos(idx: usize) -> u64 {
idx as u64 + 1
}
#[inline]
pub const fn cdot_genomic_to_closed(start: u64, end: u64) -> (u64, u64) {
(start + 1, end)
}
#[inline]
pub const fn cdot_tx_coords(tx_start: u64, tx_end: u64) -> (u64, u64) {
(tx_start, tx_end)
}
#[inline]
pub const fn spdi_to_hgvs_pos(spdi_pos: u64) -> u64 {
spdi_pos + 1
}
#[inline]
pub const fn hgvs_to_spdi_pos(hgvs_pos: u64) -> u64 {
hgvs_pos - 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zero_based_creation() {
let pos = ZeroBasedPos::new(0);
assert_eq!(pos.value(), 0);
assert_eq!(pos.as_index(), 0);
let pos = ZeroBasedPos::new(99);
assert_eq!(pos.value(), 99);
assert_eq!(pos.as_index(), 99);
}
#[test]
fn test_one_based_creation() {
let pos = OneBasedPos::new(1);
assert_eq!(pos.value(), 1);
let pos = OneBasedPos::new(100);
assert_eq!(pos.value(), 100);
}
#[test]
#[should_panic(expected = "1-based position cannot be 0")]
fn test_one_based_rejects_zero() {
let _ = OneBasedPos::new(0);
}
#[test]
fn test_try_new_one_based() {
assert!(OneBasedPos::try_new(0).is_none());
assert!(OneBasedPos::try_new(1).is_some());
}
#[test]
fn test_zero_to_one_based_conversion() {
assert_eq!(ZeroBasedPos::new(0).to_one_based().value(), 1);
assert_eq!(ZeroBasedPos::new(99).to_one_based().value(), 100);
}
#[test]
fn test_one_to_zero_based_conversion() {
assert_eq!(OneBasedPos::new(1).to_zero_based().value(), 0);
assert_eq!(OneBasedPos::new(100).to_zero_based().value(), 99);
}
#[test]
fn test_roundtrip_conversion() {
for i in 0..100 {
let zb = ZeroBasedPos::new(i);
let ob = zb.to_one_based();
let zb_again = ob.to_zero_based();
assert_eq!(zb, zb_again);
}
for i in 1..=100 {
let ob = OneBasedPos::new(i);
let zb = ob.to_zero_based();
let ob_again = zb.to_one_based();
assert_eq!(ob, ob_again);
}
}
#[test]
fn test_zero_based_interval_length() {
let interval = ZeroBasedInterval::new(ZeroBasedPos::new(0), ZeroBasedPos::new(10));
assert_eq!(interval.len(), 10);
let interval = ZeroBasedInterval::new(ZeroBasedPos::new(5), ZeroBasedPos::new(8));
assert_eq!(interval.len(), 3);
}
#[test]
fn test_one_based_interval_length() {
let interval = OneBasedInterval::new(OneBasedPos::new(1), OneBasedPos::new(10));
assert_eq!(interval.len(), 10);
let interval = OneBasedInterval::new(OneBasedPos::new(5), OneBasedPos::new(7));
assert_eq!(interval.len(), 3);
}
#[test]
fn test_interval_conversion() {
let ho = ZeroBasedInterval::new(ZeroBasedPos::new(0), ZeroBasedPos::new(10));
let closed = ho.to_one_based_closed();
assert_eq!(closed.start.value(), 1);
assert_eq!(closed.end.value(), 10);
assert_eq!(ho.len(), closed.len());
let closed = OneBasedInterval::new(OneBasedPos::new(1), OneBasedPos::new(10));
let ho = closed.to_zero_based_half_open();
assert_eq!(ho.start.value(), 0);
assert_eq!(ho.end.value(), 10);
}
#[test]
fn test_hgvs_pos_to_index() {
assert_eq!(hgvs_pos_to_index(1), 0);
assert_eq!(hgvs_pos_to_index(100), 99);
}
#[test]
fn test_index_to_hgvs_pos() {
assert_eq!(index_to_hgvs_pos(0), 1);
assert_eq!(index_to_hgvs_pos(99), 100);
}
#[test]
fn test_cdot_genomic_to_closed() {
let (start, end) = cdot_genomic_to_closed(0, 10);
assert_eq!(start, 1);
assert_eq!(end, 10);
let (start, end) = cdot_genomic_to_closed(99, 200);
assert_eq!(start, 100);
assert_eq!(end, 200);
}
#[test]
fn test_cdot_tx_coords_unchanged() {
let (start, end) = cdot_tx_coords(1, 100);
assert_eq!(start, 1);
assert_eq!(end, 100);
}
#[test]
fn test_spdi_hgvs_conversion() {
assert_eq!(spdi_to_hgvs_pos(0), 1);
assert_eq!(hgvs_to_spdi_pos(1), 0);
assert_eq!(spdi_to_hgvs_pos(12344), 12345);
assert_eq!(hgvs_to_spdi_pos(12345), 12344);
}
#[test]
fn test_checked_arithmetic() {
let zb = ZeroBasedPos::new(5);
assert_eq!(zb.checked_add(3), Some(ZeroBasedPos::new(8)));
assert_eq!(zb.checked_sub(3), Some(ZeroBasedPos::new(2)));
assert_eq!(zb.checked_sub(10), None);
let ob = OneBasedPos::new(5);
assert_eq!(ob.checked_add(3), Some(OneBasedPos::new(8)));
assert_eq!(ob.checked_sub(3), Some(OneBasedPos::new(2)));
assert_eq!(ob.checked_sub(5), None); }
#[test]
fn test_ordering() {
let a = ZeroBasedPos::new(5);
let b = ZeroBasedPos::new(10);
assert!(a < b);
let a = OneBasedPos::new(5);
let b = OneBasedPos::new(10);
assert!(a < b);
}
}