Skip to main content

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// }