more_math 1.5.0 → 1.6.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 +4 -4
- data/CHANGES.md +28 -1
- data/README.md +25 -54
- data/Rakefile +8 -2
- data/lib/more_math/cantor_pairing_function.rb +59 -0
- data/lib/more_math/constants/functions_constants.rb +37 -0
- data/lib/more_math/continued_fraction.rb +170 -60
- data/lib/more_math/distributions.rb +98 -9
- data/lib/more_math/entropy.rb +74 -2
- data/lib/more_math/exceptions.rb +26 -0
- data/lib/more_math/functions.rb +140 -4
- data/lib/more_math/histogram.rb +86 -3
- data/lib/more_math/linear_regression.rb +108 -7
- data/lib/more_math/newton_bisection.rb +71 -8
- data/lib/more_math/numberify_string_function.rb +96 -20
- data/lib/more_math/permutation.rb +132 -27
- data/lib/more_math/ranking_common.rb +38 -10
- data/lib/more_math/sequence/moving_average.rb +27 -0
- data/lib/more_math/sequence/refinement.rb +26 -0
- data/lib/more_math/sequence.rb +177 -66
- data/lib/more_math/string_numeral.rb +172 -4
- data/lib/more_math/subset.rb +49 -5
- data/lib/more_math/version.rb +1 -1
- data/lib/more_math.rb +1 -0
- data/more_math.gemspec +4 -3
- metadata +17 -3
data/lib/more_math/histogram.rb
CHANGED
@@ -1,11 +1,37 @@
|
|
1
1
|
require 'tins'
|
2
2
|
|
3
3
|
module MoreMath
|
4
|
-
#
|
4
|
+
# Represents a histogram for visualizing data distributions
|
5
|
+
#
|
6
|
+
# The Histogram class provides functionality to create and display histograms
|
7
|
+
# from sequences of numerical data. It divides the data into bins and counts
|
8
|
+
# how many elements fall into each bin, then displays this information in a
|
9
|
+
# readable format with optional UTF-8 bar characters.
|
10
|
+
#
|
11
|
+
# @example Creating a histogram
|
12
|
+
# sequence = [1, 2, 3, 4, 5, 1]
|
13
|
+
# hist = Histogram.new(sequence, bins: 3)
|
14
|
+
#
|
15
|
+
# @example Displaying a histogram
|
16
|
+
# hist.display($stdout, 80)
|
5
17
|
class Histogram
|
18
|
+
# Represents a single bin in a histogram with left boundary, right
|
19
|
+
# boundary, and count.
|
20
|
+
#
|
21
|
+
# @!attribute [r] left
|
22
|
+
# @return [Float] The left boundary of the bin
|
23
|
+
# @!attribute [r] right
|
24
|
+
# @return [Float] The right boundary of the bin
|
25
|
+
# @!attribute [r] count
|
26
|
+
# @return [Integer] The number of elements in this bin
|
6
27
|
Bin = Struct.new(:left, :right, :count)
|
7
28
|
|
8
29
|
# Create a Histogram for the elements of +sequence+ with +bins+ bins.
|
30
|
+
#
|
31
|
+
# @param sequence [Enumerable] The sequence to build the histogram from
|
32
|
+
# @param arg [Integer, Hash] Number of bins or hash with options like `:bins` and `:with_counts`
|
33
|
+
# @option arg [Integer] :bins (10) Number of bins to use
|
34
|
+
# @option arg [Boolean] :with_counts (false) Whether to display counts in output
|
9
35
|
def initialize(sequence, arg = 10)
|
10
36
|
@with_counts = false
|
11
37
|
if arg.is_a?(Hash)
|
@@ -20,23 +46,39 @@ module MoreMath
|
|
20
46
|
end
|
21
47
|
|
22
48
|
# Number of bins for this Histogram.
|
49
|
+
#
|
50
|
+
# @return [Integer]
|
23
51
|
attr_reader :bins
|
24
52
|
|
25
53
|
# Return the computed histogram as an array of Bin objects.
|
54
|
+
#
|
55
|
+
# @return [Array<Bin>]
|
26
56
|
def to_a
|
27
57
|
@result
|
28
58
|
end
|
29
59
|
|
60
|
+
# Iterate over each bin in the histogram.
|
61
|
+
#
|
62
|
+
# @yield [Bin] each bin
|
63
|
+
# @return [Array<Bin>]
|
30
64
|
def each_bin(&block)
|
31
65
|
@result.each(&block)
|
32
66
|
end
|
33
67
|
|
68
|
+
# Get an array of counts from each bin.
|
69
|
+
#
|
70
|
+
# @return [Array<Integer>]
|
34
71
|
def counts
|
35
72
|
each_bin.map(&:count)
|
36
73
|
end
|
37
74
|
|
38
|
-
# Display this histogram to +output
|
39
|
-
#
|
75
|
+
# Display this histogram to +output+ using +width+ columns. Raises
|
76
|
+
# ArgumentError if width < 15.
|
77
|
+
#
|
78
|
+
# @param output [IO] The output stream to write to (default: $stdout)
|
79
|
+
# @param width [Integer, String] Width of the display; can be a percentage string like "90%"
|
80
|
+
# @raise [ArgumentError] If width is less than 15
|
81
|
+
# @return [self]
|
40
82
|
def display(output = $stdout, width = 65)
|
41
83
|
if width.is_a?(String) && width =~ /(.+)%\z/
|
42
84
|
percentage = Float($1).clamp(0, 100)
|
@@ -50,16 +92,26 @@ module MoreMath
|
|
50
92
|
self
|
51
93
|
end
|
52
94
|
|
95
|
+
# Get terminal width using Tins::Terminal.
|
96
|
+
#
|
97
|
+
# @return [Integer]
|
53
98
|
def terminal_width
|
54
99
|
Tins::Terminal.columns
|
55
100
|
end
|
56
101
|
|
102
|
+
# Get the maximum count in any bin.
|
103
|
+
#
|
104
|
+
# @return [Integer]
|
57
105
|
def max_count
|
58
106
|
counts.max
|
59
107
|
end
|
60
108
|
|
61
109
|
private
|
62
110
|
|
111
|
+
# Generate UTF-8 bar character representation based on width.
|
112
|
+
#
|
113
|
+
# @param bar_width [Float] Width of the bar
|
114
|
+
# @return [String]
|
63
115
|
def utf8_bar(bar_width)
|
64
116
|
fract = bar_width - bar_width.floor
|
65
117
|
bar = ?⣿ * bar_width.floor
|
@@ -71,14 +123,26 @@ module MoreMath
|
|
71
123
|
bar
|
72
124
|
end
|
73
125
|
|
126
|
+
# Generate ASCII bar character representation based on width.
|
127
|
+
#
|
128
|
+
# @param bar_width [Float] Width of the bar
|
129
|
+
# @return [String]
|
74
130
|
def ascii_bar(bar_width)
|
75
131
|
?* * bar_width
|
76
132
|
end
|
77
133
|
|
134
|
+
# Determine if UTF-8 is enabled in the environment.
|
135
|
+
#
|
136
|
+
# @return [Boolean]
|
78
137
|
def utf8?
|
79
138
|
ENV['LANG'] =~ /utf-8\z/i
|
80
139
|
end
|
81
140
|
|
141
|
+
# Format a single row of histogram data for output.
|
142
|
+
#
|
143
|
+
# @param row [Array] A tuple containing [left, right, count]
|
144
|
+
# @param width [Integer] Width of the bar display area
|
145
|
+
# @return [String]
|
82
146
|
def output_row(row, width)
|
83
147
|
left, right, count = row
|
84
148
|
if @with_counts
|
@@ -88,6 +152,13 @@ module MoreMath
|
|
88
152
|
end
|
89
153
|
end
|
90
154
|
|
155
|
+
# Output a row with counts.
|
156
|
+
#
|
157
|
+
# @param left [Float] Left boundary of bin
|
158
|
+
# @param right [Float] Right boundary of bin
|
159
|
+
# @param count [Integer] Count in bin
|
160
|
+
# @param width [Integer] Width of bar display area
|
161
|
+
# @return [String]
|
91
162
|
def output_row_with_count(left, right, count, width)
|
92
163
|
width -= 15
|
93
164
|
c = utf8? ? 2 : 1
|
@@ -103,6 +174,13 @@ module MoreMath
|
|
103
174
|
[ (left + right) / 2.0, bar, count ]
|
104
175
|
end
|
105
176
|
|
177
|
+
# Output a row without counts.
|
178
|
+
#
|
179
|
+
# @param left [Float] Left boundary of bin
|
180
|
+
# @param right [Float] Right boundary of bin
|
181
|
+
# @param count [Integer] Count in bin
|
182
|
+
# @param width [Integer] Width of bar display area
|
183
|
+
# @return [String]
|
106
184
|
def output_row_without_count(left, right, count, width)
|
107
185
|
width -= 15
|
108
186
|
left_width = width
|
@@ -113,6 +191,9 @@ module MoreMath
|
|
113
191
|
"%11.5f -|%#{-width}s\n" % [ (left + right) / 2.0, bar ]
|
114
192
|
end
|
115
193
|
|
194
|
+
# Returns rows for display.
|
195
|
+
#
|
196
|
+
# @return [Array<Array>]
|
116
197
|
def rows
|
117
198
|
@result.reverse_each.map { |bin|
|
118
199
|
[ bin.left, bin.right, bin.count ]
|
@@ -120,6 +201,8 @@ module MoreMath
|
|
120
201
|
end
|
121
202
|
|
122
203
|
# Computes the histogram and returns it as an array of tuples (l, c, r).
|
204
|
+
#
|
205
|
+
# @return [Array<Bin>]
|
123
206
|
def compute
|
124
207
|
@sequence.empty? and return []
|
125
208
|
last_r = -Infinity
|
@@ -1,7 +1,48 @@
|
|
1
1
|
module MoreMath
|
2
2
|
# This class computes a linear regression for the given image and domain data
|
3
3
|
# sets.
|
4
|
+
#
|
5
|
+
# Linear regression is a statistical method that models the relationship
|
6
|
+
# between a dependent variable (image) and one or more independent variables
|
7
|
+
# (domain). It fits a linear equation to observed data points to make
|
8
|
+
# predictions or understand relationships.
|
9
|
+
#
|
10
|
+
# The implementation uses the least squares method to find the best-fit line
|
11
|
+
# y = ax + b, where 'a' is the slope and 'b' is the y-intercept.
|
12
|
+
#
|
13
|
+
# @example Basic usage
|
14
|
+
# # Create a linear regression from data points
|
15
|
+
# image_data = [2, 4, 6, 8, 10]
|
16
|
+
# domain_data = [1, 2, 3, 4, 5]
|
17
|
+
# lr = LinearRegression.new(image_data, domain_data)
|
18
|
+
#
|
19
|
+
# # Access the fitted line parameters
|
20
|
+
# puts lr.a # slope
|
21
|
+
# puts lr.b # y-intercept
|
22
|
+
#
|
23
|
+
# # Make predictions
|
24
|
+
# predicted_y = lr.a * 6 + lr.b # Predict y for x=6
|
25
|
+
#
|
26
|
+
# @example Statistical analysis
|
27
|
+
# # Check if the slope is significantly different from zero
|
28
|
+
# lr.slope_zero?(0.05) # Returns true if slope is not statistically significant
|
29
|
+
#
|
30
|
+
# # Calculate coefficient of determination (R²)
|
31
|
+
# puts lr.r2 # R-squared value indicating model fit
|
4
32
|
class LinearRegression
|
33
|
+
# Creates a new LinearRegression instance with image and domain data.
|
34
|
+
#
|
35
|
+
# Initializes the linear regression model using the provided data points.
|
36
|
+
# The domain data represents independent variables (x-values) and the image
|
37
|
+
# data represents dependent variables (y-values).
|
38
|
+
#
|
39
|
+
# @param image [Array<Numeric>] Array of dependent variable values (y-coordinates)
|
40
|
+
# @param domain [Array<Numeric>] Array of independent variable values (x-coordinates)
|
41
|
+
# @raise [ArgumentError] If image and domain arrays have unequal sizes
|
42
|
+
# @example Creating a linear regression
|
43
|
+
# image = [1, 2, 3, 4, 5]
|
44
|
+
# domain = [0, 1, 2, 3, 4]
|
45
|
+
# lr = LinearRegression.new(image, domain)
|
5
46
|
def initialize(image, domain = (0...image.size).to_a)
|
6
47
|
image.size != domain.size and raise ArgumentError,
|
7
48
|
"image and domain have unequal sizes"
|
@@ -10,30 +51,67 @@ module MoreMath
|
|
10
51
|
end
|
11
52
|
|
12
53
|
# The image data as an array.
|
54
|
+
#
|
55
|
+
# Returns the dependent variable values used in the regression.
|
56
|
+
#
|
57
|
+
# @return [Array<Numeric>] Array of y-values from the original data
|
13
58
|
attr_reader :image
|
14
59
|
|
15
60
|
# The domain data as an array.
|
61
|
+
#
|
62
|
+
# Returns the independent variable values used in the regression.
|
63
|
+
#
|
64
|
+
# @return [Array<Numeric>] Array of x-values from the original data
|
16
65
|
attr_reader :domain
|
17
66
|
|
18
67
|
# The slope of the line.
|
68
|
+
#
|
69
|
+
# Returns the calculated slope (a) of the best-fit line y = ax + b.
|
70
|
+
#
|
71
|
+
# @return [Float] The slope coefficient of the linear regression
|
19
72
|
attr_reader :a
|
20
73
|
|
21
74
|
# The offset of the line.
|
75
|
+
#
|
76
|
+
# Returns the calculated y-intercept (b) of the best-fit line y = ax + b.
|
77
|
+
#
|
78
|
+
# @return [Float] The y-intercept coefficient of the linear regression
|
22
79
|
attr_reader :b
|
23
80
|
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
81
|
+
# Checks if the slope is significantly different from zero.
|
82
|
+
#
|
83
|
+
# Performs a t-test to determine whether the slope coefficient is
|
84
|
+
# statistically significant at the given significance level (alpha).
|
85
|
+
# This test helps determine if there's a meaningful linear relationship
|
86
|
+
# between the independent and dependent variables.
|
87
|
+
#
|
88
|
+
# @param alpha [Float] The significance level (default: 0.05, or 5%)
|
89
|
+
# @return [Boolean] true if the slope is not significantly different from zero,
|
90
|
+
# false otherwise
|
91
|
+
# @raise [ArgumentError] If alpha is not in the range 0..1
|
92
|
+
# @example Testing slope significance
|
93
|
+
# lr = LinearRegression.new([1, 2, 3, 4, 5], [2, 4, 6, 8, 10])
|
94
|
+
# lr.slope_zero? # => false (slope is significantly different from zero)
|
95
|
+
# lr.slope_zero?(0.1) # => false (still significant at 10% level)
|
27
96
|
def slope_zero?(alpha = 0.05)
|
97
|
+
(0..1) === alpha or raise ArgumentError, 'alpha should be in 0..100'
|
28
98
|
df = @image.size - 2
|
29
99
|
return true if df <= 0 # not enough values to check
|
30
|
-
t = tvalue
|
100
|
+
t = tvalue
|
31
101
|
td = TDistribution.new df
|
32
102
|
t.abs <= td.inverse_probability(1 - alpha.abs / 2.0).abs
|
33
103
|
end
|
34
104
|
|
35
|
-
# Returns the residuals of this linear regression
|
36
|
-
#
|
105
|
+
# Returns the residuals of this linear regression.
|
106
|
+
#
|
107
|
+
# Residuals are the differences between observed values and predicted
|
108
|
+
# values from the regression line. They represent the error in prediction
|
109
|
+
# for each data point.
|
110
|
+
#
|
111
|
+
# @return [Array<Float>] Array of residual values (observed - predicted)
|
112
|
+
# @example Calculating residuals
|
113
|
+
# lr = LinearRegression.new([1, 2, 3], [0, 1, 2])
|
114
|
+
# puts lr.residuals # [0.0, 0.0, 0.0] for perfect fit
|
37
115
|
def residuals
|
38
116
|
result = []
|
39
117
|
@domain.zip(@image) do |x, y|
|
@@ -42,6 +120,16 @@ module MoreMath
|
|
42
120
|
result
|
43
121
|
end
|
44
122
|
|
123
|
+
# Returns the coefficient of determination (R²).
|
124
|
+
#
|
125
|
+
# R² measures the proportion of the variance in the dependent variable that is
|
126
|
+
# predictable from the independent variable(s). It ranges from 0 to 1, where
|
127
|
+
# higher values indicate better fit.
|
128
|
+
#
|
129
|
+
# @return [Float] The R-squared value (0.0 to 1.0)
|
130
|
+
# @example Checking model fit
|
131
|
+
# lr = LinearRegression.new([1, 2, 3], [0, 1, 2])
|
132
|
+
# puts lr.r2 # 1.0 for perfect linear relationship
|
45
133
|
def r2
|
46
134
|
image_seq = MoreMath::Sequence.new(@image)
|
47
135
|
sum_res = residuals.inject(0.0) { |s, r| s + r ** 2 }
|
@@ -53,6 +141,13 @@ module MoreMath
|
|
53
141
|
|
54
142
|
private
|
55
143
|
|
144
|
+
# Computes the linear regression parameters using least squares method.
|
145
|
+
#
|
146
|
+
# This internal method calculates the slope (a) and intercept (b)
|
147
|
+
# coefficients by solving the normal equations derived from minimizing the
|
148
|
+
# sum of squared residuals.
|
149
|
+
#
|
150
|
+
# @return [self] Returns self to allow method chaining
|
56
151
|
def compute
|
57
152
|
size = @image.size
|
58
153
|
sum_xx = sum_xy = sum_x = sum_y = 0.0
|
@@ -67,7 +162,13 @@ module MoreMath
|
|
67
162
|
self
|
68
163
|
end
|
69
164
|
|
70
|
-
|
165
|
+
# Calculates the t-value for testing slope significance.
|
166
|
+
#
|
167
|
+
# This internal method computes the t-statistic used in hypothesis testing
|
168
|
+
# to determine if the slope differs significantly from zero.
|
169
|
+
#
|
170
|
+
# @return [Float] The calculated t-value for the test
|
171
|
+
def tvalue
|
71
172
|
df = @image.size - 2
|
72
173
|
return 0.0 if df <= 0
|
73
174
|
sse_y = 0.0
|
@@ -3,21 +3,68 @@ require 'more_math/exceptions'
|
|
3
3
|
module MoreMath
|
4
4
|
# This class is used to find the root of a function with Newton's bisection
|
5
5
|
# method.
|
6
|
+
#
|
7
|
+
# The NewtonBisection class implements a hybrid root-finding algorithm that
|
8
|
+
# combines elements of both Newton-Raphson and bisection methods. It starts
|
9
|
+
# by attempting to bracket a root using a scaling factor, then uses a
|
10
|
+
# bisection approach to converge on the solution.
|
11
|
+
#
|
12
|
+
# @example Finding a root using Newton's bisection method
|
13
|
+
# # Define a function to find roots for (e.g., x^2 - 4 = 0)
|
14
|
+
# func = ->(x) { x ** 2 - 4 }
|
15
|
+
#
|
16
|
+
# # Create a NewtonBisection instance
|
17
|
+
# solver = MoreMath::NewtonBisection.new(&func)
|
18
|
+
#
|
19
|
+
# # Find the root in a given range
|
20
|
+
# root = solver.solve(1..3)
|
21
|
+
# puts root # => 2.0 (approximately)
|
22
|
+
#
|
23
|
+
# @example Finding a root with automatic bracketing
|
24
|
+
# func = ->(x) { Math.sin(x) }
|
25
|
+
# solver = MoreMath::NewtonBisection.new(&func)
|
26
|
+
#
|
27
|
+
# # Let the solver automatically find a bracket
|
28
|
+
# root = solver.solve
|
29
|
+
# puts root # => approximately 3.14159 (π)
|
6
30
|
class NewtonBisection
|
7
31
|
include MoreMath::Exceptions
|
8
32
|
|
9
33
|
# Creates a NewtonBisection instance for +function+, a one-argument block.
|
34
|
+
#
|
35
|
+
# @param function [Proc] A one-argument block that represents the function
|
36
|
+
# to find roots for. The function should return a numeric value.
|
37
|
+
# @example Creating a solver with a lambda
|
38
|
+
# func = ->(x) { x**2 - 4 }
|
39
|
+
# solver = MoreMath::NewtonBisection.new(&func)
|
10
40
|
def initialize(&function)
|
11
41
|
@function = function
|
12
42
|
end
|
13
43
|
|
14
44
|
# The function, passed into the constructor.
|
45
|
+
#
|
46
|
+
# @return [Proc] The function used for root finding
|
15
47
|
attr_reader :function
|
16
48
|
|
17
|
-
# Return a bracket around a root, starting from the initial +range+.
|
18
|
-
#
|
19
|
-
#
|
20
|
-
|
49
|
+
# Return a bracket around a root, starting from the initial +range+.
|
50
|
+
#
|
51
|
+
# This method attempts to find an interval that brackets a root by
|
52
|
+
# expanding the initial range using a scaling factor. It uses the property
|
53
|
+
# that if f(x1) and f(x2) have opposite signs, there must be at least one
|
54
|
+
# root in the interval [x1, x2].
|
55
|
+
#
|
56
|
+
# @param range [Range] Initial range to search for a bracket (default: -1..1)
|
57
|
+
# @param n [Integer] Maximum number of iterations to attempt bracketing (default: 50)
|
58
|
+
# @param factor [Float] Scaling factor for expanding the search range (default: 1.6)
|
59
|
+
# @return [Range, nil] A range that brackets a root, or nil if no bracket
|
60
|
+
# could be found within the specified iterations and factor
|
61
|
+
# @raise [ArgumentError] If the initial range is invalid (x1 >= x2)
|
62
|
+
# @example Finding a bracket for sin(x) function
|
63
|
+
# func = ->(x) { Math.sin(x) }
|
64
|
+
# solver = MoreMath::NewtonBisection.new(&func)
|
65
|
+
# bracket = solver.bracket(2..4)
|
66
|
+
# # Returns range that brackets root near π ≈ 3.14
|
67
|
+
def bracket(range = -1..1, n = 50, factor = 1.6)
|
21
68
|
x1, x2 = range.first.to_f, range.last.to_f
|
22
69
|
x1 >= x2 and raise ArgumentError, "bad initial range #{range}"
|
23
70
|
f1, f2 = @function[x1], @function[x2]
|
@@ -29,12 +76,28 @@ module MoreMath
|
|
29
76
|
f2 = @function[x2 += factor * (x2 - x1)]
|
30
77
|
end
|
31
78
|
end
|
32
|
-
|
79
|
+
nil
|
33
80
|
end
|
34
81
|
|
35
|
-
# Find the root of function in +range+ and return it.
|
36
|
-
#
|
37
|
-
# the
|
82
|
+
# Find the root of function in +range+ and return it.
|
83
|
+
#
|
84
|
+
# This method implements a bisection algorithm to find the root within
|
85
|
+
# the specified range. It uses a binary search approach, repeatedly halving
|
86
|
+
# the interval until convergence is achieved or maximum iterations are reached.
|
87
|
+
#
|
88
|
+
# @param range [Range] The range in which to search for a root (optional)
|
89
|
+
# If nil, attempts to automatically bracket the root first
|
90
|
+
# @param n [Integer] Maximum number of iterations (default: 2^16)
|
91
|
+
# @param epsilon [Float] Convergence threshold (default: 1E-16)
|
92
|
+
# @return [Float] The approximate root value
|
93
|
+
# @raise [ArgumentError] If the initial range is invalid or no bracket is found
|
94
|
+
# @raise [MoreMath::Exceptions::DivergentException] If no root can be found
|
95
|
+
# within the specified iterations or if convergence fails
|
96
|
+
# @example Solving x^2 - 4 = 0 with explicit range
|
97
|
+
# func = ->(x) { x**2 - 4 }
|
98
|
+
# solver = MoreMath::NewtonBisection.new(&func)
|
99
|
+
# root = solver.solve(1..3) # Finds root near +2.0
|
100
|
+
# puts root # => 2.0
|
38
101
|
def solve(range = nil, n = 1 << 16, epsilon = 1E-16)
|
39
102
|
if range
|
40
103
|
x1, x2 = range.first.to_f, range.last.to_f
|
@@ -1,11 +1,52 @@
|
|
1
1
|
module MoreMath
|
2
|
+
# Provides functions for converting between strings and numbers using a
|
3
|
+
# base-N numeral system.
|
4
|
+
#
|
5
|
+
# This module implements Gödel numbering, where strings are encoded into
|
6
|
+
# unique natural numbers and decoded back. It's particularly useful for
|
7
|
+
# applications requiring ordered enumeration of strings or mathematical
|
8
|
+
# operations on textual data.
|
9
|
+
#
|
10
|
+
# The encoding follows a positional numeral system where each character
|
11
|
+
# position represents a power of the alphabet size.
|
12
|
+
#
|
13
|
+
# @example Basic usage
|
14
|
+
# # Convert string to number
|
15
|
+
# MoreMath::NumberifyStringFunction.numberify_string("abc") # => 731
|
16
|
+
#
|
17
|
+
# # Convert number back to string
|
18
|
+
# MoreMath::NumberifyStringFunction.stringify_number(731) # => "abc"
|
19
|
+
#
|
20
|
+
# @example With custom alphabet
|
21
|
+
# alphabet = ['a', 'b', 'c']
|
22
|
+
# MoreMath::NumberifyStringFunction.numberify_string("abc", alphabet) # => 18
|
23
|
+
# MoreMath::NumberifyStringFunction.stringify_number(18, alphabet) # => "abc"
|
2
24
|
module NumberifyStringFunction
|
3
|
-
|
25
|
+
include Functions
|
4
26
|
|
5
27
|
module_function
|
6
28
|
|
29
|
+
# Converts a string into a unique natural number using the specified
|
30
|
+
# alphabet.
|
31
|
+
#
|
32
|
+
# This method implements a base-N numeral system where N is the size of the
|
33
|
+
# alphabet. Each character in the string contributes to the final number
|
34
|
+
# based on its position and value within the alphabet.
|
35
|
+
#
|
36
|
+
# @example Basic usage
|
37
|
+
# MoreMath::NumberifyStringFunction.numberify_string("hello") # => 123456789
|
38
|
+
#
|
39
|
+
# @example With custom alphabet
|
40
|
+
# alphabet = ['a', 'b', 'c']
|
41
|
+
# MoreMath::NumberifyStringFunction.numberify_string("abc", alphabet) # => 18
|
42
|
+
#
|
43
|
+
# @param string [String] The input string to convert to a number
|
44
|
+
# @param alphabet [Array<String>, Range<String>] The alphabet to use for conversion.
|
45
|
+
# Defaults to 'a'..'z' (lowercase English letters)
|
46
|
+
# @return [Integer] A unique natural number representing the input string
|
47
|
+
# @raise [ArgumentError] If any character in the string is not found in the alphabet
|
7
48
|
def numberify_string(string, alphabet = 'a'..'z')
|
8
|
-
alphabet = NumberifyStringFunction.convert_alphabet
|
49
|
+
alphabet = NumberifyStringFunction.convert_alphabet(alphabet)
|
9
50
|
s, k = string.size, alphabet.size
|
10
51
|
result = 0
|
11
52
|
for i in 0...s
|
@@ -17,6 +58,24 @@ module MoreMath
|
|
17
58
|
result
|
18
59
|
end
|
19
60
|
|
61
|
+
# Converts a natural number back into its corresponding string
|
62
|
+
# representation.
|
63
|
+
#
|
64
|
+
# This is the inverse operation of {numberify_string}. It reconstructs the
|
65
|
+
# original string by reversing the positional numeral system encoding.
|
66
|
+
#
|
67
|
+
# @example Basic usage
|
68
|
+
# MoreMath::NumberifyStringFunction.stringify_number(731) # => "abc"
|
69
|
+
#
|
70
|
+
# @example With custom alphabet
|
71
|
+
# alphabet = ['a', 'b', 'c']
|
72
|
+
# MoreMath::NumberifyStringFunction.stringify_number(18, alphabet) # => "abc"
|
73
|
+
#
|
74
|
+
# @param number [Integer] The natural number to convert back to a string
|
75
|
+
# @param alphabet [Array<String>, Range<String>] The alphabet to use for conversion.
|
76
|
+
# Defaults to 'a'..'z' (lowercase English letters)
|
77
|
+
# @return [String] The original string representation of the number
|
78
|
+
# @raise [ArgumentError] If the number is negative
|
20
79
|
def stringify_number(number, alphabet = 'a'..'z')
|
21
80
|
case
|
22
81
|
when number < 0
|
@@ -24,7 +83,7 @@ module MoreMath
|
|
24
83
|
when number == 0
|
25
84
|
return ''
|
26
85
|
end
|
27
|
-
alphabet = NumberifyStringFunction.convert_alphabet
|
86
|
+
alphabet = NumberifyStringFunction.convert_alphabet(alphabet)
|
28
87
|
s = NumberifyStringFunction.compute_size(number, alphabet.size)
|
29
88
|
k, m = alphabet.size, number
|
30
89
|
result = ' ' * s
|
@@ -38,25 +97,42 @@ module MoreMath
|
|
38
97
|
result
|
39
98
|
end
|
40
99
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
100
|
+
# Calculates the minimum number of digits needed to represent a number in
|
101
|
+
# base N.
|
102
|
+
#
|
103
|
+
# This helper method is used internally to determine how many characters
|
104
|
+
# are needed when converting a number back to its string representation.
|
105
|
+
#
|
106
|
+
# @api private
|
107
|
+
# @param n [Integer] The number to calculate size for
|
108
|
+
# @param b [Integer] The base of the numeral system
|
109
|
+
# @return [Integer] The minimum number of digits required
|
110
|
+
def compute_size(n, b)
|
111
|
+
i = 0
|
112
|
+
while n > 0
|
113
|
+
i += 1
|
114
|
+
n -= b ** i
|
50
115
|
end
|
116
|
+
i
|
117
|
+
end
|
51
118
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
119
|
+
# Converts various alphabet representations into a consistent Array format.
|
120
|
+
#
|
121
|
+
# This method handles different input types for the alphabet:
|
122
|
+
# - Range: converts to array of characters
|
123
|
+
# - String: splits into individual characters
|
124
|
+
# - Array: returns as-is
|
125
|
+
#
|
126
|
+
# @api private
|
127
|
+
# @param alphabet [Object] The alphabet in various formats (Range, String, or Array)
|
128
|
+
# @return [Array<String>] Standardized array representation of the alphabet
|
129
|
+
def convert_alphabet(alphabet)
|
130
|
+
if alphabet.respond_to?(:to_ary)
|
131
|
+
alphabet.to_ary
|
132
|
+
elsif alphabet.respond_to?(:to_str)
|
133
|
+
alphabet.to_str.split(//)
|
134
|
+
else
|
135
|
+
alphabet.to_a
|
60
136
|
end
|
61
137
|
end
|
62
138
|
end
|