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}