VectorNumber
[!TIP] You may be viewing documentation for an older (or newer) version of the gem. Look at Changelog to see all versions, including unreleased changes.
VectorNumber is a Ruby gem that provides a Numeric-like experience for doing arithmetics on heterogeneous objects, with more advanced operations based on real vector spaces available when needed.
Features:
- Add and subtract (almost) any object, with no setup or declaration.
- Multiply and divide vectors by any real number to create 1.35 of an array and -2 of a string. What does that mean? Only you know!
- Use vectors instead of inbuilt numbers in most situtations with no difference in behavior. Or, use familiar methods from numerics with sane semantics!
- Enumerate vectors in a hash-like fashion, or transform to an array or hash as needed.
- Enjoy a mix of vector-, complex- and polynomial-like behavior at appropriate times.
- No dependencies, no extensions. It just works!
Other people have created some similar gems over the years, like vector_space or named_vector but they don't have the characterics that I wanted, and none of them have been updated in years.
Table of contents
Installation
Install with gem:
gem install vector_number
If using Bundler, add gem to your Gemfile:
gem "vector_number"
Ruby engine support status
VectorNumber is developed on MRI (CRuby) but should work on other engines too.
- TruffleRuby: minor differences in behavior, but otherwise works as expected.
- JRuby: minor differences in behavior, but otherwise works as expected.
- Other engines: untested, but should work, depending on compatibility with MRI.
Usage
[!Note]
- Latest API documentation from
mainbranch is automatically deployed to GitHub Pages.- Documentation for published versions is available on RubyDoc.
Basics
VectorNumbers are mostly useful for summing up heterogeneous objects:
sum = VectorNumber[4] + "death" + "death" + nil
sum # => (17 + 2⋅"death" + 1⋅)
sum.to_h # => {unit/1 => 17, "death" => 2, nil => 1}
sum.to_a # => [[unit/1, 17], ["death", 2], [nil, 1]]
# Alternatively, the same result can be equivalently (and more efficiently)
# achieved by passing all values to a constructor:
VectorNumber[4, "death", "death", 13, nil]
VectorNumber.new([4, "death", "death", 13, nil])
Doing arithmetic with vectors is simple and intuitive:
VectorNumber["string"] + "string" # => (2⋅"string")
VectorNumber["string"] - "str" # => (1⋅"string" - 1⋅"str")
VectorNumber[5] + VectorNumber["string"] - 0.5 # => (4.5 + 1⋅"string")
VectorNumber["string", "string", "string", "str"] # => (3⋅"string" + 1⋅"str")
# Multiply and divide by any real number:
VectorNumber[:s] * 2 + VectorNumber["string"] * 0.3 # => (2⋅s + 0.3⋅"string")
VectorNumber[:s] / VectorNumber[3] # => (1/3⋅s)
Ruby numbers rely on #coerce to promote values to a common type. This allows using regular numbers as first operand in arithmetic operations:
2 + VectorNumber["string"] # => (2 + 1⋅"string")
1/3r * VectorNumber[[]] # => (1/3⋅[])
13 / VectorNumber[2] # => (13/2)
[!NOTE] VectorNumbers don't perform "integer division" to prevent unexpected loss of precision.
#divand rounding methods can achieve this if required.
Getting values back
The simplest way to get a value for a specific unit is to use the #[] method:
VectorNumber["string", "string", "string", "str"]["string"] # => 3
VectorNumber["string", "string", "string", "str"]["str"] # => 1
VectorNumber["string", "string", "string", "str"]["nonexistent"] # => 0
[!NOTE] Accessing a unit that doesn't exist returns 0, not
nilas you might expect.
(Somewhat) advanced usage
[!TIP] Look at API documentation for all methods.
Frozenness
VectorNumbers are always frozen, as a number should be. However, they hold references to units (keys), which aren't frozen or duplicated. It is the user's responsibility to ensure that keys aren't mutated, the same as it is for Hash.
As vectors are immutable, +@, dup and clone return the same instance.
Numerical behavior
VectorNumbers implement most of the methods you can find in Numeric, with appropriate behavior. For example:
abs(magnitude) calculates length of the vector;infinite?checks whether any coefficient is infinite (or NaN), in the same way asComplexdoes it;positive?is true if all coefficients are positive, the same fornegative?(though this is different fromComplex) (and they can both be false);roundand friends round each coefficient, with all the bells and whistles;divand%perform division and remainder operations elementwise;5 < VectorNumber[6]returnstrueand5 < VectorNumber["string"]raisesArgumentError, etc.
VectorNumbers, if only consisting of a real number, can mostly be used interchangeably with core numbers due to coerce and conversion (to_i, etc.) methods. They can even be used as array indices! Due to including Comparable, they can also participate in comparison and sorting.
Enumeration and Hash-like behavior
VectorNumbers implement each (each_pair) in the same way as Hash does, allowing Enumerable methods to be used in a familiar way.
There are also the usual [], unit? (key?), units (keys), coefficients (values) methods. to_h and to_a can be used if a regular Hash or Array is needed.
Conceptual basis
VectorNumbers are based on the concept of a vector space over the field of real numbers (real vector space). In the case of VectorNumber, the dimensionality of the vector space is countably infinite, as most distinct objects in Ruby signify a separate dimension.
For most dimensions, an object is that distinct dimension's unit. There are two exceptions currently: real unit (1) and imaginary unit (i) which define the real and imaginary dimensions and subsume all real and complex numbers. A VectorNumber can not be a unit itself. Distinction of objects is determined by eql?, same as for Hash.
Length of a VectorNumber in any given dimension is given by a real number, called its coefficient. All dimensions are linearly independent — change in one coefficient does not affect any other coefficient. There is no distinction between a dimension explicitly specified as having a 0 coefficient (or arriving at 0 through calculation) and a dimension not specified at all.
This might be more easily imagined as a geometric vector. For example, this is a graphic representation of a vector 3 * VectorNumber[1] + 2 * VectorNumber[1i] + 3 * VectorNumber["string"] + 4.5 * VectorNumber[[1,2,3]] in the vector space:
Development
After checking out the repo, run bundle install to install dependencies. Then, run rake spec to run the tests, rake rubocop to lint code and check style compliance, rake rbs to validate signatures or just rake to do everything above. There is also rake steep to check typing, and rake docs to generate YARD documentation.
You can also run bin/console for an interactive prompt that will allow you to experiment, or bin/benchmark to run a benchmark script and generate a StackProf flamegraph.
To install this gem onto your local machine, run rake install.
To release a new version, run rake version:{major|minor|patch}, and then run rake release, which will build the package and push the .gem file to rubygems.org. After that, push the release commit and tags to the repository with git push --follow-tags.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/trinistr/vector_number.
Checklist for a new or updated feature
- Running
rake specreports 100% coverage (unless it's impossible to achieve in one run). - Running
rake rubocopreports no offenses. - Running
rake steepreports no new warnings or errors. - Tests cover the behavior and its interactions. 100% coverage is not enough, as it does not guarantee that all code paths are tested.
- Documentation is up-to-date: generate it with
rake docsand read it. - "CHANGELOG.md" lists the change if it has impact on users.
- "README.md" is updated if the feature should be visible there.
License
This gem is available as open source under the terms of the MIT License, see LICENSE.txt.