vector2d 2.3.0 → 3.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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +1 -1
  3. data/.gitignore +3 -1
  4. data/.release-please-manifest.json +1 -1
  5. data/.rubocop.yml +1 -1
  6. data/.yardopts +11 -0
  7. data/CHANGELOG.md +216 -0
  8. data/Gemfile +5 -1
  9. data/Gemfile.lock +35 -5
  10. data/README.md +325 -12
  11. data/Rakefile +18 -0
  12. data/benchmark/vector_comparison.rb +129 -0
  13. data/lib/vector2d/angles.rb +315 -0
  14. data/lib/vector2d/arithmetic.rb +97 -0
  15. data/lib/vector2d/comparison.rb +188 -0
  16. data/lib/vector2d/componentwise.rb +239 -0
  17. data/lib/vector2d/constructors.rb +137 -0
  18. data/lib/vector2d/conversions.rb +139 -0
  19. data/lib/vector2d/coordinates.rb +22 -0
  20. data/lib/vector2d/deprecation.rb +16 -0
  21. data/lib/vector2d/dimensions.rb +297 -0
  22. data/lib/vector2d/interpolation.rb +191 -0
  23. data/lib/vector2d/lengths.rb +284 -0
  24. data/lib/vector2d/matrix_interop.rb +93 -0
  25. data/lib/vector2d/parsing.rb +142 -0
  26. data/lib/vector2d/projection.rb +208 -0
  27. data/lib/vector2d/version.rb +2 -1
  28. data/lib/vector2d.rb +172 -60
  29. data/spec/lib/vector2d/angles_spec.rb +441 -0
  30. data/spec/lib/vector2d/{calculations_spec.rb → arithmetic_spec.rb} +48 -58
  31. data/spec/lib/vector2d/comparison_spec.rb +358 -0
  32. data/spec/lib/vector2d/componentwise_spec.rb +300 -0
  33. data/spec/lib/vector2d/constructors_spec.rb +299 -0
  34. data/spec/lib/vector2d/conversions_spec.rb +234 -0
  35. data/spec/lib/vector2d/dimensions_spec.rb +820 -0
  36. data/spec/lib/vector2d/interpolation_spec.rb +322 -0
  37. data/spec/lib/vector2d/lengths_spec.rb +457 -0
  38. data/spec/lib/vector2d/matrix_interop_spec.rb +92 -0
  39. data/spec/lib/vector2d/parsing_spec.rb +329 -0
  40. data/spec/lib/vector2d/projection_spec.rb +306 -0
  41. data/spec/lib/vector2d_documentation_spec.rb +38 -0
  42. data/spec/lib/vector2d_immutability_spec.rb +263 -0
  43. data/spec/lib/vector2d_spec.rb +90 -50
  44. data/spec/lib/vector2d_subclassing_spec.rb +210 -0
  45. data/spec/lib/vector2d_tags_spec.rb +43 -0
  46. data/spec/spec_helper.rb +2 -0
  47. data/spec/support/doc_examples/comments.rb +65 -0
  48. data/spec/support/doc_examples/markdown.rb +50 -0
  49. data/spec/support/doc_examples.rb +94 -0
  50. data/spec/support/doc_tags.rb +151 -0
  51. data/spec/support/shared_examples/class_preserving_method.rb +10 -0
  52. data/spec/support/shared_examples/deprecated_method.rb +25 -0
  53. data/spec/support/shared_examples/parsed_vector.rb +11 -0
  54. data/vector2d.gemspec +2 -1
  55. metadata +42 -13
  56. data/.travis.yml +0 -10
  57. data/lib/vector2d/calculations.rb +0 -141
  58. data/lib/vector2d/coercions.rb +0 -64
  59. data/lib/vector2d/fitting.rb +0 -60
  60. data/lib/vector2d/properties.rb +0 -46
  61. data/lib/vector2d/transformations.rb +0 -98
  62. data/spec/lib/vector2d/coercions_spec.rb +0 -59
  63. data/spec/lib/vector2d/fitting_spec.rb +0 -70
  64. data/spec/lib/vector2d/properties_spec.rb +0 -47
  65. data/spec/lib/vector2d/transformations_spec.rb +0 -139
data/README.md CHANGED
@@ -3,8 +3,20 @@
3
3
 
4
4
  # Vector2d
5
5
 
6
- Vector2d handles two-dimensional coordinates and vectors.
7
- Vectors are immutable, meaning this is a purely functional library.
6
+ Vector2d is a library for handling two-dimensional vectors and coordinates.
7
+ It's fully featured, but has a particular focus on image processing, including constrained scaling and geometry string parsing.
8
+
9
+ It is strictly immutable and safe for use with Ractors.
10
+ It has no runtime dependencies, and is about 1.6x faster than the stdlib `Vector` class.
11
+ Every method is documented with examples in the [API documentation](https://rubydoc.info/gems/vector2d).
12
+
13
+ ## Installation
14
+
15
+ Vector2d requires Ruby 3.4 or later. Add the gem to your Gemfile and run `bundle install`.
16
+
17
+ ```ruby
18
+ gem "vector2d"
19
+ ```
8
20
 
9
21
  ## Quick example
10
22
 
@@ -13,29 +25,330 @@ require 'vector2d'
13
25
 
14
26
  vector = Vector2d(50, 70)
15
27
 
16
- vector.aspect_ratio # => 0.714285714285714
17
- vector.length # => 86.0232526704263
28
+ vector.aspect_ratio # => 0.7142857142857143
29
+ vector.length # => 86.02325267042627
18
30
 
19
31
  vector * 2 # => Vector2d(100,140)
20
32
  vector + Vector2d(20, 30) # => Vector2d(70,100)
21
33
 
22
- vector.fit(Vector2d(64, 64)) # => Vector2d(64,45)
34
+ vector.fit(Vector2d(64, 64)) # => Vector2d(45.714285714285715,64.0)
23
35
 
24
36
  Vector2d.parse([50, 70]) # => Vector2d(50,70)
25
37
  Vector2d.parse("50x70") # => Vector2d(50,70)
26
38
  ```
27
39
 
28
- ## Documentation
40
+ ## Fitting
41
+
42
+ These are the three operations from web image sizing:
43
+
44
+ | Vector2d | Scales to | CSS `object-fit` | ImageMagick geometry |
45
+ | ---------------------------- | -------------------------------------------- | ---------------- | -------------------- |
46
+ | `fit(other)` | the largest size that fits inside the box | `contain` | `WxH` |
47
+ | `cover(other)` | the smallest size that fills the box | `cover` | `WxH^` |
48
+ | `fit(other, upscale: false)` | the largest size that fits, never scaling up | `scale-down` | `WxH>` |
49
+
50
+ ```ruby
51
+ image = Vector2d(1600, 1200)
52
+ small = Vector2d(120, 90)
53
+ thumbnail = Vector2d(200, 200)
54
+
55
+ image.fit(thumbnail) # => Vector2d(200.0,150.0)
56
+ image.cover(thumbnail) # => Vector2d(266.66666666666663,200.0)
57
+
58
+ small.fit(thumbnail) # => Vector2d(200.0,150.0)
59
+ small.fit(thumbnail, upscale: false) # => Vector2d(120,90)
60
+ ```
61
+
62
+ The results are floats, use `#round` and `#to_s` to get a string representation to use with other tools.
63
+
64
+ ```ruby
65
+ image.fit(thumbnail).round.to_s # => "200x150"
66
+ ```
67
+
68
+ To constrain only one axis, leave the other blank or zero:
69
+
70
+ ```ruby
71
+ image.fit(Vector2d("800x")) # => Vector2d(800.0,600.0)
72
+ image.fit(Vector2d(800, 0)) # => Vector2d(800.0,600.0)
73
+ ```
74
+
75
+ `#fits?` and `#covers?` ask the question without doing the scaling.
76
+
77
+ ```ruby
78
+ image.fits?(thumbnail) # => false
79
+ small.fits?(thumbnail) # => true
80
+ image.covers?(thumbnail) # => true
81
+ ```
82
+
83
+ A vector doubles as a rectangle, and a few properties describe one.
84
+
85
+ ```ruby
86
+ image.area # => 1920000
87
+ image.aspect_ratio # => 1.3333333333333333
88
+ image.landscape? # => true
89
+ image.portrait? # => false
90
+ image.square? # => false
91
+ ```
92
+
93
+ ## Parsing and coercion
94
+
95
+ `Vector2d.parse` is quite liberal: it accepts numbers, arrays, hashes, strings, other vectors, and the `Vector` and `Matrix` classes from the standard library.
96
+ Strings are written as `"50x70"` or `"50,70"`. The separator is case insensitive and whitespace is ignored.
97
+ Coordinates can be signed, and they retain their numeric type. It is also aliased as `Vector2d()` shorthand.
98
+
99
+ ```ruby
100
+ Vector2d.parse("50x70") # => Vector2d(50,70)
101
+ Vector2d.parse("50.0x70") # => Vector2d(50.0,70)
102
+ Vector2d.parse("-50X70") # => Vector2d(-50,70)
103
+ Vector2d.parse("50, 70") # => Vector2d(50,70)
104
+
105
+ Vector2d.parse([50, 70]) # => Vector2d(50,70)
106
+ Vector2d.parse({ x: 50, y: 70 }) # => Vector2d(50,70)
107
+ Vector2d.parse(50) # => Vector2d(50,50)
108
+ ```
109
+
110
+ Coordinates are real numbers: integers, floats, rationals and decimals.
111
+
112
+ This isn't limited to construction. Every method that takes a vector runs its argument through `.parse`, so all of the forms above work at any call site.
113
+
114
+ ```ruby
115
+ Vector2d(2, 3).distance("5x7") # => 5.0
116
+ Vector2d(2, 3).min([1, 5]) # => Vector2d(1,3)
117
+ Vector2d(23, 47).snap([10, 5]) # => Vector2d(20,45)
118
+ Vector2d(3, 4).approx_equal?([3.0, 4.0]) # => true
119
+ ```
120
+
121
+ `#==` is the exception. It compares coordinates exactly and doesn't coerce, so that vectors stay usable as hash keys.
122
+
123
+ ```ruby
124
+ Vector2d(3, 4) == [3, 4] # => false
125
+ ```
126
+
127
+ `Vector2d.new` takes exactly two coordinates and nothing else.
128
+
129
+ ## Arithmetic
130
+
131
+ Arithmetic operations have the same type semantics as Ruby numbers, so integer division truncates:
132
+
133
+ ```ruby
134
+ vector = Vector2d(50, 70)
135
+
136
+ vector / 20 # => Vector2d(2,3)
137
+ vector / 20.0 # => Vector2d(2.5,3.5)
138
+ ```
139
+
140
+ A scalar argument applies to both axes, and vectors coerce, so it can come first:
141
+
142
+ ```ruby
143
+ vector - 10 # => Vector2d(40,60)
144
+ vector * 2 # => Vector2d(100,140)
145
+ 2 * vector # => Vector2d(100,140)
146
+ vector.max(60) # => Vector2d(60,70)
147
+ vector.clamp(0, 60) # => Vector2d(50,60)
148
+ ```
149
+
150
+ A vector argument applies one axis at a time:
151
+
152
+ ```ruby
153
+ vector + Vector2d(20, 30) # => Vector2d(70,100)
154
+ vector.min(Vector2d(60, 60)) # => Vector2d(50,60)
155
+ ```
156
+
157
+ Unary `-` points a vector the other way, and `#reverse` is the same thing spelled out:
158
+
159
+ ```ruby
160
+ -vector # => Vector2d(-50,-70)
161
+ vector.reverse # => Vector2d(-50,-70)
162
+ ```
163
+
164
+ `#with_x` and `#with_y` replace a single axis:
165
+
166
+ ```ruby
167
+ vector.with_x(100) # => Vector2d(100,70)
168
+ vector.with_y(100) # => Vector2d(50,100)
169
+ ```
170
+
171
+ `#round`, `#ceil` and `#floor` work one axis at a time, the way their counterparts on Ruby's numerics do, and all three take a number of digits.
172
+
173
+ ```ruby
174
+ Vector2d(2.44, 3.66).round # => Vector2d(2,4)
175
+ Vector2d(2.44, 3.66).ceil # => Vector2d(3,4)
176
+ Vector2d(2.44, 3.66).floor # => Vector2d(2,3)
177
+ Vector2d(2.44, 3.66).round(1) # => Vector2d(2.4,3.7)
178
+ ```
179
+
180
+ `#snap` rounds each axis to the nearest multiple of a step.
181
+
182
+ ```ruby
183
+ Vector2d(23, 47).snap(10) # => Vector2d(20,50)
184
+ ```
185
+
186
+ `#abs` drops the signs and `#sign` reduces each axis to -1, 0 or 1.
187
+
188
+ ```ruby
189
+ Vector2d(-2.5, 3.5).abs # => Vector2d(2.5,3.5)
190
+ Vector2d(-2.5, 3.5).sign # => Vector2d(-1,1)
191
+ ```
192
+
193
+ ## Length and distance
194
+
195
+ `#resize` scales a vector to a given length and `#normalize` scales it to one.
196
+ `#limit_length` caps the length at a maximum and `#clamp_length` bounds it at both ends, scaling a short vector up to the minimum.
197
+ All of them keep the direction, and `#clamp_length` takes its bounds the way `#clamp` does, as two arguments or as a range.
198
+
199
+ ```ruby
200
+ vector = Vector2d(3, 4)
201
+
202
+ vector.length # => 5.0
203
+ vector.length_squared # => 25
204
+ vector.normalize # => Vector2d(0.6000000000000001,0.8)
205
+ vector.resize(10) # => Vector2d(6.0,8.0)
206
+
207
+ vector.limit_length(3) # => Vector2d(1.7999999999999998,2.4)
208
+ vector.limit_length(10) # => Vector2d(3.0,4.0)
209
+ vector.clamp_length(10, 20) # => Vector2d(6.0,8.0)
210
+ vector.clamp_length(1..3) # => Vector2d(1.7999999999999998,2.4)
211
+ ```
212
+
213
+ `#normalized?` asks whether a vector is already of length one.
214
+ Distances come in the usual flavours, and `#direction_to` is the unit vector pointing from one vector to another.
215
+
216
+ ```ruby
217
+ origin = Vector2d(2, 3)
218
+
219
+ origin.distance("5x7") # => 5.0
220
+ origin.distance_squared([5, 7]) # => 25
221
+ origin.manhattan_distance([5, 7]) # => 7
222
+ origin.chebyshev_distance([5, 7]) # => 4
223
+ origin.direction_to([5, 7]) # => Vector2d(0.6000000000000001,0.8)
224
+ ```
225
+
226
+ ## Angles and rotation
227
+
228
+ Angles are radians, measured from the positive x axis.
229
+ Positive angles turn counterclockwise, and so do `#rotate` and `#perpendicular`.
230
+ The y axis grows upwards, putting `Vector2d.up` at `(0, 1)`.
231
+
232
+ ```ruby
233
+ Vector2d.right # => Vector2d(1,0)
234
+ Vector2d.up # => Vector2d(0,1)
235
+ Vector2d.left # => Vector2d(-1,0)
236
+ Vector2d.down # => Vector2d(0,-1)
237
+
238
+ Vector2d.up.angle # => 1.5707963267948966
239
+ Vector2d(2, 3).perpendicular # => Vector2d(-3,2)
240
+ Vector2d.up.angle_to(Vector2d.right) # => -1.5707963267948966
241
+ Vector2d.up.angle_between(Vector2d.right) # => 1.5707963267948966
242
+ ```
243
+
244
+ `#rotate` turns a vector about the origin and `#rotate_around` about another point.
245
+ `#perpendicular` is a quarter turn counterclockwise, `#perpendicular_cw` the other way.
246
+ `Vector2d.from_angle` builds a vector from an angle and a length, and `#to_polar` takes one apart again.
247
+
248
+ ```ruby
249
+ Vector2d(2, 3).perpendicular_cw # => Vector2d(3,-2)
250
+ Vector2d(2, 1).rotate_around([1, 1], Math::PI / 2) # => Vector2d(1.0,2.0)
251
+
252
+ Vector2d.from_angle(0, 5) # => Vector2d(5.0,0.0)
253
+ Vector2d(3, 4).to_polar # => [5.0, 0.9272952180016122]
254
+ ```
255
+
256
+ Where degrees are easier to read, there are convenience methods for them.
257
+
258
+ ```ruby
259
+ Vector2d.from_degrees(45) # => Vector2d(0.7071067811865476,0.7071067811865475)
260
+ Vector2d.up.angle_in_degrees # => 90.0
261
+ Vector2d(2, 3).rotate_degrees(90) # => Vector2d(-3.0,2.0)
262
+
263
+ Vector2d.radians(90) # => 1.5707963267948966
264
+ Vector2d.degrees(Math::PI) # => 180.0
265
+ ```
266
+
267
+ ## Comparison
268
+
269
+ `#==` compares coordinates exactly, so use `#approx_equal?` on anything you've done arithmetic to.
270
+ Its default tolerance, the one `#parallel?` and `#perpendicular?` use, is a few ulps scaled by the magnitude of the vectors; a tolerance of your own is an absolute distance, and isn't scaled.
271
+
272
+ ```ruby
273
+ drifted = Vector2d(0.1, 0.2) * 3
274
+
275
+ drifted # => Vector2d(0.30000000000000004,0.6000000000000001)
276
+ drifted == Vector2d(0.3, 0.6) # => false
277
+ drifted.approx_equal?([0.3, 0.6]) # => true
278
+
279
+ Vector2d(1e-20, 0).zero? # => false
280
+ Vector2d(1e-20, 0).approx_zero? # => true
281
+ ```
282
+
283
+ `#eql?` is stricter still and tells coordinate types apart, and `#hash` follows it, so the two vectors below are different hash keys.
284
+
285
+ ```ruby
286
+ Vector2d(3, 4) == Vector2d(3.0, 4.0) # => true
287
+ Vector2d(3, 4).eql?(Vector2d(3.0, 4.0)) # => false
288
+ ```
289
+
290
+ The directional predicates compare directions and ignore magnitudes, and `#finite?` and `#nan?` ask about the coordinates.
291
+
292
+ ```ruby
293
+ Vector2d(2, 3).parallel?([-4, -6]) # => true
294
+ Vector2d(2, 3).opposite?([-4, -6]) # => true
295
+ Vector2d(2, 3).perpendicular?([-3, 2]) # => true
296
+ Vector2d(2, 3).independent?([3, 2]) # => true
297
+ ```
298
+
299
+ ## Pattern matching
300
+
301
+ Vectors deconstruct to an array or a hash, so they can be matched against either kind of pattern.
302
+
303
+ ```ruby
304
+ case Vector2d(3, 4)
305
+ in [0, 0] then :origin
306
+ in [Integer => x, Integer => y] then x + y
307
+ end # => 7
308
+
309
+ case Vector2d(0, 4)
310
+ in {x: 0} then :on_y_axis
311
+ in {y: 0} then :on_x_axis
312
+ end # => :on_y_axis
313
+ ```
314
+
315
+ ## Conversions
316
+
317
+ A vector converts to the plain Ruby representations, `#to_s` writes the geometry string back out, and `#to_i_vector` and `#to_f_vector` convert the coordinates while keeping the class.
318
+
319
+ ```ruby
320
+ Vector2d(2.5, 3.5).to_a # => [2.5, 3.5]
321
+ Vector2d(2.5, 3.5).to_hash # => {x: 2.5, y: 3.5}
322
+ Vector2d(2.5, 3.5).to_s # => "2.5x3.5"
323
+ Vector2d(2.5, 3.5).to_i_vector # => Vector2d(2,3)
324
+ Vector2d(2, 3).to_f_vector # => Vector2d(2.0,3.0)
325
+ ```
326
+
327
+ ## Standard library compatibility
328
+
329
+ Vectors convert to and from the `Matrix` and `Vector` classes in the standard library, and the operators accept both.
330
+
331
+ ```ruby
332
+ Vector2d(2, 3).to_vector # => Vector[2, 3]
333
+ Vector2d(2, 3).to_matrix # => Matrix[[2], [3]]
334
+ Vector2d.parse(Vector[2, 3]) # => Vector2d(2,3)
335
+ Vector2d.parse(Matrix[[2], [3]]) # => Vector2d(2,3)
336
+
337
+ Vector[1, 2] + Vector2d(3, 4) # => Vector2d(4,6)
338
+ ```
339
+
340
+ `#transform` multiplies a 2x2 matrix by the vector.
341
+
342
+ ```ruby
343
+ Vector2d(3, 4).transform(Matrix[[0, -1], [1, 0]]) # => Vector2d(-4,3)
344
+ ```
29
345
 
30
- [API documentation](https://rubydoc.info/gems/vector2d)
346
+ The `matrix` library is a bundled gem, so applications using these methods need `gem "matrix"` in their Gemfile. It's only loaded when a conversion needs it.
31
347
 
32
348
  ## Contributing
33
349
 
34
- Bug reports and pull requests are welcome on
35
- [GitHub](https://github.com/elektronaut/vector2d). See
36
- [CONTRIBUTING.md](CONTRIBUTING.md) for how to run the tests and how
37
- commits are formatted, and note that this project ships with a
38
- [code of conduct](CODE_OF_CONDUCT.md).
350
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/elektronaut/vector2d).
351
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for how to run the tests and how commits are formatted, and note that this project ships with a [code of conduct](CODE_OF_CONDUCT.md).
39
352
 
40
353
  ## License
41
354
 
data/Rakefile CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rspec/core/rake_task"
5
+ require "yard"
5
6
 
6
7
  # release-please creates the tag and the release commit.
7
8
  Rake::Task["release:source_control_push"].clear
@@ -12,3 +13,20 @@ task default: :spec
12
13
 
13
14
  desc "Run tests"
14
15
  task test: :spec
16
+
17
+ desc "Benchmark against the standard library Vector"
18
+ task :benchmark do
19
+ ruby "benchmark/vector_comparison.rb"
20
+ end
21
+
22
+ desc "Generate API documentation"
23
+ YARD::Rake::YardocTask.new(:doc) do |t|
24
+ t.stats_options = ["--list-undoc"]
25
+ end
26
+
27
+ namespace :doc do
28
+ desc "Run the doc examples and check the type tags"
29
+ RSpec::Core::RakeTask.new(:check) do |t|
30
+ t.pattern = "spec/lib/vector2d_{documentation,tags}_spec.rb"
31
+ end
32
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
4
+
5
+ require "matrix"
6
+ require "vector2d"
7
+
8
+ # Times Vector2d against the standard library Vector, one operation at
9
+ # a time, and prints a table of the results.
10
+ #
11
+ # bundle exec rake benchmark
12
+ # ITERATIONS=5000000 REPETITIONS=3 bundle exec ruby \
13
+ # benchmark/vector_comparison.rb
14
+ #
15
+ module VectorComparison
16
+ ITERATIONS = Integer(ENV.fetch("ITERATIONS", "1000000"))
17
+ REPETITIONS = Integer(ENV.fetch("REPETITIONS", "5"))
18
+
19
+ ANGLE = Math::PI / 4
20
+ SCALAR = 2.0
21
+
22
+ V1 = Vector2d(3.0, 4.0)
23
+ V2 = Vector2d(1.5, 2.5)
24
+ S1 = Vector[3.0, 4.0]
25
+ S2 = Vector[1.5, 2.5]
26
+
27
+ # One operation, written both ways. The note names the shape of the
28
+ # standard library version where Vector has no equivalent method.
29
+ Case = Struct.new(:name, :ours, :theirs, :note)
30
+
31
+ # A timed case. Times are seconds for ITERATIONS operations.
32
+ Result = Struct.new(:name, :ours, :theirs, :note) do
33
+ def ratio = theirs / ours
34
+ end
35
+
36
+ CASES = [
37
+ Case.new("new", -> { Vector2d.new(3.0, 4.0) }, -> { Vector[3.0, 4.0] }),
38
+ Case.new("+", -> { V1 + V2 }, -> { S1 + S2 }),
39
+ Case.new("-", -> { V1 - V2 }, -> { S1 - S2 }),
40
+ Case.new("* scalar", -> { V1 * SCALAR }, -> { S1 * SCALAR }),
41
+ Case.new("* vector", -> { V1 * V2 },
42
+ -> { S1.map2(S2) { |a, b| a * b } },
43
+ "Vector#map2"),
44
+ Case.new("dot", -> { V1.dot(V2) }, -> { S1.inner_product(S2) }),
45
+ Case.new("length", -> { V1.length }, -> { S1.magnitude }),
46
+ Case.new("normalize", -> { V1.normalize }, -> { S1.normalize }),
47
+ Case.new("rotate", -> { V1.rotate(ANGLE) },
48
+ lambda {
49
+ cos = Math.cos(ANGLE)
50
+ sin = Math.sin(ANGLE)
51
+ Vector[(S1[0] * cos) - (S1[1] * sin),
52
+ (S1[0] * sin) + (S1[1] * cos)]
53
+ },
54
+ "the rotation written out"),
55
+ Case.new("distance", -> { V1.distance(V2) }, -> { (S1 - S2).magnitude })
56
+ ].freeze
57
+
58
+ ROW = "%-12<name>s %11<ours>s %11<theirs>s %8<ratio>s"
59
+ RULE = "-" * 45
60
+
61
+ module_function
62
+
63
+ def run
64
+ results = CASES.map { |benchmark_case| measure_case(benchmark_case) }
65
+ puts(banner + table(results) + notes(results))
66
+ end
67
+
68
+ def measure_case(benchmark_case)
69
+ Result.new(benchmark_case.name,
70
+ best_time(benchmark_case.ours),
71
+ best_time(benchmark_case.theirs),
72
+ benchmark_case.note)
73
+ end
74
+
75
+ def best_time(callable)
76
+ (ITERATIONS / 10).times { callable.call }
77
+ Array.new(REPETITIONS) { measure(callable) }.min
78
+ end
79
+
80
+ def measure(callable)
81
+ GC.start
82
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
83
+ ITERATIONS.times { callable.call }
84
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
85
+ end
86
+
87
+ def banner
88
+ ["Vector2d #{Vector2d::VERSION} against the standard library Vector",
89
+ "Ruby #{RUBY_VERSION} (#{RUBY_PLATFORM}), " \
90
+ "#{delimited(ITERATIONS)} operations, best of #{REPETITIONS}",
91
+ ""]
92
+ end
93
+
94
+ def table(results)
95
+ [format(ROW, name: "Operation", ours: "Vector2d", theirs: "Vector",
96
+ ratio: "Ratio"),
97
+ RULE,
98
+ *results.map { |result| result_row(result) },
99
+ RULE,
100
+ format(ROW, name: "geomean", ours: "", theirs: "",
101
+ ratio: format("%.2fx", geomean(results)))]
102
+ end
103
+
104
+ def result_row(result)
105
+ format(ROW,
106
+ name: result.note ? "#{result.name} *" : result.name,
107
+ ours: format("%.4f s", result.ours),
108
+ theirs: format("%.4f s", result.theirs),
109
+ ratio: format("%.2fx", result.ratio))
110
+ end
111
+
112
+ def notes(results)
113
+ ["",
114
+ "Ratio is Vector time over Vector2d time. Above 1.00x is faster.",
115
+ *results.select(&:note).map do |result|
116
+ "* #{result.name}: no equivalent in Vector, timed as #{result.note}."
117
+ end]
118
+ end
119
+
120
+ def geomean(results)
121
+ Math.exp(results.sum { |result| Math.log(result.ratio) } / results.length)
122
+ end
123
+
124
+ def delimited(number)
125
+ number.to_s.reverse.scan(/\d{1,3}/).join(",").reverse
126
+ end
127
+ end
128
+
129
+ VectorComparison.run if $PROGRAM_NAME == __FILE__