entrenar/monitor/wasm/
utils.rs1pub fn normalize_values(values: &[f64]) -> Vec<f64> {
5 if values.is_empty() {
6 return Vec::new();
7 }
8
9 let min = values.iter().copied().fold(f64::INFINITY, f64::min);
10 let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
11
12 if (max - min).abs() < 1e-10 {
13 return vec![0.5; values.len()];
15 }
16
17 values.iter().map(|v| (v - min) / (max - min)).collect()
18}
19
20pub fn generate_sparkline(values: &[f64], max_len: usize) -> String {
22 const CHARS: &[char] = &['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
23
24 if values.is_empty() {
25 return String::new();
26 }
27
28 let subsampled: Vec<f64> = if values.len() > max_len {
30 let step = values.len() as f64 / max_len as f64;
31 (0..max_len).map(|i| values[(i as f64 * step) as usize]).collect()
32 } else {
33 values.to_vec()
34 };
35
36 let normalized = normalize_values(&subsampled);
37 normalized
38 .iter()
39 .map(|&v| {
40 let idx = ((v * 7.0).round() as usize).min(7);
41 CHARS[idx]
42 })
43 .collect()
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49
50 #[test]
51 fn test_normalize_values_empty() {
52 let result = normalize_values(&[]);
53 assert!(result.is_empty());
54 }
55
56 #[test]
57 fn test_normalize_values_single() {
58 let result = normalize_values(&[5.0]);
59 assert_eq!(result.len(), 1);
60 assert!((result[0] - 0.5).abs() < 1e-6);
61 }
62
63 #[test]
64 fn test_normalize_values_range() {
65 let result = normalize_values(&[0.0, 5.0, 10.0]);
66 assert_eq!(result.len(), 3);
67 assert!((result[0] - 0.0).abs() < 1e-6);
68 assert!((result[1] - 0.5).abs() < 1e-6);
69 assert!((result[2] - 1.0).abs() < 1e-6);
70 }
71
72 #[test]
73 fn test_normalize_values_constant() {
74 let result = normalize_values(&[5.0, 5.0, 5.0]);
75 assert_eq!(result.len(), 3);
76 for v in &result {
78 assert!((v - 0.5).abs() < 1e-6);
79 }
80 }
81
82 #[test]
83 fn test_generate_sparkline_empty() {
84 let result = generate_sparkline(&[], 20);
85 assert!(result.is_empty());
86 }
87
88 #[test]
89 fn test_generate_sparkline_basic() {
90 let result = generate_sparkline(&[0.0, 0.5, 1.0], 20);
91 assert_eq!(result.chars().count(), 3);
92 }
93
94 #[test]
95 fn test_generate_sparkline_subsample() {
96 let values: Vec<f64> = (0..100).map(f64::from).collect();
97 let result = generate_sparkline(&values, 20);
98 assert_eq!(result.chars().count(), 20);
99 }
100}
101
102#[cfg(test)]
103mod proptests {
104 use super::*;
105 use proptest::prelude::*;
106
107 proptest! {
108 #[test]
110 fn prop_normalize_bounded(values in prop::collection::vec(-1000.0f64..1000.0, 2..100)) {
111 let normalized = normalize_values(&values);
112
113 for v in &normalized {
114 prop_assert!(*v >= 0.0 - 1e-10);
115 prop_assert!(*v <= 1.0 + 1e-10);
116 }
117 }
118
119 #[test]
121 fn prop_normalize_preserves_length(values in prop::collection::vec(-1000.0f64..1000.0, 1..100)) {
122 let normalized = normalize_values(&values);
123 prop_assert_eq!(normalized.len(), values.len());
124 }
125
126 #[test]
128 fn prop_sparkline_bounded(values in prop::collection::vec(0.0f64..100.0, 1..200)) {
129 let sparkline = generate_sparkline(&values, 20);
130 let char_count = sparkline.chars().count();
131 prop_assert!(char_count <= 20);
132 }
133
134 #[test]
136 fn prop_sparkline_valid_chars(values in prop::collection::vec(0.0f64..100.0, 1..50)) {
137 let sparkline = generate_sparkline(&values, 20);
138 let valid_chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
139
140 for c in sparkline.chars() {
141 prop_assert!(valid_chars.contains(&c));
142 }
143 }
144 }
145}