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}