omics_coordinate/
coordinate.rs

1//! Coordinates.
2
3use omics_core::VARIANT_SEPARATOR;
4use thiserror::Error;
5
6use crate::Contig;
7use crate::Position;
8use crate::Strand;
9use crate::System;
10use crate::position;
11use crate::position::Number;
12use crate::strand;
13
14pub mod base;
15pub mod interbase;
16
17////////////////////////////////////////////////////////////////////////////////////////
18// Errors
19////////////////////////////////////////////////////////////////////////////////////////
20
21/// A parsing error related to a coordinate.
22#[derive(Error, Debug, PartialEq, Eq)]
23pub enum ParseError {
24    /// An invalid format was encountered.
25    ///
26    /// This generally occurs when an incorrect number of colons (`:`) is used.
27    #[error("invalid coordinate format: {value}")]
28    Format {
29        /// The value that was passed.
30        value: String,
31    },
32}
33
34/// A [`Result`](std::result::Result) with a [`ParseError`].
35pub type ParseResult<T> = std::result::Result<T, ParseError>;
36
37/// An error related to a coordinate.
38#[derive(Error, Debug, PartialEq, Eq)]
39pub enum Error {
40    /// A strand error.
41    #[error("strand error: {0}")]
42    Strand(#[from] strand::Error),
43
44    /// A parse error.
45    #[error("parse error: {0}")]
46    Parse(#[from] ParseError),
47
48    /// A position error.
49    #[error("position error: {0}")]
50    Position(#[from] position::Error),
51}
52
53/// A [`Result`](std::result::Result) with an [`Error`].
54pub type Result<T> = std::result::Result<T, Error>;
55
56////////////////////////////////////////////////////////////////////////////////////////
57// The `Coordinate` trait
58////////////////////////////////////////////////////////////////////////////////////////
59
60/// Traits related to a coordinate.
61pub mod r#trait {
62    use super::*;
63
64    /// Requirements to be a coordinate.
65    pub trait Coordinate<S: System>:
66        std::fmt::Display
67        + std::fmt::Debug
68        + PartialEq
69        + Eq
70        + PartialOrd
71        + Ord
72        + std::str::FromStr<Err = Error>
73    where
74        Self: Sized,
75    {
76        /// Attempts to create a new coordinate.
77        fn try_new(
78            contig: impl Into<Contig>,
79            strand: impl TryInto<Strand, Error = strand::Error>,
80            position: Number,
81        ) -> Result<Self>;
82    }
83}
84
85////////////////////////////////////////////////////////////////////////////////////////
86// Coordinate
87////////////////////////////////////////////////////////////////////////////////////////
88
89/// A coordinate.
90#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
91pub struct Coordinate<S: System> {
92    /// The coordinate system.
93    system: S,
94
95    /// The contig.
96    contig: Contig,
97
98    /// The strand.
99    strand: Strand,
100
101    /// The position.
102    position: Position<S>,
103}
104
105impl<S: System> Coordinate<S>
106where
107    Position<S>: position::r#trait::Position<S>,
108{
109    /// Creates a new coordinate;
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use omics_coordinate::Contig;
115    /// use omics_coordinate::Coordinate;
116    /// use omics_coordinate::Strand;
117    /// use omics_coordinate::position::interbase::Position;
118    /// use omics_coordinate::system::Interbase;
119    ///
120    /// let contig = Contig::new("chr1");
121    /// let position = Position::new(0);
122    /// let strand = Strand::Positive;
123    ///
124    /// let coordinate = Coordinate::new(contig, strand, position);
125    /// ```
126    pub fn new(
127        contig: impl Into<Contig>,
128        strand: impl Into<Strand>,
129        position: impl Into<Position<S>>,
130    ) -> Self {
131        let contig = contig.into();
132        let strand = strand.into();
133        let position = position.into();
134
135        Self {
136            system: Default::default(),
137            contig,
138            strand,
139            position,
140        }
141    }
142
143    /// Attempts to create a new coordinate.
144    pub fn try_new(
145        contig: impl Into<Contig>,
146        strand: impl TryInto<Strand, Error = strand::Error>,
147        position: Number,
148    ) -> Result<Self>
149    where
150        Self: r#trait::Coordinate<S>,
151    {
152        <Self as r#trait::Coordinate<S>>::try_new(contig, strand, position)
153    }
154
155    /// Gets the contig for this coordinate by reference.
156    ///
157    /// # Examples
158    ///
159    /// ```
160    /// use omics_coordinate::Coordinate;
161    /// use omics_coordinate::system::Interbase;
162    ///
163    /// let coordinate = "seq0:+:1".parse::<Coordinate<Interbase>>()?;
164    /// assert_eq!(coordinate.contig().as_str(), "seq0");
165    ///
166    /// # Ok::<(), Box<dyn std::error::Error>>(())
167    /// ```
168    pub fn contig(&self) -> &Contig {
169        &self.contig
170    }
171
172    /// Consumes `self` and returns the inner contig from this
173    /// coordinate.
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// use omics_coordinate::Coordinate;
179    /// use omics_coordinate::system::Interbase;
180    ///
181    /// let coordinate = "seq0:+:1".parse::<Coordinate<Interbase>>()?;
182    /// assert_eq!(coordinate.into_contig().into_inner(), String::from("seq0"));
183    ///
184    /// # Ok::<(), Box<dyn std::error::Error>>(())
185    /// ```
186    pub fn into_contig(self) -> Contig {
187        self.contig
188    }
189
190    /// Gets the strand for this coordinate by reference.
191    ///
192    /// # Examples
193    ///
194    /// ```
195    /// use omics_coordinate::Coordinate;
196    /// use omics_coordinate::Strand;
197    /// use omics_coordinate::system::Interbase;
198    ///
199    /// let coordinate = "seq0:+:1".parse::<Coordinate<Interbase>>()?;
200    /// assert_eq!(coordinate.strand(), Strand::Positive);
201    ///
202    /// # Ok::<(), Box<dyn std::error::Error>>(())
203    /// ```
204    pub fn strand(&self) -> Strand {
205        self.strand
206    }
207
208    /// Gets the position for this coordinate by reference.
209    ///
210    /// # Examples
211    ///
212    /// ```
213    /// use omics_coordinate::Coordinate;
214    /// use omics_coordinate::Position;
215    /// use omics_coordinate::system::Interbase;
216    ///
217    /// let coordinate = "seq0:+:1".parse::<Coordinate<Interbase>>()?;
218    /// assert_eq!(coordinate.position().get(), 1);
219    ///
220    /// # Ok::<(), Box<dyn std::error::Error>>(())
221    /// ```
222    pub fn position(&self) -> &Position<S> {
223        &self.position
224    }
225
226    /// Consumes `self` and returns the inner position from this
227    /// coordinate.
228    ///
229    /// # Examples
230    ///
231    /// ```
232    /// use omics_coordinate::Coordinate;
233    /// use omics_coordinate::Strand;
234    /// use omics_coordinate::system::Interbase;
235    ///
236    /// let coordinate = "seq0:+:1".parse::<Coordinate<Interbase>>()?;
237    /// assert_eq!(coordinate.into_position().get(), 1);
238    ///
239    /// # Ok::<(), Box<dyn std::error::Error>>(())
240    /// ```
241    pub fn into_position(self) -> Position<S> {
242        self.position
243    }
244
245    /// Consumes `self` to return the parts that comprise this coordinate.
246    ///
247    /// # Examples
248    ///
249    /// ```
250    /// use omics_coordinate::Contig;
251    /// use omics_coordinate::Coordinate;
252    /// use omics_coordinate::Strand;
253    /// use omics_coordinate::system::Interbase;
254    ///
255    /// let coordinate = "seq0:+:1".parse::<Coordinate<Interbase>>()?;
256    ///
257    /// let (contig, strand, position) = coordinate.into_parts();
258    /// assert_eq!(contig.into_inner(), String::from("seq0"));
259    /// assert_eq!(strand, Strand::Positive);
260    /// assert_eq!(position.get(), 1);
261    ///
262    /// # Ok::<(), Box<dyn std::error::Error>>(())
263    /// ```
264    pub fn into_parts(self) -> (Contig, Strand, Position<S>) {
265        (self.contig, self.strand, self.position)
266    }
267
268    /// Consumes `self` and attempts to move the position forward by
269    /// `magnitude`.
270    ///
271    /// This method is dependent on the strand of the coordinate:
272    ///
273    /// - a coordinate on the [`Strand::Positive`] moves positively, and
274    /// - a coordinate on the [`Strand::Negative`] move negatively.
275    ///
276    /// # Examples
277    ///
278    /// ```
279    /// use omics_coordinate::Contig;
280    /// use omics_coordinate::Coordinate;
281    /// use omics_coordinate::Position;
282    /// use omics_coordinate::Strand;
283    /// use omics_coordinate::position::Number;
284    /// use omics_coordinate::system::Base;
285    /// use omics_coordinate::system::Interbase;
286    ///
287    /// // Interbase.
288    ///
289    /// let start = "seq0:+:0".parse::<Coordinate<Interbase>>()?;
290    /// let coordinate = start.clone().move_forward(10).expect("coordinate to move");
291    /// assert_eq!(coordinate.contig().as_str(), "seq0");
292    /// assert_eq!(coordinate.strand(), Strand::Positive);
293    /// assert_eq!(coordinate.position().get(), 10);
294    ///
295    /// let coordinate = start.move_forward(Number::MAX).expect("coordinate to move");
296    /// assert_eq!(coordinate.contig().as_str(), "seq0");
297    /// assert_eq!(coordinate.strand(), Strand::Positive);
298    /// assert_eq!(coordinate.position().get(), Number::MAX);
299    ///
300    /// let coordinate = coordinate.move_forward(1);
301    /// assert!(coordinate.is_none());
302    ///
303    /// // Base.
304    ///
305    /// let start = "seq0:+:1".parse::<Coordinate<Base>>()?;
306    /// let coordinate = start.clone().move_forward(10).expect("coordinate to move");
307    /// assert_eq!(coordinate.contig().as_str(), "seq0");
308    /// assert_eq!(coordinate.strand(), Strand::Positive);
309    /// assert_eq!(coordinate.position().get(), 11);
310    ///
311    /// let coordinate = start
312    ///     .move_forward(Number::MAX - 1)
313    ///     .expect("coordinate to move");
314    /// assert_eq!(coordinate.contig().as_str(), "seq0");
315    /// assert_eq!(coordinate.strand(), Strand::Positive);
316    /// assert_eq!(coordinate.position().get(), Number::MAX);
317    ///
318    /// let coordinate = coordinate.move_forward(1);
319    /// assert!(coordinate.is_none());
320    ///
321    /// # Ok::<(), Box<dyn std::error::Error>>(())
322    /// ```
323    pub fn move_forward(self, magnitude: Number) -> Option<Coordinate<S>> {
324        if magnitude == 0 {
325            return Some(self);
326        }
327
328        match self.strand {
329            Strand::Positive => self.position.checked_add(magnitude),
330            Strand::Negative => self.position.checked_sub(magnitude),
331        }
332        .map(|position| Self::new(self.contig.clone(), self.strand, position))
333    }
334
335    /// Consumes `self` and attempts to move the position backwards by
336    /// `magnitude`.
337    ///
338    /// This method is dependent on the strand of the coordinate:
339    ///
340    /// - a coordinate on the [`Strand::Positive`] moves negatively, and
341    /// - a coordinate on the [`Strand::Negative`] move positively.
342    ///
343    /// # Examples
344    ///
345    /// ```
346    /// use omics_coordinate::Contig;
347    /// use omics_coordinate::Coordinate;
348    /// use omics_coordinate::Position;
349    /// use omics_coordinate::Strand;
350    /// use omics_coordinate::position::Number;
351    /// use omics_coordinate::system::Base;
352    /// use omics_coordinate::system::Interbase;
353    ///
354    /// let value = format!("seq0:+:{}", Number::MAX);
355    ///
356    /// // Interbase.
357    ///
358    /// let start = value.clone().parse::<Coordinate<Interbase>>()?;
359    /// let coordinate = start.clone().move_backward(10).expect("coordinate to move");
360    /// assert_eq!(coordinate.contig().as_str(), "seq0");
361    /// assert_eq!(coordinate.strand(), Strand::Positive);
362    /// assert_eq!(coordinate.position().get(), Number::MAX - 10);
363    ///
364    /// let coordinate = start
365    ///     .move_backward(Number::MAX)
366    ///     .expect("coordinate to move");
367    /// assert_eq!(coordinate.contig().as_str(), "seq0");
368    /// assert_eq!(coordinate.strand(), Strand::Positive);
369    /// assert_eq!(coordinate.position().get(), 0);
370    ///
371    /// let coordinate = coordinate.move_backward(1);
372    /// assert!(coordinate.is_none());
373    ///
374    /// // Base.
375    ///
376    /// let start = value.parse::<Coordinate<Base>>()?;
377    /// let coordinate = start.clone().move_backward(10).expect("coordinate to move");
378    /// assert_eq!(coordinate.contig().as_str(), "seq0");
379    /// assert_eq!(coordinate.strand(), Strand::Positive);
380    /// assert_eq!(coordinate.position().get(), Number::MAX - 10);
381    ///
382    /// let coordinate = start
383    ///     .move_backward(Number::MAX - 1)
384    ///     .expect("coordinate to move");
385    /// assert_eq!(coordinate.contig().as_str(), "seq0");
386    /// assert_eq!(coordinate.strand(), Strand::Positive);
387    /// assert_eq!(coordinate.position().get(), 1);
388    ///
389    /// let coordinate = coordinate.move_backward(1);
390    /// assert!(coordinate.is_none());
391    ///
392    /// # Ok::<(), Box<dyn std::error::Error>>(())
393    /// ```
394    pub fn move_backward(self, magnitude: Number) -> Option<Coordinate<S>> {
395        if magnitude == 0 {
396            return Some(self);
397        }
398
399        match self.strand {
400            Strand::Positive => self.position.checked_sub(magnitude),
401            Strand::Negative => self.position.checked_add(magnitude),
402        }
403        .map(|position| Self::new(self.contig.clone(), self.strand, position))
404    }
405
406    /// Swaps the strand of the coordinate.
407    ///
408    /// # Examples
409    ///
410    /// ```
411    /// use omics_coordinate::Coordinate;
412    /// use omics_coordinate::Strand;
413    /// use omics_coordinate::system::Base;
414    /// use omics_coordinate::system::Interbase;
415    ///
416    /// //===========//
417    /// // Interbase //
418    /// //===========//
419    ///
420    /// let coordinate = Coordinate::<Interbase>::try_new("seq0", "+", 10)?;
421    /// let swapped = coordinate.swap_strand();
422    /// assert_eq!(swapped.contig().as_str(), "seq0");
423    /// assert_eq!(swapped.strand(), Strand::Negative);
424    /// assert_eq!(swapped.position().get(), 10);
425    ///
426    /// //======//
427    /// // Base //
428    /// //======//
429    ///
430    /// let coordinate = Coordinate::<Base>::try_new("seq0", "-", 10)?;
431    /// let swapped = coordinate.swap_strand();
432    /// assert_eq!(swapped.contig().as_str(), "seq0");
433    /// assert_eq!(swapped.strand(), Strand::Positive);
434    /// assert_eq!(swapped.position().get(), 10);
435    ///
436    /// # Ok::<(), Box<dyn std::error::Error>>(())
437    /// ```
438    pub fn swap_strand(self) -> Coordinate<S> {
439        let (contig, strand, position) = self.into_parts();
440        Coordinate::new(contig, strand.complement(), position)
441    }
442}
443
444////////////////////////////////////////////////////////////////////////////////////////
445// Trait implementations
446////////////////////////////////////////////////////////////////////////////////////////
447
448impl<S: System> std::fmt::Display for Coordinate<S> {
449    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450        if !f.alternate() {
451            write!(f, "{}:{}:{}", self.contig, self.strand, self.position)
452        } else {
453            write!(
454                f,
455                "{}:{}:{} ({})",
456                self.contig, self.strand, self.position, self.system
457            )
458        }
459    }
460}
461
462impl<S: System> std::str::FromStr for Coordinate<S>
463where
464    Position<S>: position::r#trait::Position<S>,
465{
466    type Err = Error;
467
468    fn from_str(s: &str) -> Result<Self> {
469        let parts = s.split(VARIANT_SEPARATOR).collect::<Vec<_>>();
470
471        if parts.len() != 3 {
472            return Err(Error::Parse(ParseError::Format {
473                value: s.to_owned(),
474            }));
475        }
476
477        let mut parts = parts.iter();
478
479        // SAFETY: we checked that there are three parts above. Given that we
480        // haven't pulled anything from the iterator, we can always safely
481        // unwrap this.
482        let contig = parts
483            .next()
484            .unwrap()
485            .parse::<Contig>()
486            // SAFETY: this conversion is infallible.
487            .unwrap();
488
489        // SAFETY: we checked that there are three parts above. Given that we
490        // have only pulled one item from the iterator, we can always safely
491        // unwrap this.
492        let strand = parts
493            .next()
494            .unwrap()
495            .parse::<Strand>()
496            .map_err(Error::Strand)?;
497
498        // SAFETY: we checked that there are three parts above. Given that we
499        // have only pulled two items from the iterator, we can always safely
500        // unwrap this.
501        let position = parts
502            .next()
503            .unwrap()
504            .parse::<Position<S>>()
505            .map_err(Error::Position)?;
506
507        Ok(Self::new(contig, strand, position))
508    }
509}