philiprehberger-math_kit 0.2.3 → 0.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e454a11061739fecb690a7246eb3b4120676ab809e103868b1c7b1c6772456f
4
- data.tar.gz: c47a3d320f89cf3446cfe4f649e1b4ddc2f058f7cbaf1d5351e25d97a60f5499
3
+ metadata.gz: a73db6abf0e0f987688786bd18c0b959182dc5290c52ac52e04a55926361436e
4
+ data.tar.gz: 85c7d136bf63030f7009b736aa0d44c81429e383973bb8bd65ca58f0aa08c927
5
5
  SHA512:
6
- metadata.gz: c6e207c58fd8512f5ef5b61bab968d4379004d3d93a118dad5b340521615f3405270c64aa253665066568249e5ab6d41e1914da73cffa2a3e4203868dc2827f7
7
- data.tar.gz: da21ff4a0bf20630b2cc46477147ad530ec79d9fdc563dea0900c972cb618d37a5ddead11b9210e8a0fee0e735c1141ed72f6ac04761b9e1065fd5dcd5c325a4
6
+ metadata.gz: 63bbbb1ca5f8c31f476dbb87f9df33569a4cdd420e49b736f2a5289c5f7e3839cff4cc3353af2c8db87f5bf909feb7b7047088e29167ea36d7318752c271848c
7
+ data.tar.gz: 183f0a9fec1904a98d37520df268f260e7f6e4dd9e32403fc5ce1ef3e3fdf1d346947d1751e757a4d81de0eae73cd215b393c19faa82a3b7510d7ff4b23d6c9a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2026-04-15
11
+
12
+ ### Added
13
+ - `Numeric` module with common integer and value helpers
14
+ - `Numeric.factorial(n)` for non-negative integer factorial (arbitrary precision)
15
+ - `Numeric.fibonacci(n)` for the n-th Fibonacci number via iterative O(n)/O(1) computation
16
+ - `Numeric.gcd(a, b)` for greatest common divisor (accepts negative inputs)
17
+ - `Numeric.lcm(a, b)` for least common multiple (accepts negative inputs)
18
+ - `Numeric.clamp(value, min, max)` for constraining a value to a range
19
+
20
+ ## [0.3.0] - 2026-04-10
21
+
22
+ ### Added
23
+ - `Stats.describe(values)` for summary statistics (count, mean, median, min, max, stddev, variance, percentiles)
24
+ - `Stats.histogram(values, bins:)` for frequency distribution into equal-width bins
25
+ - `Stats.weighted_mean(values, weights:)` for weighted arithmetic mean
26
+
10
27
  ## [0.2.3] - 2026-04-08
11
28
 
12
29
  ### Changed
@@ -59,3 +76,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
59
76
  - Linear interpolation between sorted points with extrapolation
60
77
  - Rounding modes: bankers (round half to even), ceiling, floor, truncate with precision
61
78
  - Simple moving average and exponential moving average
79
+
80
+ [Unreleased]: https://github.com/philiprehberger/rb-math-kit/compare/v0.4.0...HEAD
81
+ [0.4.0]: https://github.com/philiprehberger/rb-math-kit/compare/v0.3.0...v0.4.0
82
+ [0.3.0]: https://github.com/philiprehberger/rb-math-kit/compare/v0.2.3...v0.3.0
83
+ [0.2.3]: https://github.com/philiprehberger/rb-math-kit/compare/v0.2.2...v0.2.3
84
+ [0.2.2]: https://github.com/philiprehberger/rb-math-kit/compare/v0.2.1...v0.2.2
85
+ [0.2.1]: https://github.com/philiprehberger/rb-math-kit/compare/v0.2.0...v0.2.1
86
+ [0.2.0]: https://github.com/philiprehberger/rb-math-kit/compare/v0.1.2...v0.2.0
87
+ [0.1.2]: https://github.com/philiprehberger/rb-math-kit/compare/v0.1.1...v0.1.2
88
+ [0.1.1]: https://github.com/philiprehberger/rb-math-kit/compare/v0.1.0...v0.1.1
89
+ [0.1.0]: https://github.com/philiprehberger/rb-math-kit/releases/tag/v0.1.0
data/README.md CHANGED
@@ -43,6 +43,28 @@ Philiprehberger::MathKit::Stats.sum([1, 2, 3]) # => 6
43
43
  Philiprehberger::MathKit::Stats.range([1, 5, 3, 9, 2]) # => 8
44
44
  ```
45
45
 
46
+ ### Summary Statistics
47
+
48
+ ```ruby
49
+ Philiprehberger::MathKit::Stats.describe([1, 2, 3, 4, 5])
50
+ # => { count: 5, mean: 3.0, median: 3.0, min: 1.0, max: 5.0,
51
+ # stddev: 1.58..., variance: 2.5, p25: 2.0, p50: 3.0, p75: 4.0 }
52
+ ```
53
+
54
+ ### Weighted Mean
55
+
56
+ ```ruby
57
+ Philiprehberger::MathKit::Stats.weighted_mean([10, 20, 30], weights: [3, 1, 1])
58
+ # => 16.0
59
+ ```
60
+
61
+ ### Histogram
62
+
63
+ ```ruby
64
+ Philiprehberger::MathKit::Stats.histogram([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], bins: 5)
65
+ # => [{ min: 1.0, max: 2.8, count: 2 }, { min: 2.8, max: 4.6, count: 2 }, ...]
66
+ ```
67
+
46
68
  ### Skewness and Kurtosis
47
69
 
48
70
  ```ruby
@@ -116,6 +138,16 @@ Philiprehberger::MathKit::MovingAverage.simple([1, 2, 3, 4, 5], window: 3)
116
138
  Philiprehberger::MathKit::MovingAverage.exponential([1, 2, 3, 4, 5], alpha: 0.5) # => [1.0, 1.5, 2.25, 3.125, 4.0625]
117
139
  ```
118
140
 
141
+ ### Numeric Helpers
142
+
143
+ ```ruby
144
+ Philiprehberger::MathKit::Numeric.factorial(5) # => 120
145
+ Philiprehberger::MathKit::Numeric.fibonacci(10) # => 55
146
+ Philiprehberger::MathKit::Numeric.gcd(12, 18) # => 6
147
+ Philiprehberger::MathKit::Numeric.lcm(4, 6) # => 12
148
+ Philiprehberger::MathKit::Numeric.clamp(42, 0, 10) # => 10
149
+ ```
150
+
119
151
  ## API
120
152
 
121
153
  ### `Stats`
@@ -140,6 +172,9 @@ Philiprehberger::MathKit::MovingAverage.exponential([1, 2, 3, 4, 5], alpha: 0.5)
140
172
  | `.median_absolute_deviation(values)` | Median absolute deviation |
141
173
  | `.trimmed_mean(values, trim: 0.1)` | Trimmed mean (remove fraction from each end) |
142
174
  | `.winsorized_mean(values, trim: 0.1)` | Winsorized mean (clamp extremes) |
175
+ | `.describe(values)` | Summary statistics hash (count, mean, median, min, max, stddev, percentiles) |
176
+ | `.histogram(values, bins: 10)` | Frequency distribution as array of bin hashes |
177
+ | `.weighted_mean(values, weights:)` | Weighted arithmetic mean |
143
178
 
144
179
  ### `Regression`
145
180
 
@@ -173,6 +208,16 @@ Philiprehberger::MathKit::MovingAverage.exponential([1, 2, 3, 4, 5], alpha: 0.5)
173
208
  | `.simple(values, window:)` | Simple moving average |
174
209
  | `.exponential(values, alpha:)` | Exponential moving average |
175
210
 
211
+ ### `Numeric`
212
+
213
+ | Method | Description |
214
+ |--------|-------------|
215
+ | `.factorial(n)` | Factorial of a non-negative integer |
216
+ | `.fibonacci(n)` | N-th Fibonacci number (0-indexed) |
217
+ | `.gcd(a, b)` | Greatest common divisor of two integers |
218
+ | `.lcm(a, b)` | Least common multiple of two integers |
219
+ | `.clamp(value, min, max)` | Clamp a numeric value between min and max |
220
+
176
221
  ## Development
177
222
 
178
223
  ```bash
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Philiprehberger
4
+ module MathKit
5
+ # Numeric helpers for common integer and value operations
6
+ module Numeric
7
+ class << self
8
+ # Factorial of a non-negative integer (n!)
9
+ #
10
+ # @param n [Integer] a non-negative integer
11
+ # @return [Integer] the factorial of n
12
+ # @raise [ArgumentError] if n is negative or not an Integer
13
+ def factorial(n)
14
+ raise ArgumentError, 'factorial requires an Integer' unless n.is_a?(Integer)
15
+ raise ArgumentError, 'factorial requires a non-negative integer' if n.negative?
16
+
17
+ (1..n).reduce(1, :*)
18
+ end
19
+
20
+ # N-th Fibonacci number (0-indexed: fibonacci(0) = 0, fibonacci(1) = 1)
21
+ #
22
+ # Uses an iterative algorithm for O(n) time and O(1) space.
23
+ #
24
+ # @param n [Integer] a non-negative index
25
+ # @return [Integer] the n-th Fibonacci number
26
+ # @raise [ArgumentError] if n is negative or not an Integer
27
+ def fibonacci(n)
28
+ raise ArgumentError, 'fibonacci requires an Integer' unless n.is_a?(Integer)
29
+ raise ArgumentError, 'fibonacci requires a non-negative integer' if n.negative?
30
+
31
+ a = 0
32
+ b = 1
33
+ n.times { a, b = b, a + b }
34
+ a
35
+ end
36
+
37
+ # Greatest common divisor of two integers (Euclidean algorithm)
38
+ #
39
+ # @param a [Integer] first integer
40
+ # @param b [Integer] second integer
41
+ # @return [Integer] the non-negative greatest common divisor
42
+ # @raise [ArgumentError] if either argument is not an Integer
43
+ def gcd(a, b)
44
+ raise ArgumentError, 'gcd requires Integer arguments' unless a.is_a?(Integer) && b.is_a?(Integer)
45
+
46
+ a.abs.gcd(b.abs)
47
+ end
48
+
49
+ # Least common multiple of two integers
50
+ #
51
+ # @param a [Integer] first integer
52
+ # @param b [Integer] second integer
53
+ # @return [Integer] the non-negative least common multiple (0 if either is 0)
54
+ # @raise [ArgumentError] if either argument is not an Integer
55
+ def lcm(a, b)
56
+ raise ArgumentError, 'lcm requires Integer arguments' unless a.is_a?(Integer) && b.is_a?(Integer)
57
+
58
+ a.abs.lcm(b.abs)
59
+ end
60
+
61
+ # Clamp a numeric value between a minimum and maximum
62
+ #
63
+ # @param value [Numeric] the value to clamp
64
+ # @param min [Numeric] lower bound
65
+ # @param max [Numeric] upper bound
66
+ # @return [Numeric] the clamped value
67
+ # @raise [ArgumentError] if min is greater than max
68
+ def clamp(value, min, max)
69
+ raise ArgumentError, 'min must not be greater than max' if min > max
70
+
71
+ return min if value < min
72
+ return max if value > max
73
+
74
+ value
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -291,6 +291,71 @@ module Philiprehberger
291
291
  mean(winsorized)
292
292
  end
293
293
 
294
+ # Summary statistics for a dataset
295
+ #
296
+ # @param values [Array<Numeric>] the input values
297
+ # @return [Hash] with :count, :mean, :median, :min, :max, :stddev, :variance, :p25, :p50, :p75
298
+ # @raise [ArgumentError] if values is empty
299
+ def describe(values)
300
+ raise ArgumentError, 'values must not be empty' if values.empty?
301
+
302
+ {
303
+ count: values.size,
304
+ mean: mean(values),
305
+ median: median(values),
306
+ min: values.min.to_f,
307
+ max: values.max.to_f,
308
+ stddev: values.size >= 2 ? stddev(values, population: false) : 0.0,
309
+ variance: values.size >= 2 ? variance(values, population: false) : 0.0,
310
+ p25: percentile(values, 25),
311
+ p50: percentile(values, 50),
312
+ p75: percentile(values, 75)
313
+ }
314
+ end
315
+
316
+ # Frequency distribution (histogram)
317
+ #
318
+ # @param values [Array<Numeric>] the input values
319
+ # @param bins [Integer] number of bins (default: 10)
320
+ # @return [Array<Hash>] array of { min:, max:, count: } hashes
321
+ # @raise [ArgumentError] if values is empty or bins < 1
322
+ def histogram(values, bins: 10)
323
+ raise ArgumentError, 'values must not be empty' if values.empty?
324
+ raise ArgumentError, 'bins must be at least 1' if bins < 1
325
+
326
+ min_val = values.min.to_f
327
+ max_val = values.max.to_f
328
+ width = max_val == min_val ? 1.0 : (max_val - min_val) / bins.to_f
329
+
330
+ result = Array.new(bins) do |i|
331
+ { min: min_val + (i * width), max: min_val + ((i + 1) * width), count: 0 }
332
+ end
333
+
334
+ values.each do |v|
335
+ idx = width.zero? ? 0 : ((v - min_val) / width).floor
336
+ idx = bins - 1 if idx >= bins
337
+ result[idx][:count] += 1
338
+ end
339
+
340
+ result
341
+ end
342
+
343
+ # Weighted arithmetic mean
344
+ #
345
+ # @param values [Array<Numeric>] the input values
346
+ # @param weights [Array<Numeric>] the corresponding weights
347
+ # @return [Float] the weighted mean
348
+ # @raise [ArgumentError] if arrays differ in size, are empty, or weights sum to zero
349
+ def weighted_mean(values, weights:)
350
+ raise ArgumentError, 'values must not be empty' if values.empty?
351
+ raise ArgumentError, 'values and weights must have the same size' if values.size != weights.size
352
+
353
+ total_weight = weights.sum.to_f
354
+ raise ArgumentError, 'weights must not sum to zero' if total_weight.zero?
355
+
356
+ values.zip(weights).sum { |v, w| v * w } / total_weight
357
+ end
358
+
294
359
  private
295
360
 
296
361
  # T-distribution critical values for common confidence levels
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module MathKit
5
- VERSION = '0.2.3'
5
+ VERSION = '0.4.0'
6
6
  end
7
7
  end
@@ -6,6 +6,7 @@ require_relative 'math_kit/interpolation'
6
6
  require_relative 'math_kit/round'
7
7
  require_relative 'math_kit/moving_average'
8
8
  require_relative 'math_kit/regression'
9
+ require_relative 'math_kit/numeric'
9
10
 
10
11
  module Philiprehberger
11
12
  module MathKit
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: philiprehberger-math_kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Rehberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-09 00:00:00.000000000 Z
11
+ date: 2026-04-15 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Descriptive statistics, linear interpolation, rounding modes, and moving
14
14
  averages. Lightweight math toolkit with zero dependencies.
@@ -24,6 +24,7 @@ files:
24
24
  - lib/philiprehberger/math_kit.rb
25
25
  - lib/philiprehberger/math_kit/interpolation.rb
26
26
  - lib/philiprehberger/math_kit/moving_average.rb
27
+ - lib/philiprehberger/math_kit/numeric.rb
27
28
  - lib/philiprehberger/math_kit/regression.rb
28
29
  - lib/philiprehberger/math_kit/round.rb
29
30
  - lib/philiprehberger/math_kit/stats.rb