use super::funejson::Value; /// 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) -> 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)) }