Skip to main content

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