use rand::RngExt;
use crate::rng::Rng;
use crate::space::{Space, SpaceInfo};
#[derive(Debug, Clone)]
pub struct TextSpace {
min_length: usize,
max_length: usize,
charset: Vec<char>,
}
impl TextSpace {
#[must_use]
pub fn new(min_length: usize, max_length: usize, charset: &[char]) -> Self {
assert!(!charset.is_empty(), "TextSpace charset must not be empty");
assert!(
min_length <= max_length,
"min_length ({min_length}) must be <= max_length ({max_length})"
);
let mut chars: Vec<char> = charset.to_vec();
chars.sort_unstable();
chars.dedup();
Self {
min_length,
max_length,
charset: chars,
}
}
#[must_use]
pub fn alphanumeric(min_length: usize, max_length: usize) -> Self {
let charset: Vec<char> = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
.chars()
.collect();
Self::new(min_length, max_length, &charset)
}
#[must_use]
pub const fn min_length(&self) -> usize {
self.min_length
}
#[must_use]
pub const fn max_length(&self) -> usize {
self.max_length
}
#[must_use]
pub fn charset(&self) -> &[char] {
&self.charset
}
}
impl Space for TextSpace {
type Element = String;
#[allow(clippy::cast_possible_truncation)]
fn sample(&self, rng: &mut Rng) -> String {
use rand::seq::IndexedRandom;
let len = if self.min_length == self.max_length {
self.min_length
} else {
rng.random_range(self.min_length..=self.max_length)
};
(0..len)
.map(|_| *self.charset.choose(rng).expect("charset is non-empty"))
.collect()
}
fn contains(&self, value: &String) -> bool {
let len = value.chars().count();
if len < self.min_length || len > self.max_length {
return false;
}
value.chars().all(|c| self.charset.contains(&c))
}
fn shape(&self) -> &[usize] {
&[]
}
fn flatdim(&self) -> usize {
self.max_length
}
fn space_info(&self) -> SpaceInfo {
SpaceInfo::Text {
min_length: self.min_length,
max_length: self.max_length,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rng::create_rng;
#[test]
fn alphanumeric_sample() {
let space = TextSpace::alphanumeric(1, 10);
let mut rng = create_rng(Some(42));
for _ in 0..20 {
let s = space.sample(&mut rng);
assert!(space.contains(&s));
assert!((1..=10).contains(&s.len()));
}
}
#[test]
fn fixed_length() {
let space = TextSpace::alphanumeric(5, 5);
let mut rng = create_rng(Some(0));
let s = space.sample(&mut rng);
assert_eq!(s.len(), 5);
assert!(space.contains(&s));
}
#[test]
fn custom_charset() {
let space = TextSpace::new(3, 3, &['a', 'b', 'c']);
let mut rng = create_rng(Some(42));
let s = space.sample(&mut rng);
assert_eq!(s.len(), 3);
assert!(s.chars().all(|c| "abc".contains(c)));
assert!(space.contains(&s));
}
#[test]
fn rejects_invalid_chars() {
let space = TextSpace::new(1, 5, &['a', 'b']);
assert!(!space.contains(&"abc".to_string()));
}
#[test]
fn rejects_wrong_length() {
let space = TextSpace::alphanumeric(2, 4);
assert!(!space.contains(&"a".to_string()));
assert!(!space.contains(&"abcde".to_string()));
assert!(space.contains(&"ab".to_string()));
}
#[test]
#[should_panic(expected = "charset must not be empty")]
fn empty_charset_panics() {
let _ = TextSpace::new(1, 5, &[]);
}
#[test]
#[should_panic(expected = "min_length")]
fn inverted_length_panics() {
let _ = TextSpace::alphanumeric(10, 5);
}
}