use crate::config::Config;
use crate::project::ProjectLayout;
use crate::prose::{ProseLanguage, resolve_prose_language};
use crate::store::hierarchy::Hierarchy;
use crate::store::node::Node;
use super::scenes::chapter_texts;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Register {
pub contraction_rate: f32,
pub archaism_density: f32,
pub formality: f32,
pub latinate_density: f32,
pub word_count: u32,
}
pub(crate) fn register(text: &str, lang: &ProseLanguage) -> Register {
let l = lists(lang);
let latinate_lang = matches!(lang, ProseLanguage::En);
let (mut contractions, mut archaisms, mut formal, mut informal, mut latinate, mut words) =
(0u32, 0u32, 0u32, 0u32, 0u32, 0u32);
for raw in text.split_whitespace() {
let t = norm(raw);
if t.is_empty() {
continue;
}
words += 1;
if l.contractions.contains(&t.as_str()) {
contractions += 1;
}
if l.archaisms.contains(&t.as_str()) {
archaisms += 1;
}
if l.formal.contains(&t.as_str()) {
formal += 1;
} else if l.informal.contains(&t.as_str()) {
informal += 1;
}
if latinate_lang && is_latinate(&t) {
latinate += 1;
}
}
let wf = words.max(1) as f32;
let formality = if formal + informal == 0 {
0.0
} else {
(formal as f32 - informal as f32) / (formal + informal) as f32
};
Register {
contraction_rate: contractions as f32 / wf,
archaism_density: archaisms as f32 / wf,
formality,
latinate_density: latinate as f32 / wf,
word_count: words,
}
}
fn norm(tok: &str) -> String {
tok.replace('\u{2019}', "'")
.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'')
.to_lowercase()
}
fn is_latinate(t: &str) -> bool {
const SUF: [&str; 12] =
["tion", "sion", "ment", "ance", "ence", "ity", "ous", "ive", "ate", "ize", "ise", "ology"];
t.len() >= 6 && SUF.iter().any(|s| t.ends_with(s))
}
struct Lists {
contractions: &'static [&'static str],
archaisms: &'static [&'static str],
formal: &'static [&'static str],
informal: &'static [&'static str],
}
fn lists(lang: &ProseLanguage) -> Lists {
match lang {
ProseLanguage::Ru => Lists {
contractions: &[],
archaisms: RU_ARCHAIC,
formal: RU_FORMAL,
informal: RU_INFORMAL,
},
ProseLanguage::Fr => Lists {
contractions: FR_CONTRACTIONS,
archaisms: &[],
formal: FR_FORMAL,
informal: &[],
},
_ => Lists {
contractions: EN_CONTRACTIONS,
archaisms: EN_ARCHAIC,
formal: EN_FORMAL,
informal: EN_INFORMAL,
},
}
}
const EN_CONTRACTIONS: &[&str] = &[
"don't", "can't", "won't", "i'm", "it's", "that's", "he's", "she's", "we're", "they're",
"i've", "you're", "didn't", "wouldn't", "couldn't", "isn't", "aren't", "wasn't", "weren't",
"doesn't", "i'll", "we'll", "you'll", "he'll", "she'll", "they'll", "let's", "there's",
"what's", "who's", "i'd", "you'd", "he'd", "she'd", "we'd", "they'd", "shouldn't", "hadn't",
"hasn't", "haven't", "mustn't", "wouldn't",
];
const EN_ARCHAIC: &[&str] = &[
"thee", "thou", "thy", "thine", "hath", "doth", "dost", "art", "ere", "whilst", "betwixt",
"oft", "tis", "twas", "hither", "thither", "whence", "wherefore", "forsooth", "nay", "yea",
"verily", "mayhap", "perchance", "methinks", "prithee", "aught", "naught",
];
const EN_FORMAL: &[&str] = &[
"however", "therefore", "moreover", "nevertheless", "furthermore", "thus", "hence",
"regarding", "concerning", "subsequently", "consequently", "notwithstanding", "henceforth",
"herein", "thereby", "wherein", "whereas", "albeit", "whereby",
];
const EN_INFORMAL: &[&str] = &[
"gonna", "wanna", "gotta", "yeah", "yep", "nope", "ok", "okay", "stuff", "kinda", "sorta",
"dunno", "ain't", "guy", "guys", "kids", "folks", "cuz", "gotcha",
];
const RU_ARCHAIC: &[&str] = &[
"сей", "сия", "сие", "оный", "дабы", "ежели", "поелику", "токмо", "паче", "зело", "вельми",
"коль", "дондеже", "поныне", "поприще", "чело", "выя", "длань",
];
const RU_FORMAL: &[&str] = &[
"однако", "следовательно", "поэтому", "итак", "впрочем", "ибо", "посему", "вследствие",
"таким", "равно", "ввиду",
];
const RU_INFORMAL: &[&str] = &["ну", "ага", "типа", "короче", "блин", "чё", "щас", "ладно", "мол"];
const FR_CONTRACTIONS: &[&str] = &[
"c'est", "j'ai", "n'est", "qu'il", "d'un", "l'on", "s'il", "j'en", "m'a", "t'a", "qu'elle",
"d'une", "l'a", "n'a",
];
const FR_FORMAL: &[&str] =
&["néanmoins", "toutefois", "cependant", "ainsi", "donc", "or", "partant", "nonobstant"];
#[allow(clippy::too_many_arguments)]
fn push_drift(
out: &mut Vec<RegisterDrift>,
chapter_ord: u32,
metric: &'static str,
value: f32,
baseline: f32,
threshold: f32,
) {
let delta = value - baseline;
if delta.abs() >= threshold {
out.push(RegisterDrift { chapter_ord, metric, baseline, value, delta });
}
}
pub(crate) struct ChapterRegister {
pub chapter_ord: u32,
pub register: Register,
}
pub(crate) struct RegisterDrift {
pub chapter_ord: u32,
pub metric: &'static str,
pub baseline: f32,
pub value: f32,
pub delta: f32,
}
pub(crate) struct RegisterReport {
pub chapters: Vec<ChapterRegister>,
pub drifts: Vec<RegisterDrift>,
}
pub(crate) fn scan_register(
layout: &ProjectLayout,
h: &Hierarchy,
cfg: &Config,
book: &Node,
) -> RegisterReport {
let (lang, _) = resolve_prose_language(None, &cfg.language);
let thr = cfg.chorus.register_drift_threshold;
let chapters: Vec<ChapterRegister> = chapter_texts(layout, h, book)
.into_iter()
.map(|(ord, text)| ChapterRegister { chapter_ord: ord, register: register(&text, &lang) })
.filter(|c| c.register.word_count >= 100)
.collect();
let mut drifts = Vec::new();
if let Some(base) = chapters.first() {
let b = base.register;
for c in chapters.iter().skip(1) {
let r = c.register;
push_drift(&mut drifts, c.chapter_ord, "contraction_rate", r.contraction_rate, b.contraction_rate, thr);
push_drift(&mut drifts, c.chapter_ord, "archaism_density", r.archaism_density, b.archaism_density, thr);
push_drift(&mut drifts, c.chapter_ord, "formality", r.formality, b.formality, thr);
push_drift(&mut drifts, c.chapter_ord, "latinate_density", r.latinate_density, b.latinate_density, thr);
}
}
RegisterReport { chapters, drifts }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn register_metrics_on_fixtures() {
let r = register("I don't know but it's fine", &ProseLanguage::En);
assert!((r.contraction_rate - 2.0 / 6.0).abs() < 1e-4);
let a = register("Thou hath spoken", &ProseLanguage::En);
assert!((a.archaism_density - 2.0 / 3.0).abs() < 1e-4);
let f = register("However therefore gonna", &ProseLanguage::En);
assert!((f.formality - 1.0 / 3.0).abs() < 1e-4);
let lat = register("The information and organization", &ProseLanguage::En);
assert!((lat.latinate_density - 2.0 / 4.0).abs() < 1e-4);
}
#[test]
fn register_works_in_russian_archaism_and_formality() {
let r = register("Сей однако дабы", &ProseLanguage::Ru);
assert!(r.archaism_density > 0.0, "сей/дабы archaic");
assert!(r.formality > 0.0, "однако formal");
assert_eq!(r.latinate_density, 0.0, "latinate is English-only");
}
#[test]
fn drift_threshold_flags_a_shift() {
let base = register("The wind moved across the wide grey sea at dawn", &ProseLanguage::En);
let later = register("I don't and can't and won't and it's and that's", &ProseLanguage::En);
let mut drifts = Vec::new();
push_drift(&mut drifts, 5, "contraction_rate", later.contraction_rate, base.contraction_rate, 0.08);
assert_eq!(drifts.len(), 1);
assert_eq!(drifts[0].chapter_ord, 5);
assert!(drifts[0].delta > 0.0);
}
}