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}