class Numerals::Rounding
Constants
- DEFAULTS
- ZERO_DIGITS
Attributes
Public Class Methods
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
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
# File lib/numerals/rounding.rb, line 72 def base=(v) @base = v end
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
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
# File lib/numerals/rounding.rb, line 157 def full? preserving? end
# File lib/numerals/rounding.rb, line 107 def inspect to_s end
# File lib/numerals/rounding.rb, line 76 def mode=(mode) @mode = mode end
# 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
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
# File lib/numerals/rounding.rb, line 86 def places=(v) @places = v @precision = nil if @places end
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
# File lib/numerals/rounding.rb, line 80 def precision=(v) @precision = v @precision = :simplify if v == 0 @places = nil if @precision end
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
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 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
# File lib/numerals/rounding.rb, line 145 def short? simplifying? end
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
# 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 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
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
# 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
# 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
# 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
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 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