music21_rs/features/
language.rs1use std::collections::HashMap;
10use std::sync::OnceLock;
11
12use crate::defaults::FloatType;
13use crate::error::{Error, Result};
14
15pub const LANGUAGE_CODES: [&str; 7] = ["en", "fr", "it", "de", "cn", "la", "nl"];
18
19pub fn language_name(code: &str) -> Option<&'static str> {
21 Some(match code {
22 "en" => "English",
23 "fr" => "French",
24 "it" => "Italian",
25 "de" => "German",
26 "cn" => "Chinese",
27 "la" => "Latin",
28 "nl" => "Dutch",
29 _ => return None,
30 })
31}
32
33fn excerpt(code: &str) -> &'static str {
34 match code {
35 "en" => include_str!("language/en.txt"),
36 "fr" => include_str!("language/fr.txt"),
37 "it" => include_str!("language/it.txt"),
38 "de" => include_str!("language/de.txt"),
39 "cn" => include_str!("language/cn.txt"),
40 "la" => include_str!("language/la.txt"),
41 _ => include_str!("language/nl.txt"),
42 }
43}
44
45#[derive(Clone, Debug, Default, PartialEq)]
48pub struct Trigram {
49 counts: HashMap<(char, char), HashMap<char, u64>>,
50 length: FloatType,
51}
52
53impl Trigram {
54 pub fn from_text(text: &str) -> Self {
56 Self::counted(text.chars())
57 }
58
59 pub fn from_words<'a>(words: impl IntoIterator<Item = &'a str>) -> Self {
62 Self::counted(
63 words
64 .into_iter()
65 .flat_map(|word| word.trim().chars().chain(std::iter::once(' '))),
66 )
67 }
68
69 fn counted(letters: impl Iterator<Item = char>) -> Self {
70 let mut counts: HashMap<(char, char), HashMap<char, u64>> = HashMap::new();
71 let mut pair = (' ', ' ');
72 for letter in letters {
73 *counts.entry(pair).or_default().entry(letter).or_insert(0) += 1;
74 pair = (pair.1, letter);
75 }
76 let total: u64 = counts
77 .values()
78 .flat_map(HashMap::values)
79 .map(|count| count * count)
80 .sum();
81 Self {
82 counts,
83 length: (total as FloatType).powf(0.5),
85 }
86 }
87
88 pub fn length(&self) -> FloatType {
91 self.length
92 }
93
94 pub fn similarity(&self, other: &Trigram) -> FloatType {
97 let mut total: u64 = 0;
98 for (pair, ours) in &self.counts {
99 if let Some(theirs) = other.counts.get(pair) {
100 for (letter, count) in ours {
101 if let Some(their) = theirs.get(letter) {
102 total += count * their;
103 }
104 }
105 }
106 }
107 total as FloatType / (self.length * other.length)
108 }
109
110 pub fn distance(&self, other: &Trigram) -> FloatType {
112 1.0 - self.similarity(other)
113 }
114}
115
116fn known() -> &'static [Trigram; 7] {
118 static KNOWN: OnceLock<[Trigram; 7]> = OnceLock::new();
119 KNOWN.get_or_init(|| {
120 LANGUAGE_CODES.map(|code| Trigram::from_words(excerpt(code).split_whitespace()))
121 })
122}
123
124pub fn most_likely_language(text: &str) -> Option<&'static str> {
137 if text.is_empty() {
138 return None;
139 }
140 let trigram = Trigram::from_text(text);
141 let mut best = None;
142 let mut least = 1.0;
143 for (code, known) in LANGUAGE_CODES.iter().zip(known()) {
144 let distance = known.distance(&trigram);
145 if distance < least {
146 best = Some(*code);
147 least = distance;
148 }
149 }
150 best
151}
152
153pub fn most_likely_language_numeric(text: &str) -> Result<usize> {
161 if text.is_empty() {
162 return Ok(0);
163 }
164 let code = most_likely_language(text).ok_or_else(|| {
165 Error::Text("got a language that was not in the codes; should not happen".to_string())
166 })?;
167 Ok(LANGUAGE_CODES
168 .iter()
169 .position(|known| *known == code)
170 .map_or(0, |at| at + 1))
171}