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.
@@ -1,11 +1,37 @@
1
1
  require 'tins'
2
2
 
3
3
  module MoreMath
4
- # A histogram gives an overview of a sequence's elements.
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+, +width+ is the parameter for
39
- # +prepare_display+
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
- # Return true if the slope of the underlying data (not the sample data
25
- # passed into the constructor of this LinearRegression instance) is likely
26
- # (with alpha level _alpha_) to be zero.
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(alpha)
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 in relation to the given
36
- # domain and image.
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
- def tvalue(alpha = 0.05)
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+. The
18
- # method returns nil, if no such bracket around a root could be found after
19
- # +n+ tries with the scaling +factor+.
20
- def bracket(range = -1..1, n = 50, factor = 1.6)
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
- return
79
+ nil
33
80
  end
34
81
 
35
- # Find the root of function in +range+ and return it. The method raises a
36
- # DivergentException, if no such root could be found after +n+ tries and in
37
- # the +epsilon+ environment.
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
- Functions = MoreMath::Functions
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 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 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
- class << self
42
- memoize function:
43
- def compute_size(n, b)
44
- i = 0
45
- while n > 0
46
- i += 1
47
- n -= b ** i
48
- end
49
- i
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
- def convert_alphabet(alphabet)
53
- if alphabet.respond_to?(:to_ary)
54
- alphabet.to_ary
55
- elsif alphabet.respond_to?(:to_str)
56
- alphabet.to_str.split(//)
57
- else
58
- alphabet.to_a
59
- end
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