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}