Unwanted float expected to be integer

144 Views Asked by At

I'm trying to solve a problem :

Your task is to write a simple function that takes a number of meters, and outputs it using metric prefixes.For this exercise we just want units bigger than a meter, from meters up to yottameters, excluding decameters and hectometers.All values passed in will be positive integers Examples

meters(51500)
# returns "51.5km"

meters(5000000)
# returns "5Mm"

My code:

def meters(x)
  map_prefix={ 24=>'Y', 21=> 'Z', 18=> 'E', 15=> 'P', 12=> 'T', 9=>'G', 6=>'M', 3=>'k',0=>'' }
  digits=(x.to_i.to_s.size-1)/3  
  division=x/(10.0**(3*digits))
  "#{division}#{map_prefix[3*digits]}m".sub(/\.0([^\d])/,'\1')
end

It doesn't work for meters(56*10**24) #->expected 56Ym ,instead got 56.000000000004Ym, but it works for bigger numbers such as meters(88*10**24) #->88Ym. The code passes 49 out of 50 tests, can someone help me find the error?

5

There are 5 best solutions below

2
Justin Wood On BEST ANSWER

I think your issue is that you are multiplying by 10.0, yet you only want to deal with integers.

Something like the following is what you want. (I'm also doing a couple style changes).

def meters(x)
  digits=(x.to_i.to_s.size-1)/3
  prefix = prefixes[3*digits]
  value = x / (10 ** (3 * digits))
  "#{value}#{prefix}m".sub(/\.0([^\d])/,'\1')
end

def prefixes
  {
    24 => 'Y',
    21 => 'Z',
    18 => 'E',
    15 => 'P',
    12 => 'T',
    9 => 'G',
    6 => 'M',
    3 => 'k',
    0 => ''
  }
end

This at least gives the correct solution to the one that is wrong. I'm not going to guarantee it is the correct solution to everything.

I made the hash into its own function here because it seems like it would be static. As well was the other things I mentioned in my comment.

0
Yu Hao On

You can use Float#round to round the number to certain digits. For this certain problem, 3 should do fine.

"#{division.round(3)}#{map_prefix[3*digits]}m".sub(/\.0([^\d])/,'\1')
#          ^^^^^^^^^

The reason behind the problem is: Float can store integers that's pretty big. However, for integers bigger than a certain limit, Float cannot store them precisely. For [IEEE-754 double precision] floating point, that limit is 253.

0
Timmy On

The easiest way to hack your code to get it working seems to avoid float-pointing number, like here:

#!/usr/bin/env ruby

def meters(x)
  map_prefix={ 24=>'Y', 21=> 'Z', 18=> 'E', 15=> 'P', 12=> 'T', 9=>'G', 6=>'M', 3=>'k',0$
  map_prefix.default = 'Y'
  digits = [((x.to_s.size-1)/3)*3, 24].min
  division = x.to_s.insert(-digits - 1, '.')
  division.sub!(/0+\z/, '')
  division.sub!(/\.\z/, '')
  "#{division}#{map_prefix[digits]}m"
end

puts meters(51500)
puts meters(5000000)
puts meters(5001)
puts meters(88*10**24)
puts meters(88*10**24 + 1)
puts meters(100)
puts meters(88*10**27)
puts meters(88*10**27 + 1)

With results like:

 ./ruby.rb
51.5km
5Mm
5.001km
88Ym
88.000000000000000000000001Ym
100m
88000Ym
88000.000000000000000000000001Ym

More seriously, you need to avoid strings whatsoever (no conversions to string at all should be made). You need arbitrary precision, so float is not an option at all.

1
engineerDave On

FWIW this line will change ruby's duck typing to float. (notice you're introducing 10.0 as a float.)

division=x/(10.0**(3*digits))

When dealing with large numbers, its best to use the built in BigDecimal class. Much cleaner and less error prone, although this is definitely NOT error proof code.

require 'bigdecimal'

def meters(x)
  b = BigDecimal.new(x).split
  "#{b[1]}#{prefix[b[3] - b[1].length]}m"
end

def prefix
  {
    24 =>'Y', 21 => 'Z', 18 => 'E', 15 => 'P',
    12 => 'T', 9 =>'G',  6 =>'M', 3 =>'k',0 =>''
  }
end
0
Cary Swoveland On
UNITS = " kMGTPEZY"

def meters(x)
  sx     = x.to_s
  pwr    = sx.size - 1
  return sx if pwr < 3
  pfx_sz = (pwr < 24) ? (1 + pwr % 3) : pwr - 23
  sxs    = sx.reverse.to_i.to_s.reverse
  sxs    = sxs.ljust([sxs.size, pfx_sz].max, '0')
  pfx    = sxs[0, pfx_sz]
  pfx    << '.' if (pfx.size < sxs.size)  
  "#{ pfx }#{ sxs[pfx_sz..-1] }#{ UNITS[[pwr/3, 8].min] }"
end

meters 3             #=> "3" 
meters 100           #=> "100" 
meters 4000          #=> "4k" 
meters 5001          #=> "5.001k" 
meters 51500         #=> "51.5k" 
meters 5000000       #=> "5M" 
meters 88*10**24     #=> "88Y" 
meters 88*10**24 + 1 #=> "88.000000000000000000000001Y" 
meters 88*10**27     #=> "88000Y" 
meters 88*10**27 + 1 #=> "88000.000000000000000000000001Y"