rangeary 1.0.1 → 2.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.
@@ -0,0 +1,233 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ class Rangeary < Array
4
+ module Util
5
+
6
+ # Module to extend a Hash class instance to hold infinities information
7
+ #
8
+ # {Rangeary} needs to know what its own infinities are.
9
+ # This class holds the information.
10
+ #
11
+ # It should have only 2 keys of +:negative+ and +:positive+ though it is
12
+ # not constrained at the class level.
13
+ #
14
+ # Method {HashInf#status} returns a Hash containing status
15
+ # information about the main values (negative and positive infinities).
16
+ # Some related utility methods are also defined.
17
+ #
18
+ # The default infinity values (for negative and positive) are +false+
19
+ # (NOT +nil+ because nil can mean the border of a beginless/endless Range),
20
+ # whereas the default (uninitialized) status for it is +nil+.
21
+ #
22
+ module HashInf
23
+ # Order Array of the {#status} parameters (smaller means a higher priority).
24
+ # If +:definite+, it means it exists in a Range/Rangeary or is specified in an argument.
25
+ STATUS_ORDERS = [:definite, :guessed, nil]
26
+
27
+ # Reverse Hash of {STATUS_ORDERS} to give the priority number for {#status}.
28
+ #
29
+ # @example
30
+ # STATUS_PRIORITIES[:definite] # => 0
31
+ # STATUS_PRIORITIES[nil] # => 2
32
+ STATUS_PRIORITIES = STATUS_ORDERS.each_with_index.to_a.reverse.to_h
33
+
34
+ # Method to overwrite Hash @status . See {#status}.
35
+ attr_writer :status
36
+
37
+ # Altenative constructor
38
+ #
39
+ # Usually you can construct an instance like:
40
+ # myhs = { negative: false, positive: false }
41
+ # myhs.extend HashInf
42
+ # myhs.status = { negative: negative, positive: positive }
43
+ #
44
+ # This constructor is a shortcut.
45
+ #
46
+ # Note that the returned object is a different object from the argument,
47
+ # and so you are free to destructively modify it.
48
+ #
49
+ # If the argument is a {HashInf}, the contents are copied.
50
+ #
51
+ # @example
52
+ # HashInf.construct() # => <HashInf: {negative: false, positive: false}, status: {negative: nil, positive: nil}>
53
+ # HashInf.construct({negative: false, positive: false}, status: {negative: :guessed})
54
+ #
55
+ # @param infinities [Hash, NilClass] if given, make sure the Hash has 2 keys of :negative and :positive with relevant infinity values, e.g., +Float::INFINITY+ or nil or else.
56
+ # @param status [Hash] Optional. 1 or 2 keys of :negative and :positive with values of {HashInf::STATUS_ORDERS}.
57
+ # @return [Hash] extended with {HashInf}
58
+ def self.construct(infinities=nil, status: {})
59
+ rethsinf = {negative: false, positive: false}
60
+ rethsinf.merge!(infinities) if infinities
61
+ rethsinf.extend HashInf
62
+ rethsinf.status = {negative: nil, positive: nil}.merge(status)
63
+ rethsinf.status.merge!(infinities.status) if infinities.respond_to? :status
64
+ rethsinf.status.merge!(status)
65
+ rethsinf
66
+ end
67
+
68
+ # Reader and initializer of @status
69
+ #
70
+ # status is a two-key (+:negative+ and +:positive+) Hash with values of one of
71
+ #
72
+ # 1. nil (initial condition)
73
+ # 2. +:guessed+ (if it is guessed, such as +Float::INFINITY+ because Range contains an Integer.)
74
+ # 3. +:definite+ (if it is user-specified or exists in a Range, such as +nil+ in +(5..nil)+.
75
+ def status
76
+ return @status if @status
77
+ @status = {negative: nil, positive: nil}
78
+ end
79
+
80
+ # merges another {HashInf} (or Hash) like +Hash#merge+, taking into account the status, and returns a new {HashInf}
81
+ #
82
+ # If other is a (non-HashInf) Hash, it should have no more than 2 keys of :positive and :negative.
83
+ #
84
+ # If the statuses {HashInf#status} are the same between self and other, the more extreme
85
+ # value is adopted. Therefore,
86
+ # rang = Rangeary(?a..?c, positive: ?z)
87
+ # r = rang + Rangeary(?c..?f, positive: ?y)
88
+ # r.infinities[:positive] # => ?z (NOT ?y)
89
+ #
90
+ # If the old and new infinities are incomparable, the result is uncertain.
91
+ #
92
+ # @param other [HashInf, Hash] if it is Hash, converted into HashInf.
93
+ # @param force [Boolean] if true, other always has a higher priority like +Hash#merge+
94
+ # unless the corresponding status is +false+, in which case nothing changes.
95
+ # This is useful to handle the parameters given by the direct arguments to {Rangeary.initialize}
96
+ # @return [HashInf, Hash]
97
+ def merge_hashinf(other, force: false)
98
+ rethsinf = HashInf.construct(self)
99
+ rethsinf.merge_hashinf!(other, force: force)
100
+ rethsinf
101
+ end
102
+
103
+ # Destructive version of {#merge_hashinf}
104
+ #
105
+ # @param (see #posinega_with_status=)
106
+ # @return [HashInf, Hash]
107
+ def merge_hashinf!(other, force: false)
108
+ if !other.respond_to? :status
109
+ other = HashInf.construct(other)
110
+ end
111
+
112
+ POSNEG2METHOD.each_key do |posneg|
113
+ cmped = (force ? 1 : (STATUS_PRIORITIES[self.status[posneg]] <=> STATUS_PRIORITIES[other.status[posneg]]))
114
+ case cmped
115
+ when -1
116
+ # Do nothing
117
+ when 0, 1
118
+ next if false == other[posneg] # no change if the status of "other" is false; n.b., when force==true, chances are self[posneg]!=false and other[posneg]==false, because "cmped" for the *case* is modified!
119
+ if 0 == cmped
120
+ if false != self[posneg]
121
+ oper = ((posneg == :negative) ? :> : :<)
122
+ to_change = _former_more_extreme?(oper, self[posneg], other[posneg])
123
+ next if !to_change
124
+ end
125
+ end
126
+ set_posinega_with_status(posneg, other[posneg], (force ? :definite : other.status[posneg]))
127
+ else
128
+ raise "should not happen. Contact the code developer."
129
+ end
130
+ end
131
+
132
+ self
133
+ end
134
+
135
+ # Returns true if the former is more extreme.
136
+ #
137
+ # If +v1+ is +nil+, returns +true+.
138
+ # If not and if +v2+ is +nil+, returns +false+.
139
+ #
140
+ # The following fails because whereas the guessed positive infinity
141
+ # from the first one is Float:INFINITY, the second given infinity
142
+ # is +RangeExtd::Infinity::POSITIVE+, which is not comparable
143
+ # with +Float:INFINITY+ (although it is comparable with general Float).
144
+ #
145
+ # Rangeary(..6, 5..RangeExtd::Infinity::POSITIVE)
146
+ #
147
+ # @param oper [Symbol] Method of operation: +return v1 if v1 oper v2+
148
+ # @param v1 [Object]
149
+ # @param v2 [Object]
150
+ # @raise [ArgumentError]
151
+ def _former_more_extreme?(oper, v1, v2)
152
+ return true if v1.nil?
153
+ return false if v2.nil?
154
+ v1.send(oper, v2)
155
+ rescue ArgumentError => err
156
+ raise ArgumentError, "Inconsistent given infinities: "+err.message
157
+ end
158
+ private :_former_more_extreme?
159
+
160
+ # Recommended way to set (update) the positive infinity with a status with verification
161
+ #
162
+ # @param (see #posinega_with_status=)
163
+ # @return [self]
164
+ def set_positive_with_status(infinity, stat)
165
+ set_posinega_with_status(:positive, infinity, stat)
166
+ end
167
+
168
+ # Recommended way to set (update) the negative infinity with a status with verification
169
+ #
170
+ # @param (see #posinega_with_status=)
171
+ # @return [self]
172
+ def set_negative_with_status(infinity, stat)
173
+ set_posinega_with_status(:negative, infinity, stat)
174
+ end
175
+
176
+ # Recommended way to set (update) the infinity in either polarity with a status with verification
177
+ #
178
+ # @param infinity [Object]
179
+ # @param stat [Symbol, NilClass] one of {HashInf::STATUS_ORDERS}.
180
+ # @return [self]
181
+ def set_posinega_with_status(posneg, infinity, stat)
182
+ self[posneg] = infinity
183
+ raise ArgumentError, "stat must be one of #{STATUS_ORDERS.inspect}" if !STATUS_ORDERS.include? stat
184
+ self.status[posneg] = stat
185
+ self
186
+ end
187
+
188
+ # Return true if the infinity of the specified polarity is +:definite+
189
+ #
190
+ # @param posneg [Symbol] either :positive or :negative
191
+ def definite?(posneg)
192
+ status_is_a?(:definite, posneg)
193
+ end
194
+
195
+ # Return true if the infinity of the specified polarity is +:guessed+
196
+ #
197
+ # @param posneg [Symbol] either :positive or :negative
198
+ def guessed?(posneg)
199
+ status_is_a?(:guessed, posneg)
200
+ end
201
+
202
+ # Return true if the specified infinity is +nil+.
203
+ #
204
+ # @param posneg [Symbol] either :positive or :negative
205
+ def status_is_nil?(posneg)
206
+ status_is_a?(nil, posneg)
207
+ end
208
+
209
+ # Return true if the specified infinity is with a specified status.
210
+ #
211
+ # @param stat [Symbol, NilClass] One of the elements of {STATUS_ORDERS}
212
+ # @param posneg [Symbol] either :positive or :negative
213
+ def status_is_a?(stat, posneg)
214
+ raise ArgumentError, "Invalid specified status of #{stat.inspect}" if !STATUS_ORDERS.include? stat
215
+ case posneg
216
+ when :positive, :negative
217
+ stat == status[posneg]
218
+ else
219
+ raise ArgumentError, "Invalid argument, neither :positive nor :negative: #{posneg.inspect}"
220
+ end
221
+ end
222
+
223
+
224
+ # Alter +Hash#inspect+ (Note irb does not take this into account)
225
+ #
226
+ # @return [String]
227
+ def inspect
228
+ sprintf "<Hash(Inf): %s, status: %s>", super, status
229
+ end
230
+ end # module HashInf
231
+ end # module Util
232
+ end # class Rangeary < Array
233
+