Search code examples
rfloating-pointroundingfloor

Most significant decimal digit (or 0.3 - 0.1 = 0.1 )


This operation should return 2, but it returns 1 instead because of the floating point representation:

a <- .3
b <- .1
floor((a-b)*10)

I basically want the first digit after the point, of the actual base-10 result, not the floating-point computer's result. In this case a and b only have one decimal digit, but in most situations there will be more. Examples:

  • 0.3-0.1=0.2 so I want the 2
  • 0.5-0.001=0.499 so I want the 4
  • 0.925-0.113=0.812 so I want the 8
  • 0.57-0.11=0.46 so I want the 4
  • 0.12-0.11=0.01 so I want the 0

that is, not rounding but truncating. I thought of using this:

floor(floor((a-b)*100)/10)

but I'm not sure if that is the best I can do.

update: indeed, it doesn't work (see comments below):

floor(floor((.9-.8)*100)/10) # gives 0 instead of 1
floor(round((.5-.001)*100)/10) # gives 5 instead of 1

update 2: think this does work (at least in all cases listed so far):

substring(as.character(a-b),first=3,last=3)

Suggestions?


Solution

  • This is not possible, because the information is no longer there: doubles cannot exactly represent decimal numbers.

    If you are fine with an approximate solution, you can add a small number, and truncate the result. For instance, if you know that your numbers have at most 14 digits, the following would work:

    first_digit <- function(x, epsilon=5e-15)
      floor( (x+epsilon) * 10 )
    first_digit( .3   - .1   ) # 2
    first_digit( .5   - .001 ) # 4
    first_digit( .925 - .113 ) # 8
    first_digit( .57  - .11  ) # 4
    first_digit( .12  - .11  ) # 0
    

    If you wanted the first significant digit (that means "first non-zero digit"), you could use:

    first_significant_digit <- function(x, epsilon=5e-14)
      floor( (x+epsilon) * 10^-floor(log10(x+epsilon)) )
    first_significant_digit(0.12-0.11) # 1