Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
const MAX_DIGITS: usize = 6;
fn not_a_duration(text: &str) -> ! {
panic!("\"{}\" is not a duration: write it as 1h30m, 90m or 01:30", text)
}
/// Read a run of digits from `at`; returns (value, next index), or None if there is none.
fn read_number(text: &str, s: &[u8], at: usize) -> Option<(i64, usize)> {
let mut end = at;
while end < s.len() && s[end].is_ascii_digit() {
end += 1;
}
if end == at {
return None;
}
if end - at > MAX_DIGITS {
not_a_duration(text);
}
let mut value = 0i64;
for b in &s[at..end] {
value = value * 10 + i64::from(b - b'0');
}
Some((value, end))
}
fn skip_spaces(s: &[u8], mut at: usize) -> usize {
while at < s.len() && (s[at] == b' ' || s[at] == b'\t') {
at += 1;
}
at
}
/// One written duration, in minutes.
fn parse_one(text: &str) -> i64 {
let s = text.trim_matches(|c| c == ' ' || c == '\t').as_bytes();
if let Some(colon) = s.iter().position(|&b| b == b':') {
let hours = match read_number(text, s, 0) {
Some(h) if h.1 == colon => h,
_ => not_a_duration(text),
};
let minutes = match read_number(text, s, colon + 1) {
Some(m) if m.1 == s.len() && m.1 - colon - 1 == 2 => m,
_ => not_a_duration(text),
};
if minutes.0 >= 60 {
panic!("minutes must be under 60 when hours are given, in \"{}\"", text);
}
return hours.0 * 60 + minutes.0;
}
let mut at = 0usize;
let mut hours: Option<i64> = None;
let mut minutes: Option<i64> = None;
while at < s.len() {
let read = match read_number(text, s, at) {
Some(r) => r,
None => not_a_duration(text),
};
at = skip_spaces(s, read.1);
let unit = if at < s.len() { s[at] } else { 0 };
if (unit == b'h' || unit == b'H') && hours.is_none() && minutes.is_none() {
hours = Some(read.0);
} else if (unit == b'm' || unit == b'M') && minutes.is_none() {
minutes = Some(read.0);
} else {
not_a_duration(text);
}
at = skip_spaces(s, at + 1);
}
if hours.is_none() && minutes.is_none() {
not_a_duration(text);
}
if let (Some(_), Some(m)) = (hours, minutes) {
if m >= 60 {
panic!("minutes must be under 60 when hours are given, in \"{}\"", text);
}
}
hours.unwrap_or(0) * 60 + minutes.unwrap_or(0)
}
/// Parse each written duration, add them, and give the total in minutes and in
/// both written forms. The clock form does not wrap at 24 hours, because a total
/// of work is not a time of day.
///
/// # Panics
/// Panics on any part that is not a duration in one of the accepted forms.
pub fn duration(parts: &[String]) -> Duration {
let total: i64 = parts.iter().map(|p| parse_one(p)).sum();
let hours = total / 60;
let minutes = total % 60;
let text = if hours > 0 && minutes > 0 {
format!("{}h{}m", hours, minutes)
} else if hours > 0 {
format!("{}h", hours)
} else {
format!("{}m", minutes)
};
Duration {
minutes: total,
text,
clock: format!("{:02}:{:02}", hours, minutes),
}
}
pub fn duration_to_value(d: &Duration) -> Value {
Value::obj(vec![
("minutes", Value::Int(d.minutes)),
("text", Value::str(&d.text)),
("clock", Value::str(&d.clock)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let parts: Vec<String> = args[0].as_arr().iter().map(|v| v.as_str().to_string()).collect();
duration_to_value(&duration(&parts))
}