Skip to content

Commit cfa8bd7

Browse files
authored
refactor: modularize Rust codebase and enhance CI/CD pipeline (#45)
- Reorganize Rust modules: extract dispersion.rs and mathematics.rs from lib.rs - Implement thiserror for improved error handling and Magnus error conversion - Add rb-sys-env build dependency and enable rb-sys feature for better FFI - Introduce continuous delivery workflow with gem artifact building - Update branch protection workflow and rename from main.yml - Switch default Rake task from compile to compile:dev for development - Add .rules file to gitignore and update test coverage
1 parent a6f71ba commit cfa8bd7

File tree

12 files changed

+281
-160
lines changed

12 files changed

+281
-160
lines changed

.DS_Store

6 KB
Binary file not shown.

.github/.DS_Store

6 KB
Binary file not shown.
Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
name: Test suite
2-
1+
name: branch-protection
32
on:
4-
- push
5-
3+
pull_request:
4+
branches:
5+
- main
6+
permissions:
7+
contents: read
68
jobs:
7-
run-tests:
8-
name: Run tests
9+
test:
910
strategy:
1011
matrix:
1112
os:
@@ -21,6 +22,6 @@ jobs:
2122
- uses: ruby/setup-ruby@a9bfc2ecf3dd40734a9418f89a7e9d484c32b990
2223
with:
2324
ruby-version: ${{ matrix.ruby }}
24-
- run: gem install bundler --version 2.7.0 --no-document
25+
- run: gem install bundler --no-document
2526
- run: bundle
2627
- run: rake
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: continuous-delivery
2+
3+
permissions:
4+
packages: write
5+
6+
on:
7+
push:
8+
branches:
9+
- main
10+
concurrency:
11+
group: continuous-delivery
12+
cancel-in-progress: true
13+
14+
jobs:
15+
build:
16+
name: Run tests
17+
strategy:
18+
matrix:
19+
os:
20+
- ubuntu-latest
21+
- macos-latest
22+
runs-on: ${{ matrix.os }}
23+
steps:
24+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
25+
- uses: ruby/setup-ruby@a9bfc2ecf3dd40734a9418f89a7e9d484c32b990
26+
with:
27+
ruby-version: 3.4.4
28+
- run: gem install bundler --no-document
29+
- run: bundle
30+
- run: rake native gem
31+
- uses: actions/upload-artifact@v4
32+
with:
33+
name: gem-${{ matrix.os }}
34+
path: pkg/*-${{ matrix.os }}.gem

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,4 @@ build-iPhoneSimulator/
5959

6060
/ext/ruby_native_statistics/target
6161
/target
62+
/.rules

Cargo.lock

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Rakefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ Rake::TestTask.new benchmark: [:clean, :clobber, 'compile:release'] do |t|
1515
t.test_files = ['test/**/*_benchmark.rb']
1616
end
1717

18-
task :default => [:compile, :test]
18+
task :default => ['compile:dev', :test]

ext/ruby_native_statistics/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,8 @@ edition = "2024"
77
crate-type = ["cdylib"]
88

99
[dependencies]
10-
magnus = { version = "0.7.1" }
10+
magnus = { version = "0.7.1", features = ["rb-sys"] }
11+
thiserror = "2.0.12"
12+
13+
[build-dependencies]
14+
rb-sys-env = "0.1"
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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+
pub fn percentile(rb_self: RArray, percentile: f64) -> Result<f64, DispersionError> {
115+
let mut array = rb_self
116+
.to_vec::<f64>()
117+
.map_err(|e| DispersionError::MagnusError(e))?;
118+
119+
if array.is_empty() {
120+
return Err(DispersionError::EmptyArray);
121+
}
122+
123+
if !(0.0..=1.0).contains(&percentile) {
124+
return Err(DispersionError::RangeError);
125+
}
126+
127+
calculate_percentile(&mut array, percentile)
128+
}

0 commit comments

Comments
 (0)