quaternion_c2 0.1.1 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 11a23c8bee71f2eac9cdd28569ddbd060e1c78d6
4
- data.tar.gz: 2da47444cd5b3c7f50da7f9cb0dfc281dcd187a1
2
+ SHA256:
3
+ metadata.gz: 2d786fc0057f8807e1a12a9c55c19e124fa7edd24eccf3d686c1a927685c01bc
4
+ data.tar.gz: a98e2aaed384903bff5a13b844b5097b88f360a1ed8a182b2a1a774b45e46d8e
5
5
  SHA512:
6
- metadata.gz: 3504d452e7841683a890fb01c8ca3a82ce44c9c334aa3309b50329dd17415a99d021c6d2b56cf0358a5c04d9d09b1aaf44508d030cc75072a32626d4c1a8d1eb
7
- data.tar.gz: 6348e89acb1f9a31923c03d9ea1fac21d10d2a849d55c198fd757021ad35243dce10c411502f4ea4b6e622d331e4e66dfd4b014e46c2774d290320a77507265b
6
+ metadata.gz: 45c7c781395f45cb8a0adc3acb2e2a6c0c6f7d58e03a14da74490080048146aee760a5835eb524f1c02a4dce77d3bfbc2f008aa88b89754394d57900c34eaca8
7
+ data.tar.gz: 3afc20e3e7b5fe2874343ade34e6550f862a1f21cb097ff3b403465f63b4364d1dc050748219fdac26b0328310eb995dd1fbf38598cd4d76cc12e5fb346ce92f
@@ -0,0 +1,29 @@
1
+ name: Test
2
+
3
+ on:
4
+ push: {}
5
+ pull_request: {}
6
+ schedule:
7
+ - cron: '45 23 1 * *' # Monthly check
8
+
9
+ jobs:
10
+ unit-test:
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ ruby: [ '2.1', '2.2', '2.3', '2.4', '2.5', '2.6', '2.7',
15
+ '3.0', '3.1', '3.2',
16
+ head ]
17
+ runs-on: ubuntu-latest
18
+ name: Unit test with Ruby ${{ matrix.ruby }}
19
+ steps:
20
+ - name: Checkout repository
21
+ uses: actions/checkout@v3
22
+ - name: Setup Ruby
23
+ uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: ${{ matrix.ruby }}
26
+ bundler-cache: true
27
+ - name: Run RSpec
28
+ run: |
29
+ bundle exec rake spec
data/README.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Quaternion
2
2
 
3
+ This gem provides a `Quaternion` class highly compatible with other numeric classes.
4
+
5
+ The [quaternions](https://en.wikipedia.org/wiki/Quaternion) are an extension of complex numbers.
6
+ They have an important application to calculations of spatial rotations, such as in 3DCG.
7
+
8
+ A quaternion can be represented in several forms:
9
+
10
+ * Four real numbers: `$w + xi + yj + zk$`
11
+ * Two complex numbers: `$a + bj$`
12
+ * Scalar and 3-D vector: `$s + \vec{v}$`
13
+ * Polar form: `$r \exp{(\vec{n} \theta)}$`
14
+
15
+ where i, j, and k are imaginary units which satisfy $i^2 = j^2 = k^2 = ijk = -1$.
16
+ A scalar is a real quaternion and a vector is a pure imaginary quaternion.
17
+
18
+ The multiplication of quaternions is noncommutative unlike complex numbers.
19
+
3
20
  ## Installation
4
21
 
5
22
  Add this line to your application's Gemfile:
@@ -45,6 +62,32 @@ Complex::I * Quaternion::J #=> (0+0i+0j+1k)
45
62
  Quaternion(1, 1, 1, 1) ** 6 #=> (64+0i+0j+0k)
46
63
  ```
47
64
 
65
+ ### Spatial rotations
66
+
67
+ Unit quaternions are often used to calculate 3-D rotations around arbitrary axes.
68
+ They have a compact form, avoid gimbal lock, and easily interpolate between themselves (see [Slerp](https://en.wikipedia.org/wiki/Slerp)).
69
+
70
+ ```ruby
71
+ # theta = 120 * (Math::PI / 180)
72
+ # axis = Vector[1, 1, 1].normalize
73
+ # q = Quaternion.polar(1, theta / 2, axis)
74
+ q = Quaternion(0.5, 0.5, 0.5, 0.5)
75
+ r = Quaternion('2i+3j+4k')
76
+
77
+ r_new = q * r / q #=> (0.0+4.0i+2.0j+3.0k)
78
+ r_new = q * r * q.conj #=> (0.0+4.0i+2.0j+3.0k) # q.abs must be 1
79
+ ```
80
+
81
+ More generally, a pair of unit quaternions represents a [4-D rotation](https://en.wikipedia.org/wiki/Rotations_in_4-dimensional_Euclidean_space).
82
+
83
+ ```ruby
84
+ ql = Quaternion(0.5, 0.5, -0.5, 0.5)
85
+ qr = Quaternion(0.5, -0.5, 0.5, 0.5)
86
+ r = Quaternion('1+2i+3j+4k')
87
+
88
+ r_new = ql * r * qr #=> (-4.0-3.0i-2.0j+1.0k)
89
+ ```
90
+
48
91
  ## Development
49
92
 
50
93
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -2,12 +2,18 @@ require_relative 'base'
2
2
  require_relative 'classification'
3
3
  require_relative 'unary'
4
4
 
5
- #
6
- # Basic arithmetic operators (#+, #-, #*, #/)
7
- # and related methods
8
- #
9
-
10
5
  class Quaternion
6
+ ##
7
+ # Basic arithmetic operators (#+, #-, #*, #/)
8
+ # and related methods
9
+ #
10
+
11
+ ##
12
+ # Performs addition.
13
+ #
14
+ # @param other [Numeric]
15
+ # @return [Quaternion]
16
+ #
11
17
  def +(other)
12
18
  if other.kind_of?(Quaternion)
13
19
  __new__(@a + other.a, @b + other.b)
@@ -19,6 +25,12 @@ class Quaternion
19
25
  end
20
26
  end
21
27
 
28
+ ##
29
+ # Performs subtraction.
30
+ #
31
+ # @param other [Numeric]
32
+ # @return [Quaternion]
33
+ #
22
34
  def -(other)
23
35
  if other.kind_of?(Quaternion)
24
36
  __new__(@a - other.a, @b - other.b)
@@ -30,6 +42,12 @@ class Quaternion
30
42
  end
31
43
  end
32
44
 
45
+ ##
46
+ # Performs multiplication.
47
+ #
48
+ # @param other [Numeric]
49
+ # @return [Quaternion]
50
+ #
33
51
  def *(other)
34
52
  if other.kind_of?(Quaternion)
35
53
  _a = other.a
@@ -43,10 +61,30 @@ class Quaternion
43
61
  end
44
62
  end
45
63
 
64
+ ##
65
+ # @!method quo(other)
66
+ #
67
+ # Performs division.
68
+ #
69
+ # @param other [Numeric]
70
+ # @return [Quaternion]
71
+ # @raise [ZeroDivisionError] if +other+ is exactly zero.
72
+ #
73
+
74
+ ##
75
+ # @!method fdiv(other)
76
+ #
77
+ # Performs division as each part is a float, never returns a float.
78
+ #
79
+ # @param other [Numeric]
80
+ # @return [Quaternion]
81
+ #
82
+
46
83
  [:quo, :fdiv].each do |sym|
47
84
  define_method(sym) do |other|
48
85
  if other.kind_of?(Quaternion)
49
- self * other.conj.send(sym, other.abs2)
86
+ n = self * other.conj.send(sym, other.abs2)
87
+ __new__(n.a / 1, n.b / 1) # Canonicalize rationals as Complex does
50
88
  elsif other.kind_of?(Numeric) && other.complex?
51
89
  __new__(@a.send(sym, other), @b.send(sym, other.conj))
52
90
  else
@@ -58,10 +96,17 @@ class Quaternion
58
96
  alias / quo
59
97
  undef div, %, modulo, remainder, divmod
60
98
 
61
- #
99
+ ##
62
100
  # Conversion
63
101
  #
64
102
 
103
+ ##
104
+ # Performs type conversion.
105
+ #
106
+ # @param other [Numeric]
107
+ # @return [[Quaternion, self]]
108
+ # @raise [TypeError]
109
+ #
65
110
  def coerce(other)
66
111
  if other.kind_of?(Quaternion)
67
112
  [other, self]
@@ -9,12 +9,23 @@ class Quaternion
9
9
  defined_methods = public_instance_methods
10
10
 
11
11
  if defined_methods.include?(:finite?)
12
+ ##
13
+ # Returns true if its magnitude is finite, oterwise returns false.
14
+ #
15
+ # @return [Boolean]
16
+ #
12
17
  def finite?
13
18
  abs.finite?
14
19
  end
15
20
  end
16
21
 
17
22
  if defined_methods.include?(:infinite?)
23
+ ##
24
+ # Returns values corresponding to the value of its magnitude.
25
+ #
26
+ # @return [nil] if finite.
27
+ # @return [+1] if positive infinity.
28
+ #
18
29
  def infinite?
19
30
  abs.infinite?
20
31
  end
@@ -1,7 +1,23 @@
1
+ ##
2
+ # This class +Quaternion+ is an analogue of +Complex+.
3
+ # * A subclass of +Numeric+
4
+ # * Immutable instances
5
+ # * Common / extended methods
6
+ # Please +require 'quaternion_c2'+ to load all functions.
7
+ #
8
+ # +Quaternion+ has many constructors to accept
9
+ # the various representations of a quaternion.
10
+ # It is recommended to use +Kernel.#Quaternion+ and +Quaternion.polar+.
11
+ #
12
+ # @see https://ruby-doc.org/core/Complex.html
13
+ #
1
14
  class Quaternion < Numeric
2
15
  attr_reader :a, :b
3
16
  protected :a, :b
4
17
 
18
+ ##
19
+ # @!visibility private
20
+ #
5
21
  def initialize(a, b)
6
22
  @a = a.to_c
7
23
  @b = b.to_c
@@ -1,31 +1,43 @@
1
1
  require_relative 'base'
2
2
 
3
- #
4
- # Number system classification
5
- # (N <) Z < Q < R < C < H (< ...)
6
- #
7
- # * quaternion? : [H] Quaternion (*undefined*)
8
- # * && complex? : [C] Complex
9
- # * && real? : [R] Float, BigDecimal, Numeric
10
- # * && rational? : [Q] Rational (*undefined*)
11
- # * && integer? : [Z] Fixnum, Bignum, Integer
12
- #
13
-
14
3
  class Numeric
4
+ ##
5
+ # Number system classification
6
+ # (N <) Z < Q < R < C < H (< ...)
7
+ #
8
+ # * quaternion? : [H] Quaternion (*undefined*)
9
+ # * && complex? : [C] Complex
10
+ # * && real? : [R] Float, BigDecimal, Numeric
11
+ # * && rational? : [Q] Rational (*undefined*)
12
+ # * && integer? : [Z] Fixnum, Bignum, Integer
13
+ #
14
+
15
15
  # defined:
16
16
  # * integer? #=> false
17
17
  # * real? #=> true
18
18
 
19
+ ##
20
+ # Returns true.
21
+ #
19
22
  def complex?
20
23
  true
21
24
  end
22
25
  end
23
26
 
24
27
  class Quaternion
28
+ # defined by Numeric:
29
+ # * integer? #=> false
30
+
31
+ ##
32
+ # Returns false.
33
+ #
25
34
  def real?
26
35
  false
27
36
  end
28
37
 
38
+ ##
39
+ # Returns false.
40
+ #
29
41
  def complex?
30
42
  false
31
43
  end
@@ -0,0 +1,55 @@
1
+ require_relative 'base'
2
+ require_relative 'arithmetic' # define Quaternion#coerce
3
+
4
+ if Complex.public_method_defined?(:<=>)
5
+ class Quaternion
6
+ ##
7
+ # Compare values if both are real numbers.
8
+ #
9
+ # @param other [Object]
10
+ # @return [-1, 0, 1, or nil]
11
+ #
12
+ def <=>(other)
13
+ if other.kind_of?(Quaternion)
14
+ (@b == 0 && other.b == 0) ? (@a <=> other.a) : nil
15
+ elsif other.kind_of?(Numeric) && other.complex?
16
+ (@b == 0) ? (@a <=> other) : nil
17
+ elsif other.respond_to?(:coerce)
18
+ n1, n2 = other.coerce(self)
19
+ n1 <=> n2
20
+ else
21
+ nil
22
+ end
23
+ end
24
+ end
25
+
26
+ # Check whether built-in Complex#<=> uses type coercions.
27
+ if (Complex.rect(0, 0) <=> Quaternion.send(:new, 0, 0)).nil?
28
+ warn "Redefine Complex#<=> to allow type coercions."
29
+
30
+ class Complex
31
+ ##
32
+ # Compare values if both are real numbers.
33
+ #
34
+ # @param other [Object]
35
+ # @return [-1, 0, 1, or nil]
36
+ #
37
+ def <=>(other)
38
+ if other.kind_of?(Complex)
39
+ (imag == 0 && other.imag == 0) ? (real <=> other.real) : nil
40
+ elsif other.kind_of?(Numeric) && other.real?
41
+ (imag == 0) ? (real <=> other) : nil
42
+ elsif other.respond_to?(:coerce)
43
+ n1, n2 = other.coerce(self)
44
+ n1 <=> n2
45
+ else
46
+ nil
47
+ end
48
+ end
49
+ end
50
+ end
51
+ else
52
+ class Quaternion
53
+ undef <=>
54
+ end
55
+ end
@@ -8,11 +8,22 @@ require_relative 'to_type'
8
8
  require 'matrix'
9
9
 
10
10
  class Quaternion
11
- #
11
+ ##
12
12
  # Constructors
13
13
  #
14
14
 
15
15
  class << self
16
+ ##
17
+ # Returns a quaternion object a+bj, where both a and b are complex (or real).
18
+ #
19
+ # @param a [Complex]
20
+ # @param b [Complex]
21
+ # @return [Quaternion]
22
+ # @raise [TypeError]
23
+ #
24
+ # @example
25
+ # Quaternion.rect(1, Complex::I) #=> (1+0i+0j+1k)
26
+ #
16
27
  def rect(a, b = 0)
17
28
  unless [a, b].all? { |c| c.kind_of?(Numeric) && c.complex? }
18
29
  raise TypeError, 'not a complex'
@@ -21,6 +32,19 @@ class Quaternion
21
32
  end
22
33
  alias rectangular rect
23
34
 
35
+ ##
36
+ # Returns a quaternion object w+xi+yj+zk, where all of w, x, y, and z are real.
37
+ #
38
+ # @param w [Real]
39
+ # @param x [Real]
40
+ # @param y [Real]
41
+ # @param z [Real]
42
+ # @return [Quaternion]
43
+ # @raise [TypeError]
44
+ #
45
+ # @example
46
+ # Quaternion.hrect(1, 2, 3, 4) #=> (1+2i+3j+4k)
47
+ #
24
48
  def hrect(w, x = 0, y = 0, z = 0)
25
49
  a = Complex.rect(w, x)
26
50
  b = Complex.rect(y, z)
@@ -28,8 +52,26 @@ class Quaternion
28
52
  end
29
53
  alias hyperrectangular hrect
30
54
 
31
- # Quaternion.polar(r) == r
32
- # Quaternion.polar(r, theta) == Complex.polar(r, theta)
55
+ ##
56
+ # Returns a quaternion object which denotes the given polar form.
57
+ # The actual angle is recognized as +theta * vector.norm+.
58
+ #
59
+ # @param r [Real] absolute value
60
+ # @param theta [Real] angle in radians
61
+ # @param vector [Enumerable] 3-D vector
62
+ # @return [Quaternion]
63
+ # @raise [TypeError]
64
+ #
65
+ # @example
66
+ # Quaternion.polar(1, Math::PI/3, Vector[1,1,1].normalize)
67
+ # #=> (0.5000000000000001+0.5i+0.5j+0.5k)
68
+ # Quaternion.polar(1, 1, Math::PI/3 * Vector[1,1,1].normalize)
69
+ # #=> (0.4999999999999999+0.5i+0.5j+0.5k)
70
+ #
71
+ # r, theta = -3, -1
72
+ # Quaternion.polar(r) == r #=> true
73
+ # Quaternion.polar(r, theta) == Complex.polar(r, theta) #=> true
74
+ #
33
75
  def polar(r, theta = 0, vector = Vector[1, 0, 0])
34
76
  unless vector.kind_of?(Enumerable) && vector.size == 3
35
77
  raise TypeError, 'not a 3-D vector'
@@ -49,25 +91,57 @@ class Quaternion
49
91
  end
50
92
  end
51
93
 
52
- #
94
+ ##
53
95
  # Accessors
54
96
  #
55
97
 
98
+ ##
99
+ # Returns an array of two complex numbers.
100
+ #
101
+ # @return [[Complex, Complex]]
102
+ #
103
+ # @example
104
+ # Quaternion('1+2i+3j+4k').rect #=> [(1+2i), (3+4i)]
105
+ #
56
106
  def rect
57
107
  [@a, @b]
58
108
  end
59
109
  alias rectangular rect
60
110
 
111
+ ##
112
+ # Returns an array of four real numbers.
113
+ #
114
+ # @return [[Real, Real, Real, Real]]
115
+ #
116
+ # @example
117
+ # Quaternion('1+2i+3j+4k').hrect #=> [1, 2, 3, 4]
118
+ #
61
119
  def hrect
62
120
  rect.flat_map(&:rect)
63
121
  end
64
122
  alias hyperrectangular hrect
65
123
 
124
+ ##
125
+ # Returns the real part.
126
+ #
127
+ # @return [Real]
128
+ #
129
+ # @example
130
+ # Quaternion('1+2i+3j+4k').real #=> 1
131
+ #
66
132
  def real
67
133
  @a.real
68
134
  end
69
135
  alias scalar real
70
136
 
137
+ ##
138
+ # Returns the imaginary part as a 3-D vector.
139
+ #
140
+ # @return [Vector] 3-D vector
141
+ #
142
+ # @example
143
+ # Quaternion('1+2i+3j+4k').imag #=> Vector[2, 3, 4]
144
+ #
71
145
  def imag
72
146
  Vector[*hrect.drop(1)]
73
147
  end
@@ -78,6 +152,14 @@ class Quaternion
78
152
  # * abs
79
153
  # * magnitude
80
154
 
155
+ ##
156
+ # Returns the angle part of its polar form.
157
+ #
158
+ # @return [Real]
159
+ #
160
+ # @example
161
+ # Quaternion('1+i+j+k').arg #=> Math::PI/3
162
+ #
81
163
  def arg
82
164
  r_cos = real
83
165
  r_sin = imag.norm
@@ -86,6 +168,14 @@ class Quaternion
86
168
  alias angle arg
87
169
  alias phase arg
88
170
 
171
+ ##
172
+ # Returns the axis part of its polar form.
173
+ #
174
+ # @return [Vector] normalized 3-D vector
175
+ #
176
+ # @example
177
+ # Quaternion('1+i+j+k').axis #=> Vector[1,1,1].normalize
178
+ #
89
179
  def axis
90
180
  v = imag
91
181
  norm = v.norm
@@ -100,24 +190,55 @@ class Quaternion
100
190
  end
101
191
  end
102
192
 
193
+ ##
194
+ # Returns an array; +[q.abs, q.arg, q.axis]+.
195
+ #
196
+ # @return [[Real, Real, Vector]]
197
+ #
198
+ # @example
199
+ # Quaternion('1+i+j+k').polar
200
+ # #=> [2.0, Math::PI/3, Vector[1,1,1].normalize]
201
+ #
103
202
  def polar
104
203
  [abs, arg, axis]
105
204
  end
106
205
  end
107
206
 
108
- #
109
- # Generic constructor
110
- #
111
- # This accepts various arguments:
112
- # * (Numeric) -> real quaternion
113
- # * (Numeric, Numeric) -> a+bj
114
- # * (Numeric, Vector) -> scalar and 3-D vector
115
- # * (Numeric, Numeric, Numeric[, Numeric]) -> w+xi+yj+zk
116
- # and String instead of Numeric.
117
- #
118
207
  module Kernel
119
208
  module_function
120
209
 
210
+ ##
211
+ # Returns a quaternion.
212
+ #
213
+ # This function accepts various arguments except polar form.
214
+ # Strings are parsed to +Numeric+.
215
+ #
216
+ # @overload Quaternion(w, x, y, z)
217
+ # @param w [Numeric]
218
+ # @param x [Numeric]
219
+ # @param y [Numeric]
220
+ # @param z [Numeric]
221
+ # @return [Quaternion] w+xi+yj+zk
222
+ # @overload Quaternion(a, b)
223
+ # @param a [Numeric]
224
+ # @param b [Numeric]
225
+ # @return [Quaternion] a+bj
226
+ # @overload Quaternion(s, v)
227
+ # @param s [Numeric] scalar part
228
+ # @param v [Enumerable] vector part
229
+ # @return [Quaternion] $s+\\vec!{v}$
230
+ # @overload Quaternion(str)
231
+ # @param str [String]
232
+ # @return [Quaternion] +str.to_q+
233
+ # @raise [ArgumentError] if its format is inexact.
234
+ #
235
+ # @example
236
+ # Quaternion(1) #=> (1+0i+0j+0k)
237
+ # Quaternion(1, 2) #=> (1+0i+2j+0k) # not (1+2i+0j+0k)
238
+ # Quaternion(1, 2, 3, 4) #=> (1+2i+3j+4k)
239
+ # Quaternion(1, [2, 3, 4]) #=> (1+2i+3j+4k)
240
+ # Quaternion('1+2i+3j+4k') #=> (1+2i+3j+4k)
241
+ #
121
242
  def Quaternion(*args)
122
243
  argc = args.size
123
244
 
@@ -141,26 +262,45 @@ module Kernel
141
262
  end
142
263
 
143
264
  case argc
265
+ when 1
266
+ arg = args[0]
267
+ if arg.kind_of?(Numeric)
268
+ # a quaternion (or an octonion, etc.)
269
+ return arg.complex? ? arg.to_q : arg
270
+ else
271
+ raise TypeError,
272
+ "can't convert #{arg.class} into Quaternion"
273
+ end
144
274
  when 2
145
275
  if args[1].kind_of?(Enumerable)
146
- # scalar and 3-D vector
276
+ # scalar and 3-D vector -> expand
147
277
  if args[1].size != 3
148
278
  raise TypeError, "not a 3-D vector"
149
279
  end
150
- args = [args[0], *args[1]]
151
- else
280
+ args.flatten!(1)
281
+ elsif args.all? { |x| x.kind_of?(Numeric) && x.complex? }
152
282
  # a pair of complex numbers
283
+ return Quaternion.send(:new, *args)
284
+ else
153
285
  return args[0] + args[1] * Quaternion::J
154
286
  end
155
287
  end
156
288
 
157
- # maximum four real numbers
158
- i = Quaternion::I
159
- j = Quaternion::J
160
- k = Quaternion::K
161
- zero = Quaternion.send(:new, 0, 0)
162
- args.zip([1, i, j, k]).inject(zero) do |sum,(num,base)|
163
- sum + num * base
289
+ w, x, y, z = args
290
+ z ||= 0
291
+ if args.all? { |x| x.kind_of?(Numeric) && x.real? }
292
+ # 3 or 4 real numbers
293
+ a = Complex.rect(w, x)
294
+ b = Complex.rect(y, z)
295
+ Quaternion.send(:new, a, b)
296
+ else
297
+ a = Complex(w, x)
298
+ b = Complex(y, z)
299
+ if [a, b].all? { |x| x.kind_of?(Numeric) && x.complex? }
300
+ Quaternion.send(:new, a, b)
301
+ else
302
+ a + b * Quaternion::J
303
+ end
164
304
  end
165
305
  end
166
306
  end
@@ -2,9 +2,14 @@ require_relative 'base'
2
2
  require_relative 'classification'
3
3
 
4
4
  class Quaternion
5
- undef <=>
6
5
  undef_method(*Comparable.instance_methods)
7
6
 
7
+ ##
8
+ # Returns true if it equals to the other numerically.
9
+ #
10
+ # @param other [Object]
11
+ # @return [Boolean]
12
+ #
8
13
  def ==(other)
9
14
  if other.kind_of?(Quaternion)
10
15
  @a == other.a && @b == other.b
@@ -15,6 +20,12 @@ class Quaternion
15
20
  end
16
21
  end
17
22
 
23
+ ##
24
+ # Returns true if two quaternions have same reals.
25
+ #
26
+ # @param other [Object]
27
+ # @return [Boolean]
28
+ #
18
29
  def eql?(other)
19
30
  if other.kind_of?(Quaternion)
20
31
  @a.eql?(other.a) && @b.eql?(other.b)
@@ -23,8 +34,13 @@ class Quaternion
23
34
  end
24
35
  end
25
36
 
26
- # q1.eql?(q2) => q1.hash == q2.hash
37
+ ##
38
+ # Returns a hash.
39
+ #
40
+ # @return [Integer]
41
+ #
27
42
  def hash
43
+ # q1.eql?(q2) -> q1.hash == q2.hash
28
44
  [@a, @b].hash
29
45
  end
30
46
  end
@@ -1,18 +1,29 @@
1
1
  require_relative 'base'
2
2
  require_relative 'unary'
3
3
 
4
- #
5
- # to_xxx
6
- #
7
-
8
4
  class Quaternion
5
+ ##
6
+ # to_xxx
7
+ #
8
+
9
9
  # defined by Numeric:
10
10
  # * to_int #=> to_i
11
11
 
12
+ ##
13
+ # Returns self.
14
+ #
15
+ # @return [self]
16
+ #
12
17
  def to_q
13
18
  self
14
19
  end
15
20
 
21
+ ##
22
+ # Returns the value as a complex if possible (the discarded part should be exactly zero).
23
+ #
24
+ # @return [Complex]
25
+ # @raise [RangeError] if its yj+zk part is not exactly zero.
26
+ #
16
27
  def to_c
17
28
  unless __exact_zero__(@b)
18
29
  raise RangeError, "can't convert #{self} into Complex"
@@ -20,14 +31,71 @@ class Quaternion
20
31
  @a
21
32
  end
22
33
 
34
+ ##
35
+ # @!method to_f
36
+ #
37
+ # Returns the value as a float if possible (the imaginary part should be exactly zero).
38
+ #
39
+ # @return [Float]
40
+ # @raise [RangeError] if its imaginary part is not exactly zero.
41
+ #
42
+
43
+ ##
44
+ # @!method to_r
45
+ #
46
+ # Returns the value as a rational if possible (the imaginary part should be exactly zero).
47
+ #
48
+ # @return [Rational]
49
+ # @raise [RangeError] if its imaginary part is not exactly zero.
50
+ #
51
+
52
+ ##
53
+ # @!method to_i
54
+ #
55
+ # Returns the value as an integer if possible (the imaginary part should be exactly zero).
56
+ #
57
+ # @return [Integer]
58
+ # @raise [RangeError] if its imaginary part is not exactly zero.
59
+ #
60
+
61
+ ##
62
+ # @!method rationalize(eps = 0)
63
+ #
64
+ # Returns the value as a rational if possible (the imaginary part should be exactly zero).
65
+ #
66
+ # @param eps [Real]
67
+ # @return [Rational]
68
+ # @raise [RangeError] if its imaginary part is not exactly zero.
69
+ #
70
+
23
71
  [:to_f, :to_r, :to_i, :rationalize].each do |sym|
24
72
  define_method(sym) { |*args| to_c.send(sym, *args) }
25
73
  end
26
74
 
75
+ ##
76
+ # Returns the value as a string.
77
+ #
78
+ # @return [String]
79
+ #
80
+ # @example
81
+ # str = '1-2i-3/4j+0.56k'
82
+ # q = str.to_q #=> (1-2i-(3/4)*j+0.56k)
83
+ # q.to_s #=> "1-2i-3/4j+0.56k"
84
+ #
27
85
  def to_s
28
86
  __format__(:to_s)
29
87
  end
30
88
 
89
+ ##
90
+ # Returns the value as a string for inspection.
91
+ #
92
+ # @return [String]
93
+ #
94
+ # @example
95
+ # str = '1-2i-3/4j+0.56k'
96
+ # q = str.to_q #=> (1-2i-(3/4)*j+0.56k)
97
+ # q.inspect #=> "(1-2i-(3/4)*j+0.56k)"
98
+ #
31
99
  def inspect
32
100
  "(#{__format__(:inspect)})"
33
101
  end
@@ -53,8 +121,8 @@ class Quaternion
53
121
  end
54
122
 
55
123
  class << self
56
- def parse(str, strict = false)
57
- regexp = %r{
124
+ RE, RE_STRICT = [false, true].collect do |strict|
125
+ %r{
58
126
  \A
59
127
  \s*+
60
128
  (?:(?'real' [+-]?+ \g'rational' ) (?![ijk]))?+
@@ -62,19 +130,30 @@ class Quaternion
62
130
  (?:(?'imag_j' \g'head_or_sign' \g'rational'?+) j )?+
63
131
  (?:(?'imag_k' \g'head_or_sign' \g'rational'?+) k )?+
64
132
  \s*+
65
- |\z
66
- (?'head_or_sign' (?<=^|\s) [+-]?+ | [+-])
133
+ |(?!)
134
+ (?'head_or_sign' (?<!\S) [+-]?+ | [+-])
67
135
  (?'digits' \d++ (?:#{strict ? "_" : "_++"} \d++)*+)
68
- (?'int_or_float' (?:\g'digits')?+ (?:\. \g'digits')?+ (?<=\d) (?:e [+-]?+ \g'digits')?+)
136
+ (?'int_or_float' (?:\g'digits')?+ (?:\. \g'digits')?+
137
+ (?<=\d) (?:e [+-]?+ \g'digits')?+)
69
138
  (?'rational' \g'int_or_float' (?:/ \g'digits')?+)
70
139
  }xi
140
+ end
141
+ private_constant :RE, :RE_STRICT
142
+
143
+ private
71
144
 
72
- match_data = regexp.match(str)
73
- if strict && (!$'.empty? || match_data.captures[0,4].all?(&:nil?))
74
- raise ArgumentError, "invalid value for convert(): #{str.inspect}"
145
+ def parse(str, strict = false)
146
+ regexp = strict ? RE_STRICT : RE
147
+
148
+ # regexp.match(str) always succeeds (even if str is empty)
149
+ components = regexp.match(str).captures[0,4]
150
+ components = components.reverse.drop_while(&:nil?).reverse
151
+ if strict && (!$'.empty? || components.empty?)
152
+ raise ArgumentError,
153
+ "invalid value for convert(): #{str.inspect}"
75
154
  end
76
155
 
77
- w, x, y, z = match_data.captures[0,4].collect do |s|
156
+ components.collect! do |s|
78
157
  case s
79
158
  when %r{/} then s.to_r
80
159
  when %r{[.eE]} then s.to_f
@@ -84,24 +163,57 @@ class Quaternion
84
163
  end
85
164
  end
86
165
 
87
- new(Complex.rect(w, x), Complex.rect(y, z))
166
+ # returns the parsed number as a preferred type
167
+ case components.size
168
+ when 0
169
+ 0
170
+ when 1
171
+ components[0]
172
+ when 2
173
+ Complex.rect(*components)
174
+ else # 3 or 4
175
+ w, x, y, z = components
176
+ new(Complex.rect(w, x), Complex.rect(y, z || 0))
177
+ end
88
178
  end
89
179
  end
90
180
  end
91
181
 
92
182
  class Numeric
183
+ ##
184
+ # Returns the value as a quaternion.
185
+ #
186
+ # @return [Quaternion]
187
+ #
93
188
  def to_q
94
189
  Quaternion.send(:new, self, 0)
95
190
  end
96
191
  end
97
192
 
98
193
  class String
194
+ ##
195
+ # Returns a quaternion which denotes the string form.
196
+ # The parser ignores leading whitespaces and trailing garbage.
197
+ # Any digit sequences can be separated by an underscore.
198
+ # Returns zero for null or garbage string.
199
+ #
200
+ # @return [Quaternion]
201
+ #
202
+ # @example
203
+ # "1-2i-3/4j+0.56k".to_q #=> (1-2i-(3/4)*j+0.56k)
204
+ # "foobarbaz".to_q #=> (0+0i+0j+0k)
205
+ #
99
206
  def to_q
100
- Quaternion.send(:parse, self, false)
207
+ Quaternion.send(:parse, self, false).to_q
101
208
  end
102
209
  end
103
210
 
104
211
  class NilClass
212
+ ##
213
+ # Returns zero as a quaternion.
214
+ #
215
+ # @return [Quaternion]
216
+ #
105
217
  def to_q
106
218
  Quaternion.send(:new, 0, 0)
107
219
  end
@@ -1,23 +1,59 @@
1
1
  require_relative 'base'
2
2
 
3
- #
4
- # Unary operations
5
- #
6
-
7
3
  class Quaternion
4
+ ##
5
+ # Unary operations
6
+ #
7
+
8
8
  # defined by Numeric:
9
9
  # * +@ #=> self
10
10
  # * -@ #=> 0 - self
11
11
 
12
+ ##
13
+ # Returns negation of the value.
14
+ #
15
+ # @return [Quaternion]
16
+ #
17
+ # @example
18
+ # -Quaternion(1, 2, 3, 4) #=> (-1-2i-3j-4k)
19
+ #
20
+ def -@
21
+ __new__(-@a, -@b)
22
+ end
23
+
24
+ ##
25
+ # Returns its conjugate.
26
+ #
27
+ # @return [Quaternion]
28
+ #
29
+ # @example
30
+ # Quaternion(1, 2, 3, 4).conj #=> (1-2i-3j-4k)
31
+ #
12
32
  def conj
13
33
  __new__(@a.conj, -@b)
14
34
  end
15
35
  alias conjugate conj
16
36
 
37
+ ##
38
+ # Returns square of the absolute value.
39
+ #
40
+ # @return [Real]
41
+ #
42
+ # @example
43
+ # Quaternion(-1, 1, -1, 1).abs2 #=> 4
44
+ #
17
45
  def abs2
18
46
  @a.abs2 + @b.abs2
19
47
  end
20
48
 
49
+ ##
50
+ # Returns the absolute part of its polar form.
51
+ #
52
+ # @return [Real]
53
+ #
54
+ # @example
55
+ # Quaternion(-1, 1, -1, 1).abs #=> 2.0
56
+ #
21
57
  def abs
22
58
  a_abs = @a.abs
23
59
  b_abs = @b.abs
@@ -40,4 +76,9 @@ class Quaternion
40
76
  else
41
77
  false
42
78
  end
79
+
80
+ def __reciprocal__
81
+ d2 = abs2
82
+ __new__(@a.conj.quo(d2), @b.quo(-d2))
83
+ end
43
84
  end
@@ -1,33 +1,57 @@
1
1
  require_relative 'base'
2
2
 
3
- #
4
- # Fundamental quaternion units
5
- #
6
-
7
3
  class Quaternion
4
+ ##
5
+ # Fundamental quaternion units
6
+ #
7
+
8
8
  i = Complex::I
9
9
  I = new(i, 0)
10
10
  J = new(0, 1)
11
11
  K = new(0, i)
12
12
  end
13
13
 
14
- #
15
- # Convert to an imaginary part
16
- #
17
-
18
14
  class Numeric
15
+ ##
16
+ # Convert to an imaginary part
17
+ #
18
+
19
19
  # defined:
20
- # * #i #=> Complex.rect(0, self)
20
+ # * i #=> Complex.rect(0, self)
21
21
 
22
+ ##
23
+ # Returns the corresponding imaginary number.
24
+ # Not available for quaternions.
25
+ #
26
+ # @return [Quaternion] +self * Quaternion::J+
27
+ # @raise [NoMethodError] if +self+ is not a complex.
28
+ #
29
+ # @example
30
+ # 3.j #=> (0+0i+3j+0k)
31
+ # Complex(3, 4).j #=> (0+0i+3j+4k)
32
+ #
22
33
  def j
23
34
  Quaternion.send(:new, 0, self)
24
35
  end
25
36
 
37
+ ##
38
+ # Returns the corresponding imaginary number.
39
+ # Not available for complex numbers.
40
+ #
41
+ # @return [Quaternion] +self * Quaternion::K+
42
+ # @raise [NoMethodError] if +self+ is not a real.
43
+ #
44
+ # @example
45
+ # 4.k #=> (0+0i+0j+4k)
46
+ #
26
47
  def k
27
48
  Quaternion.send(:new, 0, self.i)
28
49
  end
29
50
  end
30
51
 
52
+ ##
53
+ # @!parse class Complex < Numeric; end
54
+ #
31
55
  class Complex
32
56
  # undefined:
33
57
  # * #i
@@ -5,58 +5,98 @@ require_relative 'arithmetic'
5
5
  require_relative 'conversion'
6
6
 
7
7
  class Quaternion
8
+ ##
9
+ # Performs exponentiation.
10
+ #
11
+ # @param index [Numeric]
12
+ # @return [Quaternion]
13
+ #
14
+ # @example
15
+ # Quaternion(1, 1, 1, 1) ** 6 #=> (64+0i+0j+0k)
16
+ # Math::E.to_q ** Quaternion(Math.log(2), [Math::PI/3 / Math.sqrt(3)] * 3)
17
+ # #=> (1.0000000000000002+1.0i+1.0j+1.0k)
18
+ #
8
19
  def **(index)
9
- if index.kind_of?(Numeric)
10
- if __exact_zero__(index)
11
- return __new__(1, 0)
12
- end
20
+ unless index.kind_of?(Numeric)
21
+ num1, num2 = index.coerce(self)
22
+ return num1 ** num2
23
+ end
24
+
25
+ if __exact_zero__(index)
26
+ return __new__(1, 0)
27
+ end
13
28
 
14
- # complex -> real
29
+ # complex -> real
30
+ # (only if the imaginary part is exactly zero)
31
+ unless index.real?
15
32
  begin
16
33
  index.to_f
17
34
  rescue
18
35
  else
19
36
  index = index.real
20
37
  end
38
+ end
21
39
 
22
- # rational -> integer
23
- if index.kind_of?(Rational) && index.denominator == 1
24
- index = index.numerator
25
- end
40
+ # rational -> integer
41
+ if index.kind_of?(Rational) && index.denominator == 1
42
+ index = index.numerator
43
+ end
26
44
 
27
- if index.integer?
28
- # binary method
29
- x = (index >= 0) ? self : 1 / self
30
- n = index.abs
45
+ # calc and return
46
+ if index.integer?
47
+ # exponentiation by squaring
48
+ x = (index >= 0) ? self : __reciprocal__
49
+ n = index.abs
31
50
 
32
- z = __new__(1, 0)
33
- while true
34
- n, i = n.divmod(2)
35
- z *= x if i == 1
36
- return z if n == 0
37
- x *= x
38
- end
39
- elsif index.real?
40
- r, theta, vector = polar
41
- return Quaternion.polar(r ** index, theta * index, vector)
42
- elsif index.complex? || index.kind_of?(Quaternion)
43
- r, theta, vector = polar
44
- q = Math.log(r) + Quaternion.hrect(0, *(theta * vector))
45
- q *= index
46
- return Quaternion.polar(Math.exp(q.real), 1, q.imag)
51
+ z = __new__(1, 0)
52
+ while true
53
+ z *= x if n.odd?
54
+ n >>= 1
55
+ return z if n.zero?
56
+ x *= x
47
57
  end
58
+ elsif index.real?
59
+ r, theta, vector = polar
60
+ Quaternion.polar(r ** index, theta * index, vector)
61
+ elsif index.complex? || index.kind_of?(Quaternion)
62
+ # assume that log(self) commutes with index under multiplication
63
+ r, theta, vector = polar
64
+ q = Quaternion.hrect(Math.log(r), *(theta * vector))
65
+ q *= index
66
+ Quaternion.polar(Math.exp(q.real), 1, q.imag)
67
+ else
68
+ num1, num2 = index.coerce(self)
69
+ num1 ** num2
48
70
  end
49
-
50
- num1, num2 = other.coerce(self)
51
- num1 ** num2
52
71
  end
53
72
 
73
+ ##
74
+ # Returns the denominator (lcm of all components' denominators).
75
+ # @see numerator
76
+ #
77
+ # @return [Integer]
78
+ #
54
79
  def denominator
55
80
  ad = @a.denominator
56
81
  bd = @b.denominator
57
82
  ad.lcm(bd)
58
83
  end
59
84
 
85
+ ##
86
+ # Returns the numerator.
87
+ #
88
+ # 1 1 1 3 4-6i-12j+9k <- numerator
89
+ # - - -i - -j + -k -> -----------
90
+ # 3 2 1 4 12 <- denominator
91
+ #
92
+ # @return [Quaternion]
93
+ #
94
+ # @example
95
+ # q = Quaternion('1/3-1/2i-j+3/4k') #=> ((1/3)-(1/2)*i-1j+(3/4)*k)
96
+ # n = q.numerator #=> (4-6i-12j+9k)
97
+ # d = q.denominator #=> 12
98
+ # n / d == q #=> true
99
+ #
60
100
  def numerator
61
101
  an = @a.numerator
62
102
  bn = @b.numerator
data/lib/quaternion_c2.rb CHANGED
@@ -8,3 +8,4 @@ require "quaternion_c2/arithmetic"
8
8
  require "quaternion_c2/to_type"
9
9
  require "quaternion_c2/conversion"
10
10
  require "quaternion_c2/utils"
11
+ require "quaternion_c2/comparison"
@@ -3,14 +3,15 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
 
4
4
  Gem::Specification.new do |spec|
5
5
  spec.name = "quaternion_c2"
6
- spec.version = "0.1.1"
6
+ spec.version = "1.0.0"
7
7
  spec.authors = ["Masahiro Nomoto"]
8
8
  spec.email = ["hmmnrst@users.noreply.github.com"]
9
9
 
10
10
  spec.summary = %q{Quaternion class}
11
- spec.description = %q{This provides a numeric class Quaternion which is similar to a built-in class Complex.}
11
+ spec.description = %q{Provides a numeric class Quaternion which is similar to a built-in class Complex.}
12
12
  spec.homepage = "https://github.com/hmmnrst/quaternion_c2"
13
13
  spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 2.1" # matrix:0.1.0 does not support ruby 2.0
14
15
 
15
16
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
16
17
  f.match(%r{^(test|spec|features)/})
@@ -19,7 +20,9 @@ Gem::Specification.new do |spec|
19
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
21
  spec.require_paths = ["lib"]
21
22
 
22
- spec.add_development_dependency "bundler", "~> 1.14"
23
- spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_dependency "matrix" # Vector
24
+
25
+ spec.add_development_dependency "bundler"
26
+ spec.add_development_dependency "rake"
24
27
  spec.add_development_dependency "rspec", "~> 3.0"
25
28
  end
metadata CHANGED
@@ -1,68 +1,83 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quaternion_c2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masahiro Nomoto
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-03-31 00:00:00.000000000 Z
11
+ date: 2022-09-12 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: matrix
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
- - - ~>
31
+ - - ">="
18
32
  - !ruby/object:Gem::Version
19
- version: '1.14'
33
+ version: '0'
20
34
  type: :development
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
- - - ~>
38
+ - - ">="
25
39
  - !ruby/object:Gem::Version
26
- version: '1.14'
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rake
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
- - - ~>
45
+ - - ">="
32
46
  - !ruby/object:Gem::Version
33
- version: '10.0'
47
+ version: '0'
34
48
  type: :development
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
- - - ~>
52
+ - - ">="
39
53
  - !ruby/object:Gem::Version
40
- version: '10.0'
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rspec
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
- - - ~>
59
+ - - "~>"
46
60
  - !ruby/object:Gem::Version
47
61
  version: '3.0'
48
62
  type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
- - - ~>
66
+ - - "~>"
53
67
  - !ruby/object:Gem::Version
54
68
  version: '3.0'
55
- description: This provides a numeric class Quaternion which is similar to a built-in
56
- class Complex.
69
+ description: Provides a numeric class Quaternion which is similar to a built-in class
70
+ Complex.
57
71
  email:
58
72
  - hmmnrst@users.noreply.github.com
59
73
  executables: []
60
74
  extensions: []
61
75
  extra_rdoc_files: []
62
76
  files:
63
- - .gitignore
64
- - .rspec
65
- - .travis.yml
77
+ - ".github/workflows/test.yml"
78
+ - ".gitignore"
79
+ - ".rspec"
80
+ - ".travis.yml"
66
81
  - Gemfile
67
82
  - LICENSE.txt
68
83
  - README.md
@@ -74,6 +89,7 @@ files:
74
89
  - lib/quaternion_c2/attributes.rb
75
90
  - lib/quaternion_c2/base.rb
76
91
  - lib/quaternion_c2/classification.rb
92
+ - lib/quaternion_c2/comparison.rb
77
93
  - lib/quaternion_c2/conversion.rb
78
94
  - lib/quaternion_c2/equality.rb
79
95
  - lib/quaternion_c2/to_type.rb
@@ -85,24 +101,23 @@ homepage: https://github.com/hmmnrst/quaternion_c2
85
101
  licenses:
86
102
  - MIT
87
103
  metadata: {}
88
- post_install_message:
104
+ post_install_message:
89
105
  rdoc_options: []
90
106
  require_paths:
91
107
  - lib
92
108
  required_ruby_version: !ruby/object:Gem::Requirement
93
109
  requirements:
94
- - - '>='
110
+ - - ">="
95
111
  - !ruby/object:Gem::Version
96
- version: '0'
112
+ version: '2.1'
97
113
  required_rubygems_version: !ruby/object:Gem::Requirement
98
114
  requirements:
99
- - - '>='
115
+ - - ">="
100
116
  - !ruby/object:Gem::Version
101
117
  version: '0'
102
118
  requirements: []
103
- rubyforge_project:
104
- rubygems_version: 2.5.0
105
- signing_key:
119
+ rubygems_version: 3.3.7
120
+ signing_key:
106
121
  specification_version: 4
107
122
  summary: Quaternion class
108
123
  test_files: []