12

It would be nice to have an equivalent of R's signif function in Ruby.

For example:

>> (11.11).signif(1)
10
>> (22.22).signif(2)
22
>> (3.333).signif(2)
3.3
>> (4.4).signif(3)
4.4 # It's usually 4.40 but that's OK. R does not print the trailing 0's
    # because it returns the float data type. For Ruby we want the same.
>> (5.55).signif(2)
5.6
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Aleksandr Levchuk
  • 3,751
  • 4
  • 35
  • 47

7 Answers7

16

There is probably better way, but this seems to work fine:

class Float
  def signif(signs)
    Float("%.#{signs}g" % self)
  end
end

(1.123).signif(2)                    # => 1.1
(11.23).signif(2)                    # => 11.0
(11.23).signif(1)                    # => 10.0
Victor Deryagin
  • 11,895
  • 1
  • 29
  • 38
8

Here's an implementation that doesn't use strings or other libraries.

class Float
  def signif(digits)
    return 0 if self.zero?
    self.round(-(Math.log10(self).ceil - digits))
  end
end
Bravoking
  • 351
  • 3
  • 5
  • This does the underlying maths with the built-in library. I'd say this is the 'right' way. nb you can simplify the line that does the work: `self.round(digits - Math.log10(self).ceil)` – guy Feb 17 '23 at 11:28
3

Some of the previous answers and comments have alluded to this solution but this is what worked for me:

# takes in a float value and returns another float value rounded to 
# given significant figures.    
def round_to_sig_figs(val, sig_figs)
  BigDecimal.new(val, sig_figs).to_f
end
radhika
  • 534
  • 2
  • 7
  • 15
3

I don't see anything like that in Float. Float is mostly a wrapper for the native double type and given the usual binary/decimal issues, I'm not that surprised that Float doesn't allow you to manipulate the significant digits.

However, BigDecimal in the standard library does understand significant digits but again, I don't see anything that allows you to directly alter the significant digits in a BigDecimal: you can ask for it but you can't change it. But, you can kludge around that by using a no-op version of the mult or add methods:

require 'bigdecimal'
a = BigDecimal.new('11.2384')
a.mult(1, 2) # the result is 0.11E2   (i.e. 11)
a.add(0, 4)  # the result is 0.1124E2 (i.e. 11.24)

The second argument to these methods:

If specified and less than the number of significant digits of the result, the result is rounded to that number of digits, according to BigDecimal.mode.

Using BigDecimal will be slower but it might be your only choice if you need fine grained control or if you need to avoid the usual floating point problems.

mu is too short
  • 426,620
  • 70
  • 833
  • 800
  • [`BigDecimal#new`](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/bigdecimal/rdoc/BigDecimal.html#method-c-new) takes a second parameter that limits the number of significant digits for the number. – jwadsack Feb 13 '14 at 21:35
  • 1
    @jwadsack: But that only works when creating a new BigDecimal, you can't change the `digits` value of an existing BigDecimal. I think that's what I meant by "I don't see anything that allows you to directly alter the significant digits in a BigDecimal". – mu is too short Feb 13 '14 at 22:13
3

You are probably looking for Ruby's Decimal.

You could then write:

require 'decimal/shortcut'
num = 1.23541764
D.context.precision = 2
num_with_2_significant_digits = +D(num.to_s) #  => Decimal('1.2')
num_with_2_significant_digits.to_f #  => 1.2000000000000002

Or if you prefer to use the same syntax add this as a function to class Float like this:

class Float
  def signif num_digits
    require 'decimal/shortcut'
    D.context.precision = num_digits
    (+D(self.to_s)).to_f
  end
end

Usage would then be the same, i.e.

 (1.23333).signif 3
 # => 1.23

To use it, install the gem

gem install ruby-decimal
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
dgasper
  • 212
  • 2
  • 7
0

@Blou91's answer is nearly there, but it returns a string, instead of a float. This below works for me:

(sprintf "%.2f", 1.23456).to_f

So as a function,

def round(val, sig_figs)
  (sprintf "%.#{sig_figs}f", val).to_f
end
habudu
  • 193
  • 3
  • 11
  • 2
    This rounds to the specific number of decimal places, but what's wanted is to round to a specific number of significant digits. – Wayne Conrad May 28 '19 at 18:29
-2

Use sprintf if you want to print trailing zeros

2.0.0-p353 :001 > sprintf "%.3f", 500
 => "500.000"
2.0.0-p353 :002 > sprintf "%.4f", 500
 => "500.0000"
2.0.0-p353 :003 >
Blou91
  • 101
  • 1
  • 2
  • 7