quaternion_c2 0.1.1 → 0.1.2

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
  SHA1:
3
- metadata.gz: 11a23c8bee71f2eac9cdd28569ddbd060e1c78d6
4
- data.tar.gz: 2da47444cd5b3c7f50da7f9cb0dfc281dcd187a1
3
+ metadata.gz: 6990b36ad863bcfeb0d147530f06461f634b13cc
4
+ data.tar.gz: 04be92b14b2149067953ac61c73151aafa42edea
5
5
  SHA512:
6
- metadata.gz: 3504d452e7841683a890fb01c8ca3a82ce44c9c334aa3309b50329dd17415a99d021c6d2b56cf0358a5c04d9d09b1aaf44508d030cc75072a32626d4c1a8d1eb
7
- data.tar.gz: 6348e89acb1f9a31923c03d9ea1fac21d10d2a849d55c198fd757021ad35243dce10c411502f4ea4b6e622d331e4e66dfd4b014e46c2774d290320a77507265b
6
+ metadata.gz: 4d62049c41e69f64f427932b91499399e122890472d963e2a2185959c197533f9fdac296301a8dd277866a74f710f6020b019fce30d584ae2f5c874b77c82593
7
+ data.tar.gz: b3dfa3a4691263ad104b5d9fc5669bd39708b1ad55ef680205f305f2012abc6512a6c93ebf93e9bb6ba660e1dc14b70a74b0e7f258da1aee3f02969ecfd4e9b2
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,6 +61,25 @@ 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)
@@ -58,10 +95,17 @@ class Quaternion
58
95
  alias / quo
59
96
  undef div, %, modulo, remainder, divmod
60
97
 
61
- #
98
+ ##
62
99
  # Conversion
63
100
  #
64
101
 
102
+ ##
103
+ # Performs type conversion.
104
+ #
105
+ # @param other [Numeric]
106
+ # @return [[Quaternion, self]]
107
+ # @raise [TypeError]
108
+ #
65
109
  def coerce(other)
66
110
  if other.kind_of?(Quaternion)
67
111
  [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
@@ -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
@@ -5,6 +5,12 @@ class Quaternion
5
5
  undef <=>
6
6
  undef_method(*Comparable.instance_methods)
7
7
 
8
+ ##
9
+ # Returns true if it equals to the other numerically.
10
+ #
11
+ # @param other [Object]
12
+ # @return [Boolean]
13
+ #
8
14
  def ==(other)
9
15
  if other.kind_of?(Quaternion)
10
16
  @a == other.a && @b == other.b
@@ -15,6 +21,12 @@ class Quaternion
15
21
  end
16
22
  end
17
23
 
24
+ ##
25
+ # Returns true if two quaternions have same reals.
26
+ #
27
+ # @param other [Object]
28
+ # @return [Boolean]
29
+ #
18
30
  def eql?(other)
19
31
  if other.kind_of?(Quaternion)
20
32
  @a.eql?(other.a) && @b.eql?(other.b)
@@ -23,8 +35,13 @@ class Quaternion
23
35
  end
24
36
  end
25
37
 
26
- # q1.eql?(q2) => q1.hash == q2.hash
38
+ ##
39
+ # Returns a hash.
40
+ #
41
+ # @return [Integer]
42
+ #
27
43
  def hash
44
+ # q1.eql?(q2) -> q1.hash == q2.hash
28
45
  [@a, @b].hash
29
46
  end
30
47
  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,6 +121,9 @@ class Quaternion
53
121
  end
54
122
 
55
123
  class << self
124
+ ##
125
+ # @!visibility private
126
+ #
56
127
  def parse(str, strict = false)
57
128
  regexp = %r{
58
129
  \A
@@ -65,16 +136,20 @@ class Quaternion
65
136
  |\z
66
137
  (?'head_or_sign' (?<=^|\s) [+-]?+ | [+-])
67
138
  (?'digits' \d++ (?:#{strict ? "_" : "_++"} \d++)*+)
68
- (?'int_or_float' (?:\g'digits')?+ (?:\. \g'digits')?+ (?<=\d) (?:e [+-]?+ \g'digits')?+)
139
+ (?'int_or_float' (?:\g'digits')?+ (?:\. \g'digits')?+
140
+ (?<=\d) (?:e [+-]?+ \g'digits')?+)
69
141
  (?'rational' \g'int_or_float' (?:/ \g'digits')?+)
70
142
  }xi
71
143
 
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}"
144
+ # regexp.match(str) always succeeds (even if str is empty)
145
+ components = regexp.match(str).captures[0,4]
146
+ components = components.reverse.drop_while(&:nil?).reverse
147
+ if strict && (!$'.empty? || components.empty?)
148
+ raise ArgumentError,
149
+ "invalid value for convert(): #{str.inspect}"
75
150
  end
76
151
 
77
- w, x, y, z = match_data.captures[0,4].collect do |s|
152
+ components.collect! do |s|
78
153
  case s
79
154
  when %r{/} then s.to_r
80
155
  when %r{[.eE]} then s.to_f
@@ -84,24 +159,57 @@ class Quaternion
84
159
  end
85
160
  end
86
161
 
87
- new(Complex.rect(w, x), Complex.rect(y, z))
162
+ # returns the parsed number as a preferred type
163
+ case components.size
164
+ when 0
165
+ 0
166
+ when 1
167
+ components[0]
168
+ when 2
169
+ Complex.rect(*components)
170
+ else # 3 or 4
171
+ w, x, y, z = components
172
+ new(Complex.rect(w, x), Complex.rect(y, z || 0))
173
+ end
88
174
  end
89
175
  end
90
176
  end
91
177
 
92
178
  class Numeric
179
+ ##
180
+ # Returns the value as a quaternion.
181
+ #
182
+ # @return [Quaternion]
183
+ #
93
184
  def to_q
94
185
  Quaternion.send(:new, self, 0)
95
186
  end
96
187
  end
97
188
 
98
189
  class String
190
+ ##
191
+ # Returns a quaternion which denotes the string form.
192
+ # The parser ignores leading whitespaces and trailing garbage.
193
+ # Any digit sequences can be separated by an underscore.
194
+ # Returns zero for null or garbage string.
195
+ #
196
+ # @return [Quaternion]
197
+ #
198
+ # @example
199
+ # "1-2i-3/4j+0.56k".to_q #=> (1-2i-(3/4)*j+0.56k)
200
+ # "foobarbaz".to_q #=> (0+0i+0j+0k)
201
+ #
99
202
  def to_q
100
- Quaternion.send(:parse, self, false)
203
+ Quaternion.send(:parse, self, false).to_q
101
204
  end
102
205
  end
103
206
 
104
207
  class NilClass
208
+ ##
209
+ # Returns zero as a quaternion.
210
+ #
211
+ # @return [Quaternion]
212
+ #
105
213
  def to_q
106
214
  Quaternion.send(:new, 0, 0)
107
215
  end
@@ -1,23 +1,47 @@
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 its conjugate.
14
+ #
15
+ # @return [Quaternion]
16
+ #
17
+ # @example
18
+ # Quaternion(1, 2, 3, 4).conj #=> (1-2i-3j-4k)
19
+ #
12
20
  def conj
13
21
  __new__(@a.conj, -@b)
14
22
  end
15
23
  alias conjugate conj
16
24
 
25
+ ##
26
+ # Returns square of the absolute value.
27
+ #
28
+ # @return [Real]
29
+ #
30
+ # @example
31
+ # Quaternion(-1, 1, -1, 1).abs2 #=> 4
32
+ #
17
33
  def abs2
18
34
  @a.abs2 + @b.abs2
19
35
  end
20
36
 
37
+ ##
38
+ # Returns the absolute part of its polar form.
39
+ #
40
+ # @return [Real]
41
+ #
42
+ # @example
43
+ # Quaternion(-1, 1, -1, 1).abs #=> 2.0
44
+ #
21
45
  def abs
22
46
  a_abs = @a.abs
23
47
  b_abs = @b.abs
@@ -40,4 +64,9 @@ class Quaternion
40
64
  else
41
65
  false
42
66
  end
67
+
68
+ def __reciprocal__
69
+ d2 = abs2
70
+ __new__(@a.conj.quo(d2), @b.quo(-d2))
71
+ end
43
72
  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
@@ -3,7 +3,7 @@ $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 = "0.1.2"
7
7
  spec.authors = ["Masahiro Nomoto"]
8
8
  spec.email = ["hmmnrst@users.noreply.github.com"]
9
9
 
metadata CHANGED
@@ -1,55 +1,55 @@
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: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masahiro Nomoto
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-03-31 00:00:00.000000000 Z
11
+ date: 2017-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ~>
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1.14'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ~>
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.14'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ~>
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
33
  version: '10.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ~>
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '10.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ~>
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
47
  version: '3.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ~>
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
55
  description: This provides a numeric class Quaternion which is similar to a built-in
@@ -60,9 +60,9 @@ executables: []
60
60
  extensions: []
61
61
  extra_rdoc_files: []
62
62
  files:
63
- - .gitignore
64
- - .rspec
65
- - .travis.yml
63
+ - ".gitignore"
64
+ - ".rspec"
65
+ - ".travis.yml"
66
66
  - Gemfile
67
67
  - LICENSE.txt
68
68
  - README.md
@@ -91,17 +91,17 @@ require_paths:
91
91
  - lib
92
92
  required_ruby_version: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - '>='
94
+ - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  required_rubygems_version: !ruby/object:Gem::Requirement
98
98
  requirements:
99
- - - '>='
99
+ - - ">="
100
100
  - !ruby/object:Gem::Version
101
101
  version: '0'
102
102
  requirements: []
103
103
  rubyforge_project:
104
- rubygems_version: 2.5.0
104
+ rubygems_version: 2.6.8
105
105
  signing_key:
106
106
  specification_version: 4
107
107
  summary: Quaternion class