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    // 1 step = 1/4 beat (subdivision of a beat, e.g., 16th note at 4/4 time)
78    let multiplier = match unit_singular {
79        "step" => 0.25,
80        "beat" => 1.0,
81        "bar" => 4.0,
82        "measure" => 4.0,
83        _ => return None,
84    };
85
86    // We return Identifier with a special format to be resolved at runtime
87    // Format: "__temporal__<beats>_beat" (always in beats, normalized)
88    let beat_count = count * multiplier;
89    let identifier = format!("__temporal__{}_beat", beat_count);
90    Some(DurationValue::Identifier(identifier))
91}
92
93fn parse_fraction(token: &str) -> Option<f32> {
94    let mut split = token.split('/');
95    let numerator: f32 = split.next()?.trim().parse().ok()?;
96    let denominator: f32 = split.next()?.trim().parse().ok()?;
97    if denominator.abs() < f32::EPSILON {
98        return None;
99    }
100    Some(numerator / denominator)
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_duration_auto() {
109        assert!(matches!(
110            parse_duration_token("auto"),
111            Ok(DurationValue::Auto)
112        ));
113        assert!(matches!(
114            parse_duration_token("AUTO"),
115            Ok(DurationValue::Auto)
116        ));
117    }
118
119    #[test]
120    fn test_duration_milliseconds() {
121        // Plain number
122        match parse_duration_token("500") {
123            Ok(DurationValue::Milliseconds(ms)) => assert_eq!(ms, 500.0),
124            _ => panic!("Expected milliseconds"),
125        }
126
127        // With ms suffix
128        match parse_duration_token("500ms") {
129            Ok(DurationValue::Milliseconds(ms)) => assert_eq!(ms, 500.0),
130            _ => panic!("Expected milliseconds"),
131        }
132
133        match parse_duration_token("1000ms") {
134            Ok(DurationValue::Milliseconds(ms)) => assert_eq!(ms, 1000.0),
135            _ => panic!("Expected milliseconds"),
136        }
137    }
138
139    #[test]
140    fn test_duration_fractions() {
141        // Quarter note
142        match parse_duration_token("1/4") {
143            Ok(DurationValue::Beats(b)) => assert_eq!(b, 0.25),
144            _ => panic!("Expected beats"),
145        }
146
147        // Half note
148        match parse_duration_token("1/2") {
149            Ok(DurationValue::Beats(b)) => assert_eq!(b, 0.5),
150            _ => panic!("Expected beats"),
151        }
152
153        // Full note
154        match parse_duration_token("1/1") {
155            Ok(DurationValue::Beats(b)) => assert_eq!(b, 1.0),
156            _ => panic!("Expected beats"),
157        }
158    }
159
160    #[test]
161    fn test_temporal_beat_with_space() {
162        // "1 beat"
163        match parse_duration_token("1 beat") {
164            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beats"),
165            other => panic!("Unexpected result: {:?}", other),
166        }
167
168        // "2 beat"
169        match parse_duration_token("2 beat") {
170            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__2_beats"),
171            other => panic!("Unexpected result: {:?}", other),
172        }
173
174        // "1 beats" (plural)
175        match parse_duration_token("1 beats") {
176            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beats"),
177            other => panic!("Unexpected result: {:?}", other),
178        }
179
180        // "3 beats" (plural)
181        match parse_duration_token("3 beats") {
182            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__3_beats"),
183            other => panic!("Unexpected result: {:?}", other),
184        }
185    }
186
187    #[test]
188    fn test_temporal_beat_without_space() {
189        // "1beat"
190        match parse_duration_token("1beat") {
191            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beats"),
192            other => panic!("Unexpected result: {:?}", other),
193        }
194
195        // "2beat"
196        match parse_duration_token("2beat") {
197            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__2_beats"),
198            other => panic!("Unexpected result: {:?}", other),
199        }
200
201        // "1beats" (without space, with 's')
202        match parse_duration_token("1beats") {
203            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beats"),
204            other => panic!("Unexpected result: {:?}", other),
205        }
206
207        // "3beats" (without space, with 's')
208        match parse_duration_token("3beats") {
209            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__3_beats"),
210            other => panic!("Unexpected result: {:?}", other),
211        }
212    }
213
214    #[test]
215    fn test_temporal_bar_with_space() {
216        // "1 bar" -> 1 * 4 beats = 4
217        match parse_duration_token("1 bar") {
218            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_bars"),
219            other => panic!("Unexpected result: {:?}", other),
220        }
221
222        // "2 bar"
223        match parse_duration_token("2 bar") {
224            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__8_bars"),
225            other => panic!("Unexpected result: {:?}", other),
226        }
227
228        // "1 bars" (plural)
229        match parse_duration_token("1 bars") {
230            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_bars"),
231            other => panic!("Unexpected result: {:?}", other),
232        }
233
234        // "3 bars" (plural)
235        match parse_duration_token("3 bars") {
236            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__12_bars"),
237            other => panic!("Unexpected result: {:?}", other),
238        }
239    }
240
241    #[test]
242    fn test_temporal_bar_without_space() {
243        // "1bar" -> 4 beats
244        match parse_duration_token("1bar") {
245            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_bars"),
246            other => panic!("Unexpected result: {:?}", other),
247        }
248
249        // "2bar"
250        match parse_duration_token("2bar") {
251            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__8_bars"),
252            other => panic!("Unexpected result: {:?}", other),
253        }
254
255        // "1bars" (without space, with 's')
256        match parse_duration_token("1bars") {
257            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_bars"),
258            other => panic!("Unexpected result: {:?}", other),
259        }
260
261        // "3bars" (without space, with 's')
262        match parse_duration_token("3bars") {
263            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__12_bars"),
264            other => panic!("Unexpected result: {:?}", other),
265        }
266    }
267
268    #[test]
269    fn test_temporal_measure_with_space() {
270        // "1 measure" (same as bar: 4 beats)
271        match parse_duration_token("1 measure") {
272            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_measures"),
273            other => panic!("Unexpected result: {:?}", other),
274        }
275
276        // "2 measure"
277        match parse_duration_token("2 measure") {
278            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__8_measures"),
279            other => panic!("Unexpected result: {:?}", other),
280        }
281
282        // "1 measures" (plural)
283        match parse_duration_token("1 measures") {
284            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_measures"),
285            other => panic!("Unexpected result: {:?}", other),
286        }
287
288        // "3 measures" (plural)
289        match parse_duration_token("3 measures") {
290            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__12_measures"),
291            other => panic!("Unexpected result: {:?}", other),
292        }
293    }
294
295    #[test]
296    fn test_temporal_measure_without_space() {
297        // "1measure" -> 4 beats
298        match parse_duration_token("1measure") {
299            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_measures"),
300            other => panic!("Unexpected result: {:?}", other),
301        }
302
303        // "2measure"
304        match parse_duration_token("2measure") {
305            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__8_measures"),
306            other => panic!("Unexpected result: {:?}", other),
307        }
308
309        // "1measures" (without space, with 's')
310        match parse_duration_token("1measures") {
311            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_measures"),
312            other => panic!("Unexpected result: {:?}", other),
313        }
314
315        // "3measures" (without space, with 's')
316        match parse_duration_token("3measures") {
317            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__12_measures"),
318            other => panic!("Unexpected result: {:?}", other),
319        }
320    }
321
322    #[test]
323    fn test_temporal_step_with_space() {
324        // "1 step" -> 1 * 0.25 beats = 0.25
325        match parse_duration_token("1 step") {
326            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__0.25_steps"),
327            other => panic!("Unexpected result: {:?}", other),
328        }
329
330        // "4 step"
331        match parse_duration_token("4 step") {
332            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_steps"),
333            other => panic!("Unexpected result: {:?}", other),
334        }
335
336        // "1 steps" (plural)
337        match parse_duration_token("1 steps") {
338            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__0.25_steps"),
339            other => panic!("Unexpected result: {:?}", other),
340        }
341
342        // "16 steps" (plural)
343        match parse_duration_token("16 steps") {
344            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_steps"),
345            other => panic!("Unexpected result: {:?}", other),
346        }
347    }
348
349    #[test]
350    fn test_temporal_step_without_space() {
351        // "1step" -> 0.25 beats
352        match parse_duration_token("1step") {
353            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__0.25_steps"),
354            other => panic!("Unexpected result: {:?}", other),
355        }
356
357        // "4step"
358        match parse_duration_token("4step") {
359            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_steps"),
360            other => panic!("Unexpected result: {:?}", other),
361        }
362
363        // "1steps" (without space, with 's')
364        match parse_duration_token("1steps") {
365            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__0.25_steps"),
366            other => panic!("Unexpected result: {:?}", other),
367        }
368
369        // "16steps" (without space, with 's')
370        match parse_duration_token("16steps") {
371            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_steps"),
372            other => panic!("Unexpected result: {:?}", other),
373        }
374    }
375
376    #[test]
377    fn test_mixed_case() {
378        // Case insensitive
379        match parse_duration_token("1 BEAT") {
380            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beats"),
381            other => panic!("Unexpected result: {:?}", other),
382        }
383
384        match parse_duration_token("2 Bar") {
385            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__8_bars"),
386            other => panic!("Unexpected result: {:?}", other),
387        }
388
389        match parse_duration_token("1MEASURE") {
390            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_measures"),
391            other => panic!("Unexpected result: {:?}", other),
392        }
393    }
394
395    #[test]
396    fn test_float_values() {
397        // Float beat values
398        match parse_duration_token("0.5 beat") {
399            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__0.5_beats"),
400            other => panic!("Unexpected result: {:?}", other),
401        }
402
403        // Float bar values
404        match parse_duration_token("1.5bar") {
405            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__6_bars"),
406            other => panic!("Unexpected result: {:?}", other),
407        }
408
409        // Float measure values
410        match parse_duration_token("2.5 measures") {
411            Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__10_measures"),
412            other => panic!("Unexpected result: {:?}", other),
413        }
414    }
415}