lib/bases/number.rb
# encoding: UTF-8
# The main class provided by Bases. It represents a base-agnostic
# number which can be forced to be interpreted in any base and then converted in
# any base.
class Bases::Number
# Create a new instance from a given `value`. If that value is an intance of a
# subclass of `Integer` (a `Bignum` or a `Fixnum`), it will be assumed to be
# in base 10 and any call to `in_base` on the new instance will result in an
# exception.
#
# If it is a `String`, no base will be assumed; if there are spaces
# in the string, they're used to separate the *multicharacter* digits,
# otherwise each character is assumed to be a digit.
#
# It `value` is an `Array`, each element in the array is assumed to be a
# digit. The array is read like a normal number, where digits on the left are
# more significative than digits on the right.
#
# @param [Integer|String|Array] value
# @raise [InvalidValueError] if `value` isn't a `String`, an `Integer` or an
# `Array`
def initialize(value)
case value
when Integer
@source_base = helpers.base_to_array(10)
@value = value
when String
@digits = (value =~ /\s/) ? value.split : value.split('')
when Array
@digits = value.map(&:to_s)
else
fail Bases::InvalidValueError, "#{value} isn't a valid value"
end
end
# Set the source base of the current number. The `base` can be either a number
# (like 2 for binary or 16 for hexadecimal) or an array. In the latter case,
# the array is considered the whole base. For example, the binary base would
# be represented as `[0, 1]`; another binary base could be `[:a, :b]` and so
# on.
#
# **Note** that when the base is an integer up to 36, the native Ruby
# `Integer#to_i(base)` method is used for efficiency and clarity. However,
# this means that digits in a base 36 are numbers *and* letters, while digits
# in base 37 and more are only numbers (interpreted as multichar digits).
#
# `self` is returned in order to allow nice-looking chaining.
#
# @param [Integer|Array] base
# @return self
# @raise [WrongDigitsError] if there are digits in the previously specified
# value that are not present in `base`.
def in_base(base)
@source_base = helpers.base_to_array(base)
if native_ruby_base?(base)
@value = @digits.join('').to_i(base)
return self
end
# Make sure `@digits` contains only valid digits.
unless helpers.only_valid_digits?(@digits, @source_base)
fail Bases::WrongDigitsError,
"Some digits weren't in base #{base}"
end
@value = algorithms.convert_from_base(@digits, @source_base)
self
end
# Return a string representation of the current number in a `new_base`.
# A **string** representation is always returned, even if `new_base` is 10. If
# you're using base 10 and want an integer, just call `to_i` on the resulting
# string.
#
# @example With the default separator
# Number.new(3).to_base(2) #=> '11'
# @example With a different separator
# Number.new(3).to_base(2, separator: ' ~ ') #=> '1 ~ 1'
#
# @param [Integer|Array] new_base The same as in `in_base`
# @param [Hash] opts A small hash of options
# @option opts [bool] :array If true, return the result as an array of digits;
# otherwise, return a string. This defaults to `false`.
# @return [Array<String>|String]
# @raise [NoBaseSpecifiedError] if no source base was specified (either by
# passing an integer to the constructor or by using the `in_base` method)
def to_base(new_base, opts = {})
opts[:array] = false if opts[:array].nil?
separator = opts.fetch(:separator, "")
unless defined?(@source_base)
fail Bases::NoBaseSpecifiedError, 'No base was specified'
end
# Let's apply the base conversion algorithm, which returns an array of
# digits.
res = if native_ruby_base?(new_base)
@value.to_s(new_base).split('')
else
algorithms.convert_to_base(@value, helpers.base_to_array(new_base))
end
opts[:array] ? res : res.join(separator)
end
# This function assumes you want the output in base 10 and returns an integer
# instead of a string (which would be returned after a call to `to_base(10)`).
# This was introduced so that `to_i` is adapted to the standard of returning
# an integer (in base 10, as Ruby represents integers).
# @return [Integer]
def to_i
to_base(10).to_i
end
# Specify that the current number is in hexadecimal representation.
# @note If you want to parse an hexadecimal number ignoring case-sensitivity,
# you **can't** use `in_base(Bases::HEX)` since that assumes
# upper case digits. You **have** to use `in_hex`, which internally just
# calls `String#hex`.
# @return [self]
def in_hex
@source_base = helpers.base_to_array(16)
@value = @digits.join('').hex
self
end
# Specify that the current number is in binary representation. This is just a
# shortcut for `in_base(2)` or `in_base([0, 1])`.
# @return [self]
def in_binary
in_base(2)
end
# Just an alias to `to_base(2)`.
# @see Number#to_base
# @return [String]
def to_binary
to_base(2)
end
# Just an alias to `to_base(Bases::HEX)`.
# @see Numer#to_base
# @return [String]
def to_hex
to_base(16)
end
private
def native_ruby_base?(base)
base.is_a?(Integer) && base.between?(2, 36)
end
# A facility method for accessing the `Algorithms` module.
def algorithms
Bases::Algorithms
end
# A facility method for accessing the `Helpers` module.
def helpers
Bases::Helpers
end
end