vector_number 0.2.4

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d8a4ba56677fa1eab6fc6dba7e8a7cc9d4afd769d9d0a4186ee51b2f400f2bab
4
+ data.tar.gz: 57f4d03d9495c2f6e98afb25008f659f63cf5cf0723664f1a3e7633568a8a179
5
+ SHA512:
6
+ metadata.gz: 020ae7c79a25d4d18b6c2798d1c903e5f3a9699f8a2822ad7617ef3ada0ed4e2ef158a324548dfdc68de1a2bd70789487929d8d68def9b47133e077a4654d277
7
+ data.tar.gz: d34fb46be17a6cec0916257afcaccb842cdd24b7aa055377c1dbd343e6ffd90295df2a9725bf549709f6997276f88e1c1766f079c4848dc002b645180678e912
data/CHANGELOG.md ADDED
@@ -0,0 +1,69 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Next]
9
+ ## [v0.2.4] — 2025-02-26
10
+
11
+ **Added**
12
+ - Add hash-like methods `#[]` and `#unit?` (aliased as `#key?`).
13
+
14
+ **Changed**
15
+ - [Breaking] Change `positive?` and `negative?` to no longer return `nil`,
16
+ those cases will now return `false`.
17
+ - Make `VectorNumber.new` accept options when initializing from a VectorNumber
18
+ instead of only copying. Options will be merged.
19
+ - Remove `Initializing` module, move its methods to the actual class.
20
+ - Updated development gem versions.
21
+
22
+ **Fixed**
23
+ - `#dup` and `#clone` now behave exactly like Numeric versions, preventing unfreezing.
24
+
25
+ ## [v0.2.3] — 2024-10-15
26
+
27
+ **Fixed**
28
+ - BigDecimal tests are now properly skipped when BigDecimal is not available, instead of always.
29
+
30
+ ## [v0.2.2] — 2024-10-07
31
+
32
+ **Added**
33
+ - Add `#abs` (aliased as `#magnitude`) and `#abs2`.
34
+ - Add `#ceil`, `#floor` and `#round`.
35
+
36
+ **Changed**
37
+ - Add ruby 3.1.0, covering the earliest supported version, and ruby-next (3.4) to CI.
38
+ - Add JRuby and TruffleRuby to CI, without full support.
39
+ - Make tests runnable even without available `bigdecimal` gem.
40
+
41
+ **Fixed**
42
+ - `Kernel#BigDecimal` refinement now correctly works without `ndigits` argument.
43
+
44
+ ## [v0.2.1] — 2024-08-24
45
+
46
+ **Added**
47
+ - Add back `#*` and `#/` for working with real numbers.
48
+ - Add `#fdiv`, `#truncate`, `#nonnumeric?` and `#integer?`.
49
+
50
+ **Changed**
51
+ - Allow to use fully real VectorNumbers as real numbers.
52
+
53
+ **Fixed**
54
+ - Fix reversed result in refined `#<=>`.
55
+
56
+ **Removed**
57
+ - Remove `#to_str`, as VectorNumber is not a String-like object.
58
+ - Due to the above, `Kernel.BigDecimal` no longer works without refinements.
59
+
60
+ ## [v0.2.0] — 2024-08-19
61
+
62
+ **Added**
63
+ - VectorNumbers can be created from any object or collection.
64
+ - Addition and subtraction are supported.
65
+ - VectorNumbers are mostly interoperable with core numbers.
66
+
67
+ ## [v0.1.0] — 2024-05-09
68
+
69
+ - Initial work.
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # VectorNumber
2
+
3
+ ![CRuby validation](https://github.com/trinistr/vector_number/actions/workflows/cruby.yaml/badge.svg)
4
+ <!--
5
+ ![TruffleRuby validation](https://github.com/trinistr/vector_number/actions/workflows/truffleruby.yaml/badge.svg)
6
+ -->
7
+
8
+ A library to add together anything: be it a number, string or random Object, it can be added together in an infinite-dimensional vector space, with math operations available on results.
9
+
10
+ This is similar in a sense to hypercomplex numbers, such as quaternions, but with a focus on arbitrary dimensions.
11
+
12
+ Similar projects:
13
+ - [vector_space](https://github.com/tomstuart/vector_space) aims to provide typed vector spaces with limited dimensions and nice formatting.
14
+ - [named_vector](https://rubygems.org/gems/named_vector) provides simple vectors with named dimensions.
15
+ - Various quaternion libraries.
16
+
17
+ ## Installation
18
+
19
+ Add gem to your Gemfile:
20
+ ```ruby
21
+ gem "vector_number", git: "https://github.com/trinistr/vector_number.git"
22
+ ```
23
+
24
+ Installation through `gem` is not currently supported.
25
+
26
+ ## Usage
27
+
28
+ Usage is pretty simple and intuitive:
29
+ ```ruby
30
+ require "vector_number"
31
+ VectorNumber["string"] + "str" # => (1⋅'string' + 1⋅'str')
32
+ VectorNumber[5] + VectorNumber["string"] - 0.5 # => (4.5 + 1⋅'string')
33
+ VectorNumber["string", "string", "string", "str"] # => (3⋅'string' + 1⋅'str')
34
+ VectorNumber[:s] * 2 + VectorNumber["string"] * 0.3 # => (2⋅s + 0.3⋅'string')
35
+ VectorNumber[:s] / 3 # => (1/3⋅s)
36
+ 1/3r * VectorNumber[[]] # => (1/3⋅[])
37
+ ```
38
+
39
+ VectorNumbers are mostly useful for summing up heterogeneous objects:
40
+ ```ruby
41
+ sum = VectorNumber[]
42
+ [4, "death", "death", 13, nil].each { sum = sum + _1 }
43
+ sum # => (17 + 2⋅'death' + 1⋅)
44
+ sum.to_a # => [[(0+0i), 17], ["death", 2], [nil, 1]]
45
+ sum.to_h # => {(0+0i)=>17, "death"=>2, nil=>1}
46
+ ```
47
+
48
+ Alternatively, the same result can be equivalently (and more efficiently) achieved by
49
+ passing all values to a constructor:
50
+ ```ruby
51
+ VectorNumber[4, "death", "death", 13, nil]
52
+ VectorNumber.new([4, "death", "death", 13, nil])
53
+ ```
54
+
55
+ ## Ruby engine support status
56
+
57
+ VectorNumber is developed on MRI (CRuby) but should work on other engines too.
58
+ - JRuby: should work, but currently CI does not pass.
59
+ - TruffleRuby: works as expected, but there are differences in core Ruby code, so some tests fail.
60
+ - Other engines: untested, but should work.
61
+
62
+ ## Development
63
+
64
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests, `rake rubocop` to check code, `rake steep` to check typing or just `rake` to do everything. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
65
+
66
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, change Next version in `CHANGELOG.md`, commit changes and tag the commit. Alternatively, an appropriate `rake bump:` command can be used.
67
+
68
+ ## Contributing
69
+
70
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/trinistr/vector_number]().
71
+
72
+ ## License
73
+
74
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ class VectorNumber
4
+ # Methods for comparing with other numbers.
5
+ module Comparing
6
+ include ::Comparable
7
+
8
+ # Compare to +other+ for equality.
9
+ #
10
+ # Values are considered equal if
11
+ # - +other+ is a VectorNumber and it is +eql?+ to this one, or
12
+ # - +other+ is a Numeric equal in value to this number.
13
+ #
14
+ # @param other [Object]
15
+ # @return [Boolean]
16
+ def ==(other)
17
+ return true if eql?(other)
18
+
19
+ case other
20
+ when Numeric
21
+ numeric?(2) && other.real == real && other.imaginary == imaginary
22
+ else
23
+ # Can't compare a number-like value to a non-number.
24
+ false
25
+ end
26
+ end
27
+
28
+ # @param other [Object]
29
+ # @return [Boolean]
30
+ def eql?(other)
31
+ return true if equal?(other)
32
+
33
+ if other.is_a?(VectorNumber)
34
+ other.size == size && other.each_pair.all? { |u, c| @data[u] == c }
35
+ else
36
+ false
37
+ end
38
+ end
39
+
40
+ # @param other [Object]
41
+ # @return [Integer, nil]
42
+ def <=>(other)
43
+ return nil unless numeric?(1)
44
+
45
+ case other
46
+ when VectorNumber
47
+ other.numeric?(1) ? real <=> other.real : nil
48
+ when Numeric
49
+ other.imaginary.zero? ? real <=> other.real : nil
50
+ else
51
+ nil
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ class VectorNumber
4
+ # Methods for converting to different number classes.
5
+ module Converting
6
+ # Return real part of the number.
7
+ # @return [Integer, Float, Rational, BigDecimal]
8
+ def real
9
+ @data[R]
10
+ end
11
+
12
+ # Return imaginary part of the number.
13
+ # @return [Integer, Float, Rational, BigDecimal]
14
+ def imaginary
15
+ @data[I]
16
+ end
17
+
18
+ alias imag imaginary
19
+
20
+ # Return value as an Integer, truncating it, if only real part is non-zero.
21
+ # @return [Integer]
22
+ # @raise [RangeError] if any non-real part is non-zero
23
+ def to_i
24
+ numeric?(1) ? real.to_i : raise_convert_error(Integer)
25
+ end
26
+
27
+ alias to_int to_i
28
+
29
+ # Return value as a Float if only real part is non-zero.
30
+ # @return [Float]
31
+ # @raise [RangeError] if any non-real part is non-zero
32
+ def to_f
33
+ numeric?(1) ? real.to_f : raise_convert_error(Float)
34
+ end
35
+
36
+ # Return value as a Rational if only real part is non-zero.
37
+ # @return [Rational]
38
+ # @raise [RangeError] if any non-real part is non-zero
39
+ def to_r
40
+ numeric?(1) ? real.to_r : raise_convert_error(Rational)
41
+ end
42
+
43
+ # Return value as a BigDecimal if only real part is non-zero.
44
+ # @param ndigits [Integer] precision
45
+ # @return [BigDecimal]
46
+ # @raise [RangeError] if any non-real part is non-zero
47
+ # @raise [NameError] if BigDecimal is not defined
48
+ def to_d(ndigits = nil)
49
+ if numeric?(1)
50
+ return BigDecimal(real, ndigits) if ndigits
51
+ return BigDecimal(real, Float::DIG) if real.is_a?(Float)
52
+
53
+ BigDecimal(real)
54
+ else
55
+ raise_convert_error(BigDecimal)
56
+ end
57
+ end
58
+
59
+ # Return value as a Complex if only real and/or imaginary parts are non-zero.
60
+ # @return [Complex]
61
+ # @raise [RangeError] if any non-real, non-imaginary part is non-zero
62
+ def to_c
63
+ numeric?(2) ? Complex(real, imaginary) : raise_convert_error(Complex)
64
+ end
65
+
66
+ private
67
+
68
+ def raise_convert_error(klass)
69
+ raise RangeError, "can't convert #{self} into #{klass}", caller
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ class VectorNumber
4
+ # Methods for enumerating values of the number.
5
+ module Enumerating
6
+ include ::Enumerable
7
+
8
+ # Iterate through every pair of unit and coefficient.
9
+ # Returns {::Enumerator} if no block is given.
10
+ # @overload each
11
+ # @yieldparam unit [Object]
12
+ # @yieldparam coefficient [Integer, Float, Rational, BigDecimal]
13
+ # @yieldreturn [void]
14
+ # @return [VectorNumber] self
15
+ # @overload each
16
+ # @return [Enumerator]
17
+ def each(&)
18
+ return to_enum { size } unless block_given?
19
+
20
+ @data.each(&)
21
+ self
22
+ end
23
+
24
+ alias each_pair each
25
+
26
+ # @return [Array<Object>]
27
+ def units
28
+ @data.keys
29
+ end
30
+
31
+ alias keys units
32
+
33
+ # @return [Array<Integer, Float, Rational, BigDecimal>]
34
+ def coefficients
35
+ @data.values
36
+ end
37
+
38
+ alias values coefficients
39
+
40
+ # @return [Hash{Object => Integer, Float, Rational, BigDecimal}]
41
+ def to_h(&)
42
+ if block_given?
43
+ @data.to_h(&)
44
+ else
45
+ @data.dup
46
+ end
47
+ end
48
+
49
+ # Get the coefficient for the unit.
50
+ #
51
+ # If the +unit?(unit)+ is false, 0 is returned.
52
+ # Note that units for real and imaginary parts are
53
+ # VectorNumber::R and VectorNumber::I respectively.
54
+ #
55
+ # @param unit [Object]
56
+ # @return [Integer, Float, Rational, BigDecimal]
57
+ def [](unit)
58
+ @data[unit] || 0
59
+ end
60
+
61
+ # Check if a unit has a non-zero coefficient.
62
+ #
63
+ # @param unit [Object]
64
+ # @return [Boolean]
65
+ def unit?(unit)
66
+ @data.key?(unit)
67
+ end
68
+
69
+ alias key? unit?
70
+ end
71
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ class VectorNumber
4
+ # Various mathematical operations that are also conversions.
5
+ module MathConverting
6
+ # Return the absolute value of a vector, i.e. its length.
7
+ # @return [Float]
8
+ def abs
9
+ Math.sqrt(coefficients.reduce(0.0) { |result, coefficient| result + coefficient.abs2 })
10
+ end
11
+
12
+ alias magnitude abs
13
+
14
+ # Return the square of absolute value.
15
+ # @return [Float]
16
+ def abs2 # rubocop:disable Naming/VariableNumber
17
+ abs**2
18
+ end
19
+
20
+ # Return a new vector with every coefficient truncated using their +#truncate+.
21
+ # @param digits [Integer]
22
+ # @return [VectorNumber]
23
+ def truncate(digits = 0)
24
+ new { _1.truncate(digits) }
25
+ end
26
+
27
+ # Return a new vector with every coefficient rounded using their +#ceil+.
28
+ # @param digits [Integer]
29
+ # @return [VectorNumber]
30
+ def ceil(digits = 0)
31
+ new { _1.ceil(digits) }
32
+ end
33
+
34
+ # Return a new vector with every coefficient rounded using their +#floor+.
35
+ # @param digits [Integer]
36
+ # @return [VectorNumber]
37
+ def floor(digits = 0)
38
+ new { _1.floor(digits) }
39
+ end
40
+
41
+ # Return a new vector with every coefficient rounded using their +#round+.
42
+ # @param digits [Integer]
43
+ # @param half [Symbol, nil] one of +:up+, +:down+ or +:even+, see +Float#round+ for meaning
44
+ # @return [VectorNumber]
45
+ def round(digits = 0, half: :up)
46
+ if defined?(BigDecimal)
47
+ bd_mode =
48
+ case half
49
+ when :down then :half_down
50
+ when :even then :half_even
51
+ else :half_up
52
+ end
53
+ new { _1.is_a?(BigDecimal) ? _1.round(digits, bd_mode) : _1.round(digits, half:) }
54
+ else
55
+ new { _1.round(digits, half:) }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ class VectorNumber
4
+ # Methods for performing actual math.
5
+ module Mathing
6
+ # The coerce method provides support for Ruby type coercion.
7
+ # Unlike most numeric types, VectorNumber can coerce *anything*.
8
+ # @param other [Object]
9
+ # @return [Array(VectorNumber, VectorNumber)]
10
+ def coerce(other)
11
+ case other
12
+ when VectorNumber
13
+ [other, self]
14
+ else
15
+ [new([other]), self]
16
+ end
17
+ end
18
+
19
+ # Return self.
20
+ # @return [VectorNumber]
21
+ def +@
22
+ self
23
+ end
24
+
25
+ # Return new vector with negated coefficients.
26
+ # This preserves order of units.
27
+ # @return [VectorNumber]
28
+ def -@
29
+ new(&:-@)
30
+ end
31
+
32
+ # Return new vector as a sum of this and other value.
33
+ # This is analogous to {VectorNumber.[]}.
34
+ # @param other [Object]
35
+ # @return [VectorNumber]
36
+ def +(other)
37
+ new([self, other])
38
+ end
39
+
40
+ # Return new vector as a sum of this and negative of the other value.
41
+ # This is analogous to {VectorNumber.[]}, but allows to negate anything.
42
+ # @param other [Object]
43
+ # @return [VectorNumber]
44
+ def -(other)
45
+ self + new([other], &:-@)
46
+ end
47
+
48
+ # Multiply all coefficients by a real number, returning new vector.
49
+ # This effectively multiplies magnitude by the specified factor.
50
+ # @param other [Integer, Float, Rational, BigDecimal, VectorNumber]
51
+ # @return [VectorNumber]
52
+ # @raise [RangeError] if +other+ is not a number or +other+ can't be multiplied by this one
53
+ def *(other)
54
+ if real_number?(other)
55
+ other = other.real
56
+ # @type var other: Float
57
+ new { _1 * other }
58
+ elsif real_number?(self) && other.is_a?(self.class)
59
+ # @type var other: untyped
60
+ other * self
61
+ else
62
+ raise RangeError, "can't multiply #{self} and #{other}"
63
+ end
64
+ end
65
+
66
+ # Divide all coefficients by a real number, returning new vector.
67
+ # This effectively multiplies magnitude by reciprocal of the specified factor.
68
+ # @param other [Integer, Float, Rational, BigDecimal, VectorNumber]
69
+ # @return [VectorNumber]
70
+ # @raise [RangeError] if +other+ is not a number or is not a real number
71
+ def /(other)
72
+ raise RangeError, "can't divide #{self} by #{other}" unless real_number?(other)
73
+
74
+ other = other.real
75
+ # Prevent integer division, but without loss of accuracy.
76
+ other = Rational(other) if other.is_a?(Integer)
77
+ # @type var other: Float
78
+ new { _1 / other }
79
+ end
80
+
81
+ # Divide all coefficients by a real number using +fdiv+, returning new vector
82
+ # with Float coefficients.
83
+ # @param other [Integer, Float, Rational, BigDecimal, VectorNumber]
84
+ # @return [VectorNumber]
85
+ # @raise [RangeError] if +other+ is not a number or is not a real number
86
+ def fdiv(other)
87
+ raise RangeError, "can't divide #{self} by #{other}" unless real_number?(other)
88
+
89
+ other = other.real
90
+ new { _1.fdiv(other) }
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ class VectorNumber
4
+ # Refinements of Numeric classes to better work with VectorNumber and similar classes.
5
+ module NumericRefinements
6
+ # Refinement module to provide a +#<=>+ method that can work backwards.
7
+ # @note Currently only applies to Complex on *3.1*,
8
+ # as other numeric classes rely on +#coerce+.
9
+ module CommutativeShuttle
10
+ # Commutative +#<=>+.
11
+ # @example without refinements
12
+ # Complex(1, 0) <=> VectorNumber[2] # => nil
13
+ # VectorNumber[2] <=> Complex(1, 0) # => 1
14
+ # @example with refinements
15
+ # require "vector_number/numeric_refinements"
16
+ # using VectorNumber::NumericRefinements
17
+ # Complex(1, 0) <=> VectorNumber[2] # => -1
18
+ # VectorNumber[2] <=> Complex(1, 0) # => 1
19
+ def <=>(other)
20
+ comparison = super
21
+ return comparison if comparison || !other.respond_to?(:<=>)
22
+
23
+ comparison = other <=> self
24
+ -comparison if comparison
25
+ end
26
+ end
27
+
28
+ if (Complex(1, 0) <=> VectorNumber[1]).nil?
29
+ refine(Complex) { import_methods CommutativeShuttle }
30
+ end
31
+
32
+ # Refinement module to change Kernel#BigDecimal so it works with +#to_d+.
33
+ # @note `BigDecimal` needs to be defined for this refinement to activate.
34
+ module BigDecimalToD
35
+ # BigDecimal() that first tries to use #to_d.
36
+ # @param value [Object]
37
+ # @param exception [Boolean]
38
+ # @overload BigDecimal(value, exception: true)
39
+ # @overload BigDecimal(value, ndigits, exception: true)
40
+ # @param ndigits [Integer]
41
+ # @return [BigDecimal, nil]
42
+ # @raise [TypeError]
43
+ def BigDecimal(value, ndigits = nil, exception: true) # rubocop:disable Naming/MethodName
44
+ if value.respond_to?(:to_d)
45
+ ndigits.nil? ? value.to_d : value.to_d(ndigits)
46
+ else
47
+ ndigits.nil? ? super(value, exception:) : super
48
+ end
49
+ end
50
+ end
51
+
52
+ refine(Kernel) { import_methods BigDecimalToD } if defined?(BigDecimal)
53
+ end
54
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ class VectorNumber
4
+ # Methods for querying state of the number.
5
+ # Mostly modeled after {::Complex}.
6
+ module Querying
7
+ # Whether this VectorNumber can be considered strictly numeric, e.g. real or complex.
8
+ # @param dimensions [Integer] number of dimensions to consider "numeric"
9
+ # - 0 — zero
10
+ # - 1 — real number
11
+ # - 2 — complex number, etc.
12
+ # @return [Boolean]
13
+ # @raise [ArgumentError] if +dimensions+ is negative
14
+ def numeric?(dimensions = 2)
15
+ raise ArgumentError, "`dimensions` must be non-negative" unless dimensions >= 0
16
+
17
+ size <= dimensions && (1..dimensions).count { @data[UNIT[_1]].nonzero? } == size
18
+ end
19
+
20
+ # Whether this VectorNumber contains any non-numeric parts.
21
+ # @param (see #numeric?)
22
+ # @return (see #numeric?)
23
+ # @raise (see #numeric?)
24
+ def nonnumeric?(dimensions = 2)
25
+ raise ArgumentError, "`dimensions` must be non-negative" unless dimensions >= 0
26
+
27
+ !numeric?(dimensions)
28
+ end
29
+
30
+ # Returns +true+ if all coefficients are finite, +false+ otherwise.
31
+ # @return [Boolean]
32
+ def finite?
33
+ all? { |_u, v| v.finite? }
34
+ end
35
+
36
+ # Returns +1+ if any coefficients are infinite, +nil+ otherwise.
37
+ # @return [1, nil]
38
+ def infinite?
39
+ finite? ? nil : 1 # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
40
+ end
41
+
42
+ # Returns +true+ if there are no non-zero coefficients, and +false+ otherwise.
43
+ # @return [Boolean]
44
+ def zero?
45
+ size.zero?
46
+ end
47
+
48
+ # Returns +self+ if there are any non-zero coefficients, +nil+ otherwise.
49
+ # @return [VectorNumber, nil]
50
+ def nonzero?
51
+ zero? ? nil : self # rubocop:disable Style/ReturnNilInPredicateMethodDefinition
52
+ end
53
+
54
+ # Returns +true+ if number is non-zero and all non-zero coefficients are positive,
55
+ # and +false+ otherwise.
56
+ # @return [Boolean]
57
+ def positive?
58
+ !zero? && all? { |_u, c| c.positive? }
59
+ end
60
+
61
+ # Returns +true+ if number is non-zero and all non-zero coefficients are negative,
62
+ # and +false+ otherwise.
63
+ # @return [Boolean]
64
+ def negative?
65
+ !zero? && all? { |_u, c| c.negative? }
66
+ end
67
+
68
+ # Always returns +false+.
69
+ # @see #numeric?
70
+ # @return [false]
71
+ def real?
72
+ false
73
+ end
74
+
75
+ # Always returns +false+.
76
+ # @return [false]
77
+ def integer?
78
+ false
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ class VectorNumber
4
+ # Methods and options for string representation.
5
+ module Stringifying
6
+ # Predefined symbols for multiplication to display between unit and coefficient.
7
+ # @return [Hash{Symbol => String}]
8
+ MULT_STRINGS = {
9
+ asterisk: "*", # U+002A
10
+ cross: "×", # U+00D7
11
+ dot: "⋅", # U+22C5
12
+ invisible: "⁢", # U+2062, zero-width multiplication operator
13
+ space: " ",
14
+ none: ""
15
+ }.freeze
16
+
17
+ # @param mult [Symbol, String]
18
+ # text to use between coefficient and unit,
19
+ # can be one of the keys in {MULT_STRINGS} or an arbitrary string
20
+ # @return [String]
21
+ # @raise [ArgumentError] if +mult+ is not in {MULT_STRINGS}'s keys
22
+ def to_s(mult: options[:mult])
23
+ return "0" if zero?
24
+
25
+ result = +""
26
+ each_with_index do |(unit, coefficient), index|
27
+ if index.zero?
28
+ result << "-" if coefficient.negative?
29
+ else
30
+ result << (coefficient.positive? ? " + " : " - ")
31
+ end
32
+ result << value_to_s(unit, coefficient.abs, mult:)
33
+ end
34
+ result
35
+ end
36
+
37
+ # @return [String]
38
+ def inspect
39
+ "(#{self})"
40
+ end
41
+
42
+ private
43
+
44
+ # @param unit [Object]
45
+ # @param coefficient [Numeric]
46
+ # @param mult [Symbol, String]
47
+ # @return [String]
48
+ # @raise [ArgumentError] if +mult+ is not in {MULT_STRINGS}'s keys
49
+ def value_to_s(unit, coefficient, mult:)
50
+ if !mult.is_a?(String) && !MULT_STRINGS.key?(mult)
51
+ raise ArgumentError, "unknown key :#{mult}", caller
52
+ end
53
+
54
+ case unit
55
+ when R
56
+ coefficient.to_s
57
+ when I
58
+ "#{coefficient}i"
59
+ else
60
+ unit = "'#{unit}'" if unit.is_a?(String)
61
+ operator = mult.is_a?(String) ? mult : MULT_STRINGS[mult]
62
+ "#{coefficient}#{operator}#{unit}"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class VectorNumber # rubocop:disable Style/StaticClass
4
+ # @return [String]
5
+ VERSION = "0.2.4"
6
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "vector_number/version"
4
+ require_relative "vector_number/mathing"
5
+ require_relative "vector_number/math_converting"
6
+ require_relative "vector_number/converting"
7
+ require_relative "vector_number/enumerating"
8
+ require_relative "vector_number/comparing"
9
+ require_relative "vector_number/querying"
10
+ require_relative "vector_number/stringifying"
11
+
12
+ # A class to add together anything.
13
+ class VectorNumber
14
+ include Mathing
15
+ include MathConverting
16
+ include Converting
17
+ include Enumerating
18
+ include Comparing
19
+ include Querying
20
+ include Stringifying
21
+
22
+ # @return [Array<Symbol>]
23
+ KNOWN_OPTIONS = %i[mult].freeze
24
+
25
+ # @return [Hash{Symbol => Object}]
26
+ DEFAULT_OPTIONS = { mult: :dot }.freeze
27
+
28
+ # Get a unit for +n+th numeric dimension, where 1 is real, 2 is imaginary.
29
+ UNIT = ->(n) { (n - 1).i }.freeze
30
+ # Constant for real unit.
31
+ R = UNIT[1]
32
+ # Constant for imaginary unit.
33
+ I = UNIT[2]
34
+
35
+ # Number of non-zero dimensions.
36
+ # @return [Integer]
37
+ attr_reader :size
38
+ # @return [Hash{Symbol => Object}]
39
+ attr_reader :options
40
+
41
+ # Create new VectorNumber from +values+.
42
+ #
43
+ # @example
44
+ # VectorNumber[1, 2, 3] #=> (6)
45
+ # VectorNumber[[1, 2, 3]] #=> (1⋅[1, 2, 3])
46
+ # VectorNumber['b', VectorNumber::I, mult: :asterisk] #=> (1*'b' + 1i)
47
+ # @param values [Array<Object>] values to put in the number
48
+ # @param options [Hash{Symbol => Object}] options for the number
49
+ # @return [VectorNumber]
50
+ def self.[](*values, **options)
51
+ new(values, options)
52
+ end
53
+
54
+ # @param values [Array, Hash{Object => Integer, Float, Rational, BigDecimal}, VectorNumber]
55
+ # values for this number, hashes are treated like plain vector numbers
56
+ # @param options [Hash{Symbol => Object}]
57
+ # options for this number, if +values+ is a VectorNumber,
58
+ # these will be merged with options from +values.options+
59
+ # @option options [Symbol, String] :mult
60
+ # text to use between unit and coefficient, see {Stringifying#to_s} for explanation
61
+ # @yieldparam coefficient [Integer, Float, Rational, BigDecimal]
62
+ # @yieldreturn [Integer, Float, Rational, BigDecimal] new coefficient
63
+ # @raise [RangeError] if any pesky non-reals get where they shouldn't
64
+ def initialize(values = nil, options = {}.freeze, &)
65
+ # TODO: propagate options properly.
66
+ # > (VectorNumber[1, 'a', mult: :invisible] + 2).options
67
+ # => {:mult=>:dot}
68
+ super()
69
+ initialize_from(values)
70
+ apply_transform(&)
71
+ finalize_contents
72
+ save_options(options, values:)
73
+ @options.freeze
74
+ @data.freeze
75
+ freeze
76
+ end
77
+
78
+ # Return self.
79
+ #
80
+ # @return [VectorNumber]
81
+ def dup
82
+ self
83
+ end
84
+
85
+ # Return self.
86
+ #
87
+ # Raises ArgumentError if +freeze+ is not +true+ or +nil+.
88
+ #
89
+ # @return [VectorNumber]
90
+ def clone(freeze: true)
91
+ case freeze
92
+ when true, nil
93
+ self
94
+ when false
95
+ raise ArgumentError, "can't unfreeze #{self.class}"
96
+ else
97
+ raise ArgumentError, "unexpected value for freeze: #{freeze.class}"
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ # Create new VectorNumber from a value or self, optionally applying a transform.
104
+ # @param from [Object] self if not specified
105
+ # @yieldparam coefficient [Integer, Float, Rational, BigDecimal]
106
+ # @yieldreturn [Integer, Float, Rational, BigDecimal] new coefficient
107
+ # @return [VectorNumber]
108
+ def new(from = self, &)
109
+ self.class.new(from, &)
110
+ end
111
+
112
+ # @param value [Object]
113
+ # @return [Boolean]
114
+ def real_number?(value)
115
+ (value.is_a?(Numeric) && value.real?) || (value.is_a?(self.class) && value.numeric?(1))
116
+ end
117
+
118
+ # @param values [Array, Hash{Object => Integer, Float, Rational, BigDecimal}, VectorNumber, nil]
119
+ # @return [void]
120
+ def initialize_from(values)
121
+ @data = Hash.new(0)
122
+
123
+ case values
124
+ when VectorNumber, Hash
125
+ add_vector_to_data(values)
126
+ when Array
127
+ values.each { |value| add_value_to_data(value) }
128
+ else
129
+ # Don't add anything.
130
+ end
131
+ end
132
+
133
+ # @param value [VectorNumber, Numeric, Object]
134
+ # @return [void]
135
+ def add_value_to_data(value)
136
+ case value
137
+ when Numeric
138
+ add_numeric_value_to_data(value)
139
+ when VectorNumber
140
+ add_vector_to_data(value)
141
+ else
142
+ @data[value] += 1
143
+ end
144
+ end
145
+
146
+ # @param value [Numeric]
147
+ # @return [void]
148
+ def add_numeric_value_to_data(value)
149
+ @data[R] += value.real
150
+ @data[I] += value.imaginary
151
+ end
152
+
153
+ # @param vector [VectorNumber, Hash{Object => Integer, Float, Rational, BigDecimal}]
154
+ # @return [void]
155
+ def add_vector_to_data(vector)
156
+ vector.each_pair do |unit, coefficient|
157
+ raise RangeError, "#{coefficient} is not a real number" unless real_number?(coefficient)
158
+
159
+ @data[unit] += coefficient.real
160
+ end
161
+ end
162
+
163
+ # @yieldparam coefficient [Integer, Float, Rational, BigDecimal]
164
+ # @yieldreturn [Integer, Float, Rational, BigDecimal]
165
+ # @return [void]
166
+ # @raise [RangeError]
167
+ def apply_transform
168
+ return unless block_given?
169
+
170
+ @data.transform_values! do |coefficient|
171
+ new_value = yield coefficient
172
+ next new_value.real if real_number?(new_value)
173
+
174
+ raise RangeError, "transform returned non-real value for #{coefficient}"
175
+ end
176
+ end
177
+
178
+ # @param options [Hash{Symbol => Object}, nil]
179
+ # @param values [Object] initializing object
180
+ # @return [void]
181
+ def save_options(options, values:)
182
+ @options =
183
+ case [options, values]
184
+ in [{} | nil, VectorNumber]
185
+ values.options
186
+ in Hash, VectorNumber
187
+ values.options.merge(options).slice(*known_options)
188
+ in Hash, _ unless options.empty?
189
+ default_options.merge(options).slice(*known_options)
190
+ else
191
+ default_options
192
+ end
193
+ end
194
+
195
+ # Compact coefficients, calculate size and freeze data.
196
+ # @return [void]
197
+ def finalize_contents
198
+ @data.delete_if { |_u, c| c.zero? }
199
+ @data.freeze
200
+ @size = @data.size
201
+ end
202
+
203
+ def default_options
204
+ DEFAULT_OPTIONS
205
+ end
206
+
207
+ def known_options
208
+ KNOWN_OPTIONS
209
+ end
210
+ end
@@ -0,0 +1,15 @@
1
+ type vector_type = VectorNumber
2
+ type real_number = Integer | Float | Rational | BigDecimal
3
+
4
+ type in_value_type = untyped
5
+ type unit_type = untyped
6
+ type coefficient_type = real_number
7
+
8
+ type list[T] = Array[T]
9
+
10
+ type plain_vector_type = Hash[unit_type, coefficient_type]
11
+ type units_list_type = list[unit_type]
12
+ type coefficients_list_type = list[coefficient_type]
13
+ type each_value_type = [unit_type, coefficient_type]
14
+
15
+ type options_type = Hash[Symbol, untyped]
@@ -0,0 +1,230 @@
1
+ interface _BaseMethods
2
+ def size: -> Integer
3
+ def options: -> options_type
4
+
5
+ # private
6
+ def new: () { (coefficient_type value) -> coefficient_type } -> vector_type
7
+ | (in_value_type) { (coefficient_type value) -> coefficient_type } -> vector_type
8
+ | () -> vector_type
9
+ | (in_value_type) -> vector_type
10
+ def real_number?: (real_number value) -> true
11
+ | (vector_type) -> bool
12
+ | (in_value_type value) -> false
13
+ end
14
+
15
+ # A class to add together anything.
16
+ class VectorNumber
17
+ include _BaseMethods
18
+ include VectorNumber::Mathing
19
+ include VectorNumber::MathConverting
20
+ include VectorNumber::Converting
21
+ include VectorNumber::Enumerating
22
+ include VectorNumber::Comparing
23
+ include VectorNumber::Querying
24
+ include VectorNumber::Stringifying
25
+
26
+ VERSION: String
27
+
28
+ KNOWN_OPTIONS: list[Symbol]
29
+ DEFAULT_OPTIONS: options_type
30
+
31
+ UNIT: ^(Integer) -> Complex
32
+ R: Complex
33
+ I: Complex
34
+
35
+ def self.[]: (*unit_type values, **options_type options) -> vector_type
36
+
37
+ def initialize:
38
+ (?(units_list_type | plain_vector_type | vector_type)? values, ?options_type options)
39
+ ?{ (coefficient_type coefficient) -> coefficient_type }
40
+ -> void
41
+
42
+ def dup: () -> self
43
+
44
+ def clone: (?freeze: bool?) -> self
45
+
46
+ private
47
+
48
+ @size: Integer
49
+ @options: options_type
50
+ @data: plain_vector_type
51
+
52
+ def initialize_from: ((units_list_type | plain_vector_type | vector_type)? values) -> void
53
+
54
+ def add_value_to_data: ((vector_type | Numeric | unit_type) value) -> void
55
+
56
+ def add_numeric_value_to_data: (Numeric value) -> void
57
+
58
+ def add_vector_to_data: ((vector_type | plain_vector_type) vector) -> void
59
+
60
+ def apply_transform: () ?{ (coefficient_type value) -> coefficient_type } -> void
61
+
62
+ def save_options: (options_type? options, values: in_value_type) -> void
63
+ def default_options: () -> options_type
64
+ def known_options: () -> list[Symbol]
65
+
66
+ def finalize_contents: () -> void
67
+
68
+ # Methods for performing actual math.
69
+ module Mathing
70
+ include _BaseMethods
71
+
72
+ def coerce: (in_value_type) -> [vector_type, self]
73
+
74
+ def +@: () -> self
75
+
76
+ def -@: () -> vector_type
77
+
78
+ def +: (in_value_type) -> vector_type
79
+
80
+ def -: (in_value_type) -> vector_type
81
+
82
+ def *: (real_number | vector_type) -> vector_type
83
+
84
+ def /: (real_number | vector_type) -> vector_type
85
+
86
+ def fdiv: (real_number | vector_type) -> vector_type
87
+ end
88
+
89
+ # Various mathematical operations that are also conversions.
90
+ module MathConverting
91
+ include _BaseMethods
92
+ include Enumerating
93
+
94
+ def abs: () -> Float
95
+
96
+ def abs2: () -> Float
97
+
98
+ def truncate: (Integer) -> vector_type
99
+
100
+ def ceil: (Integer) -> vector_type
101
+
102
+ def floor: (Integer) -> vector_type
103
+ end
104
+
105
+ # Methods for converting to different number classes.
106
+ module Converting
107
+ include _BaseMethods
108
+ include Querying
109
+
110
+ def real: () -> real_number
111
+
112
+ def imaginary: () -> real_number
113
+ alias imag imaginary
114
+
115
+ def to_i: () -> Integer
116
+ | () -> void
117
+ alias to_int to_i
118
+
119
+ def to_f: () -> Float
120
+ | () -> void
121
+
122
+ def to_r: () -> Rational
123
+ | () -> void
124
+
125
+ def to_d: (?Integer?) -> BigDecimal
126
+ | (?Integer?) -> void
127
+
128
+ def to_c: () -> Complex
129
+ | () -> void
130
+
131
+ def truncate: (?Integer digits) -> vector_type
132
+
133
+ private
134
+
135
+ def raise_convert_error: (Class) -> void
136
+ end
137
+
138
+ # Methods for enumerating values of the number.
139
+ module Enumerating
140
+ include _BaseMethods
141
+ include _Each[each_value_type]
142
+ include Enumerable[each_value_type]
143
+
144
+ def each: () { (unit_type unit, coefficient_type coefficient) -> void } -> self
145
+ | () -> Enumerator[each_value_type, Integer]
146
+ | ...
147
+ alias each_pair each
148
+
149
+ def units: () -> units_list_type
150
+ alias keys units
151
+
152
+ def coefficients: () -> coefficients_list_type
153
+ alias values coefficients
154
+
155
+ def to_h: () -> plain_vector_type
156
+ | () { (unit_type, coefficient_type) -> each_value_type } -> plain_vector_type
157
+
158
+ def []: (unit_type unit) -> coefficient_type
159
+
160
+ def unit?: (unit_type unit) -> bool
161
+ alias key? unit?
162
+ end
163
+
164
+ # Methods for comparing with other numbers.
165
+ module Comparing
166
+ include _BaseMethods
167
+ include Enumerating
168
+ include Converting
169
+ include Querying
170
+
171
+ def ==: (in_value_type other) -> bool
172
+
173
+ def eql?: (in_value_type other) -> bool
174
+
175
+ def <=>: (in_value_type other) -> Integer?
176
+ end
177
+
178
+ # Methods for querying state of the number.
179
+ # Mostly modeled after {::Complex}.
180
+ module Querying
181
+ include Enumerating
182
+
183
+ def numeric?: (?Integer) -> bool
184
+
185
+ def nonnumeric?: (?Integer) -> bool
186
+
187
+ def finite?: () -> bool
188
+
189
+ def infinite?: () -> Integer?
190
+
191
+ def zero?: () -> bool
192
+
193
+ def nonzero?: () -> self?
194
+
195
+ def positive?: () -> bool
196
+
197
+ def negative?: () -> bool
198
+
199
+ def real?: () -> false
200
+
201
+ def integer?: () -> false
202
+ end
203
+
204
+ # Methods and options for string representation.
205
+ module Stringifying
206
+ include Querying
207
+
208
+ MULT_STRINGS: Hash[Symbol, String]
209
+
210
+ def to_s: (?mult: (Symbol | String)) -> String
211
+
212
+ def inspect: () -> String
213
+
214
+ private
215
+
216
+ def value_to_s: (unit_type unit, coefficient_type coefficient, mult: (Symbol | String)) -> String
217
+ end
218
+
219
+ # Refinements of Numeric classes to better work with VectorNumber and similar classes.
220
+ module NumericRefinements
221
+ module CommutativeShuttle
222
+ def <=>: (in_value_type other) -> Integer?
223
+ end
224
+
225
+ module BigDecimalToD
226
+ def BigDecimal: (untyped value, exception: bool) -> BigDecimal?
227
+ | (untyped value, Integer ndigits, exception: bool) -> BigDecimal?
228
+ end
229
+ end
230
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vector_number
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.4
5
+ platform: ruby
6
+ authors:
7
+ - Alexandr Bulancov
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-02-25 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files:
18
+ - README.md
19
+ - CHANGELOG.md
20
+ files:
21
+ - CHANGELOG.md
22
+ - README.md
23
+ - lib/vector_number.rb
24
+ - lib/vector_number/comparing.rb
25
+ - lib/vector_number/converting.rb
26
+ - lib/vector_number/enumerating.rb
27
+ - lib/vector_number/math_converting.rb
28
+ - lib/vector_number/mathing.rb
29
+ - lib/vector_number/numeric_refinements.rb
30
+ - lib/vector_number/querying.rb
31
+ - lib/vector_number/stringifying.rb
32
+ - lib/vector_number/version.rb
33
+ - sig/definitions.rbs
34
+ - sig/vector_number.rbs
35
+ homepage: https://github.com/trinistr/vector_number
36
+ licenses:
37
+ - MIT
38
+ metadata:
39
+ homepage_uri: https://github.com/trinistr/vector_number
40
+ source_code_uri: https://github.com/trinistr/vector_number
41
+ changelog_uri: https://github.com/trinistr/vector_number/CHANGELOG.md
42
+ rubygems_mfa_required: 'true'
43
+ post_install_message:
44
+ rdoc_options:
45
+ - "--main"
46
+ - README.md
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 3.1.0
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.5.21
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: A library to add together anything.
64
+ test_files: []