///|
/// How `detect` and `rank` decide between languages.
pub(all) enum Mode {
/// franc-min 6.2.0's trigram ranking alone, without markup removal: the
/// same languages and scores as franc.
Franc
/// Markup removal, then franc's ranking, then a second pass that settles
/// closely related languages by their letters and common words. The
/// default.
Enhanced
} derive(Eq, Debug)
///|
/// Languages that franc's trigrams often mix up, with the evidence that
/// tells them apart. `markers` are letters or letter sequences, each with
/// the language it points to and a weight per occurrence; `words` are
/// common words (lowercase) that each count once per occurrence.
priv struct Group {
/// The script the group's evidence is written in.
script : Script
langs : Array[Lang]
markers : Array[(Lang, String, Int32)]
words : Array[(Lang, Array[String])]
}
///|
let groups : Array[Group] = [
{
script: Latin,
langs: [Spa, Por],
markers: [
(Spa, "ñ", 3),
(Spa, "¿", 3),
(Spa, "¡", 3),
(Por, "ã", 3),
(Por, "õ", 3),
(Por, "ç", 3),
(Por, "ê", 2),
(Por, "ô", 2),
(Por, "â", 2),
(Por, "à", 1),
(Por, "lh", 2),
(Por, "nh", 2),
],
words: [
(
Spa,
[
"el", "los", "las", "del", "y", "es", "una", "pero", "muy", "yo", "tengo",
"hay", "está", "estoy", "lo", "su", "sus", "al", "ella", "él", "usted",
"qué", "cómo", "eso", "esto", "nunca", "puedo", "quiero", "también",
"ahora",
],
),
(
Por,
[
"o", "os", "do", "da", "dos", "das", "e", "é", "um", "uma", "não", "em",
"no", "na", "com", "você", "eu", "muito", "mas", "isso", "isto", "ele",
"ela", "seu", "sua", "tenho", "há", "estou", "nunca", "posso", "quero",
"também", "agora", "pelo", "pela",
],
),
],
},
{
script: Cyrillic,
langs: [Rus, Ukr, Bel, Bul, Srp, Kaz, Bos],
markers: [
(Rus, "ы", 2),
(Rus, "э", 2),
(Rus, "ё", 2),
(Bel, "ы", 1),
(Bel, "э", 1),
(Bel, "ў", 4),
(Bel, "і", 1),
(Bel, "дз", 1),
(Bel, "ць", 1),
(Ukr, "і", 1),
(Ukr, "ї", 3),
(Ukr, "є", 3),
(Ukr, "ґ", 3),
(Bul, "ъ", 1),
(Srp, "ђ", 4),
(Srp, "ј", 4),
(Srp, "љ", 4),
(Srp, "њ", 4),
(Srp, "ћ", 4),
(Srp, "џ", 4),
(Kaz, "ә", 4),
(Kaz, "ғ", 4),
(Kaz, "қ", 4),
(Kaz, "ң", 4),
(Kaz, "ө", 4),
(Kaz, "ұ", 4),
(Kaz, "ү", 4),
(Kaz, "һ", 2),
],
words: [
(
Rus,
[
"что", "это", "он", "она", "мы", "вы", "они", "есть",
"нет", "был", "была", "было", "очень", "только",
"меня", "тебя", "его", "её", "уже", "когда", "если",
"я", "мне", "так", "здесь", "сейчас", "почему",
"хочу",
],
),
(
Ukr,
[
"що", "це", "він", "вона", "ми", "ви", "вони", "є",
"немає", "був", "була", "дуже", "тільки", "мене",
"тебе", "його", "вже", "коли", "якщо", "мені", "так",
"тут", "зараз", "чому", "хочу", "та", "й", "до", "також",
"після", "року", "років", "який", "яка", "яке",
"від", "щоб", "ще", "лише", "може", "має", "мають",
"їх", "через", "під", "між", "цього", "цей", "ця",
"проте", "однак",
],
),
(
Bel,
[
"што", "гэта", "ён", "яна", "мы", "вы", "яны", "ёсць",
"няма", "быў", "вельмі", "толькі", "мяне", "цябе",
"яго", "ужо", "калі", "мне", "таксама", "пасля",
"года", "гадоў", "ад", "каб", "яшчэ", "можа", "мае",
"маюць", "іх", "праз", "пад", "паміж", "гэтага",
"гэты", "гэтая", "аднак", "але",
],
),
(
Bul,
[
"е", "са", "съм", "си", "сме", "сте", "ще", "това",
"този", "тази", "какво", "който", "която", "той",
"тя", "ние", "вие", "те", "от", "много", "само", "мен",
"теб", "него", "вече", "когато", "ако", "тук", "сега",
"защо", "искам", "не", "се", "да",
],
),
(
Srp,
[
"је", "су", "сам", "си", "смо", "сте", "ће", "ово",
"шта", "што", "ко", "који", "она", "он", "ми", "ви",
"они", "од", "много", "само", "мене", "тебе", "њега",
"већ", "када", "ако", "овде", "сада", "зашто", "хоћу",
"време", "дете", "лепо", "река", "где", "увек",
"млеко", "недеља", "хтео", "хтела", "универзитета",
"научници", "научника", "јануара", "фебруара",
"марта", "априла", "јула", "августа", "септембра",
"октобра", "новембра", "децембра", "извештај",
"општина",
],
),
(
Bos,
[
"вријеме", "дијете", "лијепо", "ријека", "гдје",
"увијек", "млијеко", "сједмица", "хтио", "хтјела",
"извјештај", "општина", "кахва", "хисторија",
],
),
],
},
{
script: Latin,
langs: [Hrv, Srp, Bos],
markers: [],
words: [
(
Hrv,
[
"tko", "što", "tijekom", "također", "tisuću", "tisuće", "kruh", "glazba",
"sveučilište", "zrakoplov", "kazalište", "vlak", "kava", "jesi", "jeste",
"lijepo", "mlijeko", "rijeka", "vrijeme", "dijete", "htio", "htjela", "gdje",
"uvijek", "nisam", "tjedan", "siječanj", "kolovoz", "obitelj", "zrak",
"povijest", "bok", "hvala", "molim", "kćer", "kći", "znanstvenici", "znanstvenika",
"znanstveni", "znanstvene", "znanstvenik", "sveučilišta", "sveučilištu",
"tisuća", "siječnja", "veljače", "ožujka", "travnja", "svibnja", "lipnja",
"srpnja", "kolovoza", "rujna", "listopada", "studenoga", "prosinca", "nogomet",
"nogometni", "europske", "europski", "europska", "izvješće", "izvješća",
"momčad", "zrakoplova",
],
),
(
Srp,
[
"ko", "šta", "tokom", "takođe", "hiljadu", "hiljade", "hleb", "muzika",
"univerzitet", "avion", "pozorište", "voz", "kafa", "lepo", "mleko", "reka",
"vreme", "dete", "hteo", "htela", "gde", "uvek", "nedelja", "nedelju",
"porodica", "vazduh", "istorija", "hoću", "lep", "lepa", "devojka", "pevati",
"videti", "voleti", "razumem", "ćerka", "beše", "naučnici", "naučnika",
"naučni", "naučne", "naučnik", "univerziteta", "univerzitetu", "januara",
"februara", "marta", "aprila", "maja", "juna", "jula", "avgusta", "septembra",
"oktobra", "novembra", "decembra", "fudbal", "fudbalski", "evropske", "evropski",
"evropska", "izveštaj", "izveštaja", "opština", "opštine",
],
),
(
Bos,
[
"ko", "šta", "tokom", "također", "hiljadu", "hiljade", "hljeb", "muzika",
"univerzitet", "avion", "pozorište", "voz", "kahva", "kafa", "lijepo",
"mlijeko", "rijeka", "vrijeme", "dijete", "htio", "htjela", "gdje", "uvijek",
"sedmica", "sedmicu", "porodica", "zrak", "historija", "lahko", "mehko",
"hoću", "hvala", "ćerka", "ahbab", "naučnici", "naučnika", "naučni",
"naučne", "naučnik", "univerziteta", "univerzitetu", "januara", "februara",
"marta", "aprila", "maja", "juna", "jula", "augusta", "septembra", "oktobra",
"novembra", "decembra", "fudbal", "fudbalski", "evropske", "evropski",
"evropska", "izvještaj", "izvještaja", "općina", "općine",
],
),
],
},
{
script: Latin,
langs: [Ind, Zlm],
markers: [],
words: [
(
Ind,
[
"bisa", "uang", "karena", "nggak", "gak", "enggak", "aja", "kantor", "mobil",
"sepeda", "dong", "banget", "sih", "kok", "cewek", "cowok", "kalian", "mau",
"apakah", "teman", "bahwa", "setelah", "sesudah", "pemerintah", "kendaraan",
"berbagai", "internasional", "tentara", "polisi", "perusahaan", "sedangkan",
"mengatakan", "universitas", "nomor", "proyek", "kualitas", "aktivitas",
"komunitas", "kapasitas", "identitas", "musik", "film", "agustus", "desember",
"maret", "juni", "juli", "organisasi", "koran", "bilang",
],
),
(
Zlm,
[
"boleh", "wang", "kerana", "awak", "nak", "sahaja", "pejabat", "kereta",
"basikal", "encik", "bilik", "kawan", "bagaimanakah", "adakah", "mahu",
"telefon", "universiti", "bahawa", "selepas", "kerajaan", "kenderaan",
"pelbagai", "antarabangsa", "tentera", "polis", "syarikat", "iaitu", "ialah",
"semasa", "baharu", "jabatan", "hospital", "bilangan", "menerusi", "sebarang",
"akhbar", "filem", "muzik", "projek", "kualiti", "aktiviti", "komuniti",
"kapasiti", "identiti", "ogos", "disember", "mac", "julai", "pertubuhan",
"nombor",
],
),
],
},
{
script: Latin,
langs: [Run, Kin],
markers: [
(Kin, "cy", 2),
(Kin, "jy", 2),
(Kin, "shy", 2),
(Kin, "by", 2),
(Run, "vy", 2),
],
words: [
(
Kin,
[
"cyane", "ibyo", "byose", "iki", "cyangwa", "kuko", "ndetse", "nyuma",
"ariko", "u", "ubwo", "rwa",
],
),
(
Run,
[
"cane", "ivyo", "vyose", "ico", "canke", "kubera", "ningoga", "inyuma",
"mugabo", "yuko", "kuri", "rya",
],
),
],
},
{
script: Latin,
langs: [Tgl, Ceb],
markers: [],
words: [
(
Tgl,
[
"ng", "hindi", "ito", "iyan", "iyon", "ay", "po", "niya", "ko", "mo", "kayo",
"akin", "kanya", "bakit", "paano", "saan", "may", "din", "rin", "talaga",
"noong", "nang", "mula", "ayon", "isang", "kanilang", "nito", "dahil",
"upang", "sinabi", "ngunit", "subalit", "pa", "rito",
],
),
(
Ceb,
[
"ug", "og", "dili", "kini", "kana", "unsa", "ngano", "naa", "kaayo", "nga",
"imong", "akong", "nimo", "niini", "adto", "diha", "asa", "pud", "sad",
"gyud", "usa", "gikan", "miingon", "giingon", "kaniadtong", "karon", "diin",
"kining", "pinaagi", "apan", "tungod", "aron", "didto",
],
),
],
},
{
script: Latin,
langs: [Hau, Fuv],
markers: [(Hau, "ƙ", 2), (Hau, "ts", 1), (Fuv, "ŋ", 3), (Fuv, "ɲ", 3)],
words: [
(
Hau,
[
"da", "na", "ta", "ya", "wanda", "wadda", "shi", "ita", "sun", "su", "kuma",
"amma", "daga", "zai", "za", "iya", "wannan", "don", "ga", "suka", "ake",
"yana", "tana", "akwai", "kamar", "bayan", "shekara",
],
),
(
Fuv,
[
"je", "ha", "be", "nde", "ɗum", "mari", "der", "ko", "e", "kanko", "kanjum",
"ngam", "nden", "fu", "fuu", "ɓe", "ɗon", "ndu", "ngal", "dow", "nder",
"ka", "noone", "woni", "wonaa",
],
),
],
},
{
script: Latin,
langs: [Jav, Sun],
markers: [(Jav, "dh", 1), (Jav, "th", 1), (Sun, "eu", 1)],
words: [
(
Jav,
[
"ing", "sing", "lan", "kanggo", "iku", "ora", "wis", "kang", "menawa",
"saka", "uga", "dadi", "marang", "karo", "iki", "ana", "kabeh", "banjur",
"bakal", "isih", "nanging",
],
),
(
Sun,
[
"anu", "jeung", "teh", "téh", "sareng", "ti", "ka", "éta", "ieu", "henteu",
"tiasa", "nu", "oge", "ogé", "geus", "kana", "pikeun", "keur", "jang",
"urang", "sanés", "tapi", "bari",
],
),
],
},
{
script: Arabic,
langs: [Arb, Pes, Urd, Skr, Ckb, Pbu],
markers: [
(Arb, "ة", 2),
(Arb, "ي", 1),
(Arb, "ك", 1),
(Arb, "ى", 1),
(Ckb, "ڕ", 3),
(Ckb, "ڵ", 3),
(Ckb, "ۆ", 3),
(Ckb, "ێ", 3),
(Ckb, "ە", 3),
(Pbu, "ټ", 3),
(Pbu, "ډ", 3),
(Pbu, "ړ", 3),
(Pbu, "ږ", 3),
(Pbu, "ښ", 3),
(Pbu, "ځ", 3),
(Pbu, "څ", 3),
(Pbu, "ڼ", 3),
(Pbu, "ې", 3),
(Pbu, "ۍ", 3),
(Urd, "ٹ", 3),
(Urd, "ڈ", 3),
(Urd, "ڑ", 3),
(Urd, "ں", 3),
(Urd, "ے", 3),
(Urd, "ھ", 2),
(Urd, "ہ", 2),
(Skr, "ٹ", 3),
(Skr, "ڈ", 3),
(Skr, "ڑ", 3),
(Skr, "ں", 3),
(Skr, "ے", 3),
(Skr, "ھ", 2),
(Skr, "ہ", 2),
(Skr, "ٻ", 4),
(Skr, "ڄ", 4),
(Skr, "ڳ", 4),
(Skr, "ݙ", 4),
(Skr, "ݨ", 4),
],
words: [
(
Arb,
[
"في", "من", "على", "أن", "هذا", "هذه", "لا", "ما", "هل",
"إلى", "عن",
],
),
(
Pes,
[
"است", "را", "این", "آن", "که", "می", "من", "به", "از",
"با", "برای", "چه", "نمی", "هست", "بود", "شما", "ما",
"او",
],
),
(
Urd,
[
"ہے", "کے", "کی", "میں", "سے", "نہیں", "اور", "کو",
"یہ", "ہیں", "تھا", "آپ", "ہم", "کیا",
],
),
(
Skr,
[
"ہے", "دے", "دی", "وچ", "توں", "نئیں", "تے", "کوں",
"اے", "ہن", "ہا", "تساں",
],
),
(
Ckb,
[
"لە", "و", "بۆ", "ئەم", "ئەو", "من", "تۆ", "نییە", "دەکەم",
"چی",
],
),
(
Pbu,
[
"په", "د", "او", "دا", "چې", "زه", "ته", "سره", "نه",
"يې",
],
),
],
},
]
///|
/// The group of related languages that holds `lang` in `script`, if any.
/// Serbian and Bosnian are in one group for Cyrillic and another for
/// Latin script.
fn group_of(lang : Lang, script : Script) -> Group? {
for group in groups {
if group.script == script && group.langs.contains(lang) {
return Some(group)
}
}
None
}
///|
/// Lowercase words of `text`: runs of units that are not spaces, ASCII
/// punctuation or digits, or general punctuation.
fn words_of(text : String) -> Array[String] {
let out = []
let buf = StringBuilder()
let flush = () => {
let w = buf.to_string()
if w != "" {
out.push(w)
}
buf.reset()
}
for ch in text {
let cp = ch.to_int()
let separator = is_js_space(cp) ||
(cp < 0x80 && !is_ascii_alpha(cp) && cp != '\'') ||
(cp >= 0x2000 && cp <= 0x206F) ||
cp == 0x060C ||
cp == 0x061F ||
cp == 0x06D4 ||
cp == 0xBF ||
cp == 0xA1 ||
cp == 0xAB ||
cp == 0xBB
if separator {
flush()
} else {
buf.write_char(lower_simple(cp).unsafe_to_char())
}
}
flush()
out
}
///|
/// Occurrences of `needle` in `haystack`, not overlapping.
fn occurrences(haystack : String, needle : String) -> Int32 {
let n = needle.length()
if n == 0 {
return 0
}
let first = needle.unsafe_get(0)
let mut count = 0
let mut i = 0
while i + n <= haystack.length() {
// Compare code units in place: this runs for every marker and word.
let mut matched = haystack.unsafe_get(i) == first
let mut k = 1
while matched && k < n {
matched = haystack.unsafe_get(i + k) == needle.unsafe_get(k)
k += 1
}
if matched {
count += 1
i += n
} else {
i += 1
}
}
count
}
///|
/// For each group of `groups`, its markers by their first code unit, so
/// that a word is scanned once for all of them.
let marker_indexes : Array[Map[Int32, Array[Int32]]] = groups.map(group => {
let index : Map[Int32, Array[Int32]] = Map([])
for m, marker in group.markers {
let first = marker.1.unsafe_get(0).to_int()
match index.get(first) {
Some(list) => list.push(m)
None => index[first] = [m]
}
}
index
})
///|
/// For each group of `groups`, the languages whose list holds each word.
let word_indexes : Array[Map[String, Array[Lang]]] = groups.map(group => {
let index : Map[String, Array[Lang]] = Map([])
for entry in group.words {
let (lang, list) = entry
for word in list {
match index.get(word) {
Some(langs) => if !langs.contains(lang) { langs.push(lang) }
None => index[word] = [lang]
}
}
}
index
})
///|
/// Whether `needle` occurs in `word` at code unit `at`.
fn occurs_at(word : String, at : Int32, needle : String) -> Bool {
if at + needle.length() > word.length() {
return false
}
for k in 1.. Map[Lang, Int32] {
let scores : Map[Lang, Int32] = Map([])
for lang in group.langs {
scores[lang] = 0
}
guard groups.search_by(g => physical_equal(g, group)) is Some(g) else {
return scores
}
let index = marker_indexes[g]
let found = FixedArray::make(group.markers.length(), 0)
if !index.is_empty() {
for word, n in words {
for i in 0.. Array[(Lang, Double)] {
guard ranked.length() > 1 else { return ranked }
let top = ranked[0].0
guard group_of(top, script) is Some(group) else { return ranked }
let scores = evidence(group, stats.words)
let mut best_score = scores.get(top).unwrap_or(0)
for lang in group.langs {
best_score = best_score.max(scores.get(lang).unwrap_or(0))
}
if best_score <= 0 || scores.get(top).unwrap_or(0) == best_score {
return ranked
}
// The leaders in franc's order; members franc did not rank come last.
let leaders = group.langs.filter(lang => {
scores.get(lang).unwrap_or(0) == best_score
})
let position = (lang : Lang) => {
ranked.search_by(r => r.0 == lang).unwrap_or(ranked.length())
}
leaders.sort_by((a, b) => position(a) - position(b))
let best = leaders[0]
if !allowed(best, allow, deny) {
return ranked
}
promote(ranked, best)
}
///|
/// `ranked` with `best` first. franc's choice takes the score `best` had
/// and goes after every language that scores at least as much, so the
/// scores stay franc's and only their order changes. A language franc has
/// no profile for in this script (Sorani, which franc-min only knows in
/// Latin script) is not in the ranking; franc's choice then takes the
/// runner-up's score.
fn promote(
ranked : Array[(Lang, Double)],
best : Lang,
) -> Array[(Lang, Double)] {
let top = ranked[0].0
if best == top {
return ranked
}
let i = ranked.search_by(r => r.0 == best).unwrap_or(-1)
let demoted = (top, if i >= 0 { ranked[i].1 } else { ranked[1].1 })
let out = [(best, ranked[0].1)]
let mut placed = false
for k in 1..