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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +275 -2
- data/LICENSE +672 -3
- data/README.md +9 -7
- data/lib/sevgi/core.rb +4 -3
- data/lib/sevgi/function/color.rb +2 -1
- data/lib/sevgi/function/file.rb +8 -9
- data/lib/sevgi/function/locate.rb +40 -17
- data/lib/sevgi/function/math.rb +104 -22
- data/lib/sevgi/function/shell.rb +169 -22
- data/lib/sevgi/function/string.rb +18 -8
- data/lib/sevgi/function/ui.rb +2 -1
- data/lib/sevgi/function/version.rb +1 -1
- data/lib/sevgi/function.rb +14 -4
- metadata +4 -4
data/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Sevgi Function
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/sevgi/function/color.rb
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Function
|
|
5
|
-
# ANSI color and style
|
|
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
|
data/lib/sevgi/function/file.rb
CHANGED
|
@@ -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
|
|
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 [
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 [
|
|
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 [
|
|
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
|
|
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
|
|
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
|
-
#
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
attr_reader :
|
|
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
|
|
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
|
|
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
|
|
106
|
+
next if excluded?(candidate)
|
|
87
107
|
|
|
88
|
-
|
|
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
|
data/lib/sevgi/function/math.rb
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module Sevgi
|
|
4
4
|
module Function
|
|
5
|
-
# Numeric and trigonometric
|
|
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 [
|
|
33
|
-
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
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 [
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|