diff --git a/hhss/rust/.gitignore b/hhss/rust/.gitignore new file mode 100644 index 0000000..9b78076 --- /dev/null +++ b/hhss/rust/.gitignore @@ -0,0 +1,8 @@ +# Rust build artifacts +target/ + +# Built binary +hhss + +# Backup files created by rustfmt +**/*.rs.bk \ No newline at end of file diff --git a/hhss/rust/Cargo.lock b/hhss/rust/Cargo.lock new file mode 100644 index 0000000..2f620dd --- /dev/null +++ b/hhss/rust/Cargo.lock @@ -0,0 +1,133 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hhss" +version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/hhss/rust/Cargo.toml b/hhss/rust/Cargo.toml new file mode 100644 index 0000000..711446e --- /dev/null +++ b/hhss/rust/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "hhss" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "hhss" +path = "hhss.rs" + +[dependencies] +rand = "0.8" \ No newline at end of file diff --git a/hhss/rust/Makefile b/hhss/rust/Makefile new file mode 100644 index 0000000..d661e2c --- /dev/null +++ b/hhss/rust/Makefile @@ -0,0 +1,13 @@ +TARGET = hhss + +all: $(TARGET) + +$(TARGET): hhss.rs Cargo.toml + cargo build --release + cp target/release/$(TARGET) ./$(TARGET) + +clean: + cargo clean + rm -f $(TARGET) + +.PHONY: all clean \ No newline at end of file diff --git a/hhss/rust/hhss.rs b/hhss/rust/hhss.rs new file mode 100644 index 0000000..88ab8ec --- /dev/null +++ b/hhss/rust/hhss.rs @@ -0,0 +1,187 @@ +use std::env; +use std::fs; +use std::io; +use std::process; +use rand::seq::SliceRandom; +use rand::thread_rng; + +const MIN_SENTENCE_NUM: usize = 5; +const MIN_USER_NUM: usize = 1; +const SENTENCE_FILE: &str = "hsr.dat"; +const USER_FILE: &str = "usr.dat"; +const USER_TEMPLATE: &str = "${user}"; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() != 2 { + eprintln!("hhss: argc != 2."); + process::exit(1); + } + + let num_sentences = match parse_num_sentences(&args[1]) { + Ok(n) => n, + Err(e) => { + eprintln!("hhss: {}", e); + process::exit(1); + } + }; + + let sentences = match read_data_file(SENTENCE_FILE) { + Ok(lines) => lines, + Err(e) => { + eprintln!("hhss: failed to open {} as mode \"r\": {}", SENTENCE_FILE, e); + process::exit(1); + } + }; + + let users = match read_data_file(USER_FILE) { + Ok(lines) => lines, + Err(e) => { + eprintln!("hhss: failed to open {} as mode \"r\": {}", USER_FILE, e); + process::exit(1); + } + }; + + if sentences.len() < MIN_SENTENCE_NUM { + eprintln!("hhss: it is required for {} to have valid lines more than or equal to {}.", + SENTENCE_FILE, MIN_SENTENCE_NUM); + process::exit(1); + } + + if users.len() < MIN_USER_NUM { + eprintln!("hhss: it is required for {} to have valid lines more than or equal to {}.", + USER_FILE, MIN_USER_NUM); + process::exit(1); + } + + if num_sentences > sentences.len() { + eprintln!("hhss: argv[1] = {} needs to be <= {}, the number of valid lines in {}.", + num_sentences, sentences.len(), SENTENCE_FILE); + process::exit(1); + } + + let selected_sentences = select_random_sentences(&sentences, num_sentences); + + for sentence in selected_sentences { + let processed_sentence = replace_templates(&sentence, &users); + println!("{}", processed_sentence); + } +} + +fn parse_num_sentences(arg: &str) -> Result { + match arg.parse::() { + Ok(n) => { + if n < MIN_SENTENCE_NUM { + Err(format!("argv[1] = {} needs to be >= {}.", n, MIN_SENTENCE_NUM)) + } else { + Ok(n) + } + } + Err(_) => Err("no conversion can be performed.".to_string()) + } +} + +fn read_data_file(filename: &str) -> Result, io::Error> { + let contents = fs::read_to_string(filename)?; + let mut valid_lines = Vec::new(); + + for line in contents.lines() { + let trimmed = line.trim(); + // Skip empty lines and comments + if !trimmed.is_empty() && !trimmed.starts_with('#') { + valid_lines.push(trimmed.to_string()); + } + } + + Ok(valid_lines) +} + +fn select_random_sentences(sentences: &[String], count: usize) -> Vec { + let mut indices: Vec = (0..sentences.len()).collect(); + let mut rng = thread_rng(); + + // Fisher-Yates shuffle + indices.shuffle(&mut rng); + + // Take first 'count' indices and get corresponding sentences + indices.into_iter() + .take(count) + .map(|i| sentences[i].clone()) + .collect() +} + +fn replace_templates(sentence: &str, users: &[String]) -> String { + let mut result = String::new(); + let mut last_user: Option = None; + let mut rng = thread_rng(); + + let parts: Vec<&str> = sentence.split(USER_TEMPLATE).collect(); + + for (i, part) in parts.iter().enumerate() { + result.push_str(part); + + // If this is not the last part, we need to add a user name + if i < parts.len() - 1 { + let mut available_users = users.to_vec(); + + // If we have more than one user and we used one before, remove it to avoid repetition + if users.len() > 1 { + if let Some(last) = &last_user { + available_users.retain(|u| u != last); + } + } + + let selected_user = available_users.choose(&mut rng).unwrap(); + result.push_str(selected_user); + last_user = Some(selected_user.clone()); + } + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_num_sentences() { + assert_eq!(parse_num_sentences("5").unwrap(), 5); + assert_eq!(parse_num_sentences("10").unwrap(), 10); + assert!(parse_num_sentences("4").is_err()); + assert!(parse_num_sentences("abc").is_err()); + } + + #[test] + fn test_replace_templates() { + let users = vec!["Alice".to_string(), "Bob".to_string()]; + let sentence = "Hello ${user}, how are you ${user}?"; + let result = replace_templates(sentence, &users); + + // Should contain both users and they should be different + assert!(result.contains("Alice") || result.contains("Bob")); + assert!(result.contains("Hello")); + assert!(result.contains("how are you")); + assert!(!result.contains("${user}")); + } + + #[test] + fn test_select_random_sentences() { + let sentences = vec![ + "First".to_string(), + "Second".to_string(), + "Third".to_string(), + "Fourth".to_string(), + "Fifth".to_string(), + ]; + + let selected = select_random_sentences(&sentences, 3); + assert_eq!(selected.len(), 3); + + // All selected sentences should be from the original list + for sentence in &selected { + assert!(sentences.contains(sentence)); + } + } +} \ No newline at end of file