ontime/
lib.rs

1use bstr::ByteSlice;
2use duration_str::DError;
3use lazy_static::lazy_static;
4use needletail::parser::SequenceRecord;
5use regex::bytes::Regex;
6use time::format_description::well_known::Rfc3339;
7use time::{Duration, PrimitiveDateTime};
8
9lazy_static! {
10    pub static ref DATETIME_RE: Regex = Regex::new(r"(start_time=|st:Z:)(?P<time>\S+)\s*").unwrap();
11}
12
13pub trait FastxRecordExt {
14    fn start_time(&self) -> Option<PrimitiveDateTime>;
15}
16
17impl FastxRecordExt for SequenceRecord<'_> {
18    fn start_time(&self) -> Option<PrimitiveDateTime> {
19        let caps = DATETIME_RE.captures(self.id())?;
20        let m = caps.name("time")?;
21        let datetime = m.as_bytes().to_str_lossy();
22        PrimitiveDateTime::parse(&datetime, &Rfc3339).ok()
23    }
24}
25
26pub trait DurationExt {
27    fn from_str(s: &str) -> Result<Self, DError>
28    where
29        Self: Sized;
30}
31
32impl DurationExt for Duration {
33    fn from_str(s: &str) -> Result<Self, DError> {
34        if let Some(pos_s) = s.strip_prefix('-') {
35            let dur = duration_str::parse_time(pos_s)?;
36            Ok(-1 * dur)
37        } else {
38            duration_str::parse_time(s)
39        }
40    }
41}
42
43pub fn valid_indices(
44    timestamps: &[PrimitiveDateTime],
45    earliest: &PrimitiveDateTime,
46    latest: &PrimitiveDateTime,
47) -> (Vec<bool>, usize) {
48    let mut to_keep: Vec<bool> = vec![false; timestamps.len()];
49    let mut nb_reads_to_keep = 0;
50    timestamps.iter().enumerate().for_each(|(i, t)| {
51        if earliest <= t && t <= latest {
52            to_keep[i] = true;
53            nb_reads_to_keep += 1;
54        }
55    });
56
57    (to_keep, nb_reads_to_keep)
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use needletail::parse_fastx_file;
64    use std::io::Write;
65    use tempfile::Builder;
66    use time::macros::{date, time};
67    use time::Duration;
68
69    #[test]
70    fn test_no_start_time() {
71        let text = "@read1\nA\n+\n1";
72        let mut file = Builder::new().suffix(".fa").tempfile().unwrap();
73        file.write_all(text.as_bytes()).unwrap();
74
75        let mut reader = parse_fastx_file(file.path()).unwrap();
76        let rec = reader.next().unwrap();
77        let record = rec.unwrap();
78
79        let actual = record.start_time();
80        let expected = None;
81
82        assert_eq!(actual, expected)
83    }
84
85    #[test]
86    fn test_start_time_old_valid() {
87        let text = "@read1 ch=352 start_time=2022-12-12T18:39:27Z model_version_id=2021\nA\n+\n1";
88        let mut file = Builder::new().suffix(".fa").tempfile().unwrap();
89        file.write_all(text.as_bytes()).unwrap();
90
91        let mut reader = parse_fastx_file(file.path()).unwrap();
92        let rec = reader.next().unwrap();
93        let record = rec.unwrap();
94
95        let actual = record.start_time().unwrap();
96        let expected = PrimitiveDateTime::new(date!(2022 - 12 - 12), time!(18:39:27));
97
98        assert_eq!(actual, expected)
99    }
100
101    #[test]
102    fn test_start_time_offset_valid() {
103        let text =
104            "@read1 ch=352 start_time=2021-07-08T17:47:25+01:00 model_version_id=2021\nA\n+\n1";
105        let mut file = Builder::new().suffix(".fa").tempfile().unwrap();
106        file.write_all(text.as_bytes()).unwrap();
107
108        let mut reader = parse_fastx_file(file.path()).unwrap();
109        let rec = reader.next().unwrap();
110        let record = rec.unwrap();
111
112        let actual = record.start_time().unwrap();
113        let expected = PrimitiveDateTime::new(date!(2021 - 07 - 08), time!(17:47:25));
114
115        assert_eq!(actual, expected)
116    }
117
118    #[test]
119    fn test_start_time_offset_with_micro_valid() {
120        let text = "@read1 ch=352 start_time=2021-07-08T17:47:25.558027+01:00 model_version_id=2021\nA\n+\n1";
121        let mut file = Builder::new().suffix(".fa").tempfile().unwrap();
122        file.write_all(text.as_bytes()).unwrap();
123
124        let mut reader = parse_fastx_file(file.path()).unwrap();
125        let rec = reader.next().unwrap();
126        let record = rec.unwrap();
127
128        let actual = record.start_time().unwrap();
129        let expected = PrimitiveDateTime::new(date!(2021 - 07 - 08), time!(17:47:25.558027));
130
131        assert_eq!(actual, expected)
132    }
133
134    #[test]
135    fn test_start_time_invalid_without_z() {
136        let text = "@read1 ch=352 start_time=2022-12-12T18:39:27 model_version_id=2021\nA\n+\n1";
137        let mut file = Builder::new().suffix(".fa").tempfile().unwrap();
138        file.write_all(text.as_bytes()).unwrap();
139
140        let mut reader = parse_fastx_file(file.path()).unwrap();
141        let rec = reader.next().unwrap();
142        let record = rec.unwrap();
143
144        let actual = record.start_time();
145        assert!(actual.is_none())
146    }
147
148    #[test]
149    fn test_start_time_invalid() {
150        let text = "@read1 ch=352 start_time=2022-12-12T18:39Z model_version_id=2021\nA\n+\n1";
151        let mut file = Builder::new().suffix(".fa").tempfile().unwrap();
152        file.write_all(text.as_bytes()).unwrap();
153
154        let mut reader = parse_fastx_file(file.path()).unwrap();
155        let rec = reader.next().unwrap();
156        let record = rec.unwrap();
157
158        let actual = record.start_time();
159        let expected = None;
160
161        assert_eq!(actual, expected)
162    }
163
164    #[test]
165    fn test_bam_tag_start_time_is_valid() {
166        let text = "@read1 st:Z:2023-08-07T13:14:42.356+00:00\nA\n+\n1";
167        let mut file = Builder::new().suffix(".fa").tempfile().unwrap();
168        file.write_all(text.as_bytes()).unwrap();
169
170        let mut reader = parse_fastx_file(file.path()).unwrap();
171        let rec = reader.next().unwrap();
172        let record = rec.unwrap();
173
174        let actual = record.start_time().unwrap();
175        let expected = PrimitiveDateTime::new(date!(2023 - 08 - 07), time!(13:14:42.356));
176
177        assert_eq!(actual, expected)
178    }
179
180    #[test]
181    fn test_duration_from_str_negative() {
182        let s = "-1h";
183        let actual = Duration::from_str(s).unwrap();
184        let expected = Duration::hours(-1);
185
186        assert_eq!(actual, expected)
187    }
188
189    #[test]
190    fn test_duration_from_str_negative_invalid() {
191        let s = "1d-1h";
192        let actual = Duration::from_str(s);
193        assert!(actual.is_err())
194    }
195
196    #[test]
197    fn test_duration_from_str() {
198        let s = "11h30min";
199        let actual = Duration::from_str(s).unwrap();
200        let expected = Duration::seconds(41_400);
201
202        assert_eq!(actual, expected)
203    }
204
205    #[test]
206    fn test_duration_from_str_invalid() {
207        let s = "11h30min12foo";
208        let actual = Duration::from_str(s);
209        assert!(actual.is_err())
210    }
211}