pub const CHAR_DIST_DIM: usize = 960;
const NUM_CHARS: usize = 96;
const NUM_STATS: usize = 10;
const STAT_ANY: usize = 0;
const STAT_ALL: usize = 1;
const STAT_MEAN: usize = 2;
const STAT_VARIANCE: usize = 3;
const STAT_MIN: usize = 4;
const STAT_MAX: usize = 5;
const STAT_MEDIAN: usize = 6;
const STAT_SUM: usize = 7;
const STAT_SKEWNESS: usize = 8;
const STAT_KURTOSIS: usize = 9;
pub const CHAR_DIST_NAMES: [&str; CHAR_DIST_DIM] = {
let mut names = [""; CHAR_DIST_DIM];
let mut ci = 0;
while ci < NUM_CHARS {
let mut si = 0;
while si < NUM_STATS {
names[ci * NUM_STATS + si] = const_feature_name(ci, si);
si += 1;
}
ci += 1;
}
names
};
const fn const_feature_name(char_idx: usize, stat_idx: usize) -> &'static str {
FEATURE_NAME_TABLE[char_idx * NUM_STATS + stat_idx]
}
macro_rules! char_stat_names {
($($label:expr),+ $(,)?) => {
[$( concat!($label, "_any"),
concat!($label, "_all"),
concat!($label, "_mean"),
concat!($label, "_variance"),
concat!($label, "_min"),
concat!($label, "_max"),
concat!($label, "_median"),
concat!($label, "_sum"),
concat!($label, "_skewness"),
concat!($label, "_kurtosis"),
)+]
};
}
const FEATURE_NAME_TABLE: [&str; CHAR_DIST_DIM] = char_stat_names![
"space",
"exclamation",
"double_quote",
"hash",
"dollar",
"percent",
"ampersand",
"single_quote",
"open_paren",
"close_paren",
"asterisk",
"plus",
"comma",
"dash",
"period",
"slash",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"colon",
"semicolon",
"less_than",
"equals",
"greater_than",
"question",
"at",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"open_bracket",
"backslash",
"close_bracket",
"caret",
"underscore",
"backtick",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"open_brace",
"pipe",
"close_brace",
"tilde",
"del",
];
pub fn extract_char_distribution(values: &[&str]) -> Option<[f32; CHAR_DIST_DIM]> {
if values.is_empty() {
return None;
}
let n = values.len();
let n_f = n as f64;
let mut frequencies = vec![vec![0.0f64; n]; NUM_CHARS];
for (vi, value) in values.iter().enumerate() {
let len = value.len();
if len == 0 {
continue; }
let len_f = len as f64;
for &byte in value.as_bytes() {
if (32..=127).contains(&byte) {
let ci = (byte - 32) as usize;
frequencies[ci][vi] += 1.0 / len_f;
}
}
}
let mut features = [0.0f32; CHAR_DIST_DIM];
for (ci, freqs) in frequencies.iter_mut().enumerate() {
let base = ci * NUM_STATS;
let any_present = freqs.iter().any(|&f| f > 0.0);
features[base + STAT_ANY] = if any_present { 1.0 } else { 0.0 };
let all_present = freqs.iter().all(|&f| f > 0.0);
features[base + STAT_ALL] = if all_present { 1.0 } else { 0.0 };
let sum: f64 = freqs.iter().sum();
features[base + STAT_SUM] = sum as f32;
let mean = sum / n_f;
features[base + STAT_MEAN] = mean as f32;
let mut min_val = f64::INFINITY;
let mut max_val = f64::NEG_INFINITY;
for &f in freqs.iter() {
if f < min_val {
min_val = f;
}
if f > max_val {
max_val = f;
}
}
features[base + STAT_MIN] = min_val as f32;
features[base + STAT_MAX] = max_val as f32;
freqs.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let median = if n % 2 == 1 {
freqs[n / 2]
} else {
(freqs[n / 2 - 1] + freqs[n / 2]) / 2.0
};
features[base + STAT_MEDIAN] = median as f32;
let variance = freqs.iter().map(|&f| (f - mean).powi(2)).sum::<f64>() / n_f;
features[base + STAT_VARIANCE] = variance as f32;
let (skew, kurt) = skewness_kurtosis(freqs, mean, variance, n_f);
features[base + STAT_SKEWNESS] = skew as f32;
features[base + STAT_KURTOSIS] = kurt as f32;
}
Some(features)
}
fn skewness_kurtosis(values: &[f64], mean: f64, variance: f64, n: f64) -> (f64, f64) {
const EPSILON: f64 = 1e-14;
if variance < EPSILON {
return (0.0, 0.0);
}
let m3: f64 = values.iter().map(|&x| (x - mean).powi(3)).sum::<f64>() / n;
let m4: f64 = values.iter().map(|&x| (x - mean).powi(4)).sum::<f64>() / n;
let stddev = variance.sqrt();
let skewness = m3 / (stddev * stddev * stddev);
let kurtosis = m4 / (variance * variance) - 3.0;
(skewness, kurtosis)
}
#[cfg(test)]
mod tests {
use super::*;
fn feat(features: &[f32; CHAR_DIST_DIM], name: &str) -> f32 {
let idx = CHAR_DIST_NAMES
.iter()
.position(|&n| n == name)
.unwrap_or_else(|| panic!("Unknown feature: {}", name));
features[idx]
}
#[test]
fn test_dimensions() {
assert_eq!(CHAR_DIST_DIM, 960);
assert_eq!(CHAR_DIST_NAMES.len(), CHAR_DIST_DIM);
assert_eq!(NUM_CHARS * NUM_STATS, CHAR_DIST_DIM);
}
#[test]
fn test_feature_names_unique() {
let mut seen = std::collections::HashSet::new();
for name in &CHAR_DIST_NAMES {
assert!(seen.insert(name), "Duplicate feature name: {}", name);
}
}
#[test]
fn test_feature_names_format() {
assert_eq!(CHAR_DIST_NAMES[0], "space_any");
assert_eq!(CHAR_DIST_NAMES[1], "space_all");
assert_eq!(CHAR_DIST_NAMES[9], "space_kurtosis");
assert_eq!(CHAR_DIST_NAMES[10], "exclamation_any");
assert_eq!(CHAR_DIST_NAMES[959], "del_kurtosis");
}
#[test]
fn test_known_column_spot_check() {
let values: Vec<&str> = vec![
"aaa", "aab", "abc", "bbb", "bbc", "ccc", "cca", "aac", "bba", "cab",
];
let features = extract_char_distribution(&values).unwrap();
let a_freqs: Vec<f64> = vec![
1.0,
2.0 / 3.0,
1.0 / 3.0,
0.0,
0.0,
0.0,
1.0 / 3.0,
2.0 / 3.0,
1.0 / 3.0,
1.0 / 3.0,
];
assert_eq!(feat(&features, "a_any"), 1.0);
assert_eq!(feat(&features, "a_all"), 0.0);
let expected_mean: f64 = a_freqs.iter().sum::<f64>() / 10.0;
assert!(
(feat(&features, "a_mean") as f64 - expected_mean).abs() < 1e-5,
"a_mean: got {}, expected {}",
feat(&features, "a_mean"),
expected_mean
);
let expected_sum: f64 = a_freqs.iter().sum();
assert!(
(feat(&features, "a_sum") as f64 - expected_sum).abs() < 1e-5,
"a_sum: got {}, expected {}",
feat(&features, "a_sum"),
expected_sum
);
assert!(
(feat(&features, "a_min") as f64).abs() < 1e-5,
"a_min should be 0.0"
);
assert!(
(feat(&features, "a_max") as f64 - 1.0).abs() < 1e-5,
"a_max should be 1.0"
);
let expected_var: f64 = a_freqs
.iter()
.map(|f| (f - expected_mean).powi(2))
.sum::<f64>()
/ 10.0;
assert!(
(feat(&features, "a_variance") as f64 - expected_var).abs() < 1e-5,
"a_variance: got {}, expected {}",
feat(&features, "a_variance"),
expected_var
);
assert!(
(feat(&features, "a_median") as f64 - 1.0 / 3.0).abs() < 1e-5,
"a_median: got {}, expected {}",
feat(&features, "a_median"),
1.0 / 3.0
);
assert_eq!(feat(&features, "0_any"), 0.0);
assert_eq!(feat(&features, "0_all"), 0.0);
assert!((feat(&features, "0_mean") as f64).abs() < 1e-5);
assert_eq!(feat(&features, "space_any"), 0.0);
assert_eq!(feat(&features, "space_all"), 0.0);
}
#[test]
fn test_all_identical_values() {
let values: Vec<&str> = vec!["abc"; 5];
let features = extract_char_distribution(&values).unwrap();
assert_eq!(feat(&features, "a_any"), 1.0);
assert_eq!(feat(&features, "a_all"), 1.0);
assert!(
(feat(&features, "a_mean") as f64 - 1.0 / 3.0).abs() < 1e-5,
"a_mean"
);
assert!(
(feat(&features, "a_variance") as f64).abs() < 1e-5,
"a_variance should be 0 for identical values"
);
assert!(
(feat(&features, "a_skewness") as f64).abs() < 1e-5,
"a_skewness should be 0"
);
assert!(
(feat(&features, "a_kurtosis") as f64).abs() < 1e-5,
"a_kurtosis should be 0"
);
assert!(
(feat(&features, "a_min") as f64 - 1.0 / 3.0).abs() < 1e-5,
"a_min"
);
assert!(
(feat(&features, "a_max") as f64 - 1.0 / 3.0).abs() < 1e-5,
"a_max"
);
}
#[test]
fn test_empty_strings() {
let values: Vec<&str> = vec!["", "", ""];
let features = extract_char_distribution(&values).unwrap();
for ci in 0..NUM_CHARS {
let base = ci * NUM_STATS;
assert_eq!(features[base + STAT_ANY], 0.0);
assert_eq!(features[base + STAT_ALL], 0.0);
assert_eq!(features[base + STAT_MEAN], 0.0);
assert_eq!(features[base + STAT_VARIANCE], 0.0);
assert_eq!(features[base + STAT_MIN], 0.0);
assert_eq!(features[base + STAT_MAX], 0.0);
assert_eq!(features[base + STAT_MEDIAN], 0.0);
assert_eq!(features[base + STAT_SUM], 0.0);
assert_eq!(features[base + STAT_SKEWNESS], 0.0);
assert_eq!(features[base + STAT_KURTOSIS], 0.0);
}
}
#[test]
fn test_non_ascii_ignored() {
let values: Vec<&str> = vec!["hello\u{1F600}", "\u{4E16}\u{754C}ab", "\u{2764}xyz"];
let features = extract_char_distribution(&values).unwrap();
assert_eq!(features.len(), CHAR_DIST_DIM);
assert_eq!(feat(&features, "h_any"), 1.0);
for &f in features.iter() {
assert!(f.is_finite(), "Non-finite value in features");
}
}
#[test]
fn test_single_value() {
let values: Vec<&str> = vec!["test"];
let features = extract_char_distribution(&values).unwrap();
assert!(
(feat(&features, "t_mean") as f64 - 0.5).abs() < 1e-5,
"t_mean"
);
assert!(
(feat(&features, "t_variance") as f64).abs() < 1e-5,
"t_variance should be 0 for single value"
);
assert!(
(feat(&features, "t_skewness") as f64).abs() < 1e-5,
"t_skewness should be 0 for single value"
);
assert!(
(feat(&features, "t_kurtosis") as f64).abs() < 1e-5,
"t_kurtosis should be 0 for single value"
);
assert!(
(feat(&features, "t_min") as f64 - 0.5).abs() < 1e-5,
"t_min"
);
assert!(
(feat(&features, "t_max") as f64 - 0.5).abs() < 1e-5,
"t_max"
);
assert!(
(feat(&features, "t_median") as f64 - 0.5).abs() < 1e-5,
"t_median"
);
assert!(
(feat(&features, "e_mean") as f64 - 0.25).abs() < 1e-5,
"e_mean"
);
}
#[test]
fn test_empty_slice_returns_none() {
let values: Vec<&str> = vec![];
assert!(extract_char_distribution(&values).is_none());
}
#[test]
fn test_deterministic() {
let values: Vec<&str> = vec!["hello", "world", "test123", "$1,234.56", ""];
let f1 = extract_char_distribution(&values).unwrap();
let f2 = extract_char_distribution(&values).unwrap();
assert_eq!(f1, f2, "Features should be deterministic");
}
}