Search code examples
rmodulusinteger-arithmetic

Probable complete loss of accuracy in modulus warning and incorrect results when using certain numbers with the modulus function


I wrote this function in R and I am having a few issues with the modulus operator within this function. The purpose of this function is to return 1 to the n+1 decimal place if the number has any numbers after the decimal point, otherwise return 1 for any integer, for e.g. return 0.1 for 1, return 1 for 50, etc.

The code is

tolerance <- function(x){
  constant <- 1
  if (x < 0){
    constant <- -1
    x <- constant * x
  }
  exponent <- 0
  if (is.numeric(x) & !is.integer(x)) {
    while(x %% 10 > 0) {
      x <- 10 * x
      exponent <- exponent + 1
    }
    return(constant * 10 ^ (-exponent))
  }
}

These are the results that I get

> for (i in c(1,0.1,0.11,0.111, 0.1111)) {
+   print(tolerance(i))
+ }
[1] 0.1
[1] 0.01
[1] 0.001
[1] 1e-24
[1] 1e-05
Warning messages:
1: In tolerance(i) : probable complete loss of accuracy in modulus
2: In tolerance(i) : probable complete loss of accuracy in modulus
3: In tolerance(i) : probable complete loss of accuracy in modulus
4: In tolerance(i) : probable complete loss of accuracy in modulus

As you can see the abnormal behaviour occurs at 0.111 but not at 0.1111 so it doesn't seem that the number of decimal places are causing the weird warning message and incorrect results. In fact, 3 decimal places work fine when I change the number, for e.g.

> tolerance(5.111)
[1] 1e-04

0.0001 is the expected and correct result, so it appears that issues only arise with certain numbers. I am not passing very large numbers into this function because I know that R doesn't handle numeric data types with very large number of digits very well. I usually pass a maximum of 6 digits after the decimal place into this function.

Could the issue be related to conversion of decimal numbers to binary numbers when numbers are being processed? Or something else altogether?

I believe it is the section with the modulus operator that is causing the issues because when I rewrote this function without using the modulus operator, instead using string manipulation, the code seems to work as expected.

Any thoughts?


Solution

  • This is essentially one of the million variations on a theme of why are these numbers not equal?

    This is only a partial answer, but try this (debugging statements, removed code for negative x):

    tolerance <- function(x){
      constant <- 1
      exponent <- 0
      if (is.numeric(x) & !is.integer(x)) {
        while(x %% 10 > 0) {          
          x <- 10 * x
          exponent <- exponent + 1
          cat("x ",format(x,digits=22), "\n")
          cat("x %% 10",format(x %% 10,digits=22), "\n")
        }
        return(constant * 10 ^ (-exponent))
      }
    }
    

    For x=0.11,

    tolerance(0.11)
    ## x  1.100000000000000088818 
    ## x %% 10 1.100000000000000088818 
    ## x  11 
    ## x %% 10 1 
    ## x  110 
    ## x %% 10 0 
    

    In this case you're lucky that the initial floating-point noise (because 0.11 isn't exactly representable in a finite number of binary digits) gets dropped from the end due to lack of precision.

    With x=0.111, the pattern of cancellation is different (and unfortunate).

    tolerance(0.111)
    x  1.1100000000000000977 
    x %% 10 1.1100000000000000977 
    x  11.10000000000000142109 
    x %% 10 1.100000000000001421085 
    x  111.0000000000000142109 
    x %% 10 1.000000000000014210855 
    x  1110.000000000000227374 
    x %% 10 2.273736754432320594788e-13 
    x  11100.00000000000181899 
    x %% 10 1.81898940354585647583e-12 
    x  111000.0000000000145519 
    x %% 10 1.455191522836685180664e-11 
    x  1110000.000000000232831 
    x %% 10 2.328306436538696289062e-10 
    x  11100000.00000000186265 
    x %% 10 1.86264514923095703125e-09 
    x  111000000.0000000149012 
    x %% 10 1.490116119384765625e-08 
    x  1110000000.000000238419 
    x %% 10 2.384185791015625e-07 
    x  11100000000.00000190735 
    x %% 10 1.9073486328125e-06 
    x  111000000000.0000152588 
    x %% 10 1.52587890625e-05 
    x  1110000000000.000244141 
    x %% 10 0.000244140625 
    x  11100000000000.00195312 
    x %% 10 0.001953125 
    x  111000000000000.015625 
    x %% 10 0.015625 
    x  1110000000000000.125 
    x %% 10 0.125 
    x  11100000000000002 
    x %% 10 2 
    x  111000000000000016 
    x %% 10 6 
    x  1110000000000000128 
    x %% 10 8 
    x  11100000000000002048 
    x %% 10 8 
    x  111000000000000016384 
    x %% 10 4 
    x  1110000000000000131072 
    x %% 10 2 
    x  11100000000000001048576 
    x %% 10 6 
    x  111000000000000010485760 
    x %% 10 0 
    [1] 1e-24