main
annieversary 2022-10-31 19:23:52 +01:00
parent 7fb5637ecb
commit 8686a77190
5 changed files with 220 additions and 94 deletions

View File

@ -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"

44
examples/main.rs Normal file
View File

@ -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;
}
}
}

View File

@ -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));
}
}

View File

@ -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<char> with HashMap<char, f32>, 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<char>,
/// how many characters the nucleus is
pub nucleus: usize,
/// allowed characters for the nucleus
pub nucleus_dict: Vec<char>,
/// how many characters the coda is
pub coda: usize,
/// allowed characters for the coda
pub coda_dict: Vec<char>,
/// 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);
}
}

58
src/main.rs Normal file
View File

@ -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;
}
}
}