Joost Keuskamp
Joost Keuskamp

Reputation: 125

Change data type of elements in a nested list

Is it possible to scan a list of lists for elements with a certain name and change their datatype but retain their value?

As an example, the following list containing elements 'N' of class 'character' or 'numeric'

x = list(list(N=as.character(1)),
         list(a=1,b=2,c="another element",N=as.character(5)), 
         list(a=2,b=2,N=as.character(7),c=NULL), 
         list(a=2,b=2,list(N=as.character(3))))

should then become:

x = list(list(N=as.numeric(1)),
         list(a=1,b=2,c="another element",N=as.numeric(5)), 
         list(a=2,b=2,N=as.numeric(7),c=NULL), 
         list(a=2,b=2,list(N=as.numeric(3))))

To be clear, the solution should allow for deeper nesting, and respect the data type of fields with names other than "N". I have not been able to find a general solution that works for lists with an arbitrary structure.

I have tried something along the lines of the solution given in this post:

a <- as.relistable(x)
u <- unlist(a)
u[names(u) == "N"] <- as.numeric(u[names(u) == "N"])
relist(u, a)

Unfortunately the substitution does not work in it's current form. In addition, relist does not seem to work in case the list contains NULL elements.

Upvotes: 2

Views: 4802

Answers (3)

Joost Keuskamp
Joost Keuskamp

Reputation: 125

The answer provided by marco sandri can be further generalised to:

is_num <- function(x) grepl("^[-]?[0-9]+[.]?[0-9]*|^[-]?[0-9]+[L]?|^[-]?[0-9]+[.]?[0-9]*[eE][0-9]+",x)

as_num <- function(x) {
if (is.null(x)||length(x) == 0) return(x)
if (class(x)=="list") return(lapply(x, as_num))
if (is.character(x) & is_num(x)) return(as.numeric(x))
return(x)
}
y <- as_num(z)
identical(y,z)

This solution also allows for list elements to contain numerical(0) and mixed datatypes such as 'data2005'.

Upvotes: 0

Marco Sandri
Marco Sandri

Reputation: 24252

A solution that works only on a list of lists containing numbers or strings with numbers:

x <- list(list(N=as.character(1)),
         list(a=1,b=2,N=as.character(5)), 
         list(a=2,b=2,N=as.character(7)), 
         list(a=2,b=2))

y1 <- lapply(x, function(y) lapply(y, as.numeric))

y2 <- list(list(N=as.numeric(1)),
         list(a=1,b=2,N=as.numeric(5)), 
         list(a=2,b=2,N=as.numeric(7)), 
         list(a=2,b=2))

identical(y1,y2)
# [1] TRUE

EDIT. Here is a more general code that works on nested lists of number and strings. It uses a recursive function as_num and the list.apply function of the rlist package.

library(rlist)

x = list(list(N=as.character(1)),
         list(a=1,b=2,c="another element",N=as.character(5)), 
         list(a=2,b=2,N=as.character(7),c=NULL), 
         list(a=2,b=2,list(N=as.character(3))))

# Test if the string contains a number
is_num <- function(x) grepl("[-]?[0-9]+[.]?[0-9]*|[-]?[0-9]+[L]?|[-]?[0-9]+[.]?[0-9]*[eE][0-9]+",x)

# A recursive function for numeric convertion of strings containing numbers
as_num <- function(x) {
   if (!is.null(x)) {
     if (class(x)!="list") {
       y <- x
       if (is.character(x) & is_num(x)) y <- as.numeric(x)
     } else {
       y <- list.apply(x, as_num)
     }
   } else { 
     y <- x
   }
   return(y)
}

y <- list.apply(x, as_num)

z = list(list(N=as.numeric(1)),
         list(a=1,b=2,c="another element",N=as.numeric(5)), 
         list(a=2,b=2,N=as.numeric(7),c=NULL), 
         list(a=2,b=2,list(N=as.numeric(3))))

identical(y,z)
# [1] TRUE

Upvotes: 0

ulfelder
ulfelder

Reputation: 5335

Use lapply to repeat the process over the list elements with a condition to check for your element of interest, so you don't inadvertently add elements to your sublists:

x <- lapply(x, function(i) {

    if(length(i$N) > 0) {

        i$N <- as.numeric(i$N)

    }

    return(i)

})

Upvotes: 1

Related Questions