sevgi-function 0.94.0 → 0.98.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.
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Sevgi Function
2
2
 
3
- Shared helper functions used by Sevgi components.
3
+ Sevgi Function contains the supported `Sevgi::F` toolbox shared by Sevgi components and advanced extensions. It is not
4
+ intended as a general-purpose utility library; nested helper modules organize the facade implementation and are not
5
+ consumer mixins.
4
6
 
5
7
  ## Install
6
8
 
@@ -22,15 +24,15 @@ Sevgi::F.eq?(0.1 + 0.2, 0.3, precision: 12)
22
24
 
23
25
  ## Ruby compatibility
24
26
 
25
- Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4 and the current development Ruby from `.ruby-version`.
27
+ Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the development Ruby from `.ruby-version`.
26
28
 
27
29
  ## Native prerequisites
28
30
 
29
- None beyond Ruby and this gem's Ruby dependencies.
31
+ This gem needs only Ruby and its Ruby dependencies.
30
32
 
31
33
  ## Links
32
34
 
33
- - Documentation: https://sevgi.roktas.dev
34
- - API documentation: https://www.rubydoc.info/gems/sevgi-function
35
- - Source: https://github.com/roktas/sevgi/tree/main/function
36
- - Changelog: https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
35
+ - Documentation: <https://sevgi.roktas.dev>
36
+ - API documentation: <https://www.rubydoc.info/gems/sevgi-function>
37
+ - Source: <https://github.com/roktas/sevgi/tree/main/function>
38
+ - Changelog: <https://github.com/roktas/sevgi/blob/main/CHANGELOG.md>
data/lib/sevgi/core.rb CHANGED
@@ -49,8 +49,6 @@ module Sevgi
49
49
  Undefined = Object
50
50
  .new
51
51
  .tap do |undefined|
52
- const_set(:Self, -> { Undefined })
53
-
54
52
  # Returns the sentinel name.
55
53
  # @return [String]
56
54
  def undefined.to_s = "Undefined"
@@ -90,7 +88,10 @@ module Sevgi
90
88
  # Returns the first argument that is not {Sevgi::Undefined}.
91
89
  # @param args [Array<Object>] candidate values
92
90
  # @return [Object, nil] first defined value, including nil, or nil when none exists
93
- def undefined.coalesce(*args) = args.find(Self) { |x| !equal?(x) }
91
+ # @example Resolve an optional value
92
+ # Sevgi::Undefined.coalesce(Sevgi::Undefined, nil) # => nil
93
+ # @see #default
94
+ def undefined.coalesce(*args) = args.find { |x| !equal?(x) }
94
95
  end
95
96
  .freeze
96
97
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Sevgi
4
4
  module Function
5
- # ANSI color and style helpers for terminal output.
5
+ # ANSI color and style methods promoted to {Sevgi::F}. This module organizes the facade implementation; it is not a
6
+ # consumer mixin contract.
6
7
  module Color
7
8
  # Wraps a string in the blue terminal style.
8
9
  # @param string [Object] content to style
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest"
4
3
  require "fileutils"
5
4
 
6
5
  module Sevgi
7
6
  module Function
8
- # File-system helpers used by build scripts and DSL support code.
7
+ # File-system methods promoted to {Sevgi::F}. This module organizes the facade implementation; it is not a consumer
8
+ # mixin contract.
9
9
  module File
10
10
  # Checks whether a file would change if written with content.
11
11
  # @param file [String] file path to compare
@@ -14,14 +14,14 @@ module Sevgi
14
14
  # @yieldparam content [String] content to normalize
15
15
  # @yieldreturn [String]
16
16
  # @return [Boolean] true when the file is missing or content differs
17
- # @raise [Errno::EACCES] when the file cannot be read
17
+ # @raise [SystemCallError] when the file cannot be inspected or read
18
18
  def changed?(file, content, &filter)
19
19
  return true unless ::File.exist?(file)
20
20
 
21
21
  old_content = ::File.read(file)
22
22
  old_content, content = [old_content, content].map(&filter) if filter
23
23
 
24
- Digest::SHA1.digest(old_content) != Digest::SHA1.digest(content)
24
+ old_content != content
25
25
  end
26
26
 
27
27
  # Finds an existing file by exact path or by trying default extensions.
@@ -29,11 +29,11 @@ module Sevgi
29
29
  # @param extensions [Array<String>] extensions to try when file has no extension
30
30
  # @return [String, nil] matching file path, or nil when no file is found
31
31
  def existing(file, extensions)
32
- return file if ::File.exist?(file)
32
+ return file if ::File.file?(file)
33
33
  return nil unless ::File.extname(file).empty?
34
34
  return nil if extensions.empty?
35
35
 
36
- extensions.map { |ext| "#{file}.#{ext}" }.detect { |file| ::File.exist?(file) }
36
+ extensions.map { |ext| "#{file}.#{ext}" }.detect { |file| ::File.file?(file) }
37
37
  end
38
38
 
39
39
  # Finds an existing file or raises.
@@ -79,8 +79,7 @@ module Sevgi
79
79
  # @yieldparam content [String] old or new content
80
80
  # @yieldreturn [String]
81
81
  # @return [String, nil] expanded file path when written, otherwise nil
82
- # @raise [Errno::EACCES] when the file cannot be read or written
83
- # @raise [Errno::ENOENT] when the parent directory does not exist
82
+ # @raise [SystemCallError] when the destination cannot be inspected, read, or written
84
83
  def out(content, *paths, &filter)
85
84
  if paths.empty?
86
85
  ::Kernel.puts(content)
@@ -122,7 +121,7 @@ module Sevgi
122
121
  # Creates a file and any missing parent directories.
123
122
  # @param paths [Array<String>] path components for the file
124
123
  # @return [String] touched file path
125
- # @raise [Errno::EACCES] when the file or parent directory cannot be created
124
+ # @raise [SystemCallError] when the file or parent directory cannot be created
126
125
  def touch(*paths)
127
126
  ::File.join(*paths).tap do |path|
128
127
  ::FileUtils.mkdir_p(::File.dirname(path))
@@ -10,28 +10,46 @@ module Sevgi
10
10
  # @return [String] candidate path that matched
11
11
  # @!attribute [r] dir
12
12
  # @return [String] directory where the match was found
13
- Location = Data.define(:file, :slug, :dir)
13
+ Location = Data.define(:file, :slug, :dir) do
14
+ # Creates an owned immutable location snapshot.
15
+ # @param file [String] absolute matching file path
16
+ # @param slug [String] candidate path that matched
17
+ # @param dir [String] directory where the match was found
18
+ # @return [void]
19
+ def initialize(file:, slug:, dir:)
20
+ super(file: file.dup.freeze, slug: slug.dup.freeze, dir: dir.dup.freeze)
21
+ end
22
+
23
+ private_class_method :[]
24
+ end
14
25
 
15
- # Locates one of several candidate files by walking upward from a start directory.
26
+ # Locates one of several candidate files by walking upward from an immutable configuration snapshot. Each call
27
+ # observes the filesystem again and returns an owned immutable {Location} snapshot.
16
28
  class Locate
17
29
  # @overload call(paths, start = Dir.pwd, exclude: nil, &block)
18
30
  # Builds a locator and runs it.
19
31
  # @param paths [Array<String>, String] candidate file paths
20
32
  # @param start [String] directory where lookup starts
21
33
  # @param exclude [Array<String>, String, nil] paths ignored during lookup
22
- # @yield optional matcher used instead of file existence checks
34
+ # @yield optional matcher used instead of built-in file checks
23
35
  # @yieldparam path [String] candidate path
24
36
  # @yieldreturn [Boolean]
25
37
  # @return [Sevgi::Function::Location, nil] found location, or nil
38
+ # @raise [Errno::ENOENT] when the start directory does not exist
39
+ # @raise [Errno::ENOTDIR] when the start path is not a directory
26
40
  def self.call(*, **, &block) = new(*, **).call(&block)
27
41
 
28
- # @!attribute [r] paths
29
- # @return [Array<String>] candidate paths
30
- # @!attribute [r] start
31
- # @return [String] absolute start directory
32
- # @!attribute [r] exclude
33
- # @return [Array<String>, nil] expanded paths ignored during lookup
34
- attr_reader :paths, :start, :exclude
42
+ # Returns the frozen owned candidate paths.
43
+ # @return [Array<String>] frozen candidate path strings
44
+ attr_reader :paths
45
+
46
+ # Returns the frozen absolute start directory.
47
+ # @return [String] frozen absolute path
48
+ attr_reader :start
49
+
50
+ # Returns the frozen owned paths ignored during lookup.
51
+ # @return [Array<String>, nil] frozen absolute path strings, or nil
52
+ attr_reader :exclude
35
53
 
36
54
  # Builds an upward file locator.
37
55
  # @param paths [Array<String>, String] candidate file paths
@@ -39,15 +57,17 @@ module Sevgi
39
57
  # @param exclude [Array<String>, String, nil] paths ignored during lookup after absolute expansion
40
58
  # @return [void]
41
59
  def initialize(paths, start = ::Dir.pwd, exclude: nil)
42
- @paths = Array(paths)
43
- @start = ::File.expand_path(start)
44
- @exclude = [*exclude].map { ::File.expand_path(it) } unless exclude.nil?
60
+ @paths = Array(paths).map { it.dup.freeze }.freeze
61
+ @start = ::File.expand_path(start).freeze
62
+ @exclude = [*exclude].map { ::File.expand_path(it).freeze }.freeze unless exclude.nil?
63
+ freeze
45
64
  end
46
65
 
47
66
  # Runs the upward lookup.
48
- # @yield optional matcher used instead of file existence checks
67
+ # @yield optional matcher used instead of built-in file checks
49
68
  # @yieldparam path [String] absolute candidate path
50
69
  # @yieldreturn [Boolean]
70
+ # @note Absolute exclusions are applied before the default or custom matcher.
51
71
  # @return [Sevgi::Function::Location, nil] found location, or nil
52
72
  # @raise [Errno::ENOENT] when the start directory does not exist
53
73
  # @raise [Errno::ENOTDIR] when the start path is not a directory
@@ -59,7 +79,7 @@ module Sevgi
59
79
 
60
80
  slug, file = found
61
81
 
62
- return Location[file, slug, here]
82
+ return Location.new(file:, slug:, dir: here)
63
83
  end
64
84
  end
65
85
 
@@ -83,9 +103,10 @@ module Sevgi
83
103
  def match(here, &block)
84
104
  paths.each do |path|
85
105
  candidate = ::File.expand_path(path, here)
86
- next if !block && excluded?(candidate)
106
+ next if excluded?(candidate)
87
107
 
88
- return [path, candidate] if (block || proc { ::File.exist?(it) }).call(candidate)
108
+ matched = block ? block.call(candidate) : ::File.file?(candidate)
109
+ return [path, candidate] if matched
89
110
  end
90
111
 
91
112
  nil
@@ -104,6 +125,8 @@ module Sevgi
104
125
  # @param extension [String] default extension added before lookup
105
126
  # @return [Sevgi::Function::Location] found location
106
127
  # @raise [Sevgi::Error] when no matching file exists
128
+ # @raise [Errno::ENOENT] when the start directory does not exist
129
+ # @raise [Errno::ENOTDIR] when the start path is not a directory
107
130
  def self.locate(filename, start, exclude: nil, extension: EXTENSION)
108
131
  Locate.(F.qualify(filename, extension), start, exclude:).tap do |path|
109
132
  Error.("Cannot load a file matching: #{filename}") unless path
@@ -2,14 +2,17 @@
2
2
 
3
3
  module Sevgi
4
4
  module Function
5
- # Numeric and trigonometric helpers used by geometry and DSL code.
5
+ # Numeric and trigonometric methods promoted to {Sevgi::F}. This module owns the public thread-local
6
+ # {Math.precision} configuration but is not otherwise a consumer mixin contract.
6
7
  module Math
7
8
  # Default decimal precision used by approximate comparisons.
8
9
  PRECISION = 6
9
10
 
10
11
  PRECISION_KEY = :sevgi_function_math_precision
12
+ QUADRANT_COSINES = [1.0, 0.0, -1.0, 0.0].freeze
13
+ QUADRANT_SINES = [0.0, 1.0, 0.0, -1.0].freeze
11
14
 
12
- private_constant :PRECISION_KEY
15
+ private_constant :PRECISION_KEY, :QUADRANT_COSINES, :QUADRANT_SINES
13
16
 
14
17
  # Returns the current thread's default numeric precision.
15
18
  # @return [Integer] current thread precision, or {PRECISION} when no override is set
@@ -22,64 +25,82 @@ module Sevgi
22
25
  # Sets or clears the current thread's default numeric precision.
23
26
  # @param precision [Integer, nil] precision override, or nil to return to {PRECISION}
24
27
  # @return [Integer, nil] assigned precision
28
+ # @raise [Sevgi::ArgumentError] when precision is not an Integer or nil
25
29
  def self.precision=(precision)
30
+ unless precision.nil? || precision.is_a?(::Integer)
31
+ ArgumentError.("Precision must be an Integer or nil: #{precision.inspect}")
32
+ end
33
+
26
34
  Thread.current.thread_variable_set(PRECISION_KEY, precision)
27
35
  end
28
36
 
29
37
  # Returns the inverse cosine in degrees.
30
38
  # @param value [Numeric] cosine value
31
39
  # @return [Float]
32
- # @raise [Math::DomainError] when value is outside -1..1
33
- def acos(value) = to_degrees(::Math.acos(value))
40
+ # @raise [Sevgi::ArgumentError] when value is not a finite real number
41
+ # @raise [::Math::DomainError] when value is outside -1..1
42
+ def acos(value) = to_degrees(::Math.acos(finite_real(:value, value)))
34
43
 
35
44
  # Returns the inverse cotangent in degrees.
36
45
  # @param value [Numeric] cotangent value
37
46
  # @return [Float]
38
- def acot(value) = 90.0 - to_degrees(::Math.atan(value))
47
+ # @raise [Sevgi::ArgumentError] when value is not a finite real number
48
+ def acot(value) = 90.0 - to_degrees(::Math.atan(finite_real(:value, value)))
39
49
 
40
50
  # Rounds a float with an explicit or thread-local precision.
41
51
  # @param float [Numeric] value to round
42
52
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
43
53
  # @return [Numeric] rounded value
44
- # @raise [TypeError] when float cannot be rounded
54
+ # @raise [Sevgi::ArgumentError] when value is not finite real or precision is not an Integer or nil
45
55
  def approx(float, precision = nil)
46
- float.round(precision.nil? ? Function::Math.precision : precision)
56
+ precision = precision.nil? ? Function::Math.precision : valid_precision(precision)
57
+ valid_real(:value, float).round(precision)
47
58
  end
48
59
 
49
60
  # Returns the inverse sine in degrees.
50
61
  # @param value [Numeric] sine value
51
62
  # @return [Float]
52
- # @raise [Math::DomainError] when value is outside -1..1
53
- def asin(value) = to_degrees(::Math.asin(value))
63
+ # @raise [Sevgi::ArgumentError] when value is not a finite real number
64
+ # @raise [::Math::DomainError] when value is outside -1..1
65
+ def asin(value) = to_degrees(::Math.asin(finite_real(:value, value)))
54
66
 
55
67
  # Returns the inverse tangent in degrees.
56
68
  # @param value [Numeric] tangent value
57
69
  # @return [Float]
58
- def atan(value) = to_degrees(::Math.atan(value))
70
+ # @raise [Sevgi::ArgumentError] when value is not a finite real number
71
+ def atan(value) = to_degrees(::Math.atan(finite_real(:value, value)))
59
72
 
60
73
  # Returns the quadrant-aware inverse tangent in degrees.
61
74
  # @param y [Numeric] y component
62
75
  # @param x [Numeric] x component
63
76
  # @return [Float]
64
- def atan2(y, x) = to_degrees(::Math.atan2(y, x))
77
+ # @raise [Sevgi::ArgumentError] when an operand is not a finite real number
78
+ def atan2(y, x) = to_degrees(::Math.atan2(finite_real(:y, y), finite_real(:x, x)))
65
79
 
66
- # Returns the cosine of an angle expressed in degrees.
80
+ # Returns the cosine of an angle expressed in degrees. Integer quarter turns return exact `-1.0`, `0.0`, or
81
+ # `1.0`; other angles use Ruby's floating-point Math implementation.
67
82
  # @param degrees [Numeric] angle in degrees
68
83
  # @return [Float]
69
- def cos(degrees) = ::Math.cos(to_radians(degrees))
84
+ # @raise [Sevgi::ArgumentError] when degrees is not a finite real number
85
+ def cos(degrees)
86
+ degrees = finite_real(:degrees, degrees)
87
+ quadrant_value(degrees, QUADRANT_COSINES) { ::Math.cos(radians(degrees)) }
88
+ end
70
89
 
71
90
  # Returns the cotangent of an angle expressed in degrees.
72
91
  # @param degrees [Numeric] angle in degrees
73
92
  # @return [Float]
93
+ # @raise [Sevgi::ArgumentError] when degrees is not a finite real number
74
94
  def cot(degrees) = 1.0 / ::Math.tan(to_radians(degrees))
75
95
 
76
96
  # Counts complete divisions in a length.
77
- # @param length [Numeric] total length
78
- # @param division [Numeric] division size
97
+ # @param length [Numeric] finite real total length
98
+ # @param division [Numeric] finite real, non-zero division size
79
99
  # @return [Integer]
80
- # @raise [Sevgi::ArgumentError] when division is zero
100
+ # @raise [Sevgi::ArgumentError] when an operand is not a finite Numeric or division is zero
81
101
  def count(length, division)
82
- divisor = division.to_f
102
+ length = finite_real(:length, length)
103
+ divisor = finite_real(:division, division)
83
104
  ArgumentError.("Division must not be zero") if divisor.zero?
84
105
 
85
106
  (length / divisor).to_i
@@ -90,6 +111,7 @@ module Sevgi
90
111
  # @param right [Numeric] right operand
91
112
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
92
113
  # @return [Boolean]
114
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
93
115
  def eq?(left, right, precision: nil) = approx(left, precision) == approx(right, precision)
94
116
 
95
117
  # Checks whether the rounded left operand is greater than or equal to the rounded right operand.
@@ -97,6 +119,7 @@ module Sevgi
97
119
  # @param right [Numeric] right operand
98
120
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
99
121
  # @return [Boolean]
122
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
100
123
  def ge?(left, right, precision: nil) = approx(left, precision) >= approx(right, precision)
101
124
 
102
125
  # Checks whether the rounded left operand is greater than the rounded right operand.
@@ -104,6 +127,7 @@ module Sevgi
104
127
  # @param right [Numeric] right operand
105
128
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
106
129
  # @return [Boolean]
130
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
107
131
  def gt?(left, right, precision: nil) = approx(left, precision) > approx(right, precision)
108
132
 
109
133
  # Checks whether the rounded left operand is less than or equal to the rounded right operand.
@@ -111,6 +135,7 @@ module Sevgi
111
135
  # @param right [Numeric] right operand
112
136
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
113
137
  # @return [Boolean]
138
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
114
139
  def le?(left, right, precision: nil) = approx(left, precision) <= approx(right, precision)
115
140
 
116
141
  # Checks whether the rounded left operand is less than the rounded right operand.
@@ -118,33 +143,46 @@ module Sevgi
118
143
  # @param right [Numeric] right operand
119
144
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
120
145
  # @return [Boolean]
146
+ # @raise [Sevgi::ArgumentError] when an operand is not finite real or precision is invalid
121
147
  def lt?(left, right, precision: nil) = approx(left, precision) < approx(right, precision)
122
148
 
123
149
  # Rounds a value only when precision is present.
124
150
  # @param float [Numeric] value to round
125
151
  # @param precision [Integer, nil] explicit precision, or nil to return float unchanged
126
152
  # @return [Numeric]
127
- def round(float, precision) = precision ? float.round(precision) : float
153
+ # @raise [Sevgi::ArgumentError] when value is not finite real or precision is not an Integer or nil
154
+ def round(float, precision)
155
+ number = valid_real(:value, float)
156
+ precision.nil? ? number : number.round(valid_precision(precision))
157
+ end
128
158
 
129
- # Returns the sine of an angle expressed in degrees.
159
+ # Returns the sine of an angle expressed in degrees. Integer quarter turns return exact `-1.0`, `0.0`, or `1.0`;
160
+ # other angles use Ruby's floating-point Math implementation.
130
161
  # @param degrees [Numeric] angle in degrees
131
162
  # @return [Float]
132
- def sin(degrees) = ::Math.sin(to_radians(degrees))
163
+ # @raise [Sevgi::ArgumentError] when degrees is not a finite real number
164
+ def sin(degrees)
165
+ degrees = finite_real(:degrees, degrees)
166
+ quadrant_value(degrees, QUADRANT_SINES) { ::Math.sin(radians(degrees)) }
167
+ end
133
168
 
134
169
  # Returns the tangent of an angle expressed in degrees.
135
170
  # @param degrees [Numeric] angle in degrees
136
171
  # @return [Float]
172
+ # @raise [Sevgi::ArgumentError] when degrees is not a finite real number
137
173
  def tan(degrees) = ::Math.tan(to_radians(degrees))
138
174
 
139
175
  # Converts radians to degrees.
140
176
  # @param radians [Numeric] angle in radians
141
177
  # @return [Float]
142
- def to_degrees(radians) = radians.to_f * 180 / ::Math::PI
178
+ # @raise [Sevgi::ArgumentError] when radians is not a finite real number
179
+ def to_degrees(radians) = finite_real(:radians, radians) * 180 / ::Math::PI
143
180
 
144
181
  # Converts degrees to radians.
145
182
  # @param degrees [Numeric] angle in degrees
146
183
  # @return [Float]
147
- def to_radians(degrees) = degrees.to_f / 180 * ::Math::PI
184
+ # @raise [Sevgi::ArgumentError] when degrees is not a finite real number
185
+ def to_radians(degrees) = radians(finite_real(:degrees, degrees))
148
186
 
149
187
  # Runs a block with a current-thread precision override.
150
188
  # @param precision [Integer, nil] scoped precision, or nil to use {PRECISION}
@@ -152,6 +190,10 @@ module Sevgi
152
190
  # @yieldreturn [Object]
153
191
  # @return [Object] block return value
154
192
  # @raise [Sevgi::ArgumentError] when no block is given
193
+ # @raise [Sevgi::ArgumentError] when precision is not an Integer or nil
194
+ # @example Compare values under a temporary precision
195
+ # Sevgi::F.with_precision(2) { Sevgi::F.eq?(1.001, 1.0) } # => true
196
+ # Sevgi::Function::Math.precision # => 6
155
197
  def with_precision(precision, &block)
156
198
  ArgumentError.("Block required") unless block
157
199
 
@@ -166,7 +208,47 @@ module Sevgi
166
208
  # @param value [Numeric] value to check
167
209
  # @param precision [Integer, nil] explicit precision, or nil to use {Math.precision}
168
210
  # @return [Boolean]
211
+ # @raise [Sevgi::ArgumentError] when value is not finite real or precision is invalid
169
212
  def zero?(value, precision: nil) = eq?(value, 0.0, precision:)
213
+
214
+ private
215
+
216
+ def finite_real(field, value)
217
+ unless value.is_a?(::Numeric) && !value.is_a?(::Complex)
218
+ ArgumentError.("#{field} must be a finite real Numeric: #{value.inspect}")
219
+ end
220
+
221
+ number = begin
222
+ value.to_f
223
+ rescue ::StandardError => e
224
+ ArgumentError.("#{field} must be a finite real Numeric: #{value.inspect} (#{e.message})")
225
+ end
226
+
227
+ ArgumentError.("#{field} must be finite: #{value.inspect}") unless number.is_a?(::Float) && number.finite?
228
+
229
+ number
230
+ end
231
+
232
+ def quadrant_value(degrees, values)
233
+ turn = degrees % 360
234
+ return yield unless (turn % 90).zero?
235
+
236
+ values[(turn / 90).to_i]
237
+ end
238
+
239
+ def radians(degrees) = degrees / 180 * ::Math::PI
240
+
241
+ def valid_precision(precision)
242
+ return precision if precision.is_a?(::Integer)
243
+
244
+ ArgumentError.("Precision must be an Integer or nil: #{precision.inspect}")
245
+ end
246
+
247
+ def valid_real(field, value)
248
+ finite_real(field, value)
249
+ value
250
+ end
251
+
170
252
  end
171
253
 
172
254
  extend Math