1mod field;
4
5pub use self::field::Field;
6
7use std::{error, fmt, num, str::FromStr};
8
9use noodles_core::Position;
10
11const FIELD_DELIMITER: char = '\t';
12const MAX_FIELDS: usize = 6;
13
14#[derive(Clone, Debug, Default, Eq, PartialEq)]
16pub struct Record {
17 reference_sequence_id: Option<usize>,
18 alignment_start: Option<Position>,
19 alignment_span: usize,
20 offset: u64,
21 landmark: u64,
22 slice_length: u64,
23}
24
25impl Record {
26 pub fn new(
44 reference_sequence_id: Option<usize>,
45 alignment_start: Option<Position>,
46 alignment_span: usize,
47 offset: u64,
48 landmark: u64,
49 slice_length: u64,
50 ) -> Self {
51 Self {
52 reference_sequence_id,
53 alignment_start,
54 alignment_span,
55 offset,
56 landmark,
57 slice_length,
58 }
59 }
60
61 pub fn reference_sequence_id(&self) -> Option<usize> {
81 self.reference_sequence_id
82 }
83
84 pub fn alignment_start(&self) -> Option<Position> {
104 self.alignment_start
105 }
106
107 pub fn alignment_span(&self) -> usize {
127 self.alignment_span
128 }
129
130 pub fn offset(&self) -> u64 {
150 self.offset
151 }
152
153 pub fn landmark(&self) -> u64 {
173 self.landmark
174 }
175
176 pub fn slice_length(&self) -> u64 {
196 self.slice_length
197 }
198}
199
200impl fmt::Display for Record {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 const UNMAPPED: i32 = -1;
203
204 if let Some(id) = self.reference_sequence_id() {
205 write!(f, "{id}\t")?;
206 } else {
207 write!(f, "{UNMAPPED}\t")?;
208 };
209
210 let alignment_start = self.alignment_start().map(usize::from).unwrap_or_default();
211
212 write!(
213 f,
214 "{}\t{}\t{}\t{}\t{}",
215 alignment_start, self.alignment_span, self.offset, self.landmark, self.slice_length
216 )
217 }
218}
219
220#[derive(Clone, Debug, Eq, PartialEq)]
222pub enum ParseError {
223 Missing(Field),
225 Invalid(Field, std::num::ParseIntError),
227 InvalidReferenceSequenceId(num::TryFromIntError),
229}
230
231impl error::Error for ParseError {
232 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
233 match self {
234 Self::Missing(_) => None,
235 Self::Invalid(_, e) => Some(e),
236 Self::InvalidReferenceSequenceId(e) => Some(e),
237 }
238 }
239}
240
241impl fmt::Display for ParseError {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 match self {
244 Self::Missing(field) => write!(f, "missing field: {field:?}"),
245 Self::Invalid(field, _) => write!(f, "invalid field: {field:?}"),
246 Self::InvalidReferenceSequenceId(_) => f.write_str("invalid reference sequence ID"),
247 }
248 }
249}
250
251impl FromStr for Record {
252 type Err = ParseError;
253
254 fn from_str(s: &str) -> Result<Self, Self::Err> {
255 const UNMAPPED: i32 = -1;
256
257 let mut fields = s.splitn(MAX_FIELDS, FIELD_DELIMITER);
258
259 let reference_sequence_id =
260 parse_i32(&mut fields, Field::ReferenceSequenceId).and_then(|n| match n {
261 UNMAPPED => Ok(None),
262 _ => usize::try_from(n)
263 .map(Some)
264 .map_err(ParseError::InvalidReferenceSequenceId),
265 })?;
266
267 let alignment_start = parse_position(&mut fields, Field::AlignmentStart)?;
268 let alignment_span = parse_span(&mut fields, Field::AlignmentSpan)?;
269 let offset = parse_u64(&mut fields, Field::Offset)?;
270 let landmark = parse_u64(&mut fields, Field::Landmark)?;
271 let slice_length = parse_u64(&mut fields, Field::SliceLength)?;
272
273 Ok(Record::new(
274 reference_sequence_id,
275 alignment_start,
276 alignment_span,
277 offset,
278 landmark,
279 slice_length,
280 ))
281 }
282}
283
284fn parse_i32<'a, I>(fields: &mut I, field: Field) -> Result<i32, ParseError>
285where
286 I: Iterator<Item = &'a str>,
287{
288 fields
289 .next()
290 .ok_or(ParseError::Missing(field))
291 .and_then(|s| s.parse().map_err(|e| ParseError::Invalid(field, e)))
292}
293
294fn parse_u64<'a, I>(fields: &mut I, field: Field) -> Result<u64, ParseError>
295where
296 I: Iterator<Item = &'a str>,
297{
298 fields
299 .next()
300 .ok_or(ParseError::Missing(field))
301 .and_then(|s| s.parse().map_err(|e| ParseError::Invalid(field, e)))
302}
303
304fn parse_position<'a, I>(fields: &mut I, field: Field) -> Result<Option<Position>, ParseError>
305where
306 I: Iterator<Item = &'a str>,
307{
308 fields
309 .next()
310 .ok_or(ParseError::Missing(field))
311 .and_then(|s| s.parse().map_err(|e| ParseError::Invalid(field, e)))
312 .map(Position::new)
313}
314
315fn parse_span<'a, I>(fields: &mut I, field: Field) -> Result<usize, ParseError>
316where
317 I: Iterator<Item = &'a str>,
318{
319 fields
320 .next()
321 .ok_or(ParseError::Missing(field))
322 .and_then(|s| s.parse().map_err(|e| ParseError::Invalid(field, e)))
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn test_fmt() {
331 let record = Record::new(None, Position::new(10946), 6765, 17711, 233, 317811);
332 let actual = record.to_string();
333 let expected = "-1\t10946\t6765\t17711\t233\t317811";
334 assert_eq!(actual, expected);
335 }
336
337 #[test]
338 fn test_from_str() -> Result<(), Box<dyn std::error::Error>> {
339 let actual: Record = "0\t10946\t6765\t17711\t233\t317811".parse()?;
340
341 let expected = Record {
342 reference_sequence_id: Some(0),
343 alignment_start: Position::new(10946),
344 alignment_span: 6765,
345 offset: 17711,
346 landmark: 233,
347 slice_length: 317811,
348 };
349
350 assert_eq!(actual, expected);
351
352 Ok(())
353 }
354
355 #[test]
356 fn test_from_str_with_invalid_records() {
357 assert_eq!(
358 "0\t10946".parse::<Record>(),
359 Err(ParseError::Missing(Field::AlignmentSpan))
360 );
361
362 assert!(matches!(
363 "0\t10946\tnoodles".parse::<Record>(),
364 Err(ParseError::Invalid(Field::AlignmentSpan, _))
365 ));
366
367 assert!(matches!(
368 "-8\t10946\t6765\t17711\t233\t317811".parse::<Record>(),
369 Err(ParseError::InvalidReferenceSequenceId(_))
370 ));
371 }
372}