class Numerals::Rounding

Rounding of Numerals

Constants

DEFAULTS
ZERO_DIGITS

Attributes

base[R]
mode[R]

Public Class Methods

new(*args) click to toggle source

Rounding defines a rounding mode and a precision, and is used to establish the desired accuracy of a Numeral result.

Rounding also defines the base of the numerals to be rounded, which is 10 by default.

The rounding mode is the rule used to limit the precision of a numeral; the rounding modes available are those of Flt::Num, namely:

  • :half_even

  • :half_up

  • :half_down

  • :ceiling

  • :floor

  • :up

  • :down

  • :up05

Regarding the rounding precision there are two types of Roundings:

  • Fixed (limited) precision: the precision of the rounded result is either defined as relative (number of significant digits defined by the precision property) or absolute (number of fractional places –decimals for base 10– defined by the places property)

  • Free (unlimited) precision, which preserves the value of the input numeral. As much precision as needed is used to keep unambiguously the original value. When applied to exact input, this kind of rounding doesn't perform any rounding. For approximate input there are two variants:

    • Preserving the original value precision, which produces and approximate output. (All original digits are preserved; full precision mode). This is the default free precision mode, established by using the :free symbol for the precision (or its synonym :preserve).

    • Simplifiying or reducing the result to produce an exact output without unneeded digits to restore the original value within its original precision (e.g. traling zeros are not keep). This case can be defined with the :short symbol for the precision (or its synonum :simplify).

# File lib/numerals/rounding.rb, line 47
def initialize(*args)
  DEFAULTS.each do |param, value|
    instance_variable_set "@#{param}", value
  end
  set! *args
end

Public Instance Methods

absolute?() click to toggle source

Returns true if the Rounding is of fixed precision defined as a number of fractional places, i.e. independently of the number to be rounded's magnitude.

# File lib/numerals/rounding.rb, line 126
def absolute?
  @precision.nil? # fixed? && @precision # !@places.nil?
end
base=(v) click to toggle source
# File lib/numerals/rounding.rb, line 72
def base=(v)
  @base = v
end
fixed?() click to toggle source

Returns true if the Rounding is of fixed (limited) precision.

# File lib/numerals/rounding.rb, line 119
def fixed? # limited? approximate? rounding? fixed?
  !free?
end
free?() click to toggle source

Returns true if the Rounding is of free (unlimited) precision, which can be either :free (preserving) or :short (simplifying) regarding approximate input.

# File lib/numerals/rounding.rb, line 114
def free? # unlimited? exact? all? nonrounding? free?
  [:free, :short].include?(@precision)
end
full?() click to toggle source
# File lib/numerals/rounding.rb, line 157
def full?
  preserving?
end
inspect() click to toggle source
# File lib/numerals/rounding.rb, line 107
def inspect
  to_s
end
mode=(mode) click to toggle source
# File lib/numerals/rounding.rb, line 76
def mode=(mode)
  @mode = mode
end
parameters() click to toggle source
# File lib/numerals/rounding.rb, line 91
def parameters
  if @precision
    { mode: @mode, precision: @precision, base: @base }
  else
    { mode: @mode, places: @places, base: @base }
  end
end
places(value = nil, options = {}) click to toggle source

Number of fractional places for a given numerical/numeral value If no value is passed, the :places property is returned.

# File lib/numerals/rounding.rb, line 183
def places(value = nil, options = {})
  if value.nil?
    @places
  elsif is_exact?(value, options)
    @places || 0
  elsif free?
    num_digits(value, options) - num_integral_digits(value)
  else # fixed?
    if absolute?
      @places
    else # relative?
      @precision - num_integral_digits(value)
    end
  end
end
places=(v) click to toggle source
# File lib/numerals/rounding.rb, line 86
def places=(v)
  @places = v
  @precision = nil if @places
end
precision(value = nil, options = {}) click to toggle source

Number of significant digits for a given numerical/numeral value. If no value is passed, the :precision property is returned.

# File lib/numerals/rounding.rb, line 163
def precision(value = nil, options = {})
  if value.nil?
    @precision
  elsif free?
    if is_exact?(value, options)
      0
    else
      num_digits(value, options)
    end
  else # fixed?
    if absolute?
      @places + num_integral_digits(value)
    else # relative?
      @precision
    end
  end
end
precision=(v) click to toggle source
# File lib/numerals/rounding.rb, line 80
def precision=(v)
  @precision = v
  @precision = :simplify if v == 0
  @places = nil if @precision
end
preserving?() click to toggle source

Returns true if the Rounding is of free precision and the behaviour for approximate numbers is to keep its original precision (so it may include trailing zeros) and the result of rounding will be an approximate numeral.

# File lib/numerals/rounding.rb, line 153
def preserving?
  @precision == :free
end
relative?() click to toggle source

Returns true if the Rounding is of fixed precision defined as a number of significant digits (precision attribute), i.e. in relation to the number to be rounded's magnitude.

# File lib/numerals/rounding.rb, line 133
def relative?
  fixed? && !absolute?
end
round(numeral, options={}) click to toggle source

Round a numeral. If the numeral has been truncated the :round_up option must be used to pass the information about the discarded digits:

  • nil if all discarded digits where 0 (the truncated value is exact)

  • :lo if there where non-zero discarded digits, but the first discarded digit is below half the base.

  • :tie if the first discarded was half the base and there where no more nonzero digits, i.e. the original value was a 'tie', exactly halfway between the truncated value and the next value with the same number of digits.

  • :hi if the original value was above the tie value.

# File lib/numerals/rounding.rb, line 209
def round(numeral, options={})
  round_up = options[:round_up]
  numeral, round_up = truncate(numeral, round_up)
  if numeral.exact?
    numeral
  else
    adjust(numeral, round_up)
  end
end
short?() click to toggle source
# File lib/numerals/rounding.rb, line 145
def short?
  simplifying?
end
simplifying?() click to toggle source

Returns true if the Rounding is of free precision and the behaviour for approximate numbers is producing a simplified (short) result with only the needed digits to restore the original value within its precision.

# File lib/numerals/rounding.rb, line 141
def simplifying?
  @precision == :short
end
to_s() click to toggle source
# File lib/numerals/rounding.rb, line 99
def to_s
  params = parameters
  DEFAULTS.each do |param, default|
    params.delete param if params[param] == default
  end
  "Rounding[#{params.inspect.unwrap('{}')}]"
end

Private Instance Methods

adjust(numeral, round_up) click to toggle source

Adjust a truncated numeral using the round-up information

# File lib/numerals/rounding.rb, line 293
def adjust(numeral, round_up)
  check_base numeral
  point, digits = Flt::Support.adjust_digits(
    numeral.point, numeral.digits.digits_array,
    round_mode: @mode,
    negative: numeral.sign == -1,
    round_up: round_up,
    base: numeral.base
  )
  if numeral.zero? && simplifying?
    digits = []
    point = 0
  end
  normalization = simplifying? ? :exact : :approximate
  Numeral[digits, point: point, base: numeral.base, sign: numeral.sign, normalize: normalization]
end
check_base(numeral) click to toggle source

Note: since Rounding has no mutable attributes, default dup is OK otherwise we'd need to redefine it: def dup

Rounding[parameters]

end

# File lib/numerals/rounding.rb, line 227
def check_base(numeral)
  if numeral.base != @base
    raise "Invalid Numeral (base #{numeral.base}) for a base #{@base} Rounding"
  end
end
extract_options(*args) click to toggle source
# File lib/numerals/rounding.rb, line 362
def extract_options(*args)
  options = {}
  args = args.first if args.size == 1 && args.first.kind_of?(Array)
  args.each do |arg|
    case arg
    when Hash
      options.merge! arg
    when :short, :simplify
      options.merge! precision: :short
    when :free, :preserve
      options.merge! precision: :free
    when Symbol
      options[:mode] = arg
    when Integer
      options[:precision] = arg
    when Rounding
      options.merge! arg.parameters
    else
      raise "Invalid Rounding definition"
    end
  end
  options
end
is_exact?(value, options={}) click to toggle source
# File lib/numerals/rounding.rb, line 353
def is_exact?(value, options={})
  case value
  when Numeral
    value.exact?
  else
    Conversions.exact?(value, options)
  end
end
num_digits(value, options) click to toggle source
# File lib/numerals/rounding.rb, line 331
def num_digits(value, options)
  case value
  when 0
    ZERO_DIGITS
  when Numeral
    if value.zero?
      ZERO_DIGITS
    else
      if @base != value.base
        value = value.to_base(@base)
      end
      if value.repeating?
        0
      else
        value.digits.size
      end
    end
  else
    Conversions.number_of_digits(value, options.merge(base: @base))
  end
end
num_integral_digits(value) click to toggle source

Number of digits in the integer part of the value (excluding leading zeros).

# File lib/numerals/rounding.rb, line 313
def num_integral_digits(value)
  case value
  when 0
    ZERO_DIGITS
  when Numeral
    if value.zero?
      ZERO_DIGITS
    else
      if @base != value.base
        value = value.to_base(@base)
      end
      value.normalized(remove_trailing_zeros: true).point
    end
  else
    Conversions.order_of_magnitude(value, base: @base)
  end
end
truncate(numeral, round_up=nil) click to toggle source

Truncate a numeral and return also a round_up value with information about the digits beyond the truncation point that can be used to round the truncated numeral. If the numeral has already been truncated, the round_up result of that prior truncation should be passed as the second argument.

# File lib/numerals/rounding.rb, line 239
def truncate(numeral, round_up=nil)
  check_base numeral
  unless simplifying? # TODO: could simplify this just skiping on free?
    n = precision(numeral)
    if n == 0
      return numeral if numeral.repeating? # or rails inexact...
      n = numeral.digits.size
    end
    unless n >= numeral.digits.size && numeral.approximate?
      if n < numeral.digits.size - 1
        rest_digits = numeral.digits[n+1..-1]
      else
        rest_digits = []
      end
      if numeral.repeating? && numeral.repeat < numeral.digits.size && n >= numeral.repeat
        rest_digits += numeral.digits[numeral.repeat..-1]
      end
      digits = numeral.digits[0, n]
      if digits.size < n
        digits += (digits.size...n).map{|i| numeral.digit_value_at(i)}
      end
      if numeral.base % 2 == 0
        tie_digit = numeral.base / 2
        max_lo = tie_digit - 1
      else
        max_lo = numeral.base / 2
      end
      next_digit = numeral.digit_value_at(n)
      if next_digit == 0
        unless round_up.nil? && rest_digits.all?{|d| d == 0}
          round_up = :lo
        end
      elsif next_digit <= max_lo # next_digit < tie_digit
        round_up = :lo
      elsif next_digit == tie_digit
        if round_up || rest_digits.any?{|d| d != 0}
          round_up = :hi
        else
          round_up = :tie
        end
      else # next_digit > tie_digit
        round_up = :hi
      end
      numeral = Numeral[
                  digits, point: numeral.point, sign: numeral.sign,
                  base: numeral.base,
                  normalize: :approximate
                ]
    end
  end
  [numeral, round_up]
end