Skip to main content

sva_ast/
filename.rs

1// Concern: splits a bare filename into its label and optional span suffix | Non-concern: what the span means to `repeat`/`concat` (graph.rs) | IO: (&str) -> (label, Option<FileSpan>)
2
3#[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
15/// Splits on the LAST `-`; the trailing segment is a span only if it is `<number><b|s>`
16/// exactly. Anything else (including no `-` at all) leaves the whole name plain.
17pub 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}