use crate::language::syntax::ast::DurationValue;
use anyhow::{Result, anyhow};
pub fn parse_duration_token(token: &str) -> Result<DurationValue> {
let token = token.trim();
if token.eq_ignore_ascii_case("auto") {
return Ok(DurationValue::Auto);
}
if let Some(value) = token.strip_suffix("ms") {
let ms: f32 = value
.parse()
.map_err(|_| anyhow!("invalid milliseconds duration: '{}'", token))?;
return Ok(DurationValue::Milliseconds(ms));
}
if let Some(result) = parse_temporal_duration(token) {
return Ok(result);
}
if let Some(fraction) = parse_fraction(token) {
return Ok(DurationValue::Beats(fraction));
}
if let Ok(number) = token.parse::<f32>() {
return Ok(DurationValue::Milliseconds(number));
}
Ok(DurationValue::Identifier(token.to_string()))
}
fn parse_temporal_duration(token: &str) -> Option<DurationValue> {
let parts_with_space: Vec<&str> = token.split_whitespace().collect();
if parts_with_space.len() == 2 {
if let Some(result) = try_parse_temporal(parts_with_space[0], parts_with_space[1]) {
return Some(result);
}
}
let split_pos = token.chars().position(|c| c.is_alphabetic())?;
if split_pos == 0 {
return None; }
let num_str = &token[..split_pos];
let unit_str = &token[split_pos..];
try_parse_temporal(num_str, unit_str)
}
fn try_parse_temporal(num_str: &str, unit_str: &str) -> Option<DurationValue> {
let count: f32 = num_str.trim().parse().ok()?;
let unit = unit_str.trim().to_lowercase();
let unit_singular = if unit.ends_with('s') && unit.len() > 1 {
&unit[..unit.len() - 1]
} else {
&unit
};
let multiplier = match unit_singular {
"step" => 0.25,
"beat" => 1.0,
"bar" => 4.0,
"measure" => 4.0,
_ => return None,
};
let beat_count = count * multiplier;
let identifier = format!("__temporal__{}_beat", beat_count);
Some(DurationValue::Identifier(identifier))
}
fn parse_fraction(token: &str) -> Option<f32> {
let mut split = token.split('/');
let numerator: f32 = split.next()?.trim().parse().ok()?;
let denominator: f32 = split.next()?.trim().parse().ok()?;
if denominator.abs() < f32::EPSILON {
return None;
}
Some(numerator / denominator)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_duration_auto() {
assert!(matches!(
parse_duration_token("auto"),
Ok(DurationValue::Auto)
));
assert!(matches!(
parse_duration_token("AUTO"),
Ok(DurationValue::Auto)
));
}
#[test]
fn test_duration_milliseconds() {
match parse_duration_token("500") {
Ok(DurationValue::Milliseconds(ms)) => assert_eq!(ms, 500.0),
_ => panic!("Expected milliseconds"),
}
match parse_duration_token("500ms") {
Ok(DurationValue::Milliseconds(ms)) => assert_eq!(ms, 500.0),
_ => panic!("Expected milliseconds"),
}
match parse_duration_token("1000ms") {
Ok(DurationValue::Milliseconds(ms)) => assert_eq!(ms, 1000.0),
_ => panic!("Expected milliseconds"),
}
}
#[test]
fn test_duration_fractions() {
match parse_duration_token("1/4") {
Ok(DurationValue::Beats(b)) => assert_eq!(b, 0.25),
_ => panic!("Expected beats"),
}
match parse_duration_token("1/2") {
Ok(DurationValue::Beats(b)) => assert_eq!(b, 0.5),
_ => panic!("Expected beats"),
}
match parse_duration_token("1/1") {
Ok(DurationValue::Beats(b)) => assert_eq!(b, 1.0),
_ => panic!("Expected beats"),
}
}
#[test]
fn test_temporal_beat_with_space() {
match parse_duration_token("1 beat") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("2 beat") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__2_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("1 beats") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("3 beats") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__3_beat"),
other => panic!("Unexpected result: {:?}", other),
}
}
#[test]
fn test_temporal_beat_without_space() {
match parse_duration_token("1beat") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("2beat") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__2_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("1beats") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("3beats") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__3_beat"),
other => panic!("Unexpected result: {:?}", other),
}
}
#[test]
fn test_temporal_bar_with_space() {
match parse_duration_token("1 bar") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("2 bar") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__8_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("1 bars") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("3 bars") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__12_beat"),
other => panic!("Unexpected result: {:?}", other),
}
}
#[test]
fn test_temporal_bar_without_space() {
match parse_duration_token("1bar") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("2bar") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__8_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("1bars") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("3bars") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__12_beat"),
other => panic!("Unexpected result: {:?}", other),
}
}
#[test]
fn test_temporal_measure_with_space() {
match parse_duration_token("1 measure") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("2 measure") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__8_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("1 measures") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("3 measures") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__12_beat"),
other => panic!("Unexpected result: {:?}", other),
}
}
#[test]
fn test_temporal_measure_without_space() {
match parse_duration_token("1measure") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("2measure") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__8_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("1measures") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("3measures") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__12_beat"),
other => panic!("Unexpected result: {:?}", other),
}
}
#[test]
fn test_temporal_step_with_space() {
match parse_duration_token("1 step") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__0.25_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("4 step") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("1 steps") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__0.25_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("16 steps") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_beat"),
other => panic!("Unexpected result: {:?}", other),
}
}
#[test]
fn test_temporal_step_without_space() {
match parse_duration_token("1step") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__0.25_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("4step") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("1steps") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__0.25_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("16steps") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_beat"),
other => panic!("Unexpected result: {:?}", other),
}
}
#[test]
fn test_mixed_case() {
match parse_duration_token("1 BEAT") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__1_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("2 Bar") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__8_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("1MEASURE") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__4_beat"),
other => panic!("Unexpected result: {:?}", other),
}
}
#[test]
fn test_float_values() {
match parse_duration_token("0.5 beat") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__0.5_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("1.5bar") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__6_beat"),
other => panic!("Unexpected result: {:?}", other),
}
match parse_duration_token("2.5 measures") {
Ok(DurationValue::Identifier(id)) => assert_eq!(id, "__temporal__10_beat"),
other => panic!("Unexpected result: {:?}", other),
}
}
}