how to convert age in year, month, day to year with decimal

Viewed 772

Suppose I have a string

   age<-c("7y2m4d","5m4d","7y5m6d")

I want to convert it to a numeric vector like

  c(7.34, 0.43, 7.43)

How can I make the R code?

We can assume there is 365 days in a year and 365/12 days in a month.

3 Answers

lubridate::duration will convert your strings to (approximate) seconds.

library(lubridate)
library(magrittr)
age <- c("7y2m4d", "5m4d", "7y5m6d")

age_sec <- age %>%
  duration() %>%
  as.numeric()

age_sec
[1] 226508400  13494600 234570600

Then you can approximate years as 365 * 24 * 60 * 60 seconds:

age_sec / (365 * 24 * 60 * 60)
[1] 7.182534 0.427911 7.438185

Another solution with base R:

age<-c("7y2m4d","5m4d","7y5m6d")


age <- gsub('y', ' + ', age)
age <- gsub('m', ' / 12 + ', age)
age <- gsub('d', ' / 365', age)

sapply(age, function(x) eval(parse(text = x)))
#7 + 2 / 12 + 4 / 365     5 / 12 + 4 / 365 7 + 5 / 12 + 6 / 365 
#           7.1776256            0.4276256            7.4331050 

The idea is to create the formula and then evaluate it for each element of your vector.

These solutions:

  • handle missing y, m and/or d and
  • give the same answer as in the question (except for the first element of age for which the question appears to have computed the answer incorrectly)
  • avoid the use of eval
  • only use base (except for alternative 1a)

Comparing the solutions below on the basis of simplicity (1a) is the simplest and automatically handles all the edge cases without specific code for them suggesting that it is the most natural; however, it does make use of a package. (1) is only slightly more complex and uses no packages and (2) pretty short and also does not use any packages but it is not as simple as (1) or (1a).

1) Here getNum extracts and returns the number from x associated with the code (the code is "y", "m" or "d") or if the code is not present in x returns 0. We then add up the year, month/12 and day/365.

getNum <- function(code, x) {
  pat <- sprintf(".*?(\\d+)%s.*", code)
  as.numeric(ifelse(grepl(code, x), sub(pat, "\\1", x), 0))
}
getNum("y", age) + getNum("m", age) / 12 + getNum("d", age) / 365
## [1] 7.1776256 0.4276256 7.4331050

1a) This is similar to (1) except that we use strapply in gsubfn to simplify getNum. In fact getNum reduces to a single strapply call and the regular expression it uses is also simpler.

library(gsubfn)

 getNum <- function(code, x) {
   strapply(x, paste0("(\\d+)", code), as.numeric, empty = 0, simplify = TRUE)
 }
getNum("y", age) + getNum("m", age) / 12 + getNum("d", age) / 365
## [1] 7.1776256 0.4276256 7.4331050

2) This alternative converts each string to dcf format and uses read.dcf to create a matrix of the y, m and d numbers.

In detail, the first line of code is to handle certain edge cases which are not actually present in the sample data in the question. We first append 0d to age (from the question) if d is missing so that we can handle the case where y, m and d are all missing. We also prepend a dummy entry to ensure that y, m and d are present in at least one entry. If we knew that y, m and d were present in at least one component and there was no component in which y, m and d were all simultaneously missing then this first line of code could be omitted.

The second line of code converts each input character string to dcf form and reads it into a matrix ensuring that the columns are in a known order and deleting the dummy entry added above.

Finally we replace NAs with 0 and and use matrix multiplication to add up the year, month/12 and day/365.

a0 <- c("0y0m0d", paste0(age, ifelse(grepl("d", age), "", "0d")))
m <- read.dcf(textConnection(gsub("(\\d+)(\\D)", "\\2: \\1\n", a0)))[-1, c("y", "m", "d")]
m[is.na(m)] <- 0
c(array(as.numeric(m), dim(m)) %*% c(1, 1/12, 1/365))
## [1] 7.1776256 0.4276256 7.4331050

Update: Rearranged and added (1) and (1a).

Related