matchi 4.2.0 → 6.0.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/LICENSE.md +1 -1
- data/README.md +96 -227
- data/lib/matchi/be.rb +10 -65
- data/lib/matchi/be_a_kind_of.rb +21 -116
- data/lib/matchi/be_an_instance_of.rb +24 -113
- data/lib/matchi/be_within/of.rb +57 -0
- data/lib/matchi/be_within.rb +17 -134
- data/lib/matchi/change/delta.rb +56 -0
- data/lib/matchi/change/from.rb +21 -61
- data/lib/matchi/change/from_to.rb +46 -0
- data/lib/matchi/change/to.rb +17 -81
- data/lib/matchi/change.rb +73 -148
- data/lib/matchi/constant.rb +55 -0
- data/lib/matchi/eq.rb +24 -65
- data/lib/matchi/match.rb +29 -64
- data/lib/matchi/number.rb +37 -0
- data/lib/matchi/predicate.rb +40 -139
- data/lib/matchi/raise_exception.rb +83 -105
- data/lib/matchi/satisfy.rb +21 -96
- data/lib/matchi.rb +22 -60
- metadata +18 -20
- data/lib/matchi/change/by.rb +0 -137
- data/lib/matchi/change/by_at_least.rb +0 -158
- data/lib/matchi/change/by_at_most.rb +0 -171
- data/lib/matchi/change/from/to.rb +0 -132
data/lib/matchi/be_a_kind_of.rb
CHANGED
|
@@ -1,134 +1,39 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "constant"
|
|
4
|
+
|
|
3
5
|
module Matchi
|
|
4
|
-
# Type matcher
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# inheritance hierarchy. It uses the case equality operator (===) which is Ruby's
|
|
8
|
-
# built-in mechanism for type checking, ensuring consistent behavior with Ruby's
|
|
9
|
-
# own type system.
|
|
10
|
-
#
|
|
11
|
-
# @example Basic usage with simple types
|
|
12
|
-
# matcher = Matchi::BeAKindOf.new(Numeric)
|
|
13
|
-
# matcher.match? { 42 } # => true
|
|
14
|
-
# matcher.match? { 42.0 } # => true
|
|
15
|
-
# matcher.match? { "42" } # => false
|
|
16
|
-
#
|
|
17
|
-
# @example Working with inheritance hierarchies
|
|
18
|
-
# class Animal; end
|
|
19
|
-
# class Dog < Animal; end
|
|
20
|
-
# class GermanShepherd < Dog; end
|
|
21
|
-
#
|
|
22
|
-
# matcher = Matchi::BeAKindOf.new(Animal)
|
|
23
|
-
# matcher.match? { Dog.new } # => true
|
|
24
|
-
# matcher.match? { GermanShepherd.new } # => true
|
|
25
|
-
# matcher.match? { Object.new } # => false
|
|
26
|
-
#
|
|
27
|
-
# @example Using with modules and interfaces
|
|
28
|
-
# module Swimmable
|
|
29
|
-
# def swim; end
|
|
30
|
-
# end
|
|
31
|
-
#
|
|
32
|
-
# class Duck
|
|
33
|
-
# include Swimmable
|
|
34
|
-
# end
|
|
35
|
-
#
|
|
36
|
-
# matcher = Matchi::BeAKindOf.new(Swimmable)
|
|
37
|
-
# matcher.match? { Duck.new } # => true
|
|
38
|
-
# matcher.match? { Object.new } # => false
|
|
6
|
+
# Type matcher: passes when the actual value is an instance of the expected
|
|
7
|
+
# module or of one of its descendants (`Module#===`, so a `BasicObject` is
|
|
8
|
+
# handled and an object redefining `is_a?` cannot lie).
|
|
39
9
|
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
# Matchi::BeAKindOf.new(String)
|
|
10
|
+
# The expected module can be given directly, or by name; see
|
|
11
|
+
# {Matchi::Constant}.
|
|
43
12
|
#
|
|
44
|
-
#
|
|
45
|
-
# Matchi::BeAKindOf.new(
|
|
46
|
-
#
|
|
47
|
-
# # Using class name as symbol
|
|
48
|
-
# Matchi::BeAKindOf.new(:String)
|
|
49
|
-
#
|
|
50
|
-
# # Using namespaced class
|
|
51
|
-
# Matchi::BeAKindOf.new("MyModule::MyClass")
|
|
52
|
-
#
|
|
53
|
-
# @see Matchi::BeAnInstanceOf
|
|
54
|
-
# @see https://ruby-doc.org/core/Module.html#method-i-3D-3D-3D
|
|
13
|
+
# @example
|
|
14
|
+
# Matchi::BeAKindOf.new(Numeric).match? { 42 } # => true
|
|
15
|
+
# Matchi::BeAKindOf.new(:Numeric).match? { "42" } # => false
|
|
55
16
|
class BeAKindOf
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
# @api public
|
|
59
|
-
#
|
|
60
|
-
# @param expected [Class, #to_s] The expected class or its name
|
|
61
|
-
# Can be provided as a Class object, String, or Symbol
|
|
62
|
-
#
|
|
63
|
-
# @raise [ArgumentError] if the class name doesn't start with an uppercase letter
|
|
64
|
-
#
|
|
65
|
-
# @return [BeAKindOf] a new instance of the matcher
|
|
66
|
-
#
|
|
67
|
-
# @example
|
|
68
|
-
# BeAKindOf.new(String) # Using class
|
|
69
|
-
# BeAKindOf.new("String") # Using string
|
|
70
|
-
# BeAKindOf.new(:String) # Using symbol
|
|
17
|
+
# @param expected [Module, String, Symbol] the expected module, or its name
|
|
18
|
+
# @raise [ArgumentError] when expected is neither a Module nor a constant name
|
|
71
19
|
def initialize(expected)
|
|
72
|
-
@expected =
|
|
73
|
-
return if /\A[A-Z]/.match?(@expected)
|
|
74
|
-
|
|
75
|
-
raise ArgumentError,
|
|
76
|
-
"expected must start with an uppercase letter (got: #{@expected})"
|
|
20
|
+
@expected = Constant.normalize(expected, ::Module)
|
|
77
21
|
end
|
|
78
22
|
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
# @api public
|
|
86
|
-
#
|
|
87
|
-
# @yield [] Block that returns the object to check
|
|
88
|
-
# @yieldreturn [Object] The object to verify the type of
|
|
89
|
-
#
|
|
90
|
-
# @return [Boolean] true if the object is an instance of the expected class or one of its subclasses
|
|
91
|
-
#
|
|
92
|
-
# @raise [ArgumentError] if no block is provided
|
|
93
|
-
# @raise [NameError] if the expected class cannot be found
|
|
94
|
-
#
|
|
95
|
-
# @example Simple type check
|
|
96
|
-
# matcher = BeAKindOf.new(Numeric)
|
|
97
|
-
# matcher.match? { 42 } # => true
|
|
98
|
-
#
|
|
99
|
-
# @example With inheritance
|
|
100
|
-
# matcher = BeAKindOf.new(Animal)
|
|
101
|
-
# matcher.match? { Dog.new } # => true
|
|
23
|
+
# @yieldreturn [Object] the actual value
|
|
24
|
+
# @return [Boolean]
|
|
25
|
+
# @raise [ArgumentError] when no block is given
|
|
26
|
+
# @raise [NameError] when the name is not defined
|
|
27
|
+
# @raise [TypeError] when the name does not resolve to a Module
|
|
102
28
|
def match?
|
|
103
29
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
104
30
|
|
|
105
|
-
|
|
31
|
+
Constant.resolve(@expected, ::Module) === yield
|
|
106
32
|
end
|
|
107
33
|
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
# @api public
|
|
111
|
-
#
|
|
112
|
-
# @return [String] A string describing what this matcher verifies
|
|
113
|
-
#
|
|
114
|
-
# @example
|
|
115
|
-
# BeAKindOf.new(String).to_s # => "be a kind of String"
|
|
34
|
+
# @return [String]
|
|
116
35
|
def to_s
|
|
117
|
-
"be a kind of #{@expected}"
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
private
|
|
121
|
-
|
|
122
|
-
# Resolves the expected class name to an actual Class object.
|
|
123
|
-
#
|
|
124
|
-
# @api private
|
|
125
|
-
#
|
|
126
|
-
# @return [Class] The resolved class object
|
|
127
|
-
# @raise [NameError] If the class name cannot be resolved to an actual class
|
|
128
|
-
#
|
|
129
|
-
# @note This method handles both string and symbol class names through constant resolution
|
|
130
|
-
def expected_class
|
|
131
|
-
::Object.const_get(@expected)
|
|
36
|
+
"be a kind of #{Constant.label(@expected)}"
|
|
132
37
|
end
|
|
133
38
|
end
|
|
134
39
|
end
|
|
@@ -1,131 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "constant"
|
|
4
|
+
|
|
3
5
|
module Matchi
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# This matcher provides a secure way to verify an object's exact type, ensuring it
|
|
7
|
-
# matches a specific class without including subclasses. It uses Ruby's method binding
|
|
8
|
-
# mechanism to bypass potential method overrides, providing better protection against
|
|
9
|
-
# type check spoofing than standard instance_of? checks.
|
|
10
|
-
#
|
|
11
|
-
# @example Basic usage
|
|
12
|
-
# matcher = Matchi::BeAnInstanceOf.new(String)
|
|
13
|
-
# matcher.match? { "test" } # => true
|
|
14
|
-
# matcher.match? { :test } # => false
|
|
15
|
-
#
|
|
16
|
-
# @example Inheritance behavior
|
|
17
|
-
# class Animal; end
|
|
18
|
-
# class Dog < Animal; end
|
|
19
|
-
#
|
|
20
|
-
# matcher = Matchi::BeAnInstanceOf.new(Animal)
|
|
21
|
-
# matcher.match? { Animal.new } # => true
|
|
22
|
-
# matcher.match? { Dog.new } # => false # Subclass doesn't match
|
|
23
|
-
#
|
|
24
|
-
# @example Secure type checking
|
|
25
|
-
# # Consider a class that attempts to masquerade as String:
|
|
26
|
-
# class MaliciousString
|
|
27
|
-
# def class; String; end
|
|
28
|
-
# def instance_of?(klass); true; end
|
|
29
|
-
# end
|
|
30
|
-
#
|
|
31
|
-
# obj = MaliciousString.new
|
|
32
|
-
# obj.instance_of?(String) # => true (spoofed)
|
|
33
|
-
#
|
|
34
|
-
# matcher = Matchi::BeAnInstanceOf.new(String)
|
|
35
|
-
# matcher.match? { obj } # => false (secure)
|
|
6
|
+
# Exact type matcher: passes when the class of the actual value is the
|
|
7
|
+
# expected class itself, not a descendant.
|
|
36
8
|
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
9
|
+
# The class of the actual value is read with `Kernel#class` bound to it, so
|
|
10
|
+
# an object that redefines `class` cannot lie, and a `BasicObject` is
|
|
11
|
+
# handled. The expected class can be given directly, or by name; see
|
|
12
|
+
# {Matchi::Constant}.
|
|
40
13
|
#
|
|
41
|
-
#
|
|
42
|
-
# Matchi::BeAnInstanceOf.new(
|
|
43
|
-
#
|
|
44
|
-
# # Using class name as symbol
|
|
45
|
-
# Matchi::BeAnInstanceOf.new(:String)
|
|
46
|
-
#
|
|
47
|
-
# # Using namespaced class
|
|
48
|
-
# Matchi::BeAnInstanceOf.new("MyModule::MyClass")
|
|
49
|
-
#
|
|
50
|
-
# @see Matchi::BeAKindOf
|
|
51
|
-
# @see https://ruby-doc.org/core/Object.html#method-i-instance_of-3F
|
|
52
|
-
# @see https://ruby-doc.org/core/Module.html#method-i-bind_call
|
|
14
|
+
# @example
|
|
15
|
+
# Matchi::BeAnInstanceOf.new(Integer).match? { 42 } # => true
|
|
16
|
+
# Matchi::BeAnInstanceOf.new(:Numeric).match? { 42 } # => false
|
|
53
17
|
class BeAnInstanceOf
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
# @api public
|
|
57
|
-
#
|
|
58
|
-
# @param expected [Class, #to_s] The expected class or its name
|
|
59
|
-
# Can be provided as a Class object, String, or Symbol
|
|
60
|
-
#
|
|
61
|
-
# @raise [ArgumentError] if the class name doesn't start with an uppercase letter
|
|
62
|
-
#
|
|
63
|
-
# @return [BeAnInstanceOf] a new instance of the matcher
|
|
64
|
-
#
|
|
65
|
-
# @example
|
|
66
|
-
# BeAnInstanceOf.new(String) # Using class
|
|
67
|
-
# BeAnInstanceOf.new("String") # Using string
|
|
68
|
-
# BeAnInstanceOf.new(:String) # Using symbol
|
|
18
|
+
# @param expected [Class, String, Symbol] the expected class, or its name
|
|
19
|
+
# @raise [ArgumentError] when expected is neither a Class nor a constant name
|
|
69
20
|
def initialize(expected)
|
|
70
|
-
@expected =
|
|
71
|
-
return if /\A[A-Z]/.match?(@expected)
|
|
72
|
-
|
|
73
|
-
raise ::ArgumentError,
|
|
74
|
-
"expected must start with an uppercase letter (got: #{@expected})"
|
|
21
|
+
@expected = Constant.normalize(expected, ::Class)
|
|
75
22
|
end
|
|
76
23
|
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
# can be overridden.
|
|
83
|
-
#
|
|
84
|
-
# @api public
|
|
85
|
-
#
|
|
86
|
-
# @yield [] Block that returns the object to check
|
|
87
|
-
# @yieldreturn [Object] The object to verify the type of
|
|
88
|
-
#
|
|
89
|
-
# @return [Boolean] true if the object's actual class is exactly the expected class
|
|
90
|
-
#
|
|
91
|
-
# @raise [ArgumentError] if no block is provided
|
|
92
|
-
# @raise [NameError] if the expected class cannot be found
|
|
93
|
-
#
|
|
94
|
-
# @example Simple type check
|
|
95
|
-
# matcher = BeAnInstanceOf.new(String)
|
|
96
|
-
# matcher.match? { "test" } # => true
|
|
97
|
-
# matcher.match? { StringIO.new } # => false
|
|
24
|
+
# @yieldreturn [Object] the actual value
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
# @raise [ArgumentError] when no block is given
|
|
27
|
+
# @raise [NameError] when the name is not defined
|
|
28
|
+
# @raise [TypeError] when the name does not resolve to a Class
|
|
98
29
|
def match?
|
|
99
30
|
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
100
31
|
|
|
101
|
-
actual_class = ::
|
|
102
|
-
expected_class == actual_class
|
|
103
|
-
end
|
|
32
|
+
actual_class = ::Kernel.instance_method(:class).bind_call(yield)
|
|
104
33
|
|
|
105
|
-
|
|
106
|
-
#
|
|
107
|
-
# @api public
|
|
108
|
-
#
|
|
109
|
-
# @return [String] A string describing what this matcher verifies
|
|
110
|
-
#
|
|
111
|
-
# @example
|
|
112
|
-
# BeAnInstanceOf.new(String).to_s # => "be an instance of String"
|
|
113
|
-
def to_s
|
|
114
|
-
"be an instance of #{@expected}"
|
|
34
|
+
Constant.resolve(@expected, ::Class).equal?(actual_class)
|
|
115
35
|
end
|
|
116
36
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
#
|
|
121
|
-
# @api private
|
|
122
|
-
#
|
|
123
|
-
# @return [Class] The resolved class object
|
|
124
|
-
# @raise [NameError] If the class name cannot be resolved to an actual class
|
|
125
|
-
#
|
|
126
|
-
# @note This method handles both string and symbol class names through constant resolution
|
|
127
|
-
def expected_class
|
|
128
|
-
::Object.const_get(@expected)
|
|
37
|
+
# @return [String]
|
|
38
|
+
def to_s
|
|
39
|
+
"be an instance of #{Constant.label(@expected)}"
|
|
129
40
|
end
|
|
130
41
|
end
|
|
131
42
|
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../number"
|
|
4
|
+
|
|
5
|
+
module Matchi
|
|
6
|
+
class BeWithin
|
|
7
|
+
# Numeric proximity matcher: passes when the actual value is a core number
|
|
8
|
+
# whose distance to the expected value is at most delta.
|
|
9
|
+
#
|
|
10
|
+
# The actual value must be an `Integer`, a `Float`, a `Rational` or a
|
|
11
|
+
# `BigDecimal` (its class is read with `Kernel#class` bound to it). Any
|
|
12
|
+
# other value fails, including a user-defined `Numeric` subclass: such an
|
|
13
|
+
# object would control the subtraction through `coerce`.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# Matchi::BeWithin::Of.new(1, 42).match? { 41 } # => true
|
|
17
|
+
# Matchi::BeWithin::Of.new(1, 42).match? { "42" } # => false
|
|
18
|
+
class Of
|
|
19
|
+
# @param delta [Numeric] the tolerance: a finite, real, non-negative number
|
|
20
|
+
# @param expected [Numeric] the center of the interval: a finite, real number
|
|
21
|
+
# @raise [ArgumentError] when an argument is not such a number
|
|
22
|
+
def initialize(delta, expected)
|
|
23
|
+
@delta = Number.non_negative!(delta, "delta")
|
|
24
|
+
@expected = Number.finite_real!(expected, "expected")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @yieldreturn [Object] the actual value
|
|
28
|
+
# @return [Boolean]
|
|
29
|
+
# @raise [ArgumentError] when no block is given
|
|
30
|
+
def match?
|
|
31
|
+
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
32
|
+
|
|
33
|
+
actual = yield
|
|
34
|
+
return false unless core_number?(actual)
|
|
35
|
+
|
|
36
|
+
(@expected - actual).abs <= @delta
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [String]
|
|
40
|
+
def to_s
|
|
41
|
+
"be within #{@delta.inspect} of #{@expected.inspect}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
CORE = [::Integer, ::Float, ::Rational].freeze
|
|
47
|
+
private_constant :CORE
|
|
48
|
+
|
|
49
|
+
def core_number?(actual)
|
|
50
|
+
klass = ::Kernel.instance_method(:class).bind_call(actual)
|
|
51
|
+
return true if CORE.include?(klass)
|
|
52
|
+
|
|
53
|
+
defined?(::BigDecimal) ? klass.equal?(::BigDecimal) : false
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/matchi/be_within.rb
CHANGED
|
@@ -1,149 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "be_within/of"
|
|
4
|
+
require_relative "number"
|
|
5
|
+
|
|
3
6
|
module Matchi
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# of an expected value. It's particularly useful for floating-point comparisons or
|
|
8
|
-
# when checking if a value is approximately equal to another within a given tolerance.
|
|
9
|
-
#
|
|
10
|
-
# @example Basic usage with integers
|
|
11
|
-
# matcher = Matchi::BeWithin.new(1).of(10)
|
|
12
|
-
# matcher.match? { 9 } # => true
|
|
13
|
-
# matcher.match? { 10 } # => true
|
|
14
|
-
# matcher.match? { 11 } # => true
|
|
15
|
-
# matcher.match? { 12 } # => false
|
|
7
|
+
# Builder for the `be within DELTA of EXPECTED` matcher. It is not a matcher
|
|
8
|
+
# itself: it does not respond to `match?`, so a framework that checks the
|
|
9
|
+
# matcher interface rejects a forgotten `of`.
|
|
16
10
|
#
|
|
17
|
-
# @example
|
|
18
|
-
#
|
|
19
|
-
# matcher.match? { 3.1 } # => true
|
|
20
|
-
# matcher.match? { 3.2 } # => true
|
|
21
|
-
# matcher.match? { 3.0 } # => false
|
|
22
|
-
#
|
|
23
|
-
# @example Working with percentages
|
|
24
|
-
# matcher = Matchi::BeWithin.new(5).of(100) # 5% margin
|
|
25
|
-
# matcher.match? { 95 } # => true
|
|
26
|
-
# matcher.match? { 105 } # => true
|
|
27
|
-
# matcher.match? { 110 } # => false
|
|
28
|
-
#
|
|
29
|
-
# @see https://ruby-doc.org/core/Numeric.html#method-i-abs
|
|
11
|
+
# @example
|
|
12
|
+
# Matchi::BeWithin.new(0.5).of(3).match? { 3.2 } # => true
|
|
30
13
|
class BeWithin
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# @api public
|
|
34
|
-
#
|
|
35
|
-
# @param delta [Numeric] The maximum allowed difference from the expected value
|
|
36
|
-
#
|
|
37
|
-
# @raise [ArgumentError] if delta is not a Numeric
|
|
38
|
-
# @raise [ArgumentError] if delta is negative
|
|
39
|
-
#
|
|
40
|
-
# @return [BeWithin] a new instance of the matcher
|
|
41
|
-
#
|
|
42
|
-
# @example
|
|
43
|
-
# BeWithin.new(0.5) # Using float
|
|
44
|
-
# BeWithin.new(2) # Using integer
|
|
14
|
+
# @param delta [Numeric] the tolerance: a finite, real, non-negative number
|
|
15
|
+
# @raise [ArgumentError] when delta is not such a number
|
|
45
16
|
def initialize(delta)
|
|
46
|
-
|
|
47
|
-
raise ::ArgumentError, "delta must be non-negative" if delta.negative?
|
|
48
|
-
|
|
49
|
-
@delta = delta
|
|
17
|
+
@delta = Number.non_negative!(delta, "delta")
|
|
50
18
|
end
|
|
51
19
|
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
# by calling the #of method. Direct use of #match? is not supported.
|
|
56
|
-
#
|
|
57
|
-
# @api public
|
|
58
|
-
#
|
|
59
|
-
# @raise [NotImplementedError] always, as this method should not be used
|
|
60
|
-
#
|
|
61
|
-
# @example
|
|
62
|
-
# # Don't do this:
|
|
63
|
-
# BeWithin.new(0.5).match? { 42 } # Raises NotImplementedError
|
|
64
|
-
#
|
|
65
|
-
# # Do this instead:
|
|
66
|
-
# BeWithin.new(0.5).of(42).match? { 41.8 } # Works correctly
|
|
67
|
-
def match?
|
|
68
|
-
raise ::NotImplementedError, "BeWithin is not a complete matcher. Use BeWithin#of to create a valid matcher."
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Specifies the expected reference value.
|
|
72
|
-
#
|
|
73
|
-
# @api public
|
|
74
|
-
#
|
|
75
|
-
# @param expected [Numeric] The reference value to compare against
|
|
76
|
-
#
|
|
77
|
-
# @raise [ArgumentError] if expected is not a Numeric
|
|
78
|
-
#
|
|
79
|
-
# @return [#match?] A matcher that checks if a value is within the specified range
|
|
80
|
-
#
|
|
81
|
-
# @example
|
|
82
|
-
# be_within_wrapper = BeWithin.new(0.5)
|
|
83
|
-
# be_within_wrapper.of(3.14) # Creates matcher for values in range 2.64..3.64
|
|
20
|
+
# @param expected [Numeric] the center of the interval, a finite real number
|
|
21
|
+
# @return [Matchi::BeWithin::Of] the matcher
|
|
22
|
+
# @raise [ArgumentError] when expected is not a finite real Numeric
|
|
84
23
|
def of(expected)
|
|
85
24
|
Of.new(@delta, expected)
|
|
86
25
|
end
|
|
87
26
|
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
# against the expected value using the specified delta.
|
|
92
|
-
class Of
|
|
93
|
-
# Initialize the matcher with a delta and an expected value.
|
|
94
|
-
#
|
|
95
|
-
# @api private
|
|
96
|
-
#
|
|
97
|
-
# @param delta [Numeric] The maximum allowed difference from the expected value
|
|
98
|
-
# @param expected [Numeric] The reference value to compare against
|
|
99
|
-
#
|
|
100
|
-
# @raise [ArgumentError] if delta is not a Numeric
|
|
101
|
-
# @raise [ArgumentError] if expected is not a Numeric
|
|
102
|
-
# @raise [ArgumentError] if delta is negative
|
|
103
|
-
def initialize(delta, expected)
|
|
104
|
-
raise ::ArgumentError, "delta must be a Numeric" unless delta.is_a?(::Numeric)
|
|
105
|
-
raise ::ArgumentError, "expected must be a Numeric" unless expected.is_a?(::Numeric)
|
|
106
|
-
raise ::ArgumentError, "delta must be non-negative" if delta.negative?
|
|
107
|
-
|
|
108
|
-
@delta = delta
|
|
109
|
-
@expected = expected
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
# Checks if the yielded value is within the accepted range.
|
|
113
|
-
#
|
|
114
|
-
# The value is considered within range if its absolute difference from the
|
|
115
|
-
# expected value is less than or equal to the specified delta.
|
|
116
|
-
#
|
|
117
|
-
# @api public
|
|
118
|
-
#
|
|
119
|
-
# @yield [] Block that returns the value to check
|
|
120
|
-
# @yieldreturn [Numeric] The value to verify
|
|
121
|
-
#
|
|
122
|
-
# @return [Boolean] true if the value is within the accepted range
|
|
123
|
-
#
|
|
124
|
-
# @raise [ArgumentError] if no block is provided
|
|
125
|
-
#
|
|
126
|
-
# @example
|
|
127
|
-
# matcher = BeWithin.new(0.5).of(3.14)
|
|
128
|
-
# matcher.match? { 3.0 } # => true
|
|
129
|
-
# matcher.match? { 4.0 } # => false
|
|
130
|
-
def match?
|
|
131
|
-
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
132
|
-
|
|
133
|
-
(@expected - yield).abs <= @delta
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Returns a human-readable description of the matcher.
|
|
137
|
-
#
|
|
138
|
-
# @api public
|
|
139
|
-
#
|
|
140
|
-
# @return [String] A string describing what this matcher verifies
|
|
141
|
-
#
|
|
142
|
-
# @example
|
|
143
|
-
# BeWithin.new(0.5).of(3.14).to_s # => "be within 0.5 of 3.14"
|
|
144
|
-
def to_s
|
|
145
|
-
"be within #{@delta} of #{@expected}"
|
|
146
|
-
end
|
|
27
|
+
# @return [String]
|
|
28
|
+
def to_s
|
|
29
|
+
"be within #{@delta.inspect}"
|
|
147
30
|
end
|
|
148
31
|
end
|
|
149
32
|
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../number"
|
|
4
|
+
|
|
5
|
+
module Matchi
|
|
6
|
+
class Change
|
|
7
|
+
# Passes when the difference between the observed value after and before
|
|
8
|
+
# the block, `after - before`, is exactly the expected delta (`:by`), at
|
|
9
|
+
# least it (`:by_at_least`) or at most it (`:by_at_most`). The expected
|
|
10
|
+
# delta receives the comparison; the difference is computed by the
|
|
11
|
+
# observed values, so this matcher depends on their honesty.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# list = []
|
|
15
|
+
# Matchi::Change::Delta.new(:by, 2) { list.length }.match? { list.push(1, 2) } # => true
|
|
16
|
+
class Delta
|
|
17
|
+
# For each kind: the comparison the delta receives, and its description.
|
|
18
|
+
KINDS = {
|
|
19
|
+
by: [:==, "by"],
|
|
20
|
+
by_at_least: [:<=, "by at least"],
|
|
21
|
+
by_at_most: [:>=, "by at most"]
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# @param kind [Symbol] `:by`, `:by_at_least` or `:by_at_most`
|
|
25
|
+
# @param delta [Numeric] a finite real number, non-negative unless kind is `:by`
|
|
26
|
+
# @yieldreturn [Object] the observed value
|
|
27
|
+
# @raise [ArgumentError] when the kind is unknown, delta is not such a number, or no block is given
|
|
28
|
+
def initialize(kind, delta, &state)
|
|
29
|
+
raise ::ArgumentError, "unknown kind #{kind.inspect}" unless KINDS.key?(kind)
|
|
30
|
+
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
31
|
+
|
|
32
|
+
@operator, @label = KINDS.fetch(kind)
|
|
33
|
+
@delta = kind == :by ? Number.finite_real!(delta, "delta") : Number.non_negative!(delta, "delta")
|
|
34
|
+
@state = state
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @yieldreturn [Object] ignored, the block is evaluated for its effect
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
# @raise [ArgumentError] when no block is given
|
|
40
|
+
def match?
|
|
41
|
+
raise ::ArgumentError, "a block must be provided" unless block_given?
|
|
42
|
+
|
|
43
|
+
value_before = @state.call
|
|
44
|
+
yield
|
|
45
|
+
value_after = @state.call
|
|
46
|
+
|
|
47
|
+
@delta.public_send(@operator, value_after - value_before) ? true : false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [String]
|
|
51
|
+
def to_s
|
|
52
|
+
"change #{@label} #{@delta.inspect}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|