use std::collections::BTreeSet;
use std::sync::OnceLock;
use serde::Deserialize;
use super::search_tokens::fold_search_term;
#[derive(Debug, Deserialize)]
pub struct LanguageVocabulary {
languages: Vec<Language>,
}
#[derive(Debug, Deserialize)]
struct Language {
id: String,
function_words: BTreeSet<String>,
}
const SOURCE: &str = include_str!("../../language/function_words.json");
const MINIMUM_SIGNALS: usize = 3;
pub const KERNEL_LANGUAGE: &str = "english";
impl LanguageVocabulary {
pub fn shipped() -> &'static Self {
static SHIPPED: OnceLock<LanguageVocabulary> = OnceLock::new();
SHIPPED
.get_or_init(|| serde_json::from_str(SOURCE).expect("the shipped function words parse"))
}
pub fn read<'a>(&self, tokens: impl IntoIterator<Item = &'a str>) -> Option<&str> {
let mut counts = vec![0usize; self.languages.len()];
for token in tokens {
for (index, language) in self.languages.iter().enumerate() {
if language.function_words.contains(token) {
counts[index] += 1;
break;
}
}
}
let total = counts.iter().sum::<usize>();
let (index, winner) = counts
.iter()
.enumerate()
.max_by_key(|(index, count)| (**count, std::cmp::Reverse(*index)))
.map(|(index, count)| (index, *count))?;
if winner < MINIMUM_SIGNALS || winner * 3 < total * 2 {
return None;
}
Some(self.languages[index].id.as_str())
}
pub fn leans_in(&self, text: &str) -> Option<&str> {
let tokens = text
.split(|character: char| !character.is_alphanumeric())
.filter(|token| !token.is_empty())
.map(fold_search_term)
.collect::<Vec<_>>();
self.leans(tokens.iter().map(String::as_str))
}
pub fn leans<'a>(&self, tokens: impl IntoIterator<Item = &'a str>) -> Option<&str> {
let mut counts = vec![0usize; self.languages.len()];
for token in tokens {
for (index, language) in self.languages.iter().enumerate() {
if language.function_words.contains(token) {
counts[index] += 1;
break;
}
}
}
let (index, winner) = counts
.iter()
.enumerate()
.max_by_key(|(index, count)| (**count, std::cmp::Reverse(*index)))
.map(|(index, count)| (index, *count))?;
let tied = counts
.iter()
.enumerate()
.any(|(other, count)| other != index && *count == winner);
if winner == 0 || tied {
return None;
}
Some(self.languages[index].id.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn words(text: &str) -> Vec<&str> {
text.split(' ').collect()
}
#[test]
fn the_shipped_vocabulary_parses_and_keeps_its_languages_apart() {
let vocabulary = LanguageVocabulary::shipped();
let mut seen = BTreeSet::new();
for language in &vocabulary.languages {
assert!(seen.insert(language.id.as_str()), "duplicate language");
assert!(!language.function_words.is_empty());
}
assert!(
seen.contains(KERNEL_LANGUAGE),
"the kernel's own language must be readable"
);
for (index, language) in vocabulary.languages.iter().enumerate() {
for other in vocabulary.languages.iter().skip(index + 1) {
let shared = language
.function_words
.intersection(&other.function_words)
.collect::<Vec<_>>();
assert!(shared.is_empty(), "`{shared:?}` cannot decide a language");
}
}
}
#[test]
fn a_clear_majority_names_the_language() {
let vocabulary = LanguageVocabulary::shipped();
assert_eq!(
vocabulary.read(words(
"el despliegue de la pasarela se congelo por la auditoria"
)),
Some("spanish")
);
assert_eq!(
vocabulary.read(words(
"the deployment of the gateway was frozen by the audit"
)),
Some("english")
);
}
#[test]
fn a_sentence_leans_on_one_function_word() {
let vocabulary = LanguageVocabulary::shipped();
assert_eq!(
vocabulary.leans(words("lanzamiento pospuesto por la auditoria")),
Some("spanish")
);
assert_eq!(
vocabulary.leans(words("the reserve valve failed during the night shift")),
Some("english")
);
assert_eq!(vocabulary.leans(words("launch postponed audit")), None);
assert_eq!(vocabulary.leans(words("the launch fue postponed")), None);
assert_eq!(
vocabulary.leans_in("El despliegue de v0.7.0 se retrasó."),
Some("spanish")
);
}
#[test]
fn too_little_or_too_even_reads_as_nothing() {
let vocabulary = LanguageVocabulary::shipped();
assert_eq!(vocabulary.read(words("valkey")), None);
assert_eq!(
vocabulary.read(words(
"the deployment of the gateway el despliegue de la pasarela"
)),
None
);
}
}