gtars_core/models/region.rs
1use md5::{Digest, Md5};
2use std::fmt::{self, Display};
3
4use super::coords::CoordinateMode;
5
6///
7/// Region struct, representation of one Region in RegionSet files
8///
9#[derive(Eq, PartialEq, Hash, Debug, Clone)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct Region {
12 pub chr: String,
13 pub start: u32,
14 pub end: u32,
15
16 pub rest: Option<String>,
17}
18
19impl Region {
20 ///
21 /// Get length of the file
22 ///
23 pub fn width(&self) -> u32 {
24 self.end - self.start
25 }
26
27 ///
28 /// Get file string of Region
29 ///
30 pub fn as_string(&self) -> String {
31 format!(
32 "{}\t{}\t{}{}",
33 self.chr,
34 self.start,
35 self.end,
36 self.rest
37 .as_deref()
38 .map_or(String::new(), |s| format!("\t{}", s)),
39 )
40 }
41
42 ///
43 /// Calculate digest for the Region
44 ///
45 pub fn digest(&self) -> String {
46 let digest_string = format!("{},{},{}", self.chr, self.start, self.end);
47
48 let mut hasher = Md5::new();
49 hasher.update(digest_string);
50 let chrom_hash = hasher.finalize();
51 format!("{:x}", chrom_hash)
52 }
53
54 /// Calculate the midpoint of this region: `start + width / 2`.
55 ///
56 /// NOTE: R's GenomicDistributions computes midpoints using banker's
57 /// rounding in 1-based coordinates: `start + round((end - start) / 2)`.
58 /// For regions with width ≡ 2 (mod 4), this picks the left-of-center
59 /// base while our formula picks right-of-center, causing a ±1 bp
60 /// difference in ~2.6% of feature distance calculations. This is a
61 /// known discrepancy; to match GD exactly, change the formula to:
62 /// `if w % 4 == 2 { start + w/2 - 1 } else { start + w/2 }`.
63 pub fn mid_point(&self) -> u32 {
64 self.start + self.width() / 2
65 }
66
67 /// Calculate midpoint using the specified coordinate convention.
68 ///
69 /// - `Bed` (default): floor division → `start + width / 2`
70 /// - `GRanges`: banker's rounding in 1-based coords →
71 /// `if w % 4 == 2 { start + w/2 - 1 } else { start + w/2 }`
72 pub fn mid_point_with_mode(&self, mode: CoordinateMode) -> u32 {
73 match mode {
74 CoordinateMode::Bed => self.start + self.width() / 2,
75 CoordinateMode::GRanges => {
76 let w = self.width();
77 if w % 4 == 2 {
78 self.start + w / 2 - 1
79 } else {
80 self.start + w / 2
81 }
82 }
83 }
84 }
85
86 /// Gap distance between two regions.
87 ///
88 /// Returns 0 if the regions overlap, otherwise returns the positive
89 /// gap (in bases) between the closer edges of the two regions.
90 pub fn distance_to(&self, other: &Region) -> i64 {
91 if self.start < other.end && other.start < self.end {
92 0i64
93 } else if other.end <= self.start {
94 (self.start as i64) - (other.end as i64)
95 } else {
96 (other.start as i64) - (self.end as i64)
97 }
98 }
99}
100
101impl Display for Region {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 write!(f, "{}", self.as_string())
104 }
105}
106
107// TODO:
108// impl Display for ChromosomeStats {
109// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110//
111// }
112// }