devalang_wasm/language/syntax/parser/driver/
duration.rs

1use crate::language::syntax::ast::DurationValue;
2use anyhow::{Result, anyhow};
3
4pub fn parse_duration_token(token: &str) -> Result<DurationValue> {
5    let token = token.trim();
6    if token.eq_ignore_ascii_case("auto") {
7        return Ok(DurationValue::Auto);
8    }
9
10    // Check for milliseconds suffix (e.g., "500ms")
11    if let Some(value) = token.strip_suffix("ms") {
12        let ms: f32 = value
13            .parse()
14            .map_err(|_| anyhow!("invalid milliseconds duration: '{}'", token))?;
15        return Ok(DurationValue::Milliseconds(ms));
16    }
17
18    // Check for bar/beat/measure suffixes (e.g., "1 bar", "2 beat", "3 measure")
19    if let Some(result) = parse_temporal_duration(token) {
20        return Ok(result);
21    }
22
23    // Check for fraction (e.g., "1/4", "1/8")
24    if let Some(fraction) = parse_fraction(token) {
25        return Ok(DurationValue::Beats(fraction));
26    }
27
28    // Try to parse as plain number (milliseconds)
29    if let Ok(number) = token.parse::<f32>() {
30        return Ok(DurationValue::Milliseconds(number));
31    }
32
33    // Fall back to identifier (variable reference)
34    Ok(DurationValue::Identifier(token.to_string()))
35}
36
37/// Parse temporal duration formats like "1 bar", "2 beat", "3 measure"
38/// Also supports: "1bar", "2beats", "1beat", "3 measures", etc.
39/// Returns millisecond-based duration proportional to BPM
40fn parse_temporal_duration(token: &str) -> Option<DurationValue> {
41    // Try parsing with spaces first (e.g., "1 bar", "2 beats")
42    let parts_with_space: Vec<&str> = token.split_whitespace().collect();
43    if parts_with_space.len() == 2 {
44        if let Some(result) = try_parse_temporal(parts_with_space[0], parts_with_space[1]) {
45            return Some(result);
46        }
47    }
48
49    // Try parsing without space (e.g., "1bar", "2beats")
50    // Find where the unit starts (first alphabetic character)
51    let split_pos = token.chars().position(|c| c.is_alphabetic())?;
52    if split_pos == 0 {
53        return None; // No number part
54    }
55
56    let num_str = &token[..split_pos];
57    let unit_str = &token[split_pos..];
58
59    try_parse_temporal(num_str, unit_str)
60}
61
62/// Helper function to parse temporal duration once we have number and unit separated
63fn try_parse_temporal(num_str: &str, unit_str: &str) -> Option<DurationValue> {
64    let count: f32 = num_str.trim().parse().ok()?;
65    let unit = unit_str.trim().to_lowercase();
66
67    // Remove trailing 's' if present (e.g., "beats" -> "beat", "bars" -> "bar")
68    let unit_singular = if unit.ends_with('s') && unit.len() > 1 {
69        &unit[..unit.len() - 1]
70    } else {
71        &unit
72    };
73
74    // 1 bar = 4 beats (at 4/4 time)
75    // 1 beat at 120 BPM = 500ms (60000ms / 120 BPM)
76    // 1 measure = 4 beats (same as bar)
77    let multiplier = match unit_singular {
78        "beat" => 1.0,
79        "bar" => 4.0,
80        "measure" => 4.0,
81        _ => return None,
82    };
83
84    // We return Identifier with a special format to be resolved at runtime
85    // Format: "__temporal__<count>_<unit>"
86    let identifier = format!("__temporal__{}_{}s", count * multiplier, unit_singular);
87    Some(DurationValue::Identifier(identifier))
88}
89
90fn parse_fraction(token: &str) -> Option<f32> {
91    let mut split = token.split('/');
92    let numerator: f32 = split.next()?.trim().parse().ok()?;
93    let denominator: f32 = split.next()?.trim().parse().ok()?;
94    if denominator.abs() < f32::EPSILON {
95        return None;
96    }
97    Some(numerator / denominator)
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_duration_auto() {
106        assert!(matches!(
107            parse_duration_token("auto"),
108            Ok(DurationValue::Auto)
109        ));
110        assert!(matches!(
111            parse_duration_token("AUTO"),
112            Ok(DurationValue::Auto)
113        ));
114    }
115
116    #[test]
117    fn test_duration_milliseconds() {
118        // Plain number
119        match parse_duration_token("500") {
120            Ok(DurationValue::Milliseconds(ms)) => assert_eq!(ms, 500.0),
121            _ => panic!("Expected milliseconds"),
122        }
123
124        // With ms suffix
125        match parse_duration_token("500ms") {
126            Ok(DurationValue::Milliseconds(ms)) => assert_eq!(ms, 500.0),
127            _ => panic!("Expected milliseconds"),
128        }
129
130        match parse_duration_token("1000ms") {
131            Ok(DurationValue::Milliseconds(ms)) => assert_eq!(ms, 1000.0),
132            _ => panic!("Expected milliseconds"),
133        }
134    }
135
136    #[test]
137    fn test_duration_fractions() {
138        // Quarter note
139        match parse_duration_token("1/4") {
140            Ok(DurationValue::Beats(b)) => assert_eq!(b, 0.25),
141            _ => panic!("Expected beats"),
142        }
143
144        // Half note
145        match parse_duration_token("1/2") {
146            Ok(DurationValue::Beats(b)) => assert_eq!(b, 0.5),
147            _ => panic!("Expected beats"),
148        }
149
150        // Full note
151        match parse_duration_token("1/1") {
152            Ok(DurationValue::Beats(b)) => assert_eq!(b, 1.0),
153            _ => panic!("Expected beats"),
154        }
155    }
156
157    #[test]
158    fn test_temporal_beat_with_space() {
159        // "1 beat"
160        match parse_duration_token("1 beat") {
161            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beats"),
162            other => panic!("Unexpected result: {:?}", other),
163        }
164
165        // "2 beat"
166        match parse_duration_token("2 beat") {
167            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__2_beats"),
168            other => panic!("Unexpected result: {:?}", other),
169        }
170
171        // "1 beats" (plural)
172        match parse_duration_token("1 beats") {
173            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beats"),
174            other => panic!("Unexpected result: {:?}", other),
175        }
176
177        // "3 beats" (plural)
178        match parse_duration_token("3 beats") {
179            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__3_beats"),
180            other => panic!("Unexpected result: {:?}", other),
181        }
182    }
183
184    #[test]
185    fn test_temporal_beat_without_space() {
186        // "1beat"
187        match parse_duration_token("1beat") {
188            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beats"),
189            other => panic!("Unexpected result: {:?}", other),
190        }
191
192        // "2beat"
193        match parse_duration_token("2beat") {
194            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__2_beats"),
195            other => panic!("Unexpected result: {:?}", other),
196        }
197
198        // "1beats" (without space, with 's')
199        match parse_duration_token("1beats") {
200            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beats"),
201            other => panic!("Unexpected result: {:?}", other),
202        }
203
204        // "3beats" (without space, with 's')
205        match parse_duration_token("3beats") {
206            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__3_beats"),
207            other => panic!("Unexpected result: {:?}", other),
208        }
209    }
210
211    #[test]
212    fn test_temporal_bar_with_space() {
213        // "1 bar" -> 1 * 4 beats = 4
214        match parse_duration_token("1 bar") {
215            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_bars"),
216            other => panic!("Unexpected result: {:?}", other),
217        }
218
219        // "2 bar"
220        match parse_duration_token("2 bar") {
221            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__8_bars"),
222            other => panic!("Unexpected result: {:?}", other),
223        }
224
225        // "1 bars" (plural)
226        match parse_duration_token("1 bars") {
227            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_bars"),
228            other => panic!("Unexpected result: {:?}", other),
229        }
230
231        // "3 bars" (plural)
232        match parse_duration_token("3 bars") {
233            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__12_bars"),
234            other => panic!("Unexpected result: {:?}", other),
235        }
236    }
237
238    #[test]
239    fn test_temporal_bar_without_space() {
240        // "1bar" -> 4 beats
241        match parse_duration_token("1bar") {
242            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_bars"),
243            other => panic!("Unexpected result: {:?}", other),
244        }
245
246        // "2bar"
247        match parse_duration_token("2bar") {
248            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__8_bars"),
249            other => panic!("Unexpected result: {:?}", other),
250        }
251
252        // "1bars" (without space, with 's')
253        match parse_duration_token("1bars") {
254            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_bars"),
255            other => panic!("Unexpected result: {:?}", other),
256        }
257
258        // "3bars" (without space, with 's')
259        match parse_duration_token("3bars") {
260            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__12_bars"),
261            other => panic!("Unexpected result: {:?}", other),
262        }
263    }
264
265    #[test]
266    fn test_temporal_measure_with_space() {
267        // "1 measure" (same as bar: 4 beats)
268        match parse_duration_token("1 measure") {
269            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_measures"),
270            other => panic!("Unexpected result: {:?}", other),
271        }
272
273        // "2 measure"
274        match parse_duration_token("2 measure") {
275            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__8_measures"),
276            other => panic!("Unexpected result: {:?}", other),
277        }
278
279        // "1 measures" (plural)
280        match parse_duration_token("1 measures") {
281            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_measures"),
282            other => panic!("Unexpected result: {:?}", other),
283        }
284
285        // "3 measures" (plural)
286        match parse_duration_token("3 measures") {
287            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__12_measures"),
288            other => panic!("Unexpected result: {:?}", other),
289        }
290    }
291
292    #[test]
293    fn test_temporal_measure_without_space() {
294        // "1measure" -> 4 beats
295        match parse_duration_token("1measure") {
296            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_measures"),
297            other => panic!("Unexpected result: {:?}", other),
298        }
299
300        // "2measure"
301        match parse_duration_token("2measure") {
302            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__8_measures"),
303            other => panic!("Unexpected result: {:?}", other),
304        }
305
306        // "1measures" (without space, with 's')
307        match parse_duration_token("1measures") {
308            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_measures"),
309            other => panic!("Unexpected result: {:?}", other),
310        }
311
312        // "3measures" (without space, with 's')
313        match parse_duration_token("3measures") {
314            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__12_measures"),
315            other => panic!("Unexpected result: {:?}", other),
316        }
317    }
318
319    #[test]
320    fn test_mixed_case() {
321        // Case insensitive
322        match parse_duration_token("1 BEAT") {
323            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beats"),
324            other => panic!("Unexpected result: {:?}", other),
325        }
326
327        match parse_duration_token("2 Bar") {
328            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__8_bars"),
329            other => panic!("Unexpected result: {:?}", other),
330        }
331
332        match parse_duration_token("1MEASURE") {
333            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_measures"),
334            other => panic!("Unexpected result: {:?}", other),
335        }
336    }
337
338    #[test]
339    fn test_float_values() {
340        // Float beat values
341        match parse_duration_token("0.5 beat") {
342            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__0.5_beats"),
343            other => panic!("Unexpected result: {:?}", other),
344        }
345
346        // Float bar values
347        match parse_duration_token("1.5bar") {
348            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__6_bars"),
349            other => panic!("Unexpected result: {:?}", other),
350        }
351
352        // Float measure values
353        match parse_duration_token("2.5 measures") {
354            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__10_measures"),
355            other => panic!("Unexpected result: {:?}", other),
356        }
357    }
358}