vector_number 0.4.3 → 0.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.
data/lib/vector_number.rb CHANGED
@@ -1,123 +1,196 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "vector_number/comparing"
4
- require_relative "vector_number/converting"
5
- require_relative "vector_number/enumerating"
6
- require_relative "vector_number/math_converting"
7
- require_relative "vector_number/mathing"
8
- require_relative "vector_number/querying"
9
- require_relative "vector_number/stringifying"
10
- require_relative "vector_number/version"
11
-
12
- # A class to add together anything.
3
+ # VectorNumber provides a Numeric-like experience for doing arithmetics on heterogeneous objects,
4
+ # with more advanced operations based on real vector spaces available when needed.
5
+ #
6
+ # VectorNumber inherits from +Object+ and includes +Enumerable+ and +Comparable+.
7
+ # It implements mostly the same interface as +Numeric+ classes, but can {#coerce} any value.
8
+ # Its behavior follows +Complex+ when possible.
9
+ #
10
+ # All instances are frozen after creation.
11
+ #
12
+ # **Creation**
13
+ # - {.[]}: the simplest way to create a VectorNumber
14
+ # - {#initialize}: an alternative way, with some advanced features
15
+ #
16
+ # **Comparing**
17
+ # - {#==}: test for equal value
18
+ # - {#eql?}: test for equal value and type
19
+ # - {#<=>}: compare real values
20
+ # - {#hash}: calculate hash value for use in +Hash+es
21
+ #
22
+ # **Querying**
23
+ # - {#zero?}: check if all coefficients are zero
24
+ # - {#nonzero?}: check if any coefficient is non-zero
25
+ # - {#positive?}: check if all coefficients are positive
26
+ # - {#negative?}: check if all coefficients are negative
27
+ # - {#finite?}: check if all coefficients are finite
28
+ # - {#infinite?}: check if any coefficient is non-finite
29
+ # - {#numeric?}: test if value is real or complex
30
+ # - {#nonnumeric?}: test if value is not real or complex
31
+ # - {#integer?}: +false+
32
+ # - {#real?}: +false+
33
+ #
34
+ # **Unary** **math** **operations**
35
+ # - {#+}/{#dup}: return self
36
+ # - {#-@}/{#neg}: negate value
37
+ # - {#abs}/{#magnitude}: return absolute value (magnitude, length)
38
+ # - {#abs2}: return square of absolute value
39
+ #
40
+ # **Arithmetic** **operations**
41
+ # - {#coerce}: convert any object to a VectorNumber
42
+ # - {#+}/{#add}: add object
43
+ # - {#-}/{#sub}: subtract object
44
+ # - {#*}/{#mult}: multiply (scale) by a real number
45
+ # - {#/}/{#quo}: divide (scale) by a real number
46
+ # - {#fdiv}: divide (scale) by a real number, using +fdiv+
47
+ # - {#div}: perform integer division
48
+ # - {#%}/{#modulo}: return modulus from integer division
49
+ # - {#divmod}: combination of {#div} and {#modulo}
50
+ # - {#remainder}: return remainder from integer division
51
+ #
52
+ # **Rounding**
53
+ # - {#round}: round each coefficient
54
+ # - {#ceil}: round each coefficient up towards +∞
55
+ # - {#floor}: round each coefficient down towards -∞
56
+ # - {#truncate}: truncate each coefficient towards 0
57
+ #
58
+ # **Type** **conversion**
59
+ # - {#real}: return real part
60
+ # - {#imag}/{#imaginary}: return imaginary part
61
+ # - {#to_i}/{#to_int}: convert to +Integer+ if possible
62
+ # - {#to_f}: convert to +Float+ if possible
63
+ # - {#to_r}: convert to +Rational+ if possible
64
+ # - {#to_d}: convert to +BigDecimal+ if possible
65
+ # - {#to_c}: convert to +Complex+ if possible
66
+ #
67
+ # **Hash-like** **operations**
68
+ # - {#each}/{#each_pair}: iterate through every pair of unit and coefficient
69
+ # - {#[]}: get coefficient by unit
70
+ # - {#unit?}/{#key?}: check if a unit has a non-zero coefficient
71
+ # - {#units}/{#keys}: return an array of units
72
+ # - {#coefficients}/#{values}: return an array of coefficients
73
+ # - {#to_h}: convert to Hash
74
+ #
75
+ # **Miscellaneous** **methods**
76
+ # - {.numeric_unit?}: check if a unit represents a numeric dimension
77
+ # - {#size}: number of non-zero dimensions
78
+ # - {#to_s}: return string representation suitable for printing
79
+ # - {#inspect}: return string representation suitable for display
80
+ # - {#dup}/{#+}: return self
81
+ # - {#clone}: return self
82
+ #
83
+ # @since 0.1.0
13
84
  class VectorNumber
14
- include Mathing
15
- include MathConverting
16
- include Converting
17
- include Enumerating
18
- include Comparing
19
- include Querying
20
- include Stringifying
85
+ require_relative "vector_number/comparing"
86
+ require_relative "vector_number/converting"
87
+ require_relative "vector_number/enumerating"
88
+ require_relative "vector_number/math_converting"
89
+ require_relative "vector_number/mathing"
90
+ require_relative "vector_number/querying"
91
+ require_relative "vector_number/stringifying"
92
+ require_relative "vector_number/version"
21
93
 
22
- # Keys for possible options.
23
- # Unknown options will be rejected when creating a vector.
24
- #
25
- # @return [Array<Symbol>]
26
- #
27
- # @since 0.2.0
28
- KNOWN_OPTIONS = %i[mult].freeze
94
+ require_relative "vector_number/special_unit"
29
95
 
30
- # Default values for options.
96
+ # List of special numeric unit constants.
31
97
  #
32
- # @return [Hash{Symbol => Object}]
98
+ # @since 0.6.0
99
+ NUMERIC_UNITS = [SpecialUnit.new("1", "").freeze, SpecialUnit.new("i", "i").freeze].freeze
100
+ # Constant for real unit (1).
33
101
  #
34
- # @since 0.2.0
35
- DEFAULT_OPTIONS = { mult: :dot }.freeze
36
-
37
- # Get a unit for +n+th numeric dimension, where 1 is real, 2 is imaginary.
102
+ # Its string representation is an empty string.
38
103
  #
39
- # @since 0.2.0
40
- UNIT = ->(n) { n }.freeze
41
- # Constant for real unit.
104
+ # @since 0.6.0
105
+ R = NUMERIC_UNITS[0]
106
+ # Constant for imaginary unit (i).
42
107
  #
43
- # @since 0.2.0
44
- R = UNIT[1]
45
- # Constant for imaginary unit.
108
+ # Its string representation is "i".
46
109
  #
47
- # @since 0.1.0
48
- I = UNIT[2]
110
+ # @since 0.6.0
111
+ I = NUMERIC_UNITS[1]
49
112
 
50
- # Create new VectorNumber from a list of values, possibly specifying options.
113
+ # @group Creation
114
+ # Create new VectorNumber from a list of values.
51
115
  #
52
116
  # @example
53
117
  # VectorNumber[1, 2, 3] # => (6)
54
118
  # VectorNumber[[1, 2, 3]] # => (1⋅[1, 2, 3])
55
- # VectorNumber["b", VectorNumber::I, mult: :asterisk] # => (1*'b' + 1i)
56
119
  # VectorNumber[] # => (0)
57
120
  # VectorNumber["b", VectorNumber["b"]] # => (2⋅'b')
58
121
  # VectorNumber["a", "b", "a"] # => (2⋅'a' + 1⋅'b')
59
122
  #
60
123
  # @param values [Array<Object>] values to put in the number
61
- # @param options [Hash{Symbol => Object}] options for the number
62
- # @option options [Symbol, String] :mult Multiplication symbol,
63
- # either a key from {MULT_STRINGS} or a literal string to use
64
- # @return [VectorNumber]
124
+ def self.[](*values, **nil)
125
+ new(values)
126
+ end
127
+ # @endgroup
128
+
129
+ # Check if an object is a unit representing a numeric dimension
130
+ # (real or imaginary unit).
65
131
  #
66
- # @since 0.1.0
67
- def self.[](*values, **options)
68
- new(values, options)
132
+ # @example
133
+ # VectorNumber.numeric_unit?(VectorNumber::R) # => true
134
+ # VectorNumber.numeric_unit?(VectorNumber::I) # => true
135
+ # VectorNumber.numeric_unit?(:i) # => false
136
+ #
137
+ # @param unit [Object]
138
+ # @return [Boolean]
139
+ #
140
+ # @since 0.6.0
141
+ def self.numeric_unit?(unit)
142
+ NUMERIC_UNITS.include?(unit)
69
143
  end
70
144
 
71
145
  # Number of non-zero dimensions.
72
146
  #
73
147
  # @return [Integer]
74
- #
75
- # @since 0.1.0
76
148
  attr_reader :size
77
149
 
78
- # Options used for this vector.
150
+ # @group Creation
151
+ # Create new VectorNumber from an array of +values+,
152
+ # possibly modifying coefficients with a block.
79
153
  #
80
- # @see KNOWN_OPTIONS
154
+ # Using +VectorNumber.new([values...])+ directly is more efficient than +VectorNumber[values...]+.
81
155
  #
82
- # @return [Hash{Symbol => Object}]
156
+ # +values+ can be:
157
+ # - an array of values (see {.[]});
158
+ # - a VectorNumber to copy;
159
+ # - a hash in the format returned by {#to_h};
160
+ # - +nil+ to specify a 0-sized vector (same as an empty array or hash).
83
161
  #
84
- # @since 0.1.0
85
- attr_reader :options
86
-
87
- # Create new VectorNumber from +values+, possibly specifying +options+,
88
- # possibly modifying coefficients with a block.
162
+ # Using a hash as +values+ is an advanced technique which allows to quickly
163
+ # construct a VectorNumber with desired units and coefficients,
164
+ # but it can also lead to unexpected results if care is not taken
165
+ # to provide only valid keys and values.
89
166
  #
90
167
  # @example
91
168
  # VectorNumber.new(1, 2, 3) # ArgumentError
92
169
  # VectorNumber.new([1, 2, 3]) # => (6)
93
- # VectorNumber.new(["b", VectorNumber::I], mult: :asterisk) # => (1*'b' + 1i)
170
+ # VectorNumber.new(["b", VectorNumber::I]) # => (1⋅"b" + 1i)
94
171
  # VectorNumber.new # => (0)
95
172
  # @example with a block
96
- # VectorNumber.new(["a", "b", "c", 3]) { _1 * 2 } # => (2⋅'a' + 2⋅'b' + 2⋅'c' + 6)
97
- # VectorNumber.new(["a", "b", "c", 3], &:-@) # => (-1⋅'a' - 1⋅'b' - 1⋅'c' - 3)
173
+ # VectorNumber.new(["a", "b", "c", 3]) { _1 * 2 } # => (2⋅"a" + 2⋅"b" + 2⋅"c" + 6)
174
+ # VectorNumber.new(["a", "b", "c", 3], &:-@) # => (-1⋅"a" - 1⋅"b" - 1⋅"c" - 3)
98
175
  # VectorNumber.new(["a", "b", "c", 3], &:digits) # RangeError
99
- #
100
- # @param values [Array, VectorNumber, Hash{Object => Integer, Float, Rational, BigDecimal}, nil]
101
- # values for this number, hashes are treated like plain vector numbers
102
- # @param options [Hash{Symbol => Object}, nil]
103
- # options for this number, if +values+ is a VectorNumber or contains it,
104
- # these will be merged with options from its +options+
105
- # @option options [Symbol, String] :mult
106
- # text to use between unit and coefficient, see {Stringifying#to_s} for explanation
107
- # @yieldparam coefficient [Integer, Float, Rational, BigDecimal]
108
- # @yieldreturn [Integer, Float, Rational, BigDecimal] new coefficient
109
- # @raise [RangeError] if any pesky non-reals get where they shouldn't
110
- def initialize(values = nil, options = nil, &transform)
111
- # @type var options: Hash[Symbol, Object]
176
+ # @example using hash for values
177
+ # v = VectorNumber.new({1 => 15, "a" => 3.4, nil => -3}) # => (15 + 3.4⋅"a" - 3⋅)
178
+ # v.to_h # => {1 => 15, "a" => 3.4, nil => -3}
179
+ #
180
+ # @param values [Array, VectorNumber, Hash{Object => Numeric}, nil] values for this vector
181
+ # @yieldparam coefficient [Numeric] a real number
182
+ # @yieldreturn [Numeric] new coefficient
183
+ # @raise [RangeError] if a block is used and it returns a non-number or non-real number
184
+ def initialize(values = nil, **nil, &transform)
112
185
  initialize_from(values)
113
186
  apply_transform(&transform)
114
187
  finalize_contents
115
- save_options(options, values: values)
116
- @options.freeze
117
188
  @data.freeze
118
189
  freeze
119
190
  end
120
191
 
192
+ # @group Miscellaneous methods
193
+
121
194
  # Return self.
122
195
  #
123
196
  # @return [VectorNumber]
@@ -149,11 +222,11 @@ class VectorNumber
149
222
  # Create new VectorNumber from a value or self, optionally applying a transform.
150
223
  #
151
224
  # @param from [Object] self if not specified
152
- # @yieldparam coefficient [Integer, Float, Rational, BigDecimal]
153
- # @yieldreturn [Integer, Float, Rational, BigDecimal] new coefficient
225
+ # @yieldparam coefficient [Numeric] a real number
226
+ # @yieldreturn [Numeric] new coefficient
154
227
  # @return [VectorNumber]
155
228
  def new(from = self, &transform)
156
- self.class.new(from, options, &transform)
229
+ self.class.new(from, &transform)
157
230
  end
158
231
 
159
232
  # Check if +other+ is a real number.
@@ -162,16 +235,12 @@ class VectorNumber
162
235
  #
163
236
  # @param value [Object]
164
237
  # @return [Boolean]
165
- #
166
- # @since 0.1.0
167
238
  def real_number?(value)
168
239
  (value.is_a?(Numeric) && value.real?) || (value.is_a?(self.class) && value.numeric?(1))
169
240
  end
170
241
 
171
- # @param values [Array, Hash{Object => Integer, Float, Rational, BigDecimal}, VectorNumber, nil]
242
+ # @param values [Array, Hash{Object => Numeric}, VectorNumber, nil]
172
243
  # @return [void]
173
- #
174
- # @since 0.1.0
175
244
  def initialize_from(values)
176
245
  @data = values.to_h and return if values.is_a?(VectorNumber)
177
246
 
@@ -190,8 +259,6 @@ class VectorNumber
190
259
 
191
260
  # @param value [VectorNumber, Numeric, Object]
192
261
  # @return [void]
193
- #
194
- # @since 0.1.0
195
262
  def add_value_to_data(value)
196
263
  case value
197
264
  when Numeric
@@ -205,8 +272,6 @@ class VectorNumber
205
272
 
206
273
  # @param value [Numeric]
207
274
  # @return [void]
208
- #
209
- # @since 0.1.0
210
275
  def add_numeric_value_to_data(value)
211
276
  @data[R] += value.real
212
277
  # Most numbers will be real, and this extra condition appreciably speeds up addition,
@@ -214,10 +279,8 @@ class VectorNumber
214
279
  @data[I] += value.imaginary unless value.real?
215
280
  end
216
281
 
217
- # @param vector [VectorNumber, Hash{Object => Integer, Float, Rational, BigDecimal}]
282
+ # @param vector [VectorNumber, Hash{Object => Numeric}]
218
283
  # @return [void]
219
- #
220
- # @since 0.1.0
221
284
  def add_vector_to_data(vector)
222
285
  vector.each_pair do |unit, coefficient|
223
286
  raise RangeError, "#{coefficient} is not a real number" unless real_number?(coefficient)
@@ -226,12 +289,10 @@ class VectorNumber
226
289
  end
227
290
  end
228
291
 
229
- # @yieldparam coefficient [Integer, Float, Rational, BigDecimal]
230
- # @yieldreturn [Integer, Float, Rational, BigDecimal]
292
+ # @yieldparam coefficient [Numeric] a real number
293
+ # @yieldreturn [Numeric] new coefficient
231
294
  # @return [void]
232
295
  # @raise [RangeError]
233
- #
234
- # @since 0.1.0
235
296
  def apply_transform
236
297
  return unless block_given?
237
298
 
@@ -243,40 +304,8 @@ class VectorNumber
243
304
  end
244
305
  end
245
306
 
246
- # @param options [Hash{Symbol => Object}, nil]
247
- # @param values [Object] initializing object
248
- # @return [void]
249
- #
250
- # @since 0.1.0
251
- def save_options(options, values:)
252
- @options =
253
- case values
254
- in VectorNumber
255
- merge_options(values.options, options)
256
- in Array[*, VectorNumber => vector, *]
257
- merge_options(vector.options, options)
258
- else
259
- merge_options(DEFAULT_OPTIONS, options)
260
- end
261
- end
262
-
263
- # @param base_options [Hash{Symbol => Object}]
264
- # @param added_options [Hash{Symbol => Object}, nil]
265
- # @return [Hash{Symbol => Object}]
266
- #
267
- # @since 0.3.0
268
- def merge_options(base_options, added_options)
269
- return base_options if !added_options || added_options.empty?
270
- # Optimization for the common case of passing options through #new.
271
- return base_options if added_options.equal?(base_options)
272
-
273
- base_options.merge(added_options).slice(*KNOWN_OPTIONS)
274
- end
275
-
276
307
  # Compact coefficients, calculate size and freeze data.
277
308
  # @return [void]
278
- #
279
- # @since 0.1.0
280
309
  def finalize_contents
281
310
  @data.delete_if { |_u, c| c.zero? }
282
311
  @data.freeze