use super::funejson::Value; /// Largest integer JavaScript can hold exactly; beyond it the three languages disagree. const SAFE_INTEGER: i64 = 9007199254740991; /// Fold A-Z to a-z and leave every other character alone. /// /// Deliberately not `to_lowercase()`: full Unicode case mapping differs between /// JavaScript, Python and Rust (Turkish dotted I, final sigma, sharp s), and a /// search that finds a row in one service but not in another is worse than one /// that consistently ignores accents. pub fn fold_ascii(text: &str) -> String { text.chars() .map(|ch| if ch.is_ascii_uppercase() { ch.to_ascii_lowercase() } else { ch }) .collect() } /// The searchable text of a value, or None if there is nothing to search. /// /// Floats are skipped rather than rendered: no decimal form of them is /// identical in all three languages, so matching on one could not be pinned. fn searchable_text(value: &Value) -> Option { match value { Value::Str(s) => Some(s.clone()), Value::Bool(b) => Some((if *b { "true" } else { "false" }).to_string()), Value::Int(i) if i.abs() <= SAFE_INTEGER => Some(i.to_string()), _ => None, } } /// The records whose text in any of `fields` contains `query`, in input order. /// /// An empty query returns everything, because "nothing typed yet" means "show /// me everything" in every search box ever built. /// /// Records are `Value` because they are open JSON maps; a struct would be a /// lie about data whose shape differs from one record to the next. /// /// # Panics /// Panics if `fields` is empty. pub fn search_text(records: &[Value], fields: &[String], query: &str) -> Vec { // An empty field list that quietly matches nothing is the classic bug that // reaches users as "search is broken" with no other symptom. if fields.is_empty() { panic!("search_text needs at least one field to search"); } let needle = fold_ascii(query); if needle.is_empty() { return records.to_vec(); } let mut matches: Vec = Vec::new(); for record in records { // Substring matching on UTF-8 bytes is the same answer as matching on // code points, so Rust, Python and JavaScript agree on every string. let hit = fields.iter().any(|field| match searchable_text(record.get(field)) { Some(text) => fold_ascii(&text).contains(&needle), None => false, }); if hit { matches.push(record.clone()); } } matches } pub fn fune_vector(args: &[Value]) -> Value { // Refuse what the typed signature cannot hold, with the wording TypeScript // and Python use, rather than let the conversion below quietly change it. if !matches!(args[2], Value::Str(_)) { panic!("query must be a string, received {:?}", args[2]); } let fields: Vec = args[1] .as_arr() .iter() .map(|v| v.as_str().to_string()) .collect(); Value::Arr(search_text(args[0].as_arr(), &fields, args[2].as_str())) }