Functional Weave
Code in TypeScript

time.duration@1.0.0

impl/rust.rs

3,688 bytes · the Rust implementation · view raw

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))
}