1#[derive(Clone, Copy, Debug, PartialEq)]
4pub enum SpanUnit {
5 Bars,
6 Seconds,
7}
8
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct FileSpan {
11 pub amount: f64,
12 pub unit: SpanUnit,
13}
14
15pub fn parse_filename(name: &str) -> (String, Option<FileSpan>) {
18 let Some((base, suffix)) = name.rsplit_once('-') else {
19 return (name.to_string(), None);
20 };
21 let Some(unit_char) = suffix.chars().last() else {
22 return (name.to_string(), None);
23 };
24 let unit = match unit_char {
25 'b' => SpanUnit::Bars,
26 's' => SpanUnit::Seconds,
27 _ => return (name.to_string(), None),
28 };
29 let number_part = &suffix[..suffix.len() - 1];
30 match number_part.parse::<f64>() {
31 Ok(amount) if amount.is_finite() && !number_part.is_empty() => {
32 (base.to_string(), Some(FileSpan { amount, unit }))
33 }
34 _ => (name.to_string(), None),
35 }
36}
37
38#[cfg(test)]
39mod tests {
40 use super::*;
41
42 #[test]
43 fn bars_suffix_parses() {
44 assert_eq!(
45 parse_filename("kick-4b"),
46 (
47 "kick".to_string(),
48 Some(FileSpan {
49 amount: 4.0,
50 unit: SpanUnit::Bars
51 })
52 )
53 );
54 assert_eq!(
55 parse_filename("pattern-1b"),
56 (
57 "pattern".to_string(),
58 Some(FileSpan {
59 amount: 1.0,
60 unit: SpanUnit::Bars
61 })
62 )
63 );
64 }
65
66 #[test]
67 fn seconds_suffix_parses_including_fractional() {
68 assert_eq!(
69 parse_filename("pluck-1.5s"),
70 (
71 "pluck".to_string(),
72 Some(FileSpan {
73 amount: 1.5,
74 unit: SpanUnit::Seconds
75 })
76 )
77 );
78 }
79
80 #[test]
81 fn a_non_span_trailing_segment_leaves_the_name_plain() {
82 assert_eq!(parse_filename("kick-808"), ("kick-808".to_string(), None));
83 assert_eq!(parse_filename("lead-dry"), ("lead-dry".to_string(), None));
84 assert_eq!(parse_filename("kick"), ("kick".to_string(), None));
85 assert_eq!(parse_filename("song-"), ("song-".to_string(), None));
86 assert_eq!(parse_filename("song-b"), ("song-b".to_string(), None));
87 }
88}