vector_number 0.6.1 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 25f09e35dd54faff983f9cf09a5243621ddb491e537c4efededf24fe52293dd1
4
- data.tar.gz: acfe2627613a81b1ee3262a66d6eca033adb0c8eb9d00550496352d2f7e53a0f
3
+ metadata.gz: 48be1b3c651d554736ea6ae2da95e31c934441e62c35e72a204d7dcda101e770
4
+ data.tar.gz: 964308ecfcac82ea1fe0503fb336171004e837035f39f13d8128ae81fdfae900
5
5
  SHA512:
6
- metadata.gz: 111166b11c37f6a2f731d0bfb1ecd765c023002bc5f47f587dc4b8b756df6e06eb4752c9904069616c78b87a90b7562cec0d5231d7928accb6e9d72dac33384c
7
- data.tar.gz: 2221c913375975beb5e081f312c621231e423fb57a0eb5b2f3167e0f93b2c3a8518aaf3b249f1344caa02e7635ae065c0e7e6e7a7ec8d2dc770d95514b1c3a4b
6
+ metadata.gz: eb383677bb0908b8cc96b85bb25ee4a43c25767ce27d379ede252d3ed668ff26c7bba7d98831eb8bb1f5600adaa43bab91ebbc10c999a7daf79d3f304f9548e3
7
+ data.tar.gz: eac645b74326d103f270e8b9d6753c3821ca0ac430d8f129ff198e4f442ac17ae1235b916504988b5c557b2ecc3fdaf412473aad6a52f77fd98799c692dfa4d9
data/README.md CHANGED
@@ -4,33 +4,96 @@
4
4
  [![CI](https://github.com/trinistr/vector_number/actions/workflows/CI.yaml/badge.svg)](https://github.com/trinistr/vector_number/actions/workflows/CI.yaml)
5
5
 
6
6
  > [!TIP]
7
- > You may be viewing documentation for an older (or newer) version of the gem. Look at [Changelog](https://github.com/trinistr/dicey/blob/main/CHANGELOG.md) to see all versions, including unreleased changes.
7
+ > You may be viewing documentation for an older (or newer) version of the gem. Look at [Changelog](https://github.com/trinistr/vector_number/blob/main/CHANGELOG.md) to see all versions, including unreleased changes.
8
8
 
9
9
  ***
10
10
 
11
- VectorNumber is a Ruby gem that provides a Numeric-like experience for doing arithmetics on heterogeneous objects, with more advanced operations based on real vector spaces available when needed.
11
+ **Do full linear algebra on Ruby objects.** Yes, *any* objects.
12
12
 
13
- Features:
14
- - [Add and subtract](#basics) (almost) any object, with no setup or declaration.
15
- - [Multiply and divide](#basics) vectors by any real number to create 1.35 of an array and -2 of a string. What does that mean? Only you know!
16
- - [Use vectors instead of inbuilt numbers](#numerical-behavior) in most situtations with no difference in behavior. Or, use familiar methods from numerics with sane semantics!
17
- - [Enumerate vectors in a hash-like fashion](#enumeration-and-hash-like-behavior), or transform to an array or hash as needed.
18
- - Enjoy a mix of vector-, complex- and polynomial-like behavior at appropriate times.
19
- - No dependencies, no extensions. It just works!
13
+ ```ruby
14
+ # Create a vector where dimensions are whatever you want
15
+ v1 = VectorNumber[x: 3, y: 4] # 3 in :x direction, 4 in :y
16
+ v2 = VectorNumber["weight" => 2.5, :x => 1] # mix symbols and strings
17
+ v3 = VectorNumber[:y => 2, [1, 2, 3] => 5] # or any other objects
18
+
19
+ # Add them, scale them, find their lengths in a natural way
20
+ v1 + v2 # => (4⋅:x + 4⋅:y + 2.5⋅"weight")
21
+ v1 - v3 # => (3⋅:x + 2⋅:y - 5⋅[1, 2, 3])
22
+ v2 + "banana" # => (2.5⋅"weight" + 1⋅:x + 1⋅"banana")
23
+ (v1 * 2).magnitude # => 10.0 (since 2*√(3²+4²) = 10)
24
+
25
+ # Calculate dot products, angles, and projections
26
+ v1.dot_product(v2) # => 3 (= 3*1 + 4*0 + 0*2.5)
27
+ v1.angle(v2) # => 1.3460753063647353 (≈77.1°)
28
+ v1.vector_projection(v2).round(2) # => (1.03⋅"weight" + 0.41⋅:x)
29
+ ```
30
+
31
+ VectorNumber treats **every distinct Ruby object as a dimension** in a vector space over the real numbers. This means you can do proper linear algebra on anything: symbols, strings, arrays, custom classes—whatever you need.
32
+
33
+ ## 🚀 Why VectorNumber?
34
+
35
+ ### 1. Full Linear Algebra on Any Domain
36
+ Need to work with weighted tags? Feature vectors for machine learning? Coordinate systems with non-numeric axes? VectorNumber gives you the math:
37
+
38
+ ```ruby
39
+ # ML feature vectors with meaningful dimension names
40
+ doc1 = VectorNumber["word_ruby" => 3, "word_gem" => 2, "word_library" => 1]
41
+ doc2 = VectorNumber["word_ruby" => 1, "word_gem" => 3, "word_code" => 2]
42
+
43
+ # Cosine similarity for document comparison
44
+ similarity = doc1.cosine(doc2).round(5) # => 0.64286
45
+
46
+ # Find which document is "closer" to a query
47
+ query = VectorNumber["word_ruby" => 1, "word_gem" => 1]
48
+ doc1.cosine(query) > doc2.cosine(query) # => true
49
+ ```
50
+
51
+ ### 2. Numeric-Like Behavior, Hash-Like Access
52
+ It feels like a number, but you can inspect it like a hash:
53
+
54
+ ```ruby
55
+ v = VectorNumber["apple", "orange"] # => (1⋅"apple" + 1⋅"orange")
56
+ v += "orange" # => (1⋅"apple" + 2⋅"orange")
57
+ v["apple"] # => 1 (coefficient lookup)
58
+ v["kiwi"] # => 0 (missing dimensions are zero)
59
+ v.to_h # => {"apple" => 1, "orange" => 2}
60
+ v.units # => ["apple", "orange"]
61
+ v.coefficients # => [1, 2]
62
+ ```
63
+
64
+ ### 3. Plays Nicely with Ruby Numbers
65
+ Thanks to full `#coerce` support, VectorNumbers work seamlessly with Ruby's numeric types:
66
+
67
+ ```ruby
68
+ 5 + VectorNumber["x"] * 2 # => (5 + 2⋅"x")
69
+ 3.14 * VectorNumber[:theta] # => (3.14⋅:theta)
70
+ VectorNumber[8] < 10 # => true (compares real value)
71
+ ```
72
+
73
+ ### 4. Rich API
74
+ You want it? We got it!
75
+
76
+ | Category | Methods |
77
+ |-----------|---------|
78
+ | Basic Ops | `+` and `-`, `*` and `/` (scaling), `div` and `%` |
79
+ | Rounding | `round`, `ceil`, `floor`, `truncate` (per-coefficient) |
80
+ | Norms | `magnitude`/`abs`, `abs2`, `p_norm`, `maximum_norm` |
81
+ | Projections | `vector_projection`, `scalar_projection`, `vector_rejection`, `scalar_rejection` |
82
+ | Geometry | `dot_product`, `angle`, `subspace_basis`, `unit_vector` |
83
+ | Hash-like | `each`, `[]`, `transform_coefficients`, `transform_units` |
20
84
 
21
- Other people have created some similar gems over the years, like [vector_space](https://github.com/tomstuart/vector_space) or [named_vector](https://rubygems.org/gems/named_vector) but they don't have the characterics that I wanted, and none of them have been updated in *years*.
85
+ ...and many, **many** more!
22
86
 
23
- ## Table of contents
87
+ ## 📜 Table of contents
24
88
 
25
89
  - [Installation](#installation)
26
- - [Ruby engine support status](#ruby-engine-support-status)
27
90
  - [Usage](#usage)
28
- - [Basics](#basics)
29
- - [Getting values back](#getting-values-back)
30
- - [(Somewhat) advanced usage](#somewhat-advanced-usage)
31
- - [Frozenness](#frozenness)
32
- - [Numerical behavior](#numerical-behavior)
33
- - [Enumeration and hash-like behavior](#enumeration-and-hash-like-behavior)
91
+ - [API Documentation](#api-documentation)
92
+ - [Quick Start](#quick-start)
93
+ - [Real-world Examples](#real-world-examples)
94
+ - [Advanced Vector Operations](#advanced-vector-operations)
95
+ - [Hash-like Operations](#hash-like-operations)
96
+ - [Custom String Conversion](#custom-string-conversion)
34
97
  - [Conceptual basis](#conceptual-basis)
35
98
  - [Development](#development)
36
99
  - [Contributing](#contributing)
@@ -43,106 +106,212 @@ Install with `gem`:
43
106
  gem install vector_number
44
107
  ```
45
108
 
46
- If using Bundler, add gem to your Gemfile:
109
+ Or, if using Bundler, add gem to your Gemfile:
47
110
  ```ruby
48
111
  gem "vector_number"
49
112
  ```
50
113
 
51
- ### Ruby engine support status
52
-
53
- VectorNumber is developed on MRI (CRuby) but should work on other engines too.
54
- - TruffleRuby: minor differences in behavior, but otherwise works as expected.
55
- - JRuby: minor differences in behavior, but otherwise works as expected.
56
- - Other engines: untested, but should work, depending on compatibility with MRI.
114
+ > [!NOTE]
115
+ > VectorNumber is officially supported (and tested) on MRI (CRuby), JRuby and TruffleRuby.
57
116
 
58
117
  ## Usage
59
118
 
60
- > [!Note]
61
- > - Latest API documentation from `main` branch is automatically deployed to [GitHub Pages](https://trinistr.github.io/vector_number).
62
- > - Documentation for published versions is available on [RubyDoc](https://rubydoc.info/gems/vector_number).
119
+ ### API Documentation
120
+
121
+ Full documentation with all methods and examples for each method is generated from source and is available online:
122
+ - [Main branch](https://trinistr.github.io/vector_number)
123
+ - [Latest published vesion](https://rubydoc.info/gems/vector_number)
63
124
 
64
- ### Basics
125
+ ### Quick Start
65
126
 
66
- VectorNumbers are mostly useful for summing up heterogeneous objects:
67
127
  ```ruby
68
- sum = VectorNumber[4] + "death" + "death" + nil
69
- sum # => (17 + 2⋅"death" + 1⋅)
70
- sum.to_h # => {unit/1 => 17, "death" => 2, nil => 1}
71
- sum.to_a # => [[unit/1, 17], ["death", 2], [nil, 1]]
72
-
73
- # Alternatively, the same result can be equivalently (and more efficiently)
74
- # achieved by passing all values to a constructor:
75
- VectorNumber[4, "death", "death", 13, nil]
76
- VectorNumber.new([4, "death", "death", 13, nil])
128
+ require "vector_number"
129
+
130
+ # Create vectors
131
+ VectorNumber[5, "hello", 5, :sym] # => (10 + 1⋅"hello" + 1⋅:sym)
132
+ VectorNumber["x" => 3, "y" => 4] # => (3⋅"x" + 4⋅"y")
133
+ 2 * VectorNumber[:a, :b, :c] # => (2⋅:a + 2⋅:b + 2⋅:c)
134
+ # or more explicitly
135
+ VectorNumber.new([5, "hello", 5, :sym])
136
+ VectorNumber.new({"x" => 3, "y" => 4})
137
+
138
+ # Basic arithmetic
139
+ v = VectorNumber["apple" => 3] + VectorNumber["orange" => 2]
140
+ v -= "orange" # => (3⋅"apple" + 1⋅"orange")
141
+ v *= 1.5 # => (4.5⋅"apple" + 1.5⋅"orange")
77
142
  ```
78
143
 
79
- Doing arithmetic with vectors is simple and intuitive:
144
+ ### Real-world Examples
145
+
146
+ #### 📦 Inventory Management
147
+
148
+ The most basic function of VectorNumber is the ability to act similarly to a Hash but with defined arithmetic operations. This naturally leads to intuitive operations like addition and subtraction of inventory items.
149
+
80
150
  ```ruby
81
- VectorNumber["string"] + "string" # => (2⋅"string")
82
- VectorNumber["string"] - "str" # => (1⋅"string" - 1⋅"str")
83
- VectorNumber[5] + VectorNumber["string"] - 0.5 # => (4.5 + 1⋅"string")
84
- VectorNumber["string", "string", "string", "str"] # => (3⋅"string" + 1⋅"str")
85
- # Multiply and divide by any real number:
86
- VectorNumber[:s] * 2 + VectorNumber["string"] * 0.3 # => (2⋅s + 0.3⋅"string")
87
- VectorNumber[:s] / VectorNumber[3] # => (1/3⋅s)
151
+ class Inventory
152
+ def initialize(items)
153
+ @items = VectorNumber.new(items)
154
+ end
155
+
156
+ def add(item, quantity = 1)
157
+ @items += VectorNumber.new({item => quantity})
158
+ end
159
+
160
+ def remove(item, quantity = 1)
161
+ @items -= VectorNumber.new({item => quantity})
162
+ end
163
+
164
+ def has?(item, quantity = 1)
165
+ @items[item] >= quantity
166
+ end
167
+
168
+ def total_value(prices)
169
+ # Multiply each item's quantity by its price and sum them up
170
+ @items.dot_product(VectorNumber.new(prices))
171
+ end
172
+ end
173
+
174
+ inventory = Inventory.new("apple" => 10, "banana" => 5)
175
+ inventory.add("apple", 3)
176
+ inventory.remove("banana", 2)
177
+ inventory.total_value("apple" => 0.5, "banana" => 0.3) # => 7.4
88
178
  ```
89
179
 
90
- Ruby numbers rely on `#coerce` to promote values to a common type. This allows using regular numbers as first operand in arithmetic operations:
180
+ #### 📊 Weighted Scoring System
181
+
182
+ VectorNumber has several similarity measures out-of-the-box, and implementing custom ones can easily be done with `map` and `reduce`. This example shows how to calculate a match score between a candidate's skills and job requirements using cosine similarity.
183
+
91
184
  ```ruby
92
- 2 + VectorNumber["string"] # => (2 + 1⋅"string")
93
- 1/3r * VectorNumber[[]] # => (1/3⋅[])
94
- 13 / VectorNumber[2] # => (13/2)
185
+ class Candidate
186
+ attr_reader :skills
187
+
188
+ # @param skills [Hash{Symbol => Numeric}]
189
+ # keys are skills and values are proficiency levels
190
+ def initialize(skills)
191
+ @skills = VectorNumber.new(skills)
192
+ end
193
+
194
+ # Calculate similarity between candidate skills and job requirements
195
+ # @param job_requirements [Hash{Symbol => Numeric}]
196
+ # @return [Float] A score between 0 and 1
197
+ def match_score(job_requirements)
198
+ job_requirements = VectorNumber.new(job_requirements)
199
+ @skills.cosine_similarity(job_requirements)
200
+ end
201
+ end
202
+
203
+ job = {ruby: 5, rails: 4, sql: 3, nosql: 2}
204
+ alice = Candidate.new(ruby: 5, rails: 5, sql: 2, python: 3)
205
+ bob = Candidate.new(ruby: 3, rails: 2, sql: 4, java: 4)
206
+
207
+ alice.match_score(job).round(2) # => 0.87
208
+ bob.match_score(job).round(2) # => 0.71
95
209
  ```
96
210
 
97
- > [!NOTE]
98
- > VectorNumbers don't perform "integer division" to prevent unexpected loss of precision. `#div` and rounding methods can achieve this if required.
211
+ #### 🔬 Scientific/Domain Modeling
212
+
213
+ VectorNumber can be used for scientific and domain modeling where vector operations are common.
99
214
 
100
- #### Getting values back
101
- The simplest way to get a value for a specific unit is to use the `#[]` method:
102
215
  ```ruby
103
- VectorNumber["string", "string", "string", "str"]["string"] # => 3
104
- VectorNumber["string", "string", "string", "str"]["str"] # => 1
105
- VectorNumber["string", "string", "string", "str"]["nonexistent"] # => 0
216
+ # Work done by a constant force
217
+ displacement = VectorNumber[x: 3, y: -2.5]
218
+ force = VectorNumber[x: 5, y: 1]
219
+ work = force.dot_product(displacement) # => 12.5
220
+
221
+ # Gravitational force
222
+ position_massive = VectorNumber[x: 1.5, y: -200, z: -150]
223
+ position_small = VectorNumber[x: -120, y: 13, z: 15.5]
224
+ direction = position_small - position_massive
225
+ unit_direction = direction.unit_vector
226
+ gravitational_force = -unit_direction * 10_000 * 10 * 6.674 / direction.abs2
227
+ # => (3.1317735497992065⋅:x - 5.490269679894905⋅:y - 4.265913765364352⋅:z)
106
228
  ```
107
229
 
108
- > [!NOTE]
109
- > Accessing a unit that doesn't exist returns 0, not `nil` as you might expect.
110
-
111
- ### (Somewhat) advanced usage
230
+ ### Advanced Vector Operations
112
231
 
113
- > [!TIP]
114
- > Look at API documentation for all methods.
232
+ VectorNumber supports many vector operations beside vector arithmetic. This is a sample of what's available:
115
233
 
116
- #### Frozenness
117
- VectorNumbers are always frozen, as a number should be. However, they hold references to units (keys), which aren't frozen or duplicated. It is the user's responsibility to ensure that keys aren't mutated, the same as it is for Hash.
234
+ ```ruby
235
+ v = VectorNumber[x: 3, y: 4]
236
+ w = VectorNumber[x: 1, y: 2, z: 5]
237
+
238
+ # Vector properties
239
+ v.magnitude # => 5.0
240
+ v.p_norm(1) # => 7 (Manhattan distance)
241
+ v.unit_vector # => (0.6⋅:x + 0.8⋅:y)
242
+
243
+ # Relationships
244
+ v.dot_product(w) # => 11 (=3*1 + 4*2 + 0*5)
245
+ v.angle(w) # => 1.1574640509137637 (rad)
246
+ v.vector_projection(w) # => ((11/30)⋅:x + (11/15)⋅:y + (11/6)⋅:z)
247
+ v.scalar_projection(w) # => 2.008316044185609
248
+ v.vector_rejection(w) # => ((79/30)⋅:x + (49/15)⋅:y - (11/6)⋅:z)
249
+
250
+ # Basis operations
251
+ w.subspace_basis # => [(1⋅:x), (1⋅:y), (1⋅:z)]
252
+ w.uniform_vector # => (1⋅:x + 1⋅:y + 1⋅:z)
253
+
254
+ # Collinearity
255
+ v.collinear?(w) # => false
256
+ v.parallel?(v * 3) # => true
257
+ v.opposite?(v * -1) # => true
258
+ ```
118
259
 
119
- As vectors are immutable, `+@`, `dup` and `clone` return the same instance.
260
+ ### Hash-Like Operations
120
261
 
121
- #### Numerical behavior
122
- VectorNumbers implement most of the methods you can find in `Numeric`, with appropriate behavior. For example:
123
- - `abs` (`magnitude`) calculates length of the vector;
124
- - `infinite?` checks whether any coefficient is infinite (or NaN), in the same way as `Complex` does it;
125
- - `positive?` is true if all coefficients are positive, the same for `negative?` (though this is different from `Complex`) (and they can both be false);
126
- - `round` and friends round each coefficient, with all the bells and whistles;
127
- - `div` and `%` perform division and remainder operations elementwise;
128
- - `5 < VectorNumber[6]` returns `true` and `5 < VectorNumber["string"]` raises `ArgumentError`, etc.
262
+ Most of Hash interface is implemented—though much of it comes from Enumerable—with the notable exception of self-modifying methods.
129
263
 
130
- VectorNumbers, if only consisting of a real number, can mostly be used interchangeably with core numbers due to `coerce` and conversion (`to_i`, etc.) methods. They can even be used as array indices! Due to including `Comparable`, they can also participate in comparison and sorting.
264
+ ```ruby
265
+ v = VectorNumber[a: 2, b: 3, c: 5]
266
+
267
+ # Querying
268
+ v[:a] # => 2
269
+ v[:d] # => 0
270
+ v.unit?(:b) # => true
271
+ v.unit?(:d) # => false
272
+ v.fetch(:d, 42) # => 42
273
+
274
+ # Transformation
275
+ v.transform_coefficients { |c| c * 2 } # (4⋅:a + 6⋅:b + 10⋅:c)
276
+ v.transform_units { |u| u.to_s } # (2⋅"a" + 3⋅"b" + 5⋅"c")
277
+
278
+ # Enumeration
279
+ v.each { |unit, coeff| puts "#{coeff}×#{unit}" }
280
+ v.to_h # => {a: 2, b: 3, c: 5}
281
+ ```
131
282
 
132
- #### Enumeration and Hash-like behavior
133
- VectorNumbers implement `each` (`each_pair`) in the same way as Hash does, allowing `Enumerable` methods to be used in a familiar way.
283
+ ### Custom String Conversion
134
284
 
135
- There are also the usual `[]`, `unit?` (`key?`), `units` (`keys`), `coefficients` (`values`) methods. `to_h` and `to_a` can be used if a regular Hash or Array is needed.
285
+ While the default string representation works well for console output, there are many possible scenarios and use cases, so the `to_s` method supports customization:
136
286
 
137
- ## Conceptual basis
287
+ ```ruby
288
+ v = VectorNumber[:a => 2, "x" => 5.5, [] => -3.14]
289
+ # Replacing the multiplication symbol
290
+ v.to_s(mult: :asterisk)
291
+ # => "2*:a + 5.5*\"x\" - 3.14*[]"
292
+ # Custom formatting with a block
293
+ v.to_s { |unit, coeff, i| "#{' + ' unless i.zero?}(#{coeff}#{unit})" }
294
+ # => "(2a) + (5.5x) + (-3.14[])"
295
+ # Using Enumerator for complex processing
296
+ v.to_enum(:to_s).map { |unit, coeff| "#{unit.inspect}: #{coeff}" }.join(', ')
297
+ # => ":a: 2, \"x\": 5.5, []: -3.14"
298
+ ```
138
299
 
139
- VectorNumbers are based on the concept of a vector space over the field of real numbers (real vector space). In the case of VectorNumber, the dimensionality of the vector space is countably infinite, as most distinct objects in Ruby signify a separate dimension.
300
+ ## Conceptual Basis
140
301
 
141
- For most dimensions, an object is that distinct dimension's unit. There are two exceptions currently: real unit (1) and imaginary unit (i) which define the real and imaginary dimensions and subsume all real and complex numbers. A VectorNumber can not be a unit itself. Distinction of objects is determined by `eql?`, same as for Hash.
302
+ VectorNumber is built on the mathematical concept of a **real vector space** with countably infinite dimensions:
303
+ - Every distinct Ruby object (determined by `eql?`) is a dimension
304
+ - Each dimension has a coefficient (a real number)
305
+ - The real unit `1` and imaginary unit `i` are special dimensions that subsume Ruby's numeric types
306
+ - All operations follow vector space axioms
142
307
 
143
- Length of a VectorNumber in any given dimension is given by a real number, called its coefficient. All dimensions are linearly independent — change in one coefficient does not affect any other coefficient. There is no distinction between a dimension explicitly specified as having a 0 coefficient (or arriving at 0 through calculation) and a dimension not specified at all.
308
+ Furthermore, VectorNumbers exist in a normed Euclidean inner product space:
309
+ - All dimensions are orthogonal and independent
310
+ - The norm (magnitude) of a vector is calculated using the Euclidean norm
311
+ - Inner (dot) product is defined, which allows angles between vectors to be calculated
312
+ - All unit vectors have a length of 1
144
313
 
145
- This might be more easily imagined as a geometric vector. For example, this is a graphic representation of a vector `3 * VectorNumber[1] + 2 * VectorNumber[1i] + 3 * VectorNumber["string"] + 4.5 * VectorNumber[[1,2,3]]` in the vector space:
314
+ This might be more easily imagined as a geometric vector. For example, this is a graphic representation of a vector `VectorNumber[3, 2i] + VectorNumber["string" => 3, [1,2,3] => 4.5]`:
146
315
 
147
316
  ![Vector in vector space](https://raw.githubusercontent.com/trinistr/vector_number/main/doc/vector_space.svg)
148
317
 
@@ -160,7 +329,7 @@ To release a new version, run `rake version:{major|minor|patch}`, and then run `
160
329
 
161
330
  Bug reports and pull requests are welcome on GitHub at [https://github.com/trinistr/vector_number]().
162
331
 
163
- ### Checklist for a new or updated feature
332
+ **Checklist for a new or updated feature**
164
333
 
165
334
  - Running `rake spec` reports 100% coverage (unless it's impossible to achieve in one run).
166
335
  - Running `rake rubocop` reports no offenses.
@@ -25,7 +25,7 @@ class VectorNumber
25
25
  # VectorNumber["a", 14] == 14 # => false
26
26
  # VectorNumber["a"] == "a" # => false
27
27
  #
28
- # @param other [Object]
28
+ # @param other [Any]
29
29
  # @return [Boolean]
30
30
  #
31
31
  # @since 0.2.0
@@ -34,7 +34,8 @@ class VectorNumber
34
34
 
35
35
  case other
36
36
  when VectorNumber
37
- size == other.size && @data == other.to_h
37
+ # HACK: Using instance variable access to avoid hash copying overhead.
38
+ size == other.size && @data == other.instance_variable_get(:@data)
38
39
  when Numeric
39
40
  numeric?(2) && other.real == real && other.imaginary == imaginary
40
41
  else
@@ -56,13 +57,21 @@ class VectorNumber
56
57
  # VectorNumber["a", 14].eql? 14 # => false
57
58
  # VectorNumber["a"].eql? "a" # => false
58
59
  #
59
- # @param other [Object]
60
+ # @param other [Any]
60
61
  # @return [Boolean]
61
62
  def eql?(other)
62
63
  return true if equal?(other)
63
- return false unless other.is_a?(VectorNumber)
64
+ return false unless self.class == other.class
64
65
 
65
- size.eql?(other.size) && @data.eql?(other.to_h)
66
+ # @type var other : vector_instance
67
+ size.eql?(other.size) && @data.eql?(other.instance_variable_get(:@data))
68
+ rescue NoMethodError => e
69
+ # :nocov:
70
+ raise unless e.receiver.equal?(other)
71
+ # :nocov:
72
+
73
+ # Should only happen if `other.class` is undefined.
74
+ false
66
75
  end
67
76
 
68
77
  # Generate an Integer hash value for self.
@@ -89,7 +98,7 @@ class VectorNumber
89
98
  # VectorNumber[130] <=> 12 # => 1
90
99
  # 1 <=> VectorNumber[13] # => -1
91
100
  # VectorNumber[12.1] <=> Complex(12.1, 0) # => 0
92
- # # This doesn't work as expected without NumericRefinements:
101
+ # # This doesn't work as expected:
93
102
  # Complex(12.1, 0) <=> VectorNumber[12.1] # => nil
94
103
  #
95
104
  # # Any non-real comparison returns nil:
@@ -99,9 +108,8 @@ class VectorNumber
99
108
  #
100
109
  # @see #numeric?
101
110
  # @see Comparable
102
- # @see NumericRefinements
103
111
  #
104
- # @param other [Object]
112
+ # @param other [Any]
105
113
  # @return [Integer]
106
114
  # @return [nil] if +self+ or +other+ isn't a real number.
107
115
  #
@@ -1,30 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class VectorNumber
4
- # @group Converting to different numeric classes
4
+ # @group Type conversion
5
5
 
6
- # Return real part of the number.
6
+ # Get real part of the vector.
7
7
  #
8
8
  # @example
9
9
  # VectorNumber[23, "a"].real # => 23
10
10
  # VectorNumber["a"].real # => 0
11
11
  #
12
- # @return [Integer, Float, Rational, BigDecimal]
12
+ # @return [Numeric]
13
13
  def real = @data[R]
14
14
 
15
- # Return imaginary part of the number.
15
+ # Get imaginary part of the vector.
16
16
  #
17
17
  # @example
18
18
  # VectorNumber[23, "a"].imaginary # => 0
19
19
  # VectorNumber["a", Complex(1, 2r)].imag # => (2/1)
20
20
  #
21
- # @return [Integer, Float, Rational, BigDecimal]
21
+ # @return [Numeric]
22
22
  def imaginary = @data[I]
23
23
 
24
24
  # @since 0.2.1
25
25
  alias imag imaginary
26
26
 
27
- # Return value as an Integer, truncating it, if only real part is non-zero.
27
+ # Convert vector to an Integer, if only real dimension is non-zero.
28
28
  #
29
29
  # @example
30
30
  # VectorNumber[13.5].to_i # => 13
@@ -37,7 +37,7 @@ class VectorNumber
37
37
  # Integer(VectorNumber[2, "i"]) # RangeError
38
38
  #
39
39
  # @return [Integer]
40
- # @raise [RangeError] if any non-real part is non-zero
40
+ # @raise [RangeError] if any non-real dimension is non-zero
41
41
  def to_i
42
42
  raise_convert_error(Integer) unless numeric?(1)
43
43
 
@@ -46,7 +46,7 @@ class VectorNumber
46
46
 
47
47
  alias to_int to_i
48
48
 
49
- # Return value as a Float if only real part is non-zero.
49
+ # Convert vector to a Float if only real dimension is non-zero.
50
50
  #
51
51
  # @example
52
52
  # VectorNumber[13.5].to_f # => 13.5
@@ -58,14 +58,14 @@ class VectorNumber
58
58
  # Float(VectorNumber[2, "i"]) # RangeError
59
59
  #
60
60
  # @return [Float]
61
- # @raise [RangeError] if any non-real part is non-zero
61
+ # @raise [RangeError] if any non-real dimension is non-zero
62
62
  def to_f
63
63
  raise_convert_error(Float) unless numeric?(1)
64
64
 
65
65
  real.to_f
66
66
  end
67
67
 
68
- # Return value as a Rational if only real part is non-zero.
68
+ # Convert vector to a Rational if only real dimension is non-zero.
69
69
  #
70
70
  # @example
71
71
  # VectorNumber[13.5].to_r # => (27/2)
@@ -77,14 +77,14 @@ class VectorNumber
77
77
  # Rational(VectorNumber[2, "i"]) # RangeError
78
78
  #
79
79
  # @return [Rational]
80
- # @raise [RangeError] if any non-real part is non-zero
80
+ # @raise [RangeError] if any non-real dimension is non-zero
81
81
  def to_r
82
82
  raise_convert_error(Rational) unless numeric?(1)
83
83
 
84
84
  real.to_r
85
85
  end
86
86
 
87
- # Return value as a BigDecimal if only real part is non-zero.
87
+ # Convert vector to a BigDecimal if only real dimension is non-zero.
88
88
  #
89
89
  # @example
90
90
  # VectorNumber[13.5].to_d # => 0.135e2
@@ -93,29 +93,28 @@ class VectorNumber
93
93
  # VectorNumber[2, 2i].to_d # RangeError
94
94
  # VectorNumber[2, :i].to_d # RangeError
95
95
  #
96
- # # This does't work without NumericRefinements:
96
+ # # This does't work:
97
97
  # BigDecimal(VectorNumber[2]) # TypeError
98
- # # #to_s can be used as a workaround if refinements aren't used:
98
+ # # #to_s can be used as a workaround:
99
99
  # BigDecimal(VectorNumber[2].to_s) # => 0.2e1
100
100
  # BigDecimal(VectorNumber[2, "i"].to_s) # => ArgumentError
101
101
  #
102
102
  # @param ndigits [Integer] precision
103
103
  # @return [BigDecimal]
104
- # @raise [RangeError] if any non-real part is non-zero
104
+ # @raise [RangeError] if any non-real dimension is non-zero
105
105
  # @raise [NameError] if BigDecimal is not defined
106
106
  #
107
107
  # @see Kernel.BigDecimal
108
- # @see NumericRefinements
109
108
  def to_d(ndigits = nil)
110
109
  raise_convert_error(BigDecimal) unless numeric?(1)
111
110
 
112
111
  return BigDecimal(real, ndigits) if ndigits
113
- return BigDecimal(real, Float::DIG) if real.is_a?(Float)
112
+ return BigDecimal(real, Float::DIG) if Float === real
114
113
 
115
114
  BigDecimal(real)
116
115
  end
117
116
 
118
- # Return value as a Complex if only real and/or imaginary parts are non-zero.
117
+ # Convert vector to a Complex if only real and/or imaginary dimensions are non-zero.
119
118
  #
120
119
  # @example
121
120
  # VectorNumber[13.5].to_c # => (13.5+0i)
@@ -127,13 +126,32 @@ class VectorNumber
127
126
  # Complex(VectorNumber[2, "i"]) # RangeError
128
127
  #
129
128
  # @return [Complex]
130
- # @raise [RangeError] if any non-real, non-imaginary part is non-zero
129
+ # @raise [RangeError] if any non-real, non-imaginary dimension is non-zero
131
130
  def to_c
132
131
  raise_convert_error(Complex) unless numeric?(2)
133
132
 
134
133
  Complex(real, imaginary)
135
134
  end
136
135
 
136
+ # Get mutable hash with vector's data.
137
+ #
138
+ # Returned hash has a default value of 0.
139
+ #
140
+ # @example
141
+ # VectorNumber["a", "b", 6, 1i].to_h # => {"a" => 1, "b" => 1, unit/1 => 6, unit/i => 1}
142
+ # VectorNumber["a", "b", 6, 1i].to_h["c"] # => 0
143
+ #
144
+ # @return [Hash{Any => Numeric}]
145
+ def to_h(&block)
146
+ # TODO: Remove block argument.
147
+ if block_given?
148
+ # @type var block: ^(unit_type, coefficient_type) -> each_value_type
149
+ @data.to_h(&block)
150
+ else
151
+ @data.dup
152
+ end
153
+ end
154
+
137
155
  private
138
156
 
139
157
  def raise_convert_error(klass)