Functional Weave
Code in TypeScript

form.character-count@1.0.1

impl/rust.rs

3,479 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

/// The Unicode White_Space property, spelled out so all three languages agree
/// on what separates words.
fn is_whitespace(ch: char) -> bool {
    let cp = ch as u32;
    (0x09..=0x0d).contains(&cp)
        || cp == 0x20
        || cp == 0x85
        || cp == 0xa0
        || cp == 0x1680
        || (0x2000..=0x200a).contains(&cp)
        || cp == 0x2028
        || cp == 0x2029
        || cp == 0x202f
        || cp == 0x205f
        || cp == 0x3000
}

fn count_words(text: &str) -> i64 {
    let mut words = 0;
    let mut in_word = false;
    for ch in text.chars() {
        if is_whitespace(ch) {
            in_word = false;
        } else if !in_word {
            in_word = true;
            words += 1;
        }
    }
    words
}

/// GOV.UK's character count: how many characters (code points) or words have
/// been typed against a limit, and the sentence to show under the text box.
///
/// # Panics
/// Panics if `limit` is below 1, `unit` is not characters or words, or
/// `threshold` is outside 0 to 100.
pub fn character_count(text: &str, limit: i64, unit: &str, threshold: Option<i64>) -> CharacterCount {
    if limit < 1 {
        panic!("limit must be a whole number of at least 1, received {}", limit);
    }
    if unit != "characters" && unit != "words" {
        panic!("unit must be characters or words, received {}", unit);
    }
    if let Some(t) = threshold {
        if !(0..=100).contains(&t) {
            panic!("threshold must be a whole number from 0 to 100, received {}", t);
        }
    }
    let count = if unit == "words" { count_words(text) } else { text.chars().count() as i64 };
    let remaining = limit - count;
    let over = remaining < 0;
    // count >= limit * threshold / 100, kept in whole numbers so 50% of 3 is 1.5, not 1.
    let visible = over || threshold.map_or(true, |t| (count as i128) * 100 >= (limit as i128) * (t as i128));
    let one = if unit == "words" { "word" } else { "character" };
    let noun = |n: i64| if n == 1 { one } else { unit };
    let message = if over {
        format!("You have {} {} too many", -remaining, noun(-remaining))
    } else {
        format!("You have {} {} remaining", remaining, noun(remaining))
    };
    CharacterCount {
        count,
        remaining,
        over,
        visible,
        message,
        description: format!("You can enter up to {} {}", limit, noun(limit)),
    }
}

pub fn character_count_to_value(c: &CharacterCount) -> Value {
    Value::obj(vec![
        ("count", Value::Int(c.count)),
        ("remaining", Value::Int(c.remaining)),
        ("over", Value::Bool(c.over)),
        ("visible", Value::Bool(c.visible)),
        ("message", Value::Str(c.message.clone())),
        ("description", Value::Str(c.description.clone())),
    ])
}

/// A whole number from a vector, or the same refusal the other languages give.
fn whole(v: &Value, message: &str) -> i64 {
    match v {
        Value::Int(n) => *n,
        Value::Float(f) if f.fract() == 0.0 => *f as i64,
        _ => panic!("{}, received {:?}", message, v),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    let limit = whole(&args[1], "limit must be a whole number of at least 1");
    let threshold = if args[3].is_null() {
        None
    } else {
        Some(whole(&args[3], "threshold must be a whole number from 0 to 100"))
    };
    character_count_to_value(&character_count(args[0].as_str(), limit, args[2].as_str(), threshold))
}