Skip to main content

omics_coordinate/
position.rs

1//! Positions.
2
3use std::num::ParseIntError;
4
5use thiserror::Error;
6
7use crate::System;
8use crate::math::CheckedAdd;
9use crate::math::CheckedSub;
10use crate::system::Base;
11use crate::system::Interbase;
12
13pub mod base;
14pub mod interbase;
15
16////////////////////////////////////////////////////////////////////////////////////////
17// Constants and Types
18////////////////////////////////////////////////////////////////////////////////////////
19
20/// The inner representation for a numerical position value.
21///
22/// Note that `u64` positions can be enabled by turning on the `position-u64`
23/// feature for the crate.
24#[cfg(not(feature = "position-u64"))]
25pub type Number = u32;
26
27/// The inner representation for a numerical position value.
28///
29/// Note that `u32` positions can be enabled by turning off the `position-u64`
30/// feature for the crate.
31#[cfg(feature = "position-u64")]
32pub type Number = u64;
33
34////////////////////////////////////////////////////////////////////////////////////////
35// Assertions
36////////////////////////////////////////////////////////////////////////////////////////
37
38const _: () = {
39    /// A function to ensure that types are `Copy`.
40    const fn is_copy<T: Copy>() {}
41    is_copy::<Number>();
42
43    // Ensure that the types themselves are copy, as they should be able to be
44    // passed around as such as well.
45    is_copy::<Position<Interbase>>();
46    is_copy::<Position<Base>>();
47};
48
49////////////////////////////////////////////////////////////////////////////////////////
50// Errors
51////////////////////////////////////////////////////////////////////////////////////////
52
53/// A error related to parsing a position.
54#[derive(Error, Debug, PartialEq, Eq)]
55pub enum ParseError {
56    /// An integer parsing error.
57    ///
58    /// Occurs when an integer position value cannot be parsed.
59    #[error("failed to parse {system} position from `{value}`: {inner}")]
60    Int {
61        /// The coordinate system being parsed.
62        system: &'static str,
63
64        /// The inner error.
65        inner: ParseIntError,
66
67        /// The value that was attempted to be parsed.
68        value: String,
69    },
70}
71
72/// A [`Result`](std::result::Result) with a [`ParseError`].
73pub type ParseResult<T> = std::result::Result<T, ParseError>;
74
75/// A position-related error.
76#[derive(Error, Debug, PartialEq, Eq)]
77pub enum Error {
78    /// A parse error.
79    #[error("parse error: {0}")]
80    Parse(#[from] ParseError),
81
82    /// Incompatible value.
83    ///
84    /// This error represents and incompatible value for a position that is
85    /// placed within a coordinate system. For example, zero (`0`) is not a
86    /// valid numerical position within a 1-based coordinate system.
87    #[error("incompatible value for system \"{system}\": `{value}`")]
88    IncompatibleValue {
89        /// The system within when the value is incompatible.
90        system: &'static str,
91
92        /// The incompatible value.
93        value: Number,
94    },
95}
96
97/// A [`Result`](std::result::Result) with an [`Error`](enum@Error).
98pub type Result<T> = std::result::Result<T, Error>;
99
100///////////////////////////////////////////////////////////////////////////////////////
101// The `Position` trait
102///////////////////////////////////////////////////////////////////////////////////////
103
104/// Traits related to a position.
105pub mod r#trait {
106    use std::num::NonZero;
107
108    use super::*;
109
110    /// Requirements to be a position.
111    pub trait Position<S: System>:
112        std::fmt::Display
113        + std::fmt::Debug
114        + PartialEq
115        + Eq
116        + PartialOrd
117        + Ord
118        + std::str::FromStr<Err = Error>
119        + CheckedAdd<Number, Output = Self>
120        + CheckedSub<Number, Output = Self>
121        + TryFrom<Number>
122        + From<NonZero<Number>>
123    where
124        Self: Sized,
125    {
126    }
127}
128
129////////////////////////////////////////////////////////////////////////////////////////
130// Positions
131////////////////////////////////////////////////////////////////////////////////////////
132
133/// An offset from the start of a molecule.
134///
135/// For a more in-depth discussion on what positions are and the notations used
136/// within this crate, please see [this section of the docs](crate#positions).
137#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
138pub struct Position<S: System> {
139    /// The coordinate system.
140    system: S,
141
142    /// The inner value.
143    value: Number,
144}
145
146impl<S: System> Position<S> {
147    /// Gets the numerical position.
148    ///
149    /// # Examples
150    ///
151    /// ```rust
152    /// use omics_coordinate::Position;
153    /// use omics_coordinate::system::Interbase;
154    ///
155    /// let position = Position::<Interbase>::new(42);
156    /// assert_eq!(position.get(), 42);
157    /// ```
158    pub fn get(&self) -> Number {
159        self.value
160    }
161
162    /// Performs checked addition.
163    ///
164    /// # Examples
165    ///
166    /// ```rust
167    /// use omics_coordinate::Position;
168    /// use omics_coordinate::system::Interbase;
169    ///
170    /// let position = Position::<Interbase>::new(42)
171    ///     .checked_add(8)
172    ///     .expect("addition to succeed");
173    /// assert_eq!(position.get(), 50);
174    /// ```
175    pub fn checked_add(&self, rhs: Number) -> Option<Self>
176    where
177        Self: r#trait::Position<S>,
178    {
179        <Self as CheckedAdd<Number>>::checked_add(self, rhs)
180    }
181
182    /// Performs checked subtraction.
183    ///
184    /// # Examples
185    ///
186    /// ```rust
187    /// use omics_coordinate::Position;
188    /// use omics_coordinate::system::Interbase;
189    ///
190    /// let position = Position::<Interbase>::new(42)
191    ///     .checked_sub(2)
192    ///     .expect("subtraction to succeed");
193    /// assert_eq!(position.get(), 40);
194    /// ```
195    pub fn checked_sub(&self, rhs: Number) -> Option<Self>
196    where
197        Self: r#trait::Position<S>,
198    {
199        <Self as CheckedSub<Number>>::checked_sub(self, rhs)
200    }
201
202    /// Gets the magnitude of the distance between two positions.
203    ///
204    /// # Note
205    ///
206    /// This method calculates the magnitude of distance between two positions
207    /// that are assumed to be on the same number line (i.e., the same stand and
208    /// contig). Notably, **there is not check** regarding strand or contig
209    /// equivalent within this method.
210    ///
211    /// Because the use case of doing this is so niche, the method is currently
212    /// only accessible within the crate.  If you're wanting to do this kind of
213    /// thing, you're probably going to want to convert the position to
214    /// coordinates and calculate the distance between the two coordinates. If
215    /// you think you have a legitimate use case where this would be useful,
216    /// please file an issue.
217    pub(crate) fn distance_unchecked(&self, rhs: &Position<S>) -> Number {
218        let a = self.get();
219        let b = rhs.get();
220
221        // SAFETY: because these are two unsigned numbers that are being
222        // subtracted correctly (based on the `if` statement below, we
223        // expect these to always unwrap).
224        if a >= b {
225            a.checked_sub(b).unwrap()
226        } else {
227            b.checked_sub(a).unwrap()
228        }
229    }
230}
231
232impl<S: System> std::fmt::Display for Position<S> {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        if !f.alternate() {
235            write!(f, "{}", self.value)
236        } else {
237            write!(f, "{} ({})", self.value, self.system)
238        }
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use std::fmt::Write as _;
245
246    use super::*;
247    use crate::system::Interbase;
248
249    #[test]
250    fn serialize() {
251        let position = Position::<Interbase>::from(0u8);
252
253        let mut buffer = String::new();
254        write!(&mut buffer, "{position}").unwrap();
255        assert_eq!(buffer, "0");
256
257        buffer.clear();
258        write!(&mut buffer, "{position:#}").unwrap();
259        assert_eq!(buffer, "0 (interbase coordinate system)");
260    }
261}