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
@@ -0,0 +1,358 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe Vector2d::Comparison do
6
+ subject(:vector) { Vector2d.new(2, 3) }
7
+
8
+ describe "#approx_equal?" do
9
+ subject { vector.approx_equal?(other) }
10
+
11
+ context "when the vectors are equal" do
12
+ let(:other) { Vector2d.new(2, 3) }
13
+
14
+ it { is_expected.to be(true) }
15
+ end
16
+
17
+ context "when the vectors differ by a rounding error" do
18
+ let(:vector) { Vector2d.new(0.1, 0.2) + Vector2d.new(0.2, 0.4) }
19
+ let(:other) { Vector2d.new(0.3, 0.6) }
20
+
21
+ it { is_expected.to be(true) }
22
+
23
+ it "is more than #== accepts" do
24
+ expect(vector).not_to eq(other)
25
+ end
26
+ end
27
+
28
+ context "when the vectors differ" do
29
+ let(:other) { Vector2d.new(2, 4) }
30
+
31
+ it { is_expected.to be(false) }
32
+ end
33
+
34
+ context "when the other vector is coercible" do
35
+ let(:other) { [2, 3] }
36
+
37
+ it { is_expected.to be(true) }
38
+ end
39
+
40
+ it "is symmetric" do
41
+ other = Vector2d.new(0.1, 0.2) + Vector2d.new(0.2, 0.4)
42
+
43
+ expect(Vector2d.new(0.3, 0.6)).to be_approx_equal(other)
44
+ end
45
+
46
+ it "scales the tolerance with the magnitudes" do
47
+ big = Vector2d.new(1e8, 2e8)
48
+
49
+ expect(big).to be_approx_equal(big.rotate(2 * Math::PI))
50
+ end
51
+
52
+ it "doesn't accept the same drift at unit scale" do
53
+ expect(Vector2d.new(1, 2))
54
+ .not_to be_approx_equal(Vector2d.new(1, 2 + 5.5e-8))
55
+ end
56
+
57
+ context "with a tolerance" do
58
+ it "takes it as an absolute distance" do
59
+ expect(vector).to be_approx_equal(Vector2d.new(2, 4), 1.5)
60
+ end
61
+
62
+ it "is false outside the tolerance" do
63
+ expect(vector).not_to be_approx_equal(Vector2d.new(2, 4), 0.5)
64
+ end
65
+
66
+ it "includes the tolerance itself" do
67
+ expect(vector).to be_approx_equal(Vector2d.new(2, 4), 1.0)
68
+ end
69
+
70
+ it "raises an error on a complex tolerance" do
71
+ expect { vector.approx_equal?(vector, Complex(1, 2)) }.to(
72
+ raise_error(ArgumentError, "not a valid coordinate: (1+2i)")
73
+ )
74
+ end
75
+ end
76
+ end
77
+
78
+ describe "#parallel?" do
79
+ subject { vector.parallel?(other) }
80
+
81
+ context "when the other vector points the same way" do
82
+ let(:other) { Vector2d.new(4, 6) }
83
+
84
+ it { is_expected.to be(true) }
85
+ end
86
+
87
+ context "when the other vector points the opposite way" do
88
+ let(:other) { Vector2d.new(-4, -6) }
89
+
90
+ it { is_expected.to be(true) }
91
+ end
92
+
93
+ context "when the vectors aren't parallel" do
94
+ let(:other) { Vector2d.new(3, 2) }
95
+
96
+ it { is_expected.to be(false) }
97
+ end
98
+
99
+ context "with the zero vector" do
100
+ let(:other) { Vector2d.new(0, 0) }
101
+
102
+ it { is_expected.to be(true) }
103
+ end
104
+
105
+ context "when both vectors are zero" do
106
+ let(:vector) { Vector2d.new(0, 0) }
107
+ let(:other) { Vector2d.new(0, 0) }
108
+
109
+ it { is_expected.to be(true) }
110
+ end
111
+
112
+ it "coerces the argument" do
113
+ expect(vector.parallel?("4x6")).to be(true)
114
+ end
115
+
116
+ it "is true for any vector and a multiple of it" do
117
+ vectors = (1..50).flat_map do |x|
118
+ (1..50).map { |y| Vector2d.new(x, y) }
119
+ end
120
+
121
+ expect(vectors).to all(satisfy { |v| v.parallel?(v * 3.7) })
122
+ end
123
+
124
+ context "with large vectors" do
125
+ let(:vector) { Vector2d.new(1.3e8, 7.7e8) }
126
+
127
+ it "is true for a multiple of itself" do
128
+ expect(vector.parallel?(vector * Math::PI)).to be(true)
129
+ end
130
+
131
+ it "is false for a vector that is barely off" do
132
+ expect(vector.parallel?(Vector2d.new(1.3e8, 7.7e8 + 1))).to be(false)
133
+ end
134
+ end
135
+ end
136
+
137
+ describe "#independent?" do
138
+ subject { vector.independent?(other) }
139
+
140
+ context "when the vectors aren't parallel" do
141
+ let(:other) { Vector2d.new(3, 2) }
142
+
143
+ it { is_expected.to be(true) }
144
+ end
145
+
146
+ context "when the other vector points the same way" do
147
+ let(:other) { Vector2d.new(4, 6) }
148
+
149
+ it { is_expected.to be(false) }
150
+ end
151
+
152
+ context "with the zero vector" do
153
+ let(:other) { Vector2d.new(0, 0) }
154
+
155
+ it { is_expected.to be(false) }
156
+ end
157
+ end
158
+
159
+ describe "#opposite?" do
160
+ subject { vector.opposite?(other) }
161
+
162
+ context "when the other vector points the opposite way" do
163
+ let(:other) { Vector2d.new(-2, -3) }
164
+
165
+ it { is_expected.to be(true) }
166
+ end
167
+
168
+ context "when the other vector is a negative multiple" do
169
+ let(:other) { Vector2d.new(-4, -6) }
170
+
171
+ it { is_expected.to be(true) }
172
+ end
173
+
174
+ context "when the other vector points the same way" do
175
+ let(:other) { Vector2d.new(4, 6) }
176
+
177
+ it { is_expected.to be(false) }
178
+ end
179
+
180
+ context "when the vectors aren't parallel" do
181
+ let(:other) { Vector2d.new(3, 2) }
182
+
183
+ it { is_expected.to be(false) }
184
+ end
185
+
186
+ context "when the other vector is perpendicular" do
187
+ let(:other) { Vector2d.new(-3, 2) }
188
+
189
+ it { is_expected.to be(false) }
190
+ end
191
+
192
+ context "with the zero vector" do
193
+ let(:other) { Vector2d.new(0, 0) }
194
+
195
+ it { is_expected.to be(false) }
196
+ end
197
+
198
+ context "when the receiver is zero" do
199
+ let(:vector) { Vector2d.new(0, 0) }
200
+ let(:other) { Vector2d.new(2, 3) }
201
+
202
+ it { is_expected.to be(false) }
203
+ end
204
+
205
+ context "when both vectors are zero" do
206
+ let(:vector) { Vector2d.new(0, 0) }
207
+ let(:other) { Vector2d.new(0, 0) }
208
+
209
+ it { is_expected.to be(false) }
210
+ end
211
+
212
+ it "coerces the argument" do
213
+ expect(vector.opposite?("-2x-3")).to be(true)
214
+ end
215
+
216
+ it "ignores the magnitude of the vectors" do
217
+ expect(vector.opposite?(vector * -1e9)).to be(true)
218
+ end
219
+
220
+ it "is true for any vector and a negative multiple of it" do
221
+ vectors = (1..50).flat_map do |x|
222
+ (1..50).map { |y| Vector2d.new(x, y) }
223
+ end
224
+
225
+ expect(vectors).to all(satisfy { |v| v.opposite?(v * -3.7) })
226
+ end
227
+
228
+ it "is true for the reverse of the vector" do
229
+ expect(vector.opposite?(vector.reverse)).to be(true)
230
+ end
231
+
232
+ context "with large vectors" do
233
+ let(:vector) { Vector2d.new(1.3e8, 7.7e8) }
234
+
235
+ it "is true for a negative multiple of itself" do
236
+ expect(vector.opposite?(vector * -Math::PI)).to be(true)
237
+ end
238
+
239
+ it "is false for a vector that is barely off" do
240
+ expect(vector.opposite?(Vector2d.new(-1.3e8, -7.7e8 - 1))).to be(false)
241
+ end
242
+ end
243
+ end
244
+
245
+ describe "#perpendicular?" do
246
+ subject { vector.perpendicular?(other) }
247
+
248
+ context "when the other vector is perpendicular" do
249
+ let(:other) { Vector2d.new(-3, 2) }
250
+
251
+ it { is_expected.to be(true) }
252
+ end
253
+
254
+ context "when the other vector is perpendicular the other way" do
255
+ let(:other) { Vector2d.new(3, -2) }
256
+
257
+ it { is_expected.to be(true) }
258
+ end
259
+
260
+ context "when the vectors aren't perpendicular" do
261
+ let(:other) { Vector2d.new(3, 2) }
262
+
263
+ it { is_expected.to be(false) }
264
+ end
265
+
266
+ context "with the zero vector" do
267
+ let(:other) { Vector2d.new(0, 0) }
268
+
269
+ it { is_expected.to be(true) }
270
+ end
271
+
272
+ context "when both vectors are zero" do
273
+ let(:vector) { Vector2d.new(0, 0) }
274
+ let(:other) { Vector2d.new(0, 0) }
275
+
276
+ it { is_expected.to be(true) }
277
+ end
278
+
279
+ it "coerces the argument" do
280
+ expect(vector.perpendicular?("-3x2")).to be(true)
281
+ end
282
+
283
+ it "is true for any vector rotated a quarter turn" do
284
+ vectors = (1..50).flat_map do |x|
285
+ (1..50).map { |y| Vector2d.new(x, y) }
286
+ end
287
+
288
+ expect(vectors)
289
+ .to all(satisfy { |v| v.perpendicular?(v.rotate(Math::PI / 2)) })
290
+ end
291
+
292
+ context "with large vectors" do
293
+ let(:vector) { Vector2d.new(1.3e8, 7.7e8) }
294
+
295
+ it "is true for a quarter turn" do
296
+ expect(vector.perpendicular?(vector.rotate(Math::PI / 2)))
297
+ .to be(true)
298
+ end
299
+
300
+ it "is false for a vector that is barely off" do
301
+ expect(vector.perpendicular?(Vector2d.new(-7.7e8, 1.3e8 + 1)))
302
+ .to be(false)
303
+ end
304
+ end
305
+ end
306
+
307
+ describe "#finite?" do
308
+ subject { vector.finite? }
309
+
310
+ context "when both coordinates are finite" do
311
+ it { is_expected.to be(true) }
312
+ end
313
+
314
+ context "when x is infinite" do
315
+ let(:vector) { Vector2d.new(Float::INFINITY, 3) }
316
+
317
+ it { is_expected.to be(false) }
318
+ end
319
+
320
+ context "when y is infinite" do
321
+ let(:vector) { Vector2d.new(2, -Float::INFINITY) }
322
+
323
+ it { is_expected.to be(false) }
324
+ end
325
+
326
+ context "when a coordinate is NaN" do
327
+ let(:vector) { Vector2d.new(2, Float::NAN) }
328
+
329
+ it { is_expected.to be(false) }
330
+ end
331
+ end
332
+
333
+ describe "#nan?" do
334
+ subject { vector.nan? }
335
+
336
+ context "when neither coordinate is NaN" do
337
+ it { is_expected.to be(false) }
338
+ end
339
+
340
+ context "when x is NaN" do
341
+ let(:vector) { Vector2d.new(Float::NAN, 3) }
342
+
343
+ it { is_expected.to be(true) }
344
+ end
345
+
346
+ context "when y is NaN" do
347
+ let(:vector) { Vector2d.new(2, Float::NAN) }
348
+
349
+ it { is_expected.to be(true) }
350
+ end
351
+
352
+ context "when a coordinate is infinite" do
353
+ let(:vector) { Vector2d.new(2, Float::INFINITY) }
354
+
355
+ it { is_expected.to be(false) }
356
+ end
357
+ end
358
+ end
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe Vector2d::Componentwise do
6
+ subject(:vector) { Vector2d.new(2, 3) }
7
+
8
+ describe "#abs" do
9
+ it "returns the absolute value of each axis" do
10
+ expect(Vector2d.new(-2, 3).abs).to eq(Vector2d.new(2, 3))
11
+ end
12
+
13
+ it "leaves a positive vector alone" do
14
+ expect(vector.abs).to eq(vector)
15
+ end
16
+
17
+ it "is component-wise, not the length" do
18
+ expect(Vector2d.new(-2, -3).abs).to eq(Vector2d.new(2, 3))
19
+ end
20
+ end
21
+
22
+ describe "#ceil" do
23
+ subject(:vector) { Vector2d.new(2.3, 3.3) }
24
+
25
+ it "rounds the vector up" do
26
+ expect(vector.ceil).to eq(Vector2d.new(3, 4))
27
+ end
28
+
29
+ it "rounds up to the given precision" do
30
+ expect(Vector2d.new(2.441, 3.666).ceil(2))
31
+ .to eq(Vector2d.new(2.45, 3.67))
32
+ end
33
+
34
+ it "rounds up to a negative precision" do
35
+ expect(Vector2d.new(24.4, 36.6).ceil(-1)).to eq(Vector2d.new(30, 40))
36
+ end
37
+ end
38
+
39
+ describe "#floor" do
40
+ subject(:vector) { Vector2d.new(2.7, 3.6) }
41
+
42
+ it "rounds the vector down" do
43
+ expect(vector.floor).to eq(Vector2d.new(2, 3))
44
+ end
45
+
46
+ it "rounds down to the given precision" do
47
+ expect(Vector2d.new(2.444, 3.669).floor(2))
48
+ .to eq(Vector2d.new(2.44, 3.66))
49
+ end
50
+
51
+ it "rounds down to a negative precision" do
52
+ expect(Vector2d.new(24.4, 36.6).floor(-1)).to eq(Vector2d.new(20, 30))
53
+ end
54
+ end
55
+
56
+ describe "#round" do
57
+ let(:vector) { Vector2d.new(2.3333, 3.666) }
58
+
59
+ context "without argument" do
60
+ subject { vector.round }
61
+
62
+ it { is_expected.to eq(Vector2d.new(2, 4)) }
63
+ end
64
+
65
+ context "with precision" do
66
+ subject { vector.round(2) }
67
+
68
+ it { is_expected.to eq(Vector2d.new(2.33, 3.67)) }
69
+ end
70
+ end
71
+
72
+ describe "#truncate" do
73
+ subject(:vector) { Vector2d.new(2, 3).truncate(2.5) }
74
+
75
+ it_behaves_like "a deprecated method", "truncate", "#limit_length"
76
+
77
+ it "limits the length, as #limit_length does" do
78
+ expect(vector.length).to be_within(0.0001).of(2.5)
79
+ end
80
+ end
81
+
82
+ describe "#sign" do
83
+ it "returns the sign of each axis" do
84
+ expect(Vector2d.new(-2, 3).sign).to eq(Vector2d.new(-1, 1))
85
+ end
86
+
87
+ it "is zero for an axis that is zero" do
88
+ expect(Vector2d.new(0, -3).sign).to eq(Vector2d.new(0, -1))
89
+ end
90
+
91
+ it "returns integers for a float vector" do
92
+ expect(Vector2d.new(-2.5, 0.0).sign.to_a).to eq([-1, 0])
93
+ end
94
+
95
+ it "treats negative zero as zero" do
96
+ expect(Vector2d.new(-0.0, 1).sign).to eq(Vector2d.new(0, 1))
97
+ end
98
+
99
+ context "when a coordinate is NaN" do
100
+ it "raises an error" do
101
+ expect { Vector2d.new(Float::NAN, 3).sign }.to(
102
+ raise_error(ArgumentError, "NaN has no sign")
103
+ )
104
+ end
105
+ end
106
+ end
107
+
108
+ describe "#snap" do
109
+ subject(:vector) { Vector2d.new(23, 47) }
110
+
111
+ it "snaps each axis to the nearest multiple" do
112
+ expect(vector.snap(10)).to eq(Vector2d.new(20, 50))
113
+ end
114
+
115
+ it "takes a step per axis" do
116
+ expect(vector.snap(Vector2d.new(10, 5))).to eq(Vector2d.new(20, 45))
117
+ end
118
+
119
+ it "coerces the step" do
120
+ expect(vector.snap("10x5")).to eq(Vector2d.new(20, 45))
121
+ end
122
+
123
+ it "leaves a vector on the grid alone" do
124
+ expect(Vector2d.new(20, 50).snap(10)).to eq(Vector2d.new(20, 50))
125
+ end
126
+
127
+ it "snaps negative coordinates away from zero at the halfway point" do
128
+ expect(Vector2d.new(-15, -25).snap(10)).to eq(Vector2d.new(-20, -30))
129
+ end
130
+
131
+ it "handles a negative step" do
132
+ expect(vector.snap(-10)).to eq(Vector2d.new(20, 50))
133
+ end
134
+
135
+ context "with a fractional step" do
136
+ subject(:vector) { Vector2d.new(2.3, 3.7) }
137
+
138
+ it "snaps to the fractional grid" do
139
+ expect(vector.snap(0.5)).to eq(Vector2d.new(2.5, 3.5))
140
+ end
141
+
142
+ it "keeps the type of the step" do
143
+ expect(vector.snap(1)).to eql(Vector2d.new(2, 4))
144
+ end
145
+ end
146
+
147
+ context "when the step is zero" do
148
+ it "leaves both axes unchanged" do
149
+ expect(vector.snap(0)).to eq(vector)
150
+ end
151
+
152
+ it "leaves only the unstepped axis unchanged" do
153
+ expect(vector.snap(Vector2d.new(10, 0))).to eq(Vector2d.new(20, 47))
154
+ end
155
+ end
156
+ end
157
+
158
+ describe "#max" do
159
+ subject(:vector) { Vector2d.new(2, 8) }
160
+
161
+ it "returns the larger value of each axis" do
162
+ expect(vector.max(Vector2d.new(5, 5))).to eq(Vector2d.new(5, 8))
163
+ end
164
+
165
+ it "coerces the argument" do
166
+ expect(vector.max(5)).to eq(Vector2d.new(5, 8))
167
+ end
168
+
169
+ it "is symmetric" do
170
+ other = Vector2d.new(5, 5)
171
+ expect(other.max(vector)).to eq(vector.max(other))
172
+ end
173
+ end
174
+
175
+ describe "#min" do
176
+ subject(:vector) { Vector2d.new(2, 8) }
177
+
178
+ it "returns the smaller value of each axis" do
179
+ expect(vector.min(Vector2d.new(5, 5))).to eq(Vector2d.new(2, 5))
180
+ end
181
+
182
+ it "coerces the argument" do
183
+ expect(vector.min(5)).to eq(Vector2d.new(2, 5))
184
+ end
185
+
186
+ it "is symmetric" do
187
+ other = Vector2d.new(5, 5)
188
+ expect(other.min(vector)).to eq(vector.min(other))
189
+ end
190
+ end
191
+
192
+ describe "#clamp" do
193
+ subject(:vector) { Vector2d.new(2, 8) }
194
+
195
+ it "clamps each axis" do
196
+ expect(vector.clamp(Vector2d.new(3, 3), Vector2d.new(6, 6)))
197
+ .to eq(Vector2d.new(3, 6))
198
+ end
199
+
200
+ it "coerces the bounds" do
201
+ expect(vector.clamp(3, 6)).to eq(Vector2d.new(3, 6))
202
+ end
203
+
204
+ it "leaves a vector within the bounds alone" do
205
+ expect(vector.clamp(0, 10)).to eq(vector)
206
+ end
207
+
208
+ it "raises ArgumentError without an upper bound" do
209
+ expect { vector.clamp(3) }.to raise_error(ArgumentError)
210
+ end
211
+
212
+ context "with a range" do
213
+ it "clamps each axis" do
214
+ expect(vector.clamp(3..6)).to eq(Vector2d.new(3, 6))
215
+ end
216
+
217
+ it "coerces the bounds" do
218
+ expect(vector.clamp("3x3".."6x6")).to eq(Vector2d.new(3, 6))
219
+ end
220
+
221
+ it "leaves a vector within the bounds alone" do
222
+ expect(vector.clamp(0..10)).to eq(vector)
223
+ end
224
+
225
+ it "matches the two argument form" do
226
+ expect(vector.clamp(3..6)).to eq(vector.clamp(3, 6))
227
+ end
228
+
229
+ it "clamps only the upper bound of a beginless range" do
230
+ expect(vector.clamp(..6)).to eq(Vector2d.new(2, 6))
231
+ end
232
+
233
+ it "clamps only the lower bound of an endless range" do
234
+ expect(vector.clamp(3..)).to eq(Vector2d.new(3, 8))
235
+ end
236
+
237
+ it "leaves a vector bounded at neither end alone" do
238
+ expect(vector.clamp(nil..nil)).to eq(vector)
239
+ end
240
+
241
+ it "matches #min for a beginless range" do
242
+ expect(vector.clamp(..6)).to eq(vector.min(6))
243
+ end
244
+
245
+ it "matches #max for an endless range" do
246
+ expect(vector.clamp(3..)).to eq(vector.max(3))
247
+ end
248
+
249
+ it "raises ArgumentError for an exclusive range" do
250
+ expect { vector.clamp(3...6) }.to raise_error(ArgumentError)
251
+ end
252
+
253
+ it "raises ArgumentError when given a second argument" do
254
+ expect { vector.clamp(3..6, 6) }.to raise_error(ArgumentError)
255
+ end
256
+ end
257
+ end
258
+
259
+ describe "#with_x" do
260
+ it "replaces x" do
261
+ expect(vector.with_x(5)).to eq(Vector2d.new(5, 3))
262
+ end
263
+
264
+ it "keeps the type of the new coordinate" do
265
+ expect(vector.with_x(5.0).x).to be_a(Float)
266
+ end
267
+
268
+ it "leaves the receiver alone" do
269
+ expect { vector.with_x(5) }.not_to change(vector, :to_a)
270
+ end
271
+
272
+ it "raises an error on a string" do
273
+ expect { vector.with_x("5") }.to(
274
+ raise_error(ArgumentError, 'not a valid coordinate: "5"')
275
+ )
276
+ end
277
+
278
+ it "raises an error on nil" do
279
+ expect { vector.with_x(nil) }.to(
280
+ raise_error(ArgumentError, "not a valid coordinate: nil")
281
+ )
282
+ end
283
+ end
284
+
285
+ describe "#with_y" do
286
+ it "replaces y" do
287
+ expect(vector.with_y(5)).to eq(Vector2d.new(2, 5))
288
+ end
289
+
290
+ it "keeps the type of the new coordinate" do
291
+ expect(vector.with_y(5.0).y).to be_a(Float)
292
+ end
293
+
294
+ it "raises an error on a vector" do
295
+ expect { vector.with_y(Vector2d.new(2, 3)) }.to(
296
+ raise_error(ArgumentError, "not a valid coordinate: Vector2d(2,3)")
297
+ )
298
+ end
299
+ end
300
+ end