class Numerals::Numeral

A Numeral represents a numeric value as a sequence of digits (possibly repeating) in some numeric base.

A numeral can have a special value (infinity or not-a-number).

A non-special numeral is defined by:

A Numeral is equivalent to a Rational number; a quotient of integers can be converted to a Numeral in any base and back to a quotient without altering its value (although the fraction might be simplified).

By default a Numeral represents an exact quantity (rational number). A numeral can also represent an approximate value with a specific precision: the number of significant digits (numeral.digits.size), which can include significant trailing zeros. Approximate numerals are never repeating.

Exact numerals are always repeating, but, when the repeating digits are just zeros, the repeating? method returns false.

Attributes

digits[RW]
point[RW]
radix[RW]
repeat[RW]
sign[RW]
special[RW]

Public Class Methods

approximate_normalization() click to toggle source
# File lib/numerals/numeral.rb, line 222
def self.approximate_normalization
  { remove_extra_reps: false, remove_trailing_zeros: false, remove_leading_zeros: true, force_repeat: false }
end
exact_normalization() click to toggle source
# File lib/numerals/numeral.rb, line 226
def self.exact_normalization
  { remove_extra_reps: true, remove_trailing_zeros: true, remove_leading_zeros: true, force_repeat: true }
end
from_coefficient_scale(coefficient, scale, options={}) click to toggle source
# File lib/numerals/numeral.rb, line 510
def self.from_coefficient_scale(coefficient, scale, options={})
  radix = options[:base] || options[:radix] || 10
  if coefficient < 0
    sign = -1
    coefficient = -coefficient
  else
    sign = +1
  end
  digits = Digits[base: radix]
  digits.value = coefficient
  point = scale + digits.size
  normalization = options[:normalize] || :exact
  normalization = :approximate if options[:approximate]
  Numeral[digits, base: radix, point: point, sign: sign, normalize: normalization]
end
from_quotient(*args) click to toggle source

Create a Numeral from a quotient (Rational number). The quotient can be passed as an Array [numerator, denomnator]; to allow fractions with a zero denominator (representing indefinite or infinite numbers).

# File lib/numerals/numeral.rb, line 401
def self.from_quotient(*args)
  r = args.shift
  if Integer === args.first
    r = [r, args.shift]
  end
  options = args.shift || {}
  raise "Invalid number of arguments" unless args.empty?
  max_d = options.delete(:maximum_number_of_digits) || Numeral.maximum_number_of_digits
  if Rational === r
    x, y = r.numerator, r.denominator
  else
    x, y = r
  end
  return integer(x, options) if (x == 0 && y != 0) || y == 1

  radix = options[:base] || options[:radix] || 10

  xy_sign = x == 0 ? 0 : x < 0 ? -1 : +1
  xy_sign = -xy_sign if y < 0
  x = x.abs
  y = y.abs

  digits = Digits[base: radix]
  repeat = nil
  special = nil

  if y == 0
    if x == 0
      special = :nan
    else
      special = :inf
    end
  end

  return Numeral[special, sign: xy_sign] if special

  point = 1
  k = {}
  i = 0

  while (z = y*radix) < x
    y = z
    point += 1
  end

  while x > 0 && (max_d <= 0 || i < max_d)
    break if repeat = k[x]
    k[x] = i
    d, x = x.divmod(y)
    x *= radix
    digits.push d
    i += 1
  end

  while digits.size > 1 && digits.first == 0
    digits.shift
    repeat -= 1 if repeat
    point -= 1
  end

  Numeral[digits, sign: xy_sign, repeat: repeat, point: point]
end
indeterminate() click to toggle source
# File lib/numerals/numeral.rb, line 375
def self.indeterminate
  nan
end
infinity(sign=+1) click to toggle source
# File lib/numerals/numeral.rb, line 367
def self.infinity(sign=+1)
  Numeral[:inf, sign: sign]
end
integer(x, options={}) click to toggle source
# File lib/numerals/numeral.rb, line 379
def self.integer(x, options={})
  base = options[:base] || options[:radix] || 10
  if x == 0
    # we also could conventionally keep 0 either as Digits[[], ...]
    digits = Digits[0, base: base]
    sign = +1
  else
    if x < 0
      sign = -1
      x = -x
    else
      sign = +1
    end
    digits = Digits[value: x, base: base]
  end
  Numeral[digits, sign: sign]
end
maximum_number_of_digits() click to toggle source

Return the maximum number of digits that Numeral objects can handle.

# File lib/numerals/numeral.rb, line 50
def self.maximum_number_of_digits
  @maximum_number_of_digits
end
maximum_number_of_digits=(n) click to toggle source

Change the maximum number of digits that Numeral objects can handle.

# File lib/numerals/numeral.rb, line 44
def self.maximum_number_of_digits=(n)
  @maximum_number_of_digits = [n, 2048].max
end
nan() click to toggle source
# File lib/numerals/numeral.rb, line 371
def self.nan
  Numeral[:nan]
end
negative_infinity() click to toggle source
# File lib/numerals/numeral.rb, line 363
def self.negative_infinity
  Numeral[:inf, sign: -1]
end
new(*args) click to toggle source

Special numerals may be constructed with the symbols :nan, :infinity, :negative_infinity, :positive_infinity (or with :infinity and the :sign option which should be either +1 or -1)

Examples:

Numeral[:nan]
Numeral[:infinity, sign: -1]

For non-special numerals, the first argument may be a Digits object or an Array of digits, and the remaining parameters (:base, :sign, :point and :repeat) are passed as options.

Examples:

Numeral[1,2,3, base: 10, point: 1] # 1.23
Numeral[1,2,3,4, point: 1, repeat: 2] # 1.234343434...

The :normalize option can be used to specify the kind of normalization to be applied to the numeral:

  • :exact, the default, produces a normalized :exact number where no trailing zeros are kept and there is always a repeat point (which may just repeat trailing zeros)

  • :approximate produces a non-repeating numeral with a fixed number of digits (where trailing zeros are significant)

  • false or nil will not normalize the result, mantaining the digits and repeat values passed.

# File lib/numerals/numeral.rb, line 83
def initialize(*args)
  if Hash === args.last
    options = args.pop
  else
    options = {}
  end
  options = { normalize: :exact }.merge(options)
  normalize = options.delete(:normalize)
  @point  = nil
  @repeat = nil
  @sign   = nil
  @radix  = options[:base] || options[:radix] || 10
  if args.size == 1 && Symbol === args.first
    @special = args.first
    case @special
    when :positive_infinity
      @special = :inf
      @sign = +1
    when :negative_infinity
      @special = :inf
      @sign = -1
    when :infinity
      @special = :inf
    end
  elsif args.size == 1 && Digits === args.first
    @digits = args.first
    @radix = @digits.radix || @radix
  elsif args.size == 1 && Array === args.first
    @digits = Digits[args.first, base: @radix]
  else
    if args.any? { |v| Symbol === v }
      @digits = Digits[base: @radix]
      args.each do |v|
        case v
        when :point
          @point = @digits.size
        when :repeat
          @repeat = @digits.size
        else # when Integer
          @digits.push v
        end
      end
    elsif args.size > 0
      @digits = Digits[args, base: @radix]
    end
  end
  if options[:value]
    @digits = Digits[value: options[:value], base: @radix]
  end
  @sign    ||= options[:sign] || +1
  @special ||= options[:special]
  unless @special
    @point   ||= options[:point]  || @digits.size
    @repeat  ||= options[:repeat] || @digits.size
  end
  case normalize
  when :exact
    normalize! Numeral.exact_normalization
  when :approximate
    normalize! Numeral.approximate_normalization
  when Hash
    normalize! normalize
  end
end
positive_infinity() click to toggle source
# File lib/numerals/numeral.rb, line 359
def self.positive_infinity
  Numeral[:inf, sign: +1]
end
zero(options={}) click to toggle source
# File lib/numerals/numeral.rb, line 355
def self.zero(options={})
  integer 0, options
end

Public Instance Methods

-@() click to toggle source
# File lib/numerals/numeral.rb, line 347
def -@
  negated
end
approximate(number_of_digits = nil) click to toggle source
# File lib/numerals/numeral.rb, line 645
def approximate(number_of_digits = nil)
  dup.approximate! number_of_digits
end
approximate!(number_of_digits = nil) click to toggle source

Expand to the specified number of digits, then truncate and remove repetitions. If no number of digits is given, then it will be converted to approximate numeral only if it is not repeating.

# File lib/numerals/numeral.rb, line 632
def approximate!(number_of_digits = nil)
  if number_of_digits.nil?
    if exact? && !repeating?
      @repeat = nil
    end
  else
    expand! number_of_digits
    @digits.truncate! number_of_digits
    @repeat = nil
  end
  self
end
approximate?() click to toggle source

An approximate Numeral has limited precision (number of significant digits). In an approximate Numeral, trailing zeros are significant.

# File lib/numerals/numeral.rb, line 605
def approximate?
  !exact?
end
base() click to toggle source
# File lib/numerals/numeral.rb, line 150
def base
  @radix
end
base=(b) click to toggle source
# File lib/numerals/numeral.rb, line 154
def base=(b)
  @radix = b
end
digit_value_at(i) click to toggle source
# File lib/numerals/numeral.rb, line 207
def digit_value_at(i)
  if i < 0
    0
  elsif i < @digits.size
    @digits[i]
  elsif @repeat.nil? || @repeat >= @digits.size
    0
  else
    repeated_length = @digits.size - @repeat
    i = (i - @repeat) % repeated_length
    i += @repeat
    i < 0 ? 0 : @digits[i]
  end
end
dup() click to toggle source

Deep copy

Calls superclass method
# File lib/numerals/numeral.rb, line 332
def dup
  duped = super
  duped.digits = duped.digits.dup
  duped
end
exact() click to toggle source
# File lib/numerals/numeral.rb, line 653
def exact
  dup.exact!
end
exact!() click to toggle source
# File lib/numerals/numeral.rb, line 649
def exact!
  normalize! Numeral.exact_normalization
end
exact?() click to toggle source

An exact Numeral represents exactly a rational number. It always has a repeat position, although the repeated digits may all be zero.

# File lib/numerals/numeral.rb, line 599
def exact?
  !!@repeat
end
expand(minimum_number_of_digits) click to toggle source
# File lib/numerals/numeral.rb, line 623
def expand(minimum_number_of_digits)
  dup.expand! minimum_number_of_digits
end
expand!(minimum_number_of_digits) click to toggle source

Make sure the numeral has at least the given number of digits; This may denormalize the number.

# File lib/numerals/numeral.rb, line 611
def expand!(minimum_number_of_digits)
  if @repeat
    while @digits.size < minimum_number_of_digits
      @digits.push @digits[@repeat] || 0
      @repeat += 1
    end
  else
    @digits.push 0 while @digits.size < minimum_number_of_digits
  end
  self
end
indeterminate?() click to toggle source
# File lib/numerals/numeral.rb, line 170
def indeterminate?
  nan?
end
infinite?() click to toggle source
# File lib/numerals/numeral.rb, line 174
def infinite?
  @special == :inf
end
inspect() click to toggle source
# File lib/numerals/numeral.rb, line 592
def inspect
  to_s
end
nan?() click to toggle source
# File lib/numerals/numeral.rb, line 166
def nan?
  @special == :nan
end
negate!() click to toggle source
# File lib/numerals/numeral.rb, line 338
def negate!
  @sign = -@sign
  self
end
negated() click to toggle source
# File lib/numerals/numeral.rb, line 343
def negated
  dup.negate!
end
negative_infinite?() click to toggle source
# File lib/numerals/numeral.rb, line 182
def negative_infinite?
  @special == :inf && @sign == -1
end
nonrepeating?() click to toggle source
# File lib/numerals/numeral.rb, line 199
def nonrepeating?
  !special && !repeating?
end
normalize!(options = {}) click to toggle source
# File lib/numerals/numeral.rb, line 230
def normalize!(options = {})
  if @special
    if @special == :nan
      @sign = nil
    end
    @point = @repeat = nil
  else

    defaults = { remove_extra_reps: true, remove_trailing_zeros: true }
    options = defaults.merge(options)
    remove_trailing_zeros = options[:remove_trailing_zeros]
    remove_extra_reps = options[:remove_extra_reps]
    remove_leading_zeros = options[:remove_extra_reps]
    force_repeat = options[:force_repeat]

    # Remove unneeded repetitions
    if @repeat && remove_extra_reps
      rep_length = @digits.size - @repeat
      if rep_length > 0 && @digits.size >= 2*rep_length
        while @repeat > rep_length && @digits[@repeat, rep_length] == @digits[@repeat-rep_length, rep_length]
          @repeat -= rep_length
          @digits.replace @digits[0...-rep_length]
        end
      end
      # remove unneeded partial repetitions
      if rep_length > 0 && @digits.size > rep_length
        removed = 0
        while @repeat > 0 && @digits[@repeat-1] == @digits[@repeat-1+rep_length]
          @repeat -= 1
          removed += 1
        end
        @digits.replace @digits[0...-removed] if removed > 0
      end

    end

    # Replace 'nines' repetition 0.999... -> 1
    if @repeat && @repeat == @digits.size-1 && @digits[@repeat] == (@radix-1)
      @digits.pop
      @repeat = nil

      i = @digits.size - 1
      carry = 1
      while carry > 0 && i >= 0
        @digits[i] += carry
        carry = 0
        if @digits[i] > @radix
          carry = 1
          @digits[i] = 0
          @digits.pop if i == @digits.size
        end
        i -= 1
      end
      if carry > 0
        digits.unshift carry
        @point += 1
      end
    end

    # Remove zeros repetitions
    if remove_trailing_zeros
      if @repeat && @repeat >= @digits.size
        @repeat = @digits.size
      end
      if @repeat && @repeat >= 0
        unless @digits[@repeat..-1].any? { |x| x != 0 }
          @digits.replace @digits[0...@repeat]
          @repeat = nil
        end
      end
    end

    if force_repeat
      @repeat ||= @digits.size
    else
      @repeat = nil if @repeat && @repeat >= @digits.size
    end

    # Remove leading zeros
    if remove_leading_zeros
      # if all digits are zero, we consider all to be trailing zeros
      unless !remove_trailing_zeros && @digits.zero?
        while @digits.first == 0
          @digits.shift
          @repeat -= 1 if @repeat
          @point -= 1
        end
      end
    end

    # Remove trailing zeros
    if remove_trailing_zeros && !repeating?
      while @digits.last == 0
        @digits.pop
        @repeat -= 1 if @repeat
      end
    end
  end
  self
end
normalized(options={}) click to toggle source
# File lib/numerals/numeral.rb, line 351
def normalized(options={})
  dup.normalize! options
end
parameters() click to toggle source
# File lib/numerals/numeral.rb, line 550
def parameters
  if special?
    params = { special: @special }
    params.merge! sign: @sign if @special == :inf
  else
    params = {
      digits: @digits,
      sign:   @sign,
      point:  @point
    }
    params.merge! repeat: @repeat if @repeat
    if approximate?
      params.merge! normalize: :approximate
    end
  end
  params
end
positive_infinite?() click to toggle source
# File lib/numerals/numeral.rb, line 178
def positive_infinite?
  @special == :inf && @sign == +1
end
repeating?() click to toggle source
# File lib/numerals/numeral.rb, line 195
def repeating?
  !special? && @repeat && @repeat < @digits.size
end
repeating_position() click to toggle source

unlike the repeat attribute, this is nevel nil

# File lib/numerals/numeral.rb, line 191
def repeating_position
  @repeat || @digits.size
end
scale() click to toggle source
# File lib/numerals/numeral.rb, line 158
def scale
  @point - @digits.size
end
scale=(s) click to toggle source
# File lib/numerals/numeral.rb, line 203
def scale=(s)
  @point = s + @digits.size
end
special?() click to toggle source
# File lib/numerals/numeral.rb, line 162
def special?
  !!@special
end
split() click to toggle source
# File lib/numerals/numeral.rb, line 526
def split
  if @special || (@repeat && @repeat < @digits.size)
    raise NumeralError, "Numeral cannot be represented as sign, coefficient, scale"
  end
  [@sign, @digits.value, scale]
end
to_base(other_base) click to toggle source

Convert a Numeral to a different base

# File lib/numerals/numeral.rb, line 541
def to_base(other_base)
  if other_base == @radix
    dup
  else
    normalization = exact? ? :exact : :approximate
    Numeral.from_quotient to_quotient, base: other_base, normalize: normalization
  end
end
to_quotient() click to toggle source

Return a quotient (Rational) that represents the exact value of the numeral. The quotient is returned as an Array, so that fractions with a zero denominator can be handled (representing indefinite or infinite numbers).

# File lib/numerals/numeral.rb, line 467
def to_quotient
  if @special
    y = 0
    case @special
    when :nan
      x = 0
    when :inf
      x = @sign
    end
    return [x, y]
  end

  n = @digits.size
  a = 0
  b = a

  repeat = @repeat
  repeat = nil if repeat && repeat >= n

  for i in 0...n
    a *= @radix
    a += @digits[i]
    if repeat && i < repeat
      b *= @radix
      b += @digits[i]
    end
  end

  x = a
  x -= b if repeat

  y = @radix**(n - @point)
  y -= @radix**(repeat - @point) if repeat

  d = Numerals.gcd(x, y)
  x /= d
  y /= d

  x = -x if @sign < 0

  [x.to_i, y.to_i]
end
to_s() click to toggle source
# File lib/numerals/numeral.rb, line 568
def to_s
  case @special
  when :nan
    'Numeral[:nan]'
  when :inf
    if @sign < 0
      'Numeral[:inf, sign: -1]'
    else
      'Numeral[:inf]'
    end
  else
    args = ''
    if @digits.size > 0
      args = @digits.digits_array.to_s.unwrap('[]')
      args << ', '
    end
    params = parameters
    params.delete :digits
    params.merge! base: @radix
    args << params.to_s.unwrap('{}')
    "Numeral[#{args}]"
  end
end
to_value_scale() click to toggle source
# File lib/numerals/numeral.rb, line 533
def to_value_scale
  if @special || (@repeat && @repeat < @digits.size)
    raise NumeralError, "Numeral cannot be represented as value, scale"
  end
  [@digits.value*@sign, scale]
end
zero?() click to toggle source
# File lib/numerals/numeral.rb, line 186
def zero?
  !special? && @digits.zero?
end

Private Instance Methods

test_equal(other) click to toggle source
# File lib/numerals/numeral.rb, line 659
def test_equal(other)
  return false if other.nil? || !other.is_a?(Numeral)
  if self.special? || other.special?
    self.special == other.special && self.sign == other.sign
  else
    this = self.normalized
    that = other.normalized
    this.sign == that.sign && this.point == that.point && this.repeat == that.repeat && this.digits == that.digits
  end
end