From 8686a77190224ad1038d78a090f4e731fa1dd63a Mon Sep 17 00:00:00 2001 From: annieversary Date: Mon, 31 Oct 2022 19:23:52 +0100 Subject: [PATCH] make cli --- Cargo.toml | 5 +- examples/main.rs | 44 ++++++++++++ src/bin.rs | 38 ----------- src/lib.rs | 169 ++++++++++++++++++++++++++++++++--------------- src/main.rs | 58 ++++++++++++++++ 5 files changed, 220 insertions(+), 94 deletions(-) create mode 100644 examples/main.rs delete mode 100644 src/bin.rs create mode 100644 src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 27fd3d0..8bd6e09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "incantata" version = "0.1.0" -edition = "2018" +edition = "2021" [lib] name = "incantata" @@ -9,7 +9,8 @@ path = "src/lib.rs" [[bin]] name = "incantata" -path = "src/bin.rs" +path = "src/main.rs" [dependencies] +clap = { version = "3.2.21", features = ["derive"] } rand = "0.8.4" diff --git a/examples/main.rs b/examples/main.rs new file mode 100644 index 0000000..f1ca14d --- /dev/null +++ b/examples/main.rs @@ -0,0 +1,44 @@ +use incantata::*; + +fn main() { + loop { + // structure of the language + let s = Structure { + // how many characters the onset is + onset: 1, + // allowed characters for the onset + onset_dict: CONSONANTS.chars().collect(), + + // how many characters the nucleus is + nucleus: 1, + // allowed characters for the nucleus + nucleus_dict: VOCALS + .chars() + // .cycle() + // .take(VOCALS.len() * 5) + // .chain(VOCALS_ACCENTS.chars()) + .collect(), + + // how many characters the coda is + coda: 0, + // allowed characters for the coda + coda_dict: CONSONANTS.chars().collect(), + + // minimum length of a word + min_len: 4, + // the words will be generated to be around this length + // due to the way incantata works (by combining valid syllables), + // we can't actually make a word of a given length + suggested_len: 15, + }; + + // generate 10 words + for _ in 0..10 { + println!("{}", s.generate()); + } + + if std::io::stdin().read_line(&mut String::new()).is_err() { + break; + } + } +} diff --git a/src/bin.rs b/src/bin.rs deleted file mode 100644 index 98f4c53..0000000 --- a/src/bin.rs +++ /dev/null @@ -1,38 +0,0 @@ -use incantata::*; - -fn main() { - // structure of the language - let s = Structure { - // how many characters the onset is - onset: 1, - // allowed characters for the onset - onset_dict: CONSONANTS.chars().collect(), - - // how many characters the nucleus is - nucleus: 1, - // allowed characters for the nucleus - nucleus_dict: VOCALS - .chars() - .cycle() - .take(VOCALS.len() * 5) - // .chain(VOCALS_ACCENTS.chars()) - .collect(), - - // how many characters the coda is - coda: 0, - // allowed characters for the coda - coda_dict: CONSONANTS.chars().collect(), - - // minimum length of a word - min_len: 4, - // the words will be generated to be around this length - // due to the way incantata works (by combining valid syllables), - // we can't actually make a word of a given length - suggested_len: 15, - }; - - // generate 10 words - for _ in 0..10 { - println!("{}", incantata(&s)); - } -} diff --git a/src/lib.rs b/src/lib.rs index eec5a1a..2dbf761 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,38 +1,107 @@ use rand::{thread_rng, Rng}; -pub const CONSONANTS: &'static str = "bcdfghjklmnpqrstvwxyz"; +pub const CONSONANTS: &str = "bcdfghjklmnpqrstvwxyz"; -pub const VOCALS: &'static str = "aeiou"; -pub const VOCALS_ACCENTS: &'static str = "aeiouàèìòùáéíóúäëïöü"; - -// TODO replace Vec with HashMap, where the value is the probability +pub const VOCALS: &str = "aeiou"; +pub const VOCALS_ACCENTS: &str = "aeiouàèìòùáéíóúäëïöü"; +/// structure of the language pub struct Structure { + /// how many characters the onset is pub onset: usize, + /// allowed characters for the onset pub onset_dict: Vec, + /// how many characters the nucleus is pub nucleus: usize, + /// allowed characters for the nucleus pub nucleus_dict: Vec, + /// how many characters the coda is pub coda: usize, + /// allowed characters for the coda pub coda_dict: Vec, + /// minimum length of a word pub min_len: usize, + // the words will try to be close to `suggested_len` + // + // due to the way incantata works (by combining valid syllables), + // it can't actually make a word of a given length pub suggested_len: usize, } -pub fn incantata(structure: &Structure) -> String { - let mut rng = thread_rng(); - - let len = rng.gen_range(structure.min_len..structure.suggested_len); - - let mut s = String::new(); - while s.len() < len { - let syl = syllable(structure); - s.push_str(&syl); +impl Default for Structure { + fn default() -> Self { + Self { + onset: 1, + onset_dict: CONSONANTS.chars().collect(), + nucleus: 1, + nucleus_dict: VOCALS.chars().collect(), + coda: 0, + coda_dict: Default::default(), + min_len: 4, + suggested_len: 5, + } } +} - s +impl Structure { + pub fn generate(&self) -> String { + assert!( + self.onset > 0 || self.nucleus > 0 || self.coda > 0, + "at least one should be non-zero" + ); + + let mut rng = thread_rng(); + + let len = rng.gen_range(self.min_len..self.suggested_len); + + let mut s = String::new(); + while s.len() < len { + let syl = self.syllable(); + s.push_str(&syl); + } + + s + } + fn syllable(&self) -> String { + let mut rng = thread_rng(); + + let mut state = State::Onset(0); + + let mut s = String::new(); + 'syl: loop { + match state { + State::Onset(n) => { + if n >= self.onset || rng.gen_bool(0.3) { + state = State::Nucleus(0); + } else { + state = State::Onset(n + 1); + s.push(self.onset_dict[rng.gen_range(0..self.onset_dict.len())]); + } + } + State::Nucleus(n) => { + // nucleus is one or more + if n >= self.nucleus || (n > 0 && rng.gen_bool(0.3)) { + state = State::Coda(0); + } else { + state = State::Nucleus(n + 1); + s.push(self.nucleus_dict[rng.gen_range(0..self.nucleus_dict.len())]); + } + } + State::Coda(n) => { + if n >= self.coda || rng.gen_bool(0.3) { + break 'syl; + } else { + state = State::Coda(n + 1); + s.push(self.coda_dict[rng.gen_range(0..self.coda_dict.len())]); + } + } + } + } + s + } } enum State { @@ -41,44 +110,6 @@ enum State { Coda(usize), } -fn syllable(structure: &Structure) -> String { - let mut rng = thread_rng(); - - let mut state = State::Onset(0); - - let mut s = String::new(); - 'syl: loop { - match state { - State::Onset(n) => { - if n >= structure.onset || rng.gen_bool(0.3) { - state = State::Nucleus(0); - } else { - state = State::Onset(n + 1); - s.push(structure.onset_dict[rng.gen_range(0..structure.onset_dict.len())]); - } - } - State::Nucleus(n) => { - // nucleus is one or more - if n >= structure.nucleus || (n > 0 && rng.gen_bool(0.3)) { - state = State::Coda(0); - } else { - state = State::Nucleus(n + 1); - s.push(structure.nucleus_dict[rng.gen_range(0..structure.nucleus_dict.len())]); - } - } - State::Coda(n) => { - if n >= structure.coda || rng.gen_bool(0.3) { - break 'syl; - } else { - state = State::Coda(n + 1); - s.push(structure.coda_dict[rng.gen_range(0..structure.coda_dict.len())]); - } - } - } - } - s -} - #[cfg(test)] mod tests { use super::*; @@ -97,7 +128,37 @@ mod tests { }; for _ in 0..100 { - let r = incantata(&s); + let r = s.generate(); + assert!(r.len() >= 4); + } + } + + #[test] + fn default_works() { + let s = Structure::default(); + + for _ in 0..100 { + let r = s.generate(); + assert!(r.len() >= 4); + } + } + + #[test] + #[should_panic] + fn empty_panics() { + let s = Structure { + onset: 0, + onset_dict: CONSONANTS.chars().collect(), + nucleus: 0, + nucleus_dict: VOCALS.chars().collect(), + coda: 0, + coda_dict: CONSONANTS.chars().collect(), + min_len: 4, + suggested_len: 10, + }; + + for _ in 0..100 { + let r = s.generate(); assert!(r.len() >= 4); } } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..20d3d68 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,58 @@ +use clap::*; +use incantata::*; + +#[derive(Parser)] +#[clap(author, version, about, long_about = None)] +struct Cli { + #[clap(short, long, default_value_t = 1)] + onset: usize, + #[clap(short, long, default_value_t = 1)] + nucleus: usize, + #[clap(short, long, default_value_t = 0)] + coda: usize, + /// minimum length of a word + #[clap(short, long, default_value_t = 4)] + min_len: usize, + #[clap(short, long, default_value_t = 15)] + suggested_len: usize, +} + +fn main() { + let cli = Cli::parse(); + + loop { + // structure of the language + let s = Structure { + // how many characters the onset is + onset: cli.onset, + // allowed characters for the onset + onset_dict: CONSONANTS.chars().collect(), + + // how many characters the nucleus is + nucleus: cli.nucleus, + // allowed characters for the nucleus + nucleus_dict: VOCALS.chars().collect(), + + // how many characters the coda is + coda: cli.coda, + // allowed characters for the coda + coda_dict: CONSONANTS.chars().collect(), + + // minimum length of a word + min_len: cli.min_len, + // the words will be generated to be around this length + // due to the way incantata works (by combining valid syllables), + // we can't actually make a word of a given length + suggested_len: cli.suggested_len, + }; + + // generate 10 words + for _ in 0..10 { + println!("{}", s.generate()); + } + + if std::io::stdin().read_line(&mut String::new()).is_err() { + break; + } + } +}