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