27

I am working in R. I have a series of coordinates in decimal degrees, and I would like to sort these coordinates by how many decimal places these numbers have (i.e. I will want to discard coordinates that have too few decimal places).
Is there a function in R that can return the number of decimal places a number has, that I would be able to incorporate into function writing?
Example of input:

AniSom4     -17.23300000        -65.81700

AniSom5     -18.15000000        -63.86700

AniSom6       1.42444444        -75.86972

AniSom7       2.41700000        -76.81700

AniLac9       8.6000000        -71.15000

AniLac5      -0.4000000        -78.00000

I would ideally write a script that would discard AniLac9 and AniLac 5 because those coordinates were not recorded with enough precision. I would like to discard coordinates for which both the longitude and the latitude have fewer than 3 non-zero decimal values.

Bas
  • 1,066
  • 1
  • 10
  • 28
Pascal
  • 273
  • 1
  • 3
  • 5
  • 2
    How are decimal places recorded in your data file? For example, might it have both 34.4 and 34.400, and would those be considered different? Example input and desired output would be helpful. – Aaron left Stack Overflow Mar 02 '11 at 21:36
  • 2
    Warning; fractional decimal numbers are not represented accurately in x86 and other mainstream processors. You will get spurious results working in anything but text representation. – Alex Brown Mar 03 '11 at 00:15

13 Answers13

48

You could write a small function for the task with ease, e.g.:

decimalplaces <- function(x) {
    if ((x %% 1) != 0) {
        nchar(strsplit(sub('0+$', '', as.character(x)), ".", fixed=TRUE)[[1]][[2]])
    } else {
        return(0)
    }
}

And run:

> decimalplaces(23.43234525)
[1] 8
> decimalplaces(334.3410000000000000)
[1] 3
> decimalplaces(2.000)
[1] 0

Update (Apr 3, 2018) to address @owen88's report on error due to rounding double precision floating point numbers -- replacing the x %% 1 check:

decimalplaces <- function(x) {
    if (abs(x - round(x)) > .Machine$double.eps^0.5) {
        nchar(strsplit(sub('0+$', '', as.character(x)), ".", fixed = TRUE)[[1]][[2]])
    } else {
        return(0)
    }
}
daroczig
  • 28,004
  • 7
  • 90
  • 124
  • I like the look of this. Thanks so much for your help! – Pascal Mar 02 '11 at 21:54
  • Thank you @Pascal! I just realized I had a typo in the function (wrote 'num' instead of 'x' in the `as.character` function), I have corrected this. Also I added the regexpr part, so zeros from the end of the number/string will be deleted automatically. – daroczig Mar 02 '11 at 22:07
  • This function is great, however when given a number such as 63.0000, it returns an error. Would there be a way to modify it such that in those cases it would return a 0? – Pascal Mar 02 '11 at 22:48
  • @Pascal: I updated my answer to your needs, sorry for not taking attention to this problem before. – daroczig Mar 02 '11 at 23:16
  • This is a nice answer, but it errors out for numbers such as 2e-05. as.character(2e-05) = "2e-05". I changed the function to "as.character(x+1)" and it seems to work – schnee Mar 28 '15 at 14:13
  • 2
    @schnee thanks for the feedback. Alternatively you can set `options(scipen = 999)` not to use scientific format. – daroczig Mar 28 '15 at 19:52
  • Just to note, that this function doesn't work if "piping" it with other operations. For instance `decimalplaces( ( 45 * 1.4) )` returns an error. This is related to my question [here](https://stackoverflow.com/questions/49636555/counter-intuitive-testing-for-whole-numbers-63-45-x-1-4-62/49636681#49636681). – owen88 Apr 03 '18 at 19:03
  • strangely, the updated function was not working properly when the value was 100.00, so I had to adjust to this: decimalnumcount <- function(x) { out <- ifelse(abs(x - round(x)) > .Machine$double.eps^0.5, nchar(strsplit(sub('.0+$', '', as.character(x)), ".", fixed = TRUE)[[1]][[2]]), 0) return(out) } – M4hd1 Jul 03 '18 at 09:39
  • 2
    `as.character` is no good -- use `sprintf` instead. `as.character` will fail for small / large "round" numbers like 1e6, 1e-6. – Dmitry Zotikov Jun 08 '20 at 19:03
  • You can alwas wrap the whole thing in a `vapply` to vectorize the operation. – bdempe Nov 30 '20 at 15:31
  • Using `sprintf` gives you this error: Error in sprintf(x) : 'fmt' is not a character vector . – JAQuent May 07 '21 at 14:20
  • This fails when I have NA values in data. There may be a solution by changing the function – user424821 Feb 15 '23 at 02:18
16

Here is one way. It checks the first 20 places after the decimal point, but you can adjust the number 20 if you have something else in mind.

x <- pi
match(TRUE, round(x, 1:20) == x)

Here is another way.

nchar(strsplit(as.character(x), "\\.")[[1]][2])
J. Win.
  • 6,662
  • 7
  • 34
  • 52
  • 1
    The first option returns 1 for integers. The second option returns NA for integers, and it also gives the wrong result for numbers that are between -1e-4 and 1e-4 but not zero because they are displayed in scientific notation by `as.character` (but you can use `format(scientific=F)` instead). – nisetama Jul 15 '22 at 03:17
9

Rollowing up on Roman's suggestion:

num.decimals <- function(x) {
    stopifnot(class(x)=="numeric")
    x <- sub("0+$","",x)
    x <- sub("^.+[.]","",x)
    nchar(x)
}
x <- "5.2300000"
num.decimals(x)

If your data isn't guaranteed to be of the proper form, you should do more checking to ensure other characters aren't sneaking in.

Community
  • 1
  • 1
Ari B. Friedman
  • 71,271
  • 35
  • 175
  • 235
  • This retuns 1 for positive integers and 2 for negative integers. And this returns 5 for `1e-4`, because it's converted to scientific notation when it's converted to a string by `sub`. – nisetama Jul 15 '22 at 03:21
3

Not sure why this simple approach was not used above (load the pipe from tidyverse/magrittr).

count_decimals = function(x) {
  #length zero input
  if (length(x) == 0) return(numeric())

  #count decimals
  x_nchr = x %>% abs() %>% as.character() %>% nchar() %>% as.numeric()
  x_int = floor(x) %>% abs() %>% nchar()
  x_nchr = x_nchr - 1 - x_int
  x_nchr[x_nchr < 0] = 0

  x_nchr
}
> #tests
> c(1, 1.1, 1.12, 1.123, 1.1234, 1.1, 1.10, 1.100, 1.1000) %>% count_decimals()
[1] 0 1 2 3 4 1 1 1 1
> c(1.1, 12.1, 123.1, 1234.1, 1234.12, 1234.123, 1234.1234) %>% count_decimals()
[1] 1 1 1 1 2 3 4
> seq(0, 1000, by = 100) %>% count_decimals()
 [1] 0 0 0 0 0 0 0 0 0 0 0
> c(100.1234, -100.1234) %>% count_decimals()
[1] 4 4
> c() %>% count_decimals()
numeric(0)

So R does not seem internally to distinguish between getting 1.000 and 1 initially. So if one has a vector input of various decimal numbers, one can see how many digits it initially had (at least) by taking the max value of the number of decimals.

Edited: fixed bugs

CoderGuy123
  • 6,219
  • 5
  • 59
  • 89
  • 1
    The `- 2` assumes you have just a single digit before the decimal. Won't work nicely for numbers `>= 10` or `< 0` (because the negative sign will also be counted). One possible solution would be to use `(abs(x) %% 1) %>% ...`. But even then I run into floating point issues. `abs(-23342.2) %% 1`, on my machine, prints as `0.2`, but `as.character(abs(-23342.2) %% 1)` gives `"0.200000000000728"` – Gregor Thomas Apr 23 '19 at 20:16
  • 1
    (Though now that I look up, seems the top answer had an issue with `%% 1` and found a workaround.) A simpler workaround than my first comment would be `x_nchr - nchar(round(x))` instead of `x_nchr - 2`. That should take care of negatives and more than one leading digit numbers just fine. – Gregor Thomas Apr 23 '19 at 20:21
  • Good point Gregor. I've added a simple fix for that. Also added a simple fix for negative values. – CoderGuy123 Apr 23 '19 at 20:30
  • This worked with NA values in data – user424821 Feb 15 '23 at 02:16
3

If someone here needs a vectorized version of the function provided by Gergely Daróczi above:

decimalplaces <- function(x) {
  ifelse(abs(x - round(x)) > .Machine$double.eps^0.5,
         nchar(sub('^\\d+\\.', '', sub('0+$', '', as.character(x)))),
         0)
}

decimalplaces(c(234.1, 3.7500, 1.345, 3e-15))
#> 1 2 3 0
dmi3kno
  • 2,943
  • 17
  • 31
2

I have tested some solutions and I found this one robust to the bugs reported in the others.

countDecimalPlaces <- function(x) {
  if ((x %% 1) != 0) {
    strs <- strsplit(as.character(format(x, scientific = F)), "\\.")
    n <- nchar(strs[[1]][2])
  } else {
    n <- 0
  }
  return(n) 
}

# example to prove the function with some values
xs <- c(1000.0, 100.0, 10.0, 1.0, 0, 0.1, 0.01, 0.001, 0.0001)
sapply(xs, FUN = countDecimalPlaces)
fvfaleiro
  • 183
  • 1
  • 6
1

For the common application, here's modification of daroczig's code to handle vectors:

decimalplaces <- function(x) {
    y = x[!is.na(x)]
    if (length(y) == 0) {
      return(0)
    }
    if (any((y %% 1) != 0)) {
      info = strsplit(sub('0+$', '', as.character(y)), ".", fixed=TRUE)
      info = info[sapply(info, FUN=length) == 2]
      dec = nchar(unlist(info))[seq(2, length(info), 2)]
      return(max(dec, na.rm=T))
    } else {
      return(0)
    }
}

In general, there can be issues with how a floating point number is stored as binary. Try this:

> sprintf("%1.128f", 0.00000000001)
[1] "0.00000000000999999999999999939458150688409432405023835599422454833984375000000000000000000000000000000000000000000000000000000000"

How many decimals do we now have?

BurninLeo
  • 4,240
  • 4
  • 39
  • 56
  • Good idea! I think there still must be a bug: `decimalplaces2(c(1.2, 2.34, 3))` returns 1 - also: Passing less than 3 numbers causes an error. – R Yoda Feb 08 '17 at 19:22
  • I get `Error in seq.default(2, length(info), 2) : wrong sign in 'by' argument` – CoderGuy123 Nov 01 '17 at 21:39
1

Interesting question. Here is another tweak on the above respondents' work, vectorized, and extended to handle the digits on the left of the decimal point. Tested against negative digits, which would give an incorrect result for the previous strsplit() approach.

If it's desired to only count the ones on the right, the trailingonly argument can be set to TRUE.

nd1 <- function(xx,places=15,trailingonly=F) {
  xx<-abs(xx); 
  if(length(xx)>1) {
    fn<-sys.function();
    return(sapply(xx,fn,places=places,trailingonly=trailingonly))};
  if(xx %in% 0:9) return(!trailingonly+0); 
  mtch0<-round(xx,nds <- 0:places); 
  out <- nds[match(TRUE,mtch0==xx)]; 
  if(trailingonly) return(out); 
  mtch1 <- floor(xx*10^-nds); 
  out + nds[match(TRUE,mtch1==0)]
}

Here is the strsplit() version.

nd2 <- function(xx,trailingonly=F,...) if(length(xx)>1) {
  fn<-sys.function();
  return(sapply(xx,fn,trailingonly=trailingonly))
  } else {
    sum(c(nchar(strsplit(as.character(abs(xx)),'\\.')[[1]][ifelse(trailingonly, 2, T)]),0),na.rm=T);
  }

The string version cuts off at 15 digits (actually, not sure why the other one's places argument is off by one... the reason it's exceeded through is that it counts digits in both directions so it could go up to twice the size if the number is sufficiently large). There is probably some formatting option to as.character() that can give nd2() an equivalent option to the places argument of nd1().

nd1(c(1.1,-8.5,-5,145,5,10.15,pi,44532456.345243627,0));
# 2  2  1  3  1  4 16 17  1
nd2(c(1.1,-8.5,-5,145,5,10.15,pi,44532456.345243627,0));
# 2  2  1  3  1  4 15 15  1

nd1() is faster.

rowSums(replicate(10,system.time(replicate(100,nd1(c(1.1,-8.5,-5,145,5,10.15,pi,44532456.345243627,0))))));
rowSums(replicate(10,system.time(replicate(100,nd2(c(1.1,-8.5,-5,145,5,10.15,pi,44532456.345243627,0))))));
bokov
  • 3,444
  • 2
  • 31
  • 49
1

Don't mean to hijack the thread, just posting it here as it might help someone to deal with the task I tried to accomplish with the proposed code.

Unfortunately, even the updated @daroczig's solution didn't work for me to check if a number has less than 8 decimal digits.

@daroczig's code:

decimalplaces <- function(x) {
    if (abs(x - round(x)) > .Machine$double.eps^0.5) {
        nchar(strsplit(sub('0+$', '', as.character(x)), ".", fixed = TRUE)[[1]][[2]])
    } else {
        return(0)
    }
}

In my case produced the following results

NUMBER / NUMBER OF DECIMAL DIGITS AS PRODUCED BY THE CODE ABOVE
[1] "0.0000437 7"
[1] "0.000195 6"
[1] "0.00025 20"
[1] "0.000193 6"
[1] "0.000115 6"
[1] "0.00012501 8"
[1] "0.00012701 20"

etc.

So far was able to accomplish the required tests with the following clumsy code:

if (abs(x*10^8 - floor(as.numeric(as.character(x*10^8)))) > .Machine$double.eps*10^8) 
   {
   print("The number has more than 8 decimal digits")
   }

PS: I might be missing something in regard to not taking the root of the .Machine$double.eps so please take caution

1

Another contribution, keeping fully as numeric representations without converting to character:

countdecimals <- function(x) 
{
  n <- 0
  while (!isTRUE(all.equal(floor(x),x)) & n <= 1e6) { x <- x*10; n <- n+1 }
  return (n)
}
Soren
  • 1,792
  • 1
  • 13
  • 16
  • This runs into what I assume is floating point precision issues. For example, `countdecimals(4.56)` returns 8. – Oliver Apr 29 '19 at 12:47
1

In [R] there is no difference between 2.30000 and 2.3, both get rounded to 2.3 so the one is not more precise than the other if that is what you want to check. On the other hand if that is not what you meant: If you really want to do this you can use 1) multiply by 10, 2) use floor() function 3) divide by 10 4) check for equality with the original. (However be aware that comparing floats for equality is bad practice, make sure this is really what you want)

Bernd Elkemann
  • 23,242
  • 4
  • 37
  • 66
0

Vector solution based on daroczig's function (can also deal with dirty columns containing strings and numerics):

decimalplaces_vec <- function(x) {

  vector <- c()
  for (i in 1:length(x)){

    if(!is.na(as.numeric(x[i]))){

      if ((as.numeric(x[i]) %% 1) != 0) {
        vector <- c(vector, nchar(strsplit(sub('0+$', '', as.character(x[i])), ".", fixed=TRUE)[[1]][[2]]))


      }else{
        vector <- c(vector, 0)
      }
    }else{
      vector <- c(vector, NA)
    }
  }
  return(max(vector))
}
rtastic
  • 1
  • 1
0

as.character uses scientific notation for numbers that are between -1e-4 and 1e-4 but not zero:

> as.character(0.0001)
[1] "1e-04"

You can use format(scientific=F) instead:

> format(0.0001,scientific=F)
[1] "0.0001"

Then do this:

nchar(sub("^-?\\d*\\.?","",format(x,scientific=F)))

Or in vectorized form:

> nplaces=function(x)sapply(x,function(y)nchar(sub("^-?\\d*\\.?","",format(y,scientific=F))))
> nplaces(c(0,-1,1.1,0.123,1e-8,-1e-8))
[1] 0 0 1 3 8 8
nisetama
  • 7,764
  • 1
  • 34
  • 21