chris_lib 2.2.1 → 2.2.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/lib/chris_lib/chris_math.rb +117 -23
- data/lib/chris_lib/date_ext.rb +13 -1
- data/lib/chris_lib/for_chris_lib.rb +835 -0
- data/lib/chris_lib/shell_methods.rb +80 -10
- data/lib/chris_lib/test_access.rb +25 -6
- data/lib/chris_lib/version.rb +1 -1
- data/lib/chris_lib.rb +3 -0
- metadata +62 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed68c4652e1c8bb9171e1c6faa07dbcfd8dfacfc4ade1eefba8f64698040a367
|
4
|
+
data.tar.gz: 7b6e5522b4861ea44572dcee282119409383e2434bff297229ed5330b8191629
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f10142175fdfbd73a02475c480ab41f6040355c00fd2e6382bd8cfc92fe8328ba34255f9bdd0a165bcf261ef58c1afff88fd73b39e1c29923df6e79b13892c61
|
7
|
+
data.tar.gz: 2bdda10220d21a555725f6c88d71746f5db73813c67a9f587e6ada6792c1a1817c003fbead5dae9aad481e4c5fd236c7717ab898f8172b123f3ec8bff9df936b
|
data/lib/chris_lib/chris_math.rb
CHANGED
@@ -1,30 +1,38 @@
|
|
1
1
|
require 'matrix'
|
2
2
|
require 'quaternion'
|
3
3
|
Quaternion.class_eval do
|
4
|
-
#
|
4
|
+
# @param n [Integer] number of decimal places to retain
|
5
|
+
# @return [Quaternion] a new quaternion with each component rounded
|
5
6
|
def round(n)
|
6
7
|
q = self
|
7
8
|
Quaternion.new(q[0].round(n), q[1].round(n), q[2].round(n), q[3].round(n))
|
8
9
|
end
|
9
10
|
|
10
|
-
#
|
11
|
+
# @return [Quaternion] the identity quaternion (1, 0, 0, 0)
|
11
12
|
def self.identity
|
12
13
|
Quaternion.new(1.0, 0.0, 0.0, 0.0)
|
13
14
|
end
|
14
15
|
|
15
|
-
#
|
16
|
+
# @return [Quaternion] the zero quaternion (0, 0, 0, 0)
|
16
17
|
def self.zero
|
17
18
|
Quaternion.new(0.0, 0.0, 0.0, 0.0)
|
18
19
|
end
|
19
20
|
|
20
21
|
# Creates quaternion using square brackets (for compatability with Matrix and Vector)
|
21
|
-
# see https://stackoverflow.com/questions/69155796/how-to-define-a-class-method-when-using-class-eval
|
22
|
+
# @see https://stackoverflow.com/questions/69155796/how-to-define-a-class-method-when-using-class-eval
|
23
|
+
# @param q0 [Numeric]
|
24
|
+
# @param q1 [Numeric]
|
25
|
+
# @param q2 [Numeric]
|
26
|
+
# @param q3 [Numeric]
|
27
|
+
# @return [Quaternion]
|
22
28
|
def self.[](q0, q1, q2, q3)
|
23
29
|
Quaternion.new(q0, q1, q2, q3)
|
24
30
|
end
|
25
31
|
end
|
26
32
|
|
27
33
|
Integer.class_eval do
|
34
|
+
# @return [Integer] factorial of the number
|
35
|
+
# @raise [RuntimeError] when the receiver is greater than 20 to avoid overflow
|
28
36
|
def factorial
|
29
37
|
n = self
|
30
38
|
if n > 20
|
@@ -36,49 +44,63 @@ Integer.class_eval do
|
|
36
44
|
end
|
37
45
|
|
38
46
|
Matrix.class_eval do
|
39
|
-
#
|
47
|
+
# Right pseudo-inverse for linearly independent rows
|
48
|
+
# @return [Matrix]
|
49
|
+
# @raise [ExceptionForMatrix::ErrNotRegular] when the matrix is rank-deficient
|
40
50
|
def pinv
|
41
51
|
full_rank = (rank == [row_count, column_count].min)
|
42
52
|
raise ExceptionForMatrix::ErrNotRegular unless full_rank
|
43
53
|
transpose * (self * transpose).inv
|
44
54
|
end
|
45
55
|
|
46
|
-
# for
|
56
|
+
# Alias for {#pinv} maintained for backwards compatibility
|
57
|
+
# @return [Matrix]
|
58
|
+
# @raise [ExceptionForMatrix::ErrNotRegular] when the matrix is rank-deficient
|
47
59
|
def pinv_right
|
48
60
|
full_rank = (rank == [row_count, column_count].min)
|
49
61
|
raise ExceptionForMatrix::ErrNotRegular unless full_rank
|
50
62
|
transpose * (self * transpose).inv
|
51
63
|
end
|
52
64
|
|
53
|
-
# for linearly independent columns
|
65
|
+
# Left pseudo-inverse for linearly independent columns
|
66
|
+
# @return [Matrix]
|
54
67
|
def pinv_left
|
55
68
|
(transpose * self).inv * transpose
|
56
69
|
end
|
57
70
|
end
|
58
71
|
|
59
72
|
Array.class_eval do
|
60
|
-
#
|
61
|
-
#
|
73
|
+
# Converts radians to degrees for each element
|
74
|
+
# @param n_decimals [Integer, nil] decimal places to round to, pass nil for no rounding
|
75
|
+
# @return [Array<Float>] converted values
|
62
76
|
def to_deg(n_decimals = nil)
|
63
77
|
map { |e| e.to_deg(n_decimals) }
|
64
78
|
end
|
65
79
|
|
66
|
-
#
|
67
|
-
#
|
80
|
+
# Converts degrees to radians for each element
|
81
|
+
# @param n_decimals [Integer, nil] decimal places to round to, pass nil for no rounding
|
82
|
+
# @return [Array<Float>] converted values
|
68
83
|
def to_rad(n_decimals = nil)
|
69
84
|
map { |e| e.to_rad(n_decimals) }
|
70
85
|
end
|
71
86
|
|
72
|
-
#
|
87
|
+
# Rounds each element in-place
|
88
|
+
# @param decimal_points [Integer] decimal places to round to
|
89
|
+
# @return [Array<Numeric>] rounded values
|
73
90
|
def round(decimal_points = 0)
|
74
91
|
map { |e| e.round(decimal_points) }
|
75
92
|
end
|
76
93
|
|
94
|
+
# Rounds elements using {Float#eround}
|
95
|
+
# @param decimal_points [Integer]
|
96
|
+
# @return [Array<Float>]
|
77
97
|
def eround(decimal_points = 0)
|
78
98
|
map { |e| e.eround(decimal_points) }
|
79
99
|
end
|
80
100
|
|
81
|
-
# mean of
|
101
|
+
# Calculates the arithmetic mean of elements
|
102
|
+
# @return [Numeric, Vector]
|
103
|
+
# @raise [RuntimeError] when the array is empty
|
82
104
|
def mean
|
83
105
|
raise 'chris_lib - f - Length must be greater than 0.' if length < 1
|
84
106
|
return sum.to_f / length unless all? { |e| e.class == Vector }
|
@@ -86,24 +108,29 @@ Array.class_eval do
|
|
86
108
|
Vector.elements ary
|
87
109
|
end
|
88
110
|
|
89
|
-
# unbiased sample variance
|
111
|
+
# Computes the unbiased sample variance
|
112
|
+
# @return [Float]
|
113
|
+
# @raise [RuntimeError] when the array has fewer than two elements
|
90
114
|
def var
|
91
115
|
raise 'Length must be greater than 1' if length < 2
|
92
116
|
mu = mean
|
93
117
|
map { |v| (v**2 - mu**2) }.sum.to_f / (length - 1)
|
94
118
|
end
|
95
119
|
|
96
|
-
# sample standard deviation
|
120
|
+
# Computes the sample standard deviation
|
121
|
+
# @return [Float]
|
97
122
|
def std
|
98
123
|
return 0 if var < 0.0 # perhaps due to rounding errors
|
99
124
|
Math.sqrt(var)
|
100
125
|
end
|
101
126
|
|
102
|
-
#
|
127
|
+
# Standard error of the sample mean
|
128
|
+
# @return [Float]
|
103
129
|
def std_err
|
104
130
|
std / Math.sqrt(size)
|
105
131
|
end
|
106
132
|
|
133
|
+
# @return [Numeric] median value of the array
|
107
134
|
def median
|
108
135
|
return self[0] if length <= 1
|
109
136
|
sorted = sort
|
@@ -115,16 +142,18 @@ Array.class_eval do
|
|
115
142
|
end
|
116
143
|
end
|
117
144
|
|
118
|
-
#
|
119
|
-
#
|
145
|
+
# Deep duplicate the array
|
146
|
+
# @return [Object] a deep copy of the array
|
147
|
+
# @see https://www.thoughtco.com/making-deep-copies-in-ruby-2907749
|
120
148
|
def deep_dup
|
121
149
|
Marshal.load(Marshal.dump(self))
|
122
150
|
end
|
123
151
|
end
|
124
152
|
|
125
153
|
Float.class_eval do
|
126
|
-
#
|
127
|
-
#
|
154
|
+
# Converts radians to degrees
|
155
|
+
# @param n_decimals [Integer, nil] decimal places to round to, pass nil for no rounding
|
156
|
+
# @return [Float]
|
128
157
|
def to_deg(n_decimals = nil)
|
129
158
|
include Math unless defined?(Math)
|
130
159
|
degrees = self * 180.0 / PI
|
@@ -132,8 +161,9 @@ Float.class_eval do
|
|
132
161
|
degrees.round(n_decimals)
|
133
162
|
end
|
134
163
|
|
135
|
-
#
|
136
|
-
#
|
164
|
+
# Converts degrees to radians
|
165
|
+
# @param n_decimals [Integer, nil] decimal places to round to, pass nil for no rounding
|
166
|
+
# @return [Float]
|
137
167
|
def to_rad(n_decimals = nil)
|
138
168
|
include Math unless defined?(Math)
|
139
169
|
radians = self * PI / 180.0
|
@@ -141,39 +171,57 @@ Float.class_eval do
|
|
141
171
|
radians.round(n_decimals)
|
142
172
|
end
|
143
173
|
|
144
|
-
#
|
174
|
+
# Rounds a float represented in exponential notation
|
175
|
+
# @param decimal_points [Integer]
|
176
|
+
# @return [Float]
|
145
177
|
def eround(decimal_points = 0)
|
146
178
|
("%.#{decimal_points}e" % self).to_f
|
147
179
|
end
|
148
180
|
|
181
|
+
# @param n [Integer] decimal place to round down to
|
182
|
+
# @return [Float]
|
149
183
|
def round_down(n=0)
|
150
184
|
# n is decimal place to round down at
|
151
185
|
int,dec=self.to_s.split('.')
|
152
186
|
"#{int}.#{dec[0...n]}".to_f
|
153
187
|
end
|
154
188
|
|
189
|
+
# @return [Float]
|
155
190
|
def round1
|
156
191
|
round(1)
|
157
192
|
end
|
158
193
|
|
194
|
+
# @return [Float]
|
159
195
|
def round2
|
160
196
|
round(2)
|
161
197
|
end
|
162
198
|
|
199
|
+
# @return [Float]
|
163
200
|
def round3
|
164
201
|
round(3)
|
165
202
|
end
|
166
203
|
|
204
|
+
# @return [Float]
|
167
205
|
def round4
|
168
206
|
round(4)
|
169
207
|
end
|
170
208
|
end
|
171
209
|
|
210
|
+
# Numerically focused helpers and distribution utilities.
|
172
211
|
module ChrisMath
|
173
212
|
|
174
213
|
include Math
|
175
214
|
|
215
|
+
# Generates normally distributed random numbers using Box-Muller
|
216
|
+
# @param n [Integer] number of samples to generate
|
217
|
+
# @return [Array<Float>] array of N(0,1) samples
|
218
|
+
# @note Returns an empty array and warns when `n <= 0`.
|
176
219
|
def gaussian_array(n = 1)
|
220
|
+
raise ArgumentError, 'n must be an Integer' unless n.is_a?(Integer)
|
221
|
+
if n <= 0
|
222
|
+
warn 'gaussian_array called with non-positive sample size; returning empty array'
|
223
|
+
return []
|
224
|
+
end
|
177
225
|
(1..n).map do
|
178
226
|
u1 = rand()
|
179
227
|
u2 = rand()
|
@@ -181,6 +229,8 @@ module ChrisMath
|
|
181
229
|
end
|
182
230
|
end
|
183
231
|
|
232
|
+
# Generates a pair of independent standard normal deviates
|
233
|
+
# @return [Array<Float>] two-element array [z0, z1]
|
184
234
|
def bi_gaussian_rand
|
185
235
|
u1 = rand()
|
186
236
|
u2 = rand()
|
@@ -189,7 +239,21 @@ module ChrisMath
|
|
189
239
|
[z0,z1]
|
190
240
|
end
|
191
241
|
|
242
|
+
# Generates a normally distributed random number with the provided mean and std
|
243
|
+
# @param mean [Numeric]
|
244
|
+
# @param std [Numeric]
|
245
|
+
# @return [Float]
|
246
|
+
# @note Warns when `std <= 0`, returning the mean or using the absolute value.
|
192
247
|
def gaussian_rand(mean,std)
|
248
|
+
raise ArgumentError, 'mean must be Numeric' unless mean.is_a?(Numeric)
|
249
|
+
raise ArgumentError, 'std must be Numeric' unless std.is_a?(Numeric)
|
250
|
+
if std.negative?
|
251
|
+
warn 'std supplied to gaussian_rand was negative; using absolute value'
|
252
|
+
std = std.abs
|
253
|
+
elsif std.zero?
|
254
|
+
warn 'std supplied to gaussian_rand was zero; returning mean'
|
255
|
+
return mean
|
256
|
+
end
|
193
257
|
u1 = rand()
|
194
258
|
u2 = rand()
|
195
259
|
z0 = sqrt(-2*log(u1))*cos(2*PI*u2)
|
@@ -197,7 +261,12 @@ module ChrisMath
|
|
197
261
|
end
|
198
262
|
|
199
263
|
|
264
|
+
# Sample standard deviation for raw values
|
265
|
+
# @param values [Array<Numeric>]
|
266
|
+
# @return [Float]
|
267
|
+
# @raise [RuntimeError] when fewer than two observations are provided
|
200
268
|
def std(values)
|
269
|
+
raise ArgumentError, 'values must respond to #length and #inject' unless values.respond_to?(:length) && values.respond_to?(:inject)
|
201
270
|
n = values.length
|
202
271
|
raise 'n = #{n} but must be greater than 1' if n < 2
|
203
272
|
m = mean(values)
|
@@ -206,7 +275,16 @@ module ChrisMath
|
|
206
275
|
end
|
207
276
|
|
208
277
|
|
278
|
+
# Cumulative probability of at least r successes in n Bernoulli trials
|
279
|
+
# @param n [Integer]
|
280
|
+
# @param r [Integer]
|
281
|
+
# @param p [Float] probability of success per trial
|
282
|
+
# @return [Float]
|
283
|
+
# @raise [RuntimeError] when r is outside 0..n
|
209
284
|
def combinatorial_distribution(n,r,p)
|
285
|
+
raise ArgumentError, 'n must be a non-negative Integer' unless n.is_a?(Integer) && n >= 0
|
286
|
+
raise ArgumentError, 'r must be a non-negative Integer' unless r.is_a?(Integer) && r >= 0
|
287
|
+
raise ArgumentError, 'p must be between 0 and 1' unless p.is_a?(Numeric) && p.between?(0.0,1.0)
|
210
288
|
# probability that r out n or greater hits with the
|
211
289
|
# probability of one hit is p.
|
212
290
|
if r <= n && r >= 0
|
@@ -220,13 +298,29 @@ module ChrisMath
|
|
220
298
|
end
|
221
299
|
end
|
222
300
|
|
301
|
+
# Factorial without overflow protection
|
302
|
+
# @param n [Integer]
|
303
|
+
# @return [Integer]
|
304
|
+
# @note Warns and returns `nil` when `n` is negative.
|
223
305
|
def factorial(n)
|
224
306
|
#from rosetta code
|
307
|
+
raise ArgumentError, 'n must be an Integer' unless n.is_a?(Integer)
|
308
|
+
if n.negative?
|
309
|
+
warn 'factorial called with negative n; returning nil'
|
310
|
+
return nil
|
311
|
+
end
|
225
312
|
(1..n).inject {|prod, i| prod * i}
|
226
313
|
end
|
227
314
|
|
315
|
+
# Binomial coefficient "n choose k"
|
316
|
+
# @param n [Integer]
|
317
|
+
# @param k [Integer]
|
318
|
+
# @return [Integer]
|
228
319
|
def combinatorial(n, k)
|
229
320
|
#from rosetta code
|
321
|
+
raise ArgumentError, 'n must be a non-negative Integer' unless n.is_a?(Integer) && n >= 0
|
322
|
+
raise ArgumentError, 'k must be a non-negative Integer' unless k.is_a?(Integer) && k >= 0
|
323
|
+
raise ArgumentError, 'k must be <= n' if k > n
|
230
324
|
(0...k).inject(1) do |m,i| (m * (n - i)) / (i + 1) end
|
231
325
|
end
|
232
326
|
|
data/lib/chris_lib/date_ext.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
#
|
1
|
+
# Extensions to {Date} for friendly formatting helpers.
|
2
|
+
require 'date'
|
3
|
+
|
2
4
|
module DateExt
|
3
5
|
# To format a date, use <tt>date.charmians_format</tt>
|
4
6
|
# on any Date object.
|
@@ -10,23 +12,28 @@ module DateExt
|
|
10
12
|
# If using a DateTime object, need to convert to date i.e.
|
11
13
|
# <tt>datetime.to_date.charmians_format</tt>.
|
12
14
|
Date.class_eval do
|
15
|
+
# @return [String] Month day, year (e.g. "January 3, 2025")
|
13
16
|
def us_format
|
14
17
|
"#{strftime('%B')} #{day}, #{year}"
|
15
18
|
end
|
16
19
|
|
20
|
+
# @return [String] Short month name day, year (e.g. "Jan 3, 2025")
|
17
21
|
def short_format
|
18
22
|
"#{strftime('%b')} #{day}, #{year}"
|
19
23
|
end
|
20
24
|
|
25
|
+
# @return [String] Weekday, Month day, year (e.g. "Monday, January 3, 2025")
|
21
26
|
def us_format_with_weekday
|
22
27
|
"#{strftime('%A')}, #{strftime('%B')} #{day}, #{year}"
|
23
28
|
end
|
24
29
|
|
30
|
+
# @return [String] "Weekday, Day<sup>suffix</sup> Month, Year" with textual suffix
|
25
31
|
def charmians_format
|
26
32
|
d = cardinalation(day)
|
27
33
|
"#{strftime('%A')}, #{d} #{strftime('%B')}, #{year}"
|
28
34
|
end
|
29
35
|
|
36
|
+
# @return [String] HTML-safe string with ordinal suffix wrapped in `<sup>`
|
30
37
|
def charmians_format_sup
|
31
38
|
d = cardinalation(day, true)
|
32
39
|
"#{strftime('%A')}, #{d} #{strftime('%B')}, #{year}"
|
@@ -34,12 +41,17 @@ module DateExt
|
|
34
41
|
|
35
42
|
private
|
36
43
|
|
44
|
+
# @param day [Integer]
|
45
|
+
# @param html_sup [Boolean]
|
46
|
+
# @return [String]
|
37
47
|
def cardinalation(day, html_sup = false)
|
38
48
|
s = suffix day
|
39
49
|
s = "<sup>#{s}</sup>" if html_sup
|
40
50
|
"#{day}#{s}"
|
41
51
|
end
|
42
52
|
|
53
|
+
# @param day [Integer]
|
54
|
+
# @return [String] ordinal suffix for the provided day of month
|
43
55
|
def suffix(day)
|
44
56
|
case day
|
45
57
|
when 1, 21, 31
|