Class: Flt::Support::Formatter
Overview
Burger and Dybvig free formatting algorithm, from their paper: “Printing Floating-Point Numbers Quickly and Accurately” (Robert G. Burger, R. Kent Dybvig)
This algorithm formats arbitrary base floating point numbers as decimal text literals. The floating-point (with fixed precision) is interpreted as an approximate value, representing any value in its ‘rounding-range’ (the interval where all values round to the floating-point value, with the given precision and rounding mode). An alternative approach which is not taken here would be to represent the exact floating-point value with some given precision and rounding mode requirements; that can be achieved with Clinger algorithm (which may fail for exact precision).
The variables used by the algorithm are stored in instance variables: Quotients of integers will be used to hold the magnitudes: All numbers in the randound-range are rounded to @v (with the given precision p) significant digit right after the radix point. @b**@k is the first power of @b >= u
The rounding range of @v is the interval of values that round to @v under the runding-mode. If the rounding mode is one of the round-to-nearest variants (even, up, down), then it is ((v+v-)/2 = (@[email protected]_m)/@s, (v+v+)/2 = (@[email protected]_)/2) whith the boundaries open or closed as explained below. In this case:
@m_m/@s = (@v - (v + v-)/2) where v- = @v.next_minus is the lower adjacent to v floating point value
@m_p/@s = ((v + v+)/2 - @v) where v+ = @v.next_plus is the upper adjacent to v floating point value
If the rounding is directed, then the rounding interval is either (v-, @v] or [@v, v+] if @roundh, then @k is the minimum @k with (@[email protected]_p)/@s <= @output_b**@k
@k = ceil(logB((@[email protected]_p)/2)) with lobB the @output_b base logarithm
if @roundh, then @k is the minimum @k with (@[email protected]_p)/@s < @output_b**@k
@k = 1+floor(logB((@r+@m_p)/2))
p is the input floating point precision
Constant Summary collapse
- ITERATIONS_BEFORE_KEEPING_TRACK_OF_REMAINDERS =
10000
- ESTIMATE_FLOAT_LOG_B =
{2=>1/Math.log(2), 10=>1/Math.log(10), 16=>1/Math.log(16)}
Instance Attribute Summary collapse
-
#repeat ⇒ Object
readonly
Returns the value of attribute repeat.
-
#round_up ⇒ Object
readonly
Returns the value of attribute round_up.
Instance Method Summary collapse
-
#adjusted_digits(round_mode) ⇒ Object
Access rounded result of format operation: scaling (position of radix point) and digits.
- #b_power(n) ⇒ Object
-
#detect_repetitions(r) ⇒ Object
Detect indefinite repetitions in generate_max returns the number of digits that are being repeated (0 indicates the next digit would repeat and it would be a zero).
-
#digits ⇒ Object
Access result of format operation: scaling (position of radix point) and digits.
-
#format(v, f, e, round_mode, p = nil, all = false) ⇒ Object
This method converts v = f*b**e into a sequence of
output_b
-base digits, so that if the digits are converted back to a floating-point value of precision p (correctly rounded), the result is exactly v. - #generate ⇒ Object
- #generate_max ⇒ Object
-
#initialize(input_b, input_min_e, output_b, options = {}) ⇒ Formatter
constructor
A Formatted object is created to format floating point numbers given: * The input base in which numbers to be formatted are defined * The input minimum exponent * The output base to which the input is converted.
- #output_b_power(n) ⇒ Object
- #remove_redundant_repetitions ⇒ Object
-
#scale! ⇒ Object
using local vars instead of instance vars: it makes a difference in performance.
-
#scale_fixup! ⇒ Object
fix up scaling (final step): specialized version of scale! This performs a single up scaling step, i.e.
-
#scale_optimized! ⇒ Object
scale_o1! is an optimized version of scale!; it requires an additional parameters with the floating-point number v=r/s.
-
#scale_original!(really = false) ⇒ Object
Given r/s = v (number to convert to text), m_m/s = (v - v-)/s, m_p/s = (v+ - v)/s Scale the fractions so that the first significant digit is right after the radix point, i.e.
- #start_repetition_dectection ⇒ Object
Constructor Details
#initialize(input_b, input_min_e, output_b, options = {}) ⇒ Formatter
A Formatted object is created to format floating point numbers given:
-
The input base in which numbers to be formatted are defined
-
The input minimum exponent
-
The output base to which the input is converted.
-
The :raise_on_repeat option, true by default specifies that when an infinite sequence of repeating significant digits is found on the output (which may occur when using the all-digits options and using directed-rounding) an InfiniteLoopError exception is raised. If this option is false, then no exception occurs, and instead of generating an infinite sequence of digits, the formatter object will have a ‘repeat’ property which designs the first digit to be repeated (it is an index into digits). If this equals the size of digits, it is assumend, that the digit to be repeated is a zero which follows the last digit present in digits.
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/flt/support/formatter.rb', line 70 def initialize(input_b, input_min_e, output_b, ={}) @b = input_b @min_e = input_min_e @output_b = output_b # result of last operation @adjusted_digits = @digits = nil # for "all-digits" mode results (which are truncated, rather than rounded), # round_up contains information to round the result: # * it is nil if the rest of digits are zero (the result is exact) # * it is :lo if there exist non-zero digits beyond the significant ones (those returned), but # the value is below the tie (the value must be rounded up only for :up rounding mode) # * it is :tie if there exists exactly one nonzero digit after the significant and it is radix/2, # for round-to-nearest it is atie. # * it is :hi otherwise (the value should be rounded-up except for the :down mode) @round_up = nil = { :raise_on_repeat => true }.merge() # when significant repeating digits occur (+all+ parameter and directed rounding) # @repeat is set to the index of the first repeating digit in @digits; # (if equal to @digits.size, that would indicate an infinite sequence of significant zeros) @repeat = nil # the :raise_on_repeat options (by default true) causes exceptions when repeating is found @raise_on_repeat = [:raise_on_repeat] end |
Instance Attribute Details
#repeat ⇒ Object (readonly)
Returns the value of attribute repeat.
232 233 234 |
# File 'lib/flt/support/formatter.rb', line 232 def repeat @repeat end |
#round_up ⇒ Object (readonly)
Returns the value of attribute round_up.
232 233 234 |
# File 'lib/flt/support/formatter.rb', line 232 def round_up @round_up end |
Instance Method Details
#adjusted_digits(round_mode) ⇒ Object
Access rounded result of format operation: scaling (position of radix point) and digits
235 236 237 238 239 240 241 242 243 244 245 |
# File 'lib/flt/support/formatter.rb', line 235 def adjusted_digits(round_mode) if @adjusted_digits.nil? && !@digits.nil? @adjusted_k, @adjusted_digits = Support.adjust_digits( @k, @digits, :round_mode => round_mode, :negative => @minus, :round_up => @round_up, :base => @output_b) end return @adjusted_k, @adjusted_digits end |
#b_power(n) ⇒ Object
294 295 296 |
# File 'lib/flt/support/formatter.rb', line 294 def b_power(n) @b**n end |
#detect_repetitions(r) ⇒ Object
Detect indefinite repetitions in generate_max returns the number of digits that are being repeated (0 indicates the next digit would repeat and it would be a zero)
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 |
# File 'lib/flt/support/formatter.rb', line 313 def detect_repetitions(r) return nil unless @may_repeat @n_iters += 1 if r == 0 && @m_p == 0 repeat_count = 0 elsif (@n_iters > ITERATIONS_BEFORE_KEEPING_TRACK_OF_REMAINDERS) if @rs.include?(r) repeat_count = @rs.index(r) - @rs.size else @rs << r end end if repeat_count raise InfiniteLoopError, "Infinite digit sequence." if @raise_on_repeat repeat_count else nil end end |
#digits ⇒ Object
Access result of format operation: scaling (position of radix point) and digits
228 229 230 |
# File 'lib/flt/support/formatter.rb', line 228 def digits return @k, @digits end |
#format(v, f, e, round_mode, p = nil, all = false) ⇒ Object
This method converts v = f*b**e into a sequence of output_b
-base digits, so that if the digits are converted back to a floating-point value of precision p (correctly rounded), the result is exactly v.
If round_mode
is not nil, then just enough digits to produce v using that rounding is used; otherwise enough digits to produce v with any rounding are delivered.
If the all
parameter is true, all significant digits are generated without rounding, Significant digits here are all digits that, if used on input, cannot arbitrarily change while preserving the parsed value of the floating point number. Since the digits are not rounded more digits may be needed to assure round-trip value preservation.
This is useful to reflect the precision of the floating point value in the output; in particular trailing significant zeros are shown. But note that, for directed rounding and base conversion this may need to produce an infinite number of digits, in which case an exception will be raised unless the :raise_on_repeat option has been set to false in the Formatter object. In that case the formatter objetct will have a repeat
property that specifies the point in the digit sequence where irepetition starts. The digits from that point to the end to the digits sequence repeat indefinitely.
This digit-repetition is specially frequent for the :up rounding mode, in which any number with a finite numberof nonzero digits equal to or less than the precision will haver and infinite sequence of zero significant digits.
The:down rounding (truncation) could be used to show the exact value of the floating point but beware: if the value has not an exact representation in the output base this will lead to an infinite loop or repeating squence.
When the all
parameters is used the result is not rounded (is truncated), and the round_up flag is set to indicate that nonzero digits exists beyond the returned digits; the possible values of the round_up flag are:
-
nil : the rest of digits are zero or repeat (the result is exact)
-
:lo : there exist non-zero digits beyond the significant ones (those returned), but the value is below the tie (the value must be rounded up only for :up rounding mode)
-
:tie : there exists exactly one nonzero digit after the significant and it is radix/2, for round-to-nearest it is atie.
-
:hi : the value is closer to the rounded-up value (incrementing the last significative digit.)
Note that the round_mode here is not the rounding mode applied to the output; it is the rounding mode that applied to input preserves the original floating-point value (with the same precision as input). should be rounded-up.
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'lib/flt/support/formatter.rb', line 139 def format(v, f, e, round_mode, p=nil, all=false) context = v.class.context # TODO: consider removing parameters f,e and using v.split instead @minus = (context.sign(v)==-1) @v = context.copy_sign(v, +1) # don't use context.abs(v) because it rounds (and may overflow also) @f = f.abs @e = e @round_mode = round_mode @all_digits = all p ||= context.precision # adjust the rounding mode to work only with positive numbers @round_mode = Support.simplified_round_mode(@round_mode, @minus) # determine the high,low inclusion flags of the rounding limits case @round_mode when :half_even # rounding rage is (v-m-,v+m+) if v is odd and [v+m-,v+m+] if even @round_l = @round_h = ((@f % 2) == 0) when :up # rounding rage is (v-,v] # ceiling is treated here assuming f>0 @round_l, @round_h = false, true when :down # rounding rage is [v,v+) # floor is treated here assuming f>0 @round_l, @round_h = true, false when :half_up # rounding rage is [v+m-,v+m+) @round_l, @round_h = true, false when :half_down # rounding rage is (v+m-,v+m+] @round_l, @round_h = false, true else # :nearest # Here assume only that round-to-nearest will be used, but not which variant of it # The result is valid for any rounding (to nearest) but may produce more digits # than stricly necessary for specific rounding modes. # That is, enough digits are generated so that when the result is # converted to floating point with the specified precision and # correct rounding (to nearest), the result is the original number. # rounding range is (v+m-,v+m+) @round_l = @round_h = false end # TODO: use context.next_minus, next_plus instead of direct computing, don't require min_e & ps # Now compute the working quotients @r/@s, @m_p/@s = (v+ - @v), @m_m/@s = (@v - v-) and scale them. if @e >= 0 if @f != b_power(p-1) be = b_power(@e) @r, @s, @m_p, @m_m = @f*be*2, 2, be, be else be = b_power(@e) be1 = be*@b @r, @s, @m_p, @m_m = @f*be1*2, @b*2, be1, be end else if @e == @min_e || @f != b_power(p-1) @r, @s, @m_p, @m_m = @f*2, b_power(-@e)*2, 1, 1 else @r, @s, @m_p, @m_m = @f*@b*2, b_power(1-@e)*2, @b, 1 end end @k = 0 @context = context scale_optimized! # The value to be formatted is @v=@r/@s; m- = @m_m/@s = (@v - v-)/@s; m+ = @m_p/@s = (v+ - @v)/@s # Now adjust @m_m, @m_p so that they define the rounding range case @round_mode when :up # ceiling is treated here assuming @f>0 # rounding range is -v,@v @m_m, @m_p = @m_m*2, 0 when :down # floor is treated here assuming #f>0 # rounding range is @v,v+ @m_m, @m_p = 0, @m_p*2 else # rounding range is v-,v+ # @m_m, @m_p = @m_m, @m_p end # Now m_m, m_p define the rounding range all ? generate_max : generate end |
#generate ⇒ Object
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 |
# File 'lib/flt/support/formatter.rb', line 393 def generate list = [] r, s, m_p, m_m, = @r, @s, @m_p, @m_m loop do d,r = (r*@output_b).divmod(s) m_p *= @output_b m_m *= @output_b tc1 = @round_l ? (r<=m_m) : (r<m_m) tc2 = @round_h ? (r+m_p >= s) : (r+m_p > s) if not tc1 if not tc2 list << d else list << d+1 break end else if not tc2 list << d break else if r*2 < s list << d break else list << d+1 break end end end end @digits = list end |
#generate_max ⇒ Object
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 |
# File 'lib/flt/support/formatter.rb', line 352 def generate_max @round_up = false list = [] r, s, m_p, m_m, = @r, @s, @m_p, @m_m start_repetition_dectection loop do if repeat_count = detect_repetitions(r) @repeat = list.size + repeat_count break end d,r = (r*@output_b).divmod(s) m_p *= @output_b m_m *= @output_b list << d tc1 = @round_l ? (r<=m_m) : (r<m_m) tc2 = @round_h ? (r+m_p >= s) : (r+m_p > s) if tc1 && tc2 if r != 0 r *= 2 if r > s @round_up = :hi elsif r == s @round_up = :tie else @rund_up = :lo end end break end end @digits = list remove_redundant_repetitions end |
#output_b_power(n) ⇒ Object
298 299 300 |
# File 'lib/flt/support/formatter.rb', line 298 def output_b_power(n) @output_b**n end |
#remove_redundant_repetitions ⇒ Object
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 |
# File 'lib/flt/support/formatter.rb', line 333 def remove_redundant_repetitions if ITERATIONS_BEFORE_KEEPING_TRACK_OF_REMAINDERS > 0 && @repeat if @repeat < @digits.size repeating_digits = @digits[@repeat..-1] l = repeating_digits.size pos = @repeat - l while pos >= 0 && @digits[pos, l] == repeating_digits pos -= l end first_repeat = pos + l if first_repeat < @repeat @repeat = first_repeat @digits = @digits[0, @repeat+l] end end end @digits end |
#scale! ⇒ Object
using local vars instead of instance vars: it makes a difference in performance
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 |
# File 'lib/flt/support/formatter.rb', line 272 def scale! r, s, m_p, m_m, k,output_b = @r, @s, @m_p, @m_m, @k,@output_b loop do if (@round_h ? (r+m_p >= s) : (r+m_p > s)) # k is too low s *= output_b k += 1 elsif (@round_h ? ((r+m_p)*output_b<s) : ((r+m_p)*output_b<=s)) # k is too high r *= output_b m_p *= output_b m_m *= output_b k -= 1 else @s = s @r = r @m_p = m_p @m_m = m_m @k = k break end end end |
#scale_fixup! ⇒ Object
fix up scaling (final step): specialized version of scale! This performs a single up scaling step, i.e. behaves like scale2, but the input must be at most one step down from the final result
503 504 505 506 507 508 |
# File 'lib/flt/support/formatter.rb', line 503 def scale_fixup! if (@round_h ? (@r+@m_p >= @s) : (@r+@m_p > @s)) # too low? @s *= @output_b @k += 1 end end |
#scale_optimized! ⇒ Object
scale_o1! is an optimized version of scale!; it requires an additional parameters with the floating-point number v=r/s
It uses a Float estimate of ceil(logB(v)) that may need to adjusted one unit up TODO: find easy to use estimate; determine max distance to correct value and use it for fixing,
or use the general scale! for fixing (but remembar to multiply by exptt(...))
(determine when Math.log is aplicable, etc.)
437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 |
# File 'lib/flt/support/formatter.rb', line 437 def scale_optimized! context = @context # @v.class.context return scale! if context.zero?(@v) # 1. compute estimated_scale # 1.1. try to use Float logarithms (Math.log) v = @v v_abs = context.copy_sign(v, +1) # don't use v.abs because it rounds (and may overflow also) v_flt = v_abs.to_f b = @output_b log_b = ESTIMATE_FLOAT_LOG_B[b] log_b = ESTIMATE_FLOAT_LOG_B[b] = 1.0/Math.log(b) if log_b.nil? estimated_scale = nil fixup = false begin l = ((b==10) ? Math.log10(v_flt) : Math.log(v_flt)*log_b) estimated_scale =(l - 1E-10).ceil fixup = true rescue # rescuing errors is more efficient than checking (v_abs < Float::MAX.to_i) && (v_flt > Float::MIN) when v is a Flt else # estimated_scale = nil end # 1.2. Use Flt::DecNum logarithm if estimated_scale.nil? v.to_decimal_exact(:precision=>12) if v.is_a?(BinNum) if v.is_a?(DecNum) l = nil DecNum.context(:precision=>12) do case b when 10 l = v_abs.log10 else l = v_abs.ln/Flt.DecNum(b).ln end end l -= Flt.DecNum(+1,1,-10) estimated_scale = l.ceil fixup = true end end # 1.3 more rough Float aproximation # TODO: optimize denominator, correct numerator for more precision with first digit or part # of the coefficient (like _log_10_lb) estimated_scale ||= (v.adjusted_exponent.to_f * Math.log(v.class.context.radix) * log_b).ceil if estimated_scale >= 0 @k = estimated_scale @s *= output_b_power(estimated_scale) else sc = output_b_power(-estimated_scale) @k = estimated_scale @r *= sc @m_p *= sc @m_m *= sc end fixup ? scale_fixup! : scale! end |
#scale_original!(really = false) ⇒ Object
Given r/s = v (number to convert to text), m_m/s = (v - v-)/s, m_p/s = (v+ - v)/s Scale the fractions so that the first significant digit is right after the radix point, i.e. find k = ceil(logB((r+m_p)/s)), the smallest integer such that (r+m_p)/s <= B^k if k>=0 return:
r=r, s=s*B^k, m_p=m_p, m_m=m_m
if k<0 return:
r=r*B^k, s=s, m_p=m_p*B^k, m_m=m_m*B^k
scale! is a general iterative method using only (multiprecision) integer arithmetic.
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/flt/support/formatter.rb', line 256 def scale_original!(really=false) loop do if (@round_h ? (@r+@m_p >= @s) : (@r+@m_p > @s)) # k is too low @s *= @output_b @k += 1 elsif (@round_h ? ((@r+@m_p)*@output_b<@s) : ((@r+@m_p)*@output_b<=@s)) # k is too high @r *= @output_b @m_p *= @output_b @m_m *= @output_b @k -= 1 else break end end end |
#start_repetition_dectection ⇒ Object
302 303 304 305 306 |
# File 'lib/flt/support/formatter.rb', line 302 def start_repetition_dectection @may_repeat = (@m_p == 0 || @m_m == 0) @n_iters = 0 @rs = [] end |