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}