ruby_native_statistics 1.1.1 → 2.0.0.rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Cargo.lock +399 -0
- data/Cargo.toml +3 -0
- data/README.md +16 -23
- data/Rakefile +12 -14
- data/changelog.md +5 -0
- data/ext/ruby_native_statistics/Cargo.toml +19 -0
- data/ext/ruby_native_statistics/extconf.rb +4 -7
- data/ext/ruby_native_statistics/src/dispersion.rs +319 -0
- data/ext/ruby_native_statistics/src/lib.rs +20 -0
- data/ext/ruby_native_statistics/src/mathematics.rs +255 -0
- data/lib/ruby_native_statistics/version.rb +3 -1
- data/lib/ruby_native_statistics.rb +3 -6
- metadata +28 -44
- data/.gitignore +0 -58
- data/Gemfile +0 -3
- data/Gemfile.lock +0 -43
- data/ext/ruby_native_statistics/conversions.c +0 -56
- data/ext/ruby_native_statistics/conversions.h +0 -5
- data/ext/ruby_native_statistics/dispersion.c +0 -102
- data/ext/ruby_native_statistics/dispersion.h +0 -10
- data/ext/ruby_native_statistics/mathematics.c +0 -84
- data/ext/ruby_native_statistics/mathematics.h +0 -9
- data/ext/ruby_native_statistics/ruby_native_statistics.c +0 -16
- data/ext/ruby_native_statistics/ruby_native_statistics.h +0 -5
- data/ruby_native_statistics.gemspec +0 -31
@@ -0,0 +1,319 @@
|
|
1
|
+
use magnus::{Error, RArray, Ruby};
|
2
|
+
|
3
|
+
#[derive(thiserror::Error, Debug)]
|
4
|
+
pub enum DispersionError {
|
5
|
+
#[error("Array must have at least one element")]
|
6
|
+
EmptyArray,
|
7
|
+
|
8
|
+
#[error("Input is out of range")]
|
9
|
+
RangeError,
|
10
|
+
|
11
|
+
#[error("Magnus error")]
|
12
|
+
MagnusError(magnus::Error),
|
13
|
+
}
|
14
|
+
|
15
|
+
impl magnus::error::IntoError for DispersionError {
|
16
|
+
fn into_error(self, ruby: &Ruby) -> Error {
|
17
|
+
match self {
|
18
|
+
DispersionError::EmptyArray => {
|
19
|
+
Error::new(ruby.exception_range_error(), self.to_string())
|
20
|
+
}
|
21
|
+
DispersionError::RangeError => {
|
22
|
+
Error::new(ruby.exception_range_error(), self.to_string())
|
23
|
+
}
|
24
|
+
DispersionError::MagnusError(err) => err,
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
fn calculate_mean(array: &[f64]) -> f64 {
|
30
|
+
let length = array.len() as f64;
|
31
|
+
let sum = array.iter().sum::<f64>();
|
32
|
+
sum / length
|
33
|
+
}
|
34
|
+
|
35
|
+
fn calculate_variance(array: &[f64], population: bool) -> f64 {
|
36
|
+
let length = array.len() as f64;
|
37
|
+
let distance_from_mean = distance_from_mean(array);
|
38
|
+
let divisor = if population { length } else { length - 1.0 };
|
39
|
+
distance_from_mean / divisor
|
40
|
+
}
|
41
|
+
|
42
|
+
fn calculate_stdev(array: &[f64], population: bool) -> f64 {
|
43
|
+
calculate_variance(array, population).sqrt()
|
44
|
+
}
|
45
|
+
|
46
|
+
fn distance_from_mean(array: &[f64]) -> f64 {
|
47
|
+
let mean = calculate_mean(array);
|
48
|
+
|
49
|
+
array.iter().fold(0.0, |acc, x| acc + (x - mean).powi(2))
|
50
|
+
}
|
51
|
+
|
52
|
+
pub fn var(rb_self: RArray) -> Result<f64, DispersionError> {
|
53
|
+
let array = rb_self
|
54
|
+
.to_vec::<f64>()
|
55
|
+
.map_err(|e| DispersionError::MagnusError(e))?;
|
56
|
+
|
57
|
+
if array.is_empty() {
|
58
|
+
return Err(DispersionError::EmptyArray);
|
59
|
+
}
|
60
|
+
|
61
|
+
Ok(calculate_variance(&array, false))
|
62
|
+
}
|
63
|
+
|
64
|
+
pub fn stdev(rb_self: RArray) -> Result<f64, DispersionError> {
|
65
|
+
let array = rb_self
|
66
|
+
.to_vec::<f64>()
|
67
|
+
.map_err(|e| DispersionError::MagnusError(e))?;
|
68
|
+
|
69
|
+
if array.is_empty() {
|
70
|
+
return Err(DispersionError::EmptyArray);
|
71
|
+
}
|
72
|
+
|
73
|
+
Ok(calculate_stdev(&array, false))
|
74
|
+
}
|
75
|
+
|
76
|
+
pub fn varp(rb_self: RArray) -> Result<f64, DispersionError> {
|
77
|
+
let array = rb_self
|
78
|
+
.to_vec::<f64>()
|
79
|
+
.map_err(|e| DispersionError::MagnusError(e))?;
|
80
|
+
|
81
|
+
if array.is_empty() {
|
82
|
+
return Err(DispersionError::EmptyArray);
|
83
|
+
}
|
84
|
+
|
85
|
+
Ok(calculate_variance(&array, true))
|
86
|
+
}
|
87
|
+
|
88
|
+
pub fn stdevp(rb_self: RArray) -> Result<f64, DispersionError> {
|
89
|
+
let array = rb_self
|
90
|
+
.to_vec::<f64>()
|
91
|
+
.map_err(|e| DispersionError::MagnusError(e))?;
|
92
|
+
|
93
|
+
if array.is_empty() {
|
94
|
+
return Err(DispersionError::EmptyArray);
|
95
|
+
}
|
96
|
+
|
97
|
+
Ok(calculate_stdev(&array, true))
|
98
|
+
}
|
99
|
+
|
100
|
+
fn calculate_percentile(array: &mut [f64], percentile: f64) -> Result<f64, DispersionError> {
|
101
|
+
let length = array.len() as f64;
|
102
|
+
|
103
|
+
array.sort_by(|a, b| a.total_cmp(b));
|
104
|
+
|
105
|
+
let h = (length - 1.0) * percentile + 1.0;
|
106
|
+
if h.trunc() == h {
|
107
|
+
Ok(array[(h as usize) - 1])
|
108
|
+
} else {
|
109
|
+
let h_floor = h.trunc() as usize;
|
110
|
+
|
111
|
+
Ok((h - h_floor as f64) * (array[h_floor] - array[h_floor - 1]) + array[h_floor - 1])
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
pub fn percentile(rb_self: RArray, percentile: f64) -> Result<f64, DispersionError> {
|
116
|
+
let mut array = rb_self
|
117
|
+
.to_vec::<f64>()
|
118
|
+
.map_err(|e| DispersionError::MagnusError(e))?;
|
119
|
+
|
120
|
+
if array.is_empty() {
|
121
|
+
return Err(DispersionError::EmptyArray);
|
122
|
+
}
|
123
|
+
|
124
|
+
if !(0.0..=1.0).contains(&percentile) {
|
125
|
+
return Err(DispersionError::RangeError);
|
126
|
+
}
|
127
|
+
|
128
|
+
calculate_percentile(&mut array, percentile)
|
129
|
+
}
|
130
|
+
|
131
|
+
#[cfg(test)]
|
132
|
+
mod tests {
|
133
|
+
use super::*;
|
134
|
+
|
135
|
+
#[test]
|
136
|
+
fn test_calculate_mean() {
|
137
|
+
assert_eq!(calculate_mean(&[1.0, 2.0, 3.0, 4.0, 5.0]), 3.0);
|
138
|
+
assert_eq!(calculate_mean(&[10.0]), 10.0);
|
139
|
+
assert_eq!(calculate_mean(&[1.5, 2.5]), 2.0);
|
140
|
+
assert_eq!(calculate_mean(&[-1.0, 0.0, 1.0]), 0.0);
|
141
|
+
assert_eq!(calculate_mean(&[2.5, 7.5, 15.0, 5.0]), 7.5);
|
142
|
+
}
|
143
|
+
|
144
|
+
#[test]
|
145
|
+
fn test_distance_from_mean() {
|
146
|
+
// For [1, 2, 3], mean = 2, distances = [1, 0, 1], sum of squares = 2
|
147
|
+
assert_eq!(distance_from_mean(&[1.0, 2.0, 3.0]), 2.0);
|
148
|
+
|
149
|
+
// For single element, distance should be 0
|
150
|
+
assert_eq!(distance_from_mean(&[5.0]), 0.0);
|
151
|
+
|
152
|
+
// For [0, 0, 0], all distances are 0
|
153
|
+
assert_eq!(distance_from_mean(&[0.0, 0.0, 0.0]), 0.0);
|
154
|
+
|
155
|
+
// For [-2, 0, 2], mean = 0, distances = [4, 0, 4], sum = 8
|
156
|
+
assert_eq!(distance_from_mean(&[-2.0, 0.0, 2.0]), 8.0);
|
157
|
+
}
|
158
|
+
|
159
|
+
#[test]
|
160
|
+
fn test_calculate_variance_sample() {
|
161
|
+
// Sample variance: divide by n-1
|
162
|
+
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
163
|
+
let expected = 2.5; // distance_from_mean = 10, n-1 = 4, variance = 2.5
|
164
|
+
assert_eq!(calculate_variance(&data, false), expected);
|
165
|
+
|
166
|
+
// Two elements
|
167
|
+
let data = [1.0, 3.0];
|
168
|
+
let expected = 2.0; // distance_from_mean = 2, n-1 = 1, variance = 2.0
|
169
|
+
assert_eq!(calculate_variance(&data, false), expected);
|
170
|
+
|
171
|
+
// All same values
|
172
|
+
let data = [5.0, 5.0, 5.0];
|
173
|
+
assert_eq!(calculate_variance(&data, false), 0.0);
|
174
|
+
}
|
175
|
+
|
176
|
+
#[test]
|
177
|
+
fn test_calculate_variance_population() {
|
178
|
+
// Population variance: divide by n
|
179
|
+
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
180
|
+
let expected = 2.0; // distance_from_mean = 10, n = 5, variance = 2.0
|
181
|
+
assert_eq!(calculate_variance(&data, true), expected);
|
182
|
+
|
183
|
+
// Single element
|
184
|
+
let data = [10.0];
|
185
|
+
assert_eq!(calculate_variance(&data, true), 0.0);
|
186
|
+
|
187
|
+
// Two elements
|
188
|
+
let data = [1.0, 3.0];
|
189
|
+
let expected = 1.0; // distance_from_mean = 2, n = 2, variance = 1.0
|
190
|
+
assert_eq!(calculate_variance(&data, true), expected);
|
191
|
+
}
|
192
|
+
|
193
|
+
#[test]
|
194
|
+
fn test_calculate_stdev_sample() {
|
195
|
+
// Sample standard deviation is sqrt of sample variance
|
196
|
+
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
197
|
+
let expected = 2.5_f64.sqrt(); // sample variance = 2.5
|
198
|
+
assert_eq!(calculate_stdev(&data, false), expected);
|
199
|
+
|
200
|
+
// All same values
|
201
|
+
let data = [7.0, 7.0, 7.0, 7.0];
|
202
|
+
assert_eq!(calculate_stdev(&data, false), 0.0);
|
203
|
+
}
|
204
|
+
|
205
|
+
#[test]
|
206
|
+
fn test_calculate_stdev_population() {
|
207
|
+
// Population standard deviation is sqrt of population variance
|
208
|
+
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
209
|
+
let expected = 2.0_f64.sqrt(); // population variance = 2.0
|
210
|
+
assert_eq!(calculate_stdev(&data, true), expected);
|
211
|
+
|
212
|
+
// Single element
|
213
|
+
let data = [42.0];
|
214
|
+
assert_eq!(calculate_stdev(&data, true), 0.0);
|
215
|
+
}
|
216
|
+
|
217
|
+
#[test]
|
218
|
+
fn test_calculate_percentile_basic() {
|
219
|
+
let mut data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
220
|
+
|
221
|
+
// 0th percentile (minimum)
|
222
|
+
assert_eq!(calculate_percentile(&mut data, 0.0).unwrap(), 1.0);
|
223
|
+
|
224
|
+
// 50th percentile (median)
|
225
|
+
assert_eq!(calculate_percentile(&mut data, 0.5).unwrap(), 3.0);
|
226
|
+
|
227
|
+
// 100th percentile (maximum)
|
228
|
+
assert_eq!(calculate_percentile(&mut data, 1.0).unwrap(), 5.0);
|
229
|
+
}
|
230
|
+
|
231
|
+
#[test]
|
232
|
+
fn test_calculate_percentile_interpolation() {
|
233
|
+
let mut data = [1.0, 2.0, 3.0, 4.0];
|
234
|
+
|
235
|
+
// 25th percentile: h = (4-1)*0.25 + 1 = 1.75
|
236
|
+
// Interpolate between index 0 (value 1) and index 1 (value 2)
|
237
|
+
// Result = 0.75 * (2-1) + 1 = 1.75
|
238
|
+
assert_eq!(calculate_percentile(&mut data, 0.25).unwrap(), 1.75);
|
239
|
+
|
240
|
+
// 75th percentile: h = (4-1)*0.75 + 1 = 3.25
|
241
|
+
// Interpolate between index 2 (value 3) and index 3 (value 4)
|
242
|
+
// Result = 0.25 * (4-3) + 3 = 3.25
|
243
|
+
assert_eq!(calculate_percentile(&mut data, 0.75).unwrap(), 3.25);
|
244
|
+
}
|
245
|
+
|
246
|
+
#[test]
|
247
|
+
fn test_calculate_percentile_single_element() {
|
248
|
+
let mut data = [42.0];
|
249
|
+
|
250
|
+
assert_eq!(calculate_percentile(&mut data, 0.0).unwrap(), 42.0);
|
251
|
+
assert_eq!(calculate_percentile(&mut data, 0.5).unwrap(), 42.0);
|
252
|
+
assert_eq!(calculate_percentile(&mut data, 1.0).unwrap(), 42.0);
|
253
|
+
}
|
254
|
+
|
255
|
+
#[test]
|
256
|
+
fn test_calculate_percentile_unsorted_data() {
|
257
|
+
let mut data = [5.0, 1.0, 3.0, 2.0, 4.0];
|
258
|
+
|
259
|
+
// Should sort internally and return correct percentiles
|
260
|
+
assert_eq!(calculate_percentile(&mut data, 0.0).unwrap(), 1.0);
|
261
|
+
assert_eq!(calculate_percentile(&mut data, 0.5).unwrap(), 3.0);
|
262
|
+
assert_eq!(calculate_percentile(&mut data, 1.0).unwrap(), 5.0);
|
263
|
+
}
|
264
|
+
|
265
|
+
#[test]
|
266
|
+
fn test_calculate_percentile_with_duplicates() {
|
267
|
+
let mut data = [1.0, 2.0, 2.0, 3.0, 4.0];
|
268
|
+
|
269
|
+
assert_eq!(calculate_percentile(&mut data, 0.0).unwrap(), 1.0);
|
270
|
+
assert_eq!(calculate_percentile(&mut data, 1.0).unwrap(), 4.0);
|
271
|
+
|
272
|
+
// 50th percentile should handle duplicates correctly
|
273
|
+
let result = calculate_percentile(&mut data, 0.5).unwrap();
|
274
|
+
assert!(result >= 2.0 && result <= 3.0);
|
275
|
+
}
|
276
|
+
|
277
|
+
#[test]
|
278
|
+
fn test_calculate_percentile_negative_numbers() {
|
279
|
+
let mut data = [-5.0, -2.0, 0.0, 2.0, 5.0];
|
280
|
+
|
281
|
+
assert_eq!(calculate_percentile(&mut data, 0.0).unwrap(), -5.0);
|
282
|
+
assert_eq!(calculate_percentile(&mut data, 0.5).unwrap(), 0.0);
|
283
|
+
assert_eq!(calculate_percentile(&mut data, 1.0).unwrap(), 5.0);
|
284
|
+
}
|
285
|
+
|
286
|
+
#[test]
|
287
|
+
fn test_variance_and_stdev_consistency() {
|
288
|
+
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
289
|
+
|
290
|
+
// Sample variance and stdev should be consistent
|
291
|
+
let sample_var = calculate_variance(&data, false);
|
292
|
+
let sample_stdev = calculate_stdev(&data, false);
|
293
|
+
assert!((sample_stdev * sample_stdev - sample_var).abs() < 1e-14);
|
294
|
+
|
295
|
+
// Population variance and stdev should be consistent
|
296
|
+
let pop_var = calculate_variance(&data, true);
|
297
|
+
let pop_stdev = calculate_stdev(&data, true);
|
298
|
+
assert!((pop_stdev * pop_stdev - pop_var).abs() < 1e-14);
|
299
|
+
}
|
300
|
+
|
301
|
+
#[test]
|
302
|
+
fn test_mathematical_properties() {
|
303
|
+
let data = [2.0, 4.0, 6.0, 8.0, 10.0];
|
304
|
+
let mean = calculate_mean(&data);
|
305
|
+
|
306
|
+
// Mean should be the average
|
307
|
+
assert_eq!(mean, 6.0);
|
308
|
+
|
309
|
+
// Population variance should be less than sample variance (when n > 1)
|
310
|
+
let pop_var = calculate_variance(&data, true);
|
311
|
+
let sample_var = calculate_variance(&data, false);
|
312
|
+
assert!(pop_var < sample_var);
|
313
|
+
|
314
|
+
// Population stdev should be less than sample stdev
|
315
|
+
let pop_stdev = calculate_stdev(&data, true);
|
316
|
+
let sample_stdev = calculate_stdev(&data, false);
|
317
|
+
assert!(pop_stdev < sample_stdev);
|
318
|
+
}
|
319
|
+
}
|
@@ -0,0 +1,20 @@
|
|
1
|
+
use magnus::{Error, Module, Ruby, method};
|
2
|
+
|
3
|
+
mod dispersion;
|
4
|
+
mod mathematics;
|
5
|
+
|
6
|
+
#[magnus::init]
|
7
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
8
|
+
let array = ruby.class_array();
|
9
|
+
|
10
|
+
array.define_method("mean", method!(mathematics::mean, 0))?;
|
11
|
+
array.define_method("median", method!(mathematics::median, 0))?;
|
12
|
+
array.define_method("stdev", method!(dispersion::stdev, 0))?;
|
13
|
+
array.define_method("stdevs", method!(dispersion::stdev, 0))?;
|
14
|
+
array.define_method("stdevp", method!(dispersion::stdevp, 0))?;
|
15
|
+
array.define_method("var", method!(dispersion::var, 0))?;
|
16
|
+
array.define_method("varp", method!(dispersion::varp, 0))?;
|
17
|
+
array.define_method("percentile", method!(dispersion::percentile, 1))?;
|
18
|
+
|
19
|
+
Ok(())
|
20
|
+
}
|
@@ -0,0 +1,255 @@
|
|
1
|
+
use magnus::{Error, RArray, Ruby};
|
2
|
+
|
3
|
+
#[derive(thiserror::Error, Debug)]
|
4
|
+
pub enum MathematicsError {
|
5
|
+
#[error("Array must have at least one element")]
|
6
|
+
EmptyArray,
|
7
|
+
|
8
|
+
#[error("Magnus error")]
|
9
|
+
MagnusError(magnus::Error),
|
10
|
+
}
|
11
|
+
|
12
|
+
impl magnus::error::IntoError for MathematicsError {
|
13
|
+
fn into_error(self, ruby: &Ruby) -> Error {
|
14
|
+
match self {
|
15
|
+
MathematicsError::EmptyArray => {
|
16
|
+
Error::new(ruby.exception_range_error(), self.to_string())
|
17
|
+
}
|
18
|
+
MathematicsError::MagnusError(err) => err,
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
pub fn calculate_mean(array: &[f64]) -> f64 {
|
24
|
+
let length = array.len() as f64;
|
25
|
+
let sum = array.iter().sum::<f64>();
|
26
|
+
sum / length
|
27
|
+
}
|
28
|
+
|
29
|
+
pub fn calculate_median(array: &[f64]) -> Result<f64, MathematicsError> {
|
30
|
+
if array.is_empty() {
|
31
|
+
return Err(MathematicsError::EmptyArray);
|
32
|
+
}
|
33
|
+
|
34
|
+
let mut sorted_array = array.to_vec();
|
35
|
+
sorted_array.sort_by(|a, b| a.total_cmp(b));
|
36
|
+
|
37
|
+
let length = sorted_array.len();
|
38
|
+
let array_even_size = (length % 2) == 0;
|
39
|
+
let middle = length / 2;
|
40
|
+
|
41
|
+
if !array_even_size {
|
42
|
+
Ok(sorted_array[middle])
|
43
|
+
} else {
|
44
|
+
Ok((sorted_array[middle - 1] + sorted_array[middle]) / 2.0)
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
pub fn mean(rb_self: RArray) -> Result<f64, MathematicsError> {
|
49
|
+
let array = rb_self
|
50
|
+
.to_vec::<f64>()
|
51
|
+
.map_err(|e| MathematicsError::MagnusError(e))?;
|
52
|
+
|
53
|
+
if array.is_empty() {
|
54
|
+
return Err(MathematicsError::EmptyArray);
|
55
|
+
}
|
56
|
+
|
57
|
+
Ok(calculate_mean(&array))
|
58
|
+
}
|
59
|
+
|
60
|
+
pub fn median(rb_self: RArray) -> Result<f64, MathematicsError> {
|
61
|
+
let array = rb_self
|
62
|
+
.to_vec::<f64>()
|
63
|
+
.map_err(|e| MathematicsError::MagnusError(e))?;
|
64
|
+
|
65
|
+
calculate_median(&array)
|
66
|
+
}
|
67
|
+
|
68
|
+
#[cfg(test)]
|
69
|
+
mod tests {
|
70
|
+
use super::*;
|
71
|
+
|
72
|
+
#[test]
|
73
|
+
fn test_calculate_mean_single_element() {
|
74
|
+
assert_eq!(calculate_mean(&[5.0]), 5.0);
|
75
|
+
}
|
76
|
+
|
77
|
+
#[test]
|
78
|
+
fn test_calculate_mean_multiple_elements() {
|
79
|
+
assert_eq!(calculate_mean(&[1.0, 2.0, 3.0, 4.0, 5.0]), 3.0);
|
80
|
+
}
|
81
|
+
|
82
|
+
#[test]
|
83
|
+
fn test_calculate_mean_with_negative_numbers() {
|
84
|
+
assert_eq!(calculate_mean(&[-2.0, -1.0, 0.0, 1.0, 2.0]), 0.0);
|
85
|
+
}
|
86
|
+
|
87
|
+
#[test]
|
88
|
+
fn test_calculate_mean_with_decimals() {
|
89
|
+
let result = calculate_mean(&[1.5, 2.5, 3.5]);
|
90
|
+
assert!((result - 2.5).abs() < f64::EPSILON);
|
91
|
+
}
|
92
|
+
|
93
|
+
#[test]
|
94
|
+
fn test_calculate_mean_large_numbers() {
|
95
|
+
let result = calculate_mean(&[1000000.0, 2000000.0, 3000000.0]);
|
96
|
+
assert_eq!(result, 2000000.0);
|
97
|
+
}
|
98
|
+
|
99
|
+
#[test]
|
100
|
+
fn test_calculate_median_empty_array() {
|
101
|
+
let result = calculate_median(&[]);
|
102
|
+
assert!(matches!(result, Err(MathematicsError::EmptyArray)));
|
103
|
+
}
|
104
|
+
|
105
|
+
#[test]
|
106
|
+
fn test_calculate_median_single_element() {
|
107
|
+
assert_eq!(calculate_median(&[42.0]).unwrap(), 42.0);
|
108
|
+
}
|
109
|
+
|
110
|
+
#[test]
|
111
|
+
fn test_calculate_median_odd_length_sorted() {
|
112
|
+
assert_eq!(calculate_median(&[1.0, 2.0, 3.0]).unwrap(), 2.0);
|
113
|
+
}
|
114
|
+
|
115
|
+
#[test]
|
116
|
+
fn test_calculate_median_odd_length_unsorted() {
|
117
|
+
assert_eq!(calculate_median(&[3.0, 1.0, 2.0]).unwrap(), 2.0);
|
118
|
+
}
|
119
|
+
|
120
|
+
#[test]
|
121
|
+
fn test_calculate_median_even_length_sorted() {
|
122
|
+
assert_eq!(calculate_median(&[1.0, 2.0, 3.0, 4.0]).unwrap(), 2.5);
|
123
|
+
}
|
124
|
+
|
125
|
+
#[test]
|
126
|
+
fn test_calculate_median_even_length_unsorted() {
|
127
|
+
assert_eq!(calculate_median(&[4.0, 1.0, 3.0, 2.0]).unwrap(), 2.5);
|
128
|
+
}
|
129
|
+
|
130
|
+
#[test]
|
131
|
+
fn test_calculate_median_with_duplicates() {
|
132
|
+
assert_eq!(calculate_median(&[1.0, 2.0, 2.0, 3.0]).unwrap(), 2.0);
|
133
|
+
assert_eq!(calculate_median(&[1.0, 2.0, 2.0, 2.0, 3.0]).unwrap(), 2.0);
|
134
|
+
}
|
135
|
+
|
136
|
+
#[test]
|
137
|
+
fn test_calculate_median_with_negative_numbers() {
|
138
|
+
assert_eq!(calculate_median(&[-3.0, -1.0, 0.0, 1.0, 3.0]).unwrap(), 0.0);
|
139
|
+
assert_eq!(calculate_median(&[-4.0, -2.0, -1.0, 1.0]).unwrap(), -1.5);
|
140
|
+
}
|
141
|
+
|
142
|
+
#[test]
|
143
|
+
fn test_calculate_median_with_decimals() {
|
144
|
+
let result = calculate_median(&[1.1, 2.2, 3.3]).unwrap();
|
145
|
+
assert!((result - 2.2).abs() < f64::EPSILON);
|
146
|
+
}
|
147
|
+
|
148
|
+
#[test]
|
149
|
+
fn test_calculate_median_preserves_original_array() {
|
150
|
+
let original = vec![3.0, 1.0, 4.0, 2.0];
|
151
|
+
let original_copy = original.clone();
|
152
|
+
let _result = calculate_median(&original).unwrap();
|
153
|
+
assert_eq!(original, original_copy);
|
154
|
+
}
|
155
|
+
|
156
|
+
#[test]
|
157
|
+
fn test_calculate_median_with_infinity() {
|
158
|
+
assert_eq!(
|
159
|
+
calculate_median(&[f64::NEG_INFINITY, 0.0, f64::INFINITY]).unwrap(),
|
160
|
+
0.0
|
161
|
+
);
|
162
|
+
}
|
163
|
+
|
164
|
+
#[test]
|
165
|
+
fn test_calculate_median_with_nan() {
|
166
|
+
// NaN values should be handled by total_cmp, but behavior may be implementation-defined
|
167
|
+
let result = calculate_median(&[1.0, f64::NAN, 3.0]);
|
168
|
+
// We can't easily test the exact result with NaN, but it shouldn't panic
|
169
|
+
assert!(result.is_ok());
|
170
|
+
}
|
171
|
+
|
172
|
+
#[test]
|
173
|
+
fn test_mathematics_error_display() {
|
174
|
+
let empty_error = MathematicsError::EmptyArray;
|
175
|
+
assert_eq!(
|
176
|
+
empty_error.to_string(),
|
177
|
+
"Array must have at least one element"
|
178
|
+
);
|
179
|
+
}
|
180
|
+
|
181
|
+
#[test]
|
182
|
+
fn test_mathematics_error_debug() {
|
183
|
+
let empty_error = MathematicsError::EmptyArray;
|
184
|
+
let debug_string = format!("{:?}", empty_error);
|
185
|
+
assert!(debug_string.contains("EmptyArray"));
|
186
|
+
}
|
187
|
+
|
188
|
+
// Integration-style tests for the core functions
|
189
|
+
#[test]
|
190
|
+
fn test_mean_and_median_consistency_single_element() {
|
191
|
+
let data = [7.5];
|
192
|
+
let mean_result = calculate_mean(&data);
|
193
|
+
let median_result = calculate_median(&data).unwrap();
|
194
|
+
assert_eq!(mean_result, median_result);
|
195
|
+
}
|
196
|
+
|
197
|
+
#[test]
|
198
|
+
fn test_mean_and_median_symmetric_distribution() {
|
199
|
+
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
200
|
+
let mean_result = calculate_mean(&data);
|
201
|
+
let median_result = calculate_median(&data).unwrap();
|
202
|
+
assert_eq!(mean_result, median_result);
|
203
|
+
}
|
204
|
+
|
205
|
+
#[test]
|
206
|
+
fn test_large_dataset_performance() {
|
207
|
+
let large_data: Vec<f64> = (1..=10000).map(|x| x as f64).collect();
|
208
|
+
|
209
|
+
let mean_result = calculate_mean(&large_data);
|
210
|
+
assert_eq!(mean_result, 5000.5);
|
211
|
+
|
212
|
+
let median_result = calculate_median(&large_data).unwrap();
|
213
|
+
assert_eq!(median_result, 5000.5);
|
214
|
+
}
|
215
|
+
|
216
|
+
#[test]
|
217
|
+
fn test_edge_case_very_small_numbers() {
|
218
|
+
let data = [
|
219
|
+
f64::MIN_POSITIVE,
|
220
|
+
f64::MIN_POSITIVE * 2.0,
|
221
|
+
f64::MIN_POSITIVE * 3.0,
|
222
|
+
];
|
223
|
+
let mean_result = calculate_mean(&data);
|
224
|
+
let median_result = calculate_median(&data).unwrap();
|
225
|
+
|
226
|
+
assert!(mean_result > 0.0);
|
227
|
+
assert!(median_result > 0.0);
|
228
|
+
}
|
229
|
+
|
230
|
+
#[test]
|
231
|
+
fn test_edge_case_very_large_numbers() {
|
232
|
+
let large_value = f64::MAX / 10.0;
|
233
|
+
let data = [large_value, large_value, large_value];
|
234
|
+
let mean_result = calculate_mean(&data);
|
235
|
+
let median_result = calculate_median(&data).unwrap();
|
236
|
+
|
237
|
+
assert!(!mean_result.is_infinite());
|
238
|
+
assert!(!median_result.is_infinite());
|
239
|
+
assert_eq!(mean_result, median_result);
|
240
|
+
assert_eq!(mean_result, large_value);
|
241
|
+
}
|
242
|
+
|
243
|
+
#[test]
|
244
|
+
fn test_precision_with_many_small_values() {
|
245
|
+
let data: Vec<f64> = vec![0.1; 1000];
|
246
|
+
|
247
|
+
let median_result = calculate_median(&data).unwrap();
|
248
|
+
assert!((median_result - 0.1).abs() < f64::EPSILON);
|
249
|
+
|
250
|
+
let mean_result = calculate_mean(&data);
|
251
|
+
// Use a reasonable tolerance for accumulated floating point errors
|
252
|
+
let tolerance = 1e-10;
|
253
|
+
assert!((mean_result - 0.1).abs() < tolerance);
|
254
|
+
}
|
255
|
+
}
|
@@ -1,7 +1,4 @@
|
|
1
|
-
|
2
|
-
require "ruby_native_statistics/ruby_native_statistics"
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
include Dispersion
|
7
|
-
end
|
3
|
+
require 'ruby_native_statistics/version'
|
4
|
+
require 'ruby_native_statistics/ruby_native_statistics'
|