How to recursively rename a list based on its list items

How to recursively rename a list based on its list items


7

I’d like to recursively rename (or name, as those items are currently unnamed) a list() based on its items (here text). There are several similar questions, however I haven’t found one with a list structure as follows and I can’t seem to find a general recursive approach to solve this.

The example data comes from here:

nodes <- list(
  list(
    text = "RootA",
    children = list(
      list(
        text = "ChildA1"
      ),
      list(
        text = "ChildA2"
      )
    )
  ),
  list(
    text = "RootB",
    children = list(
      list(
        text = "ChildB1"
      ),
      list(
        text = "ChildB2"
      )
    )
  )
)
# hard coded solution:
names(nodes) <- c(nodes[[1]]$text, nodes[[2]]$text)
names(nodes[[1]]$children) <- c(nodes[[1]]$children[[1]]$text, nodes[[1]]$children[[2]]$text)
names(nodes[[2]]$children) <- c(nodes[[2]]$children[[1]]$text, nodes[[2]]$children[[2]]$text)
str(nodes)

Expected output:

List of 2
 $ RootA:List of 2
  ..$ text    : chr "RootA"
  ..$ children:List of 2
  .. ..$ ChildA1:List of 1
  .. .. ..$ text: chr "ChildA1"
  .. ..$ ChildA2:List of 1
  .. .. ..$ text: chr "ChildA2"
 $ RootB:List of 2
  ..$ text    : chr "RootB"
  ..$ children:List of 2
  .. ..$ ChildB1:List of 1
  .. .. ..$ text: chr "ChildB1"
  .. ..$ ChildB2:List of 1
  .. .. ..$ text: chr "ChildB2"

Edit: I just benchmarked the three answer given on my system. The function provided by @knitz3 seems to be the fastest. Thanks everyone – I learned a lot.

Unit: microseconds
                  expr     min        lq     mean   median        uq     max neval
 list_rename_recursive  46.200   64.7010  458.389   79.601   95.2510 36040.6   100
           modify_tree 886.102 1929.4005 2787.664 2302.801 2779.1010 18778.5   100
            names_text 101.001  207.8015  575.603  246.852  305.9505 30270.8   100

3 Answers
3


7

We can use purrr:modify_tree().

modify_tree() allows you to recursively modify a list, supplying functions that either modify each leaf or each node (or both).

We can check if each node has a field called "text" and if so use that to setNames() of that node.

l  <- nodes |>
    purrr::modify_tree(
        pre = (x) {
            if ("text" %in% sapply(x, (l) names(l))) {
                return(setNames(x, sapply(x, (l) l$text)))
            }
            x
        }
    )

The definition of the pre parameter is:

pre, post Functions applied to each node. pre is applied on the way "down", i.e. before the leaves are transformed with leaf, while post is applied on the way "up", i.e. after the leaves are transformed.

We are not using a leaf function here so we could equally use post and the output would be identical.

Incidentally, I’ve used sapply() for illustrative purposes but as this can sometimes return an array or matrix, it’s generally safer to use unlist(lapply()).

Output

R doesn’t print lists like these very nicely, so here it is as a json:

jsonlite::toJSON(l, pretty = TRUE)
{
  "RootA": {
    "text": ["RootA"],
    "children": {
      "ChildA1": {
        "text": ["ChildA1"]
      },
      "ChildA2": {
        "text": ["ChildA2"]
      }
    }
  },
  "RootB": {
    "text": ["RootB"],
    "children": {
      "ChildB1": {
        "text": ["ChildB1"]
      },
      "ChildB2": {
        "text": ["ChildB2"]
      }
    }
  }
} 

And just to confirm this is the same as the hardcoded output:

all(
    names(l) == c(l[[1]]$text, l[[2]]$text),
    names(l[[1]]$children) == c(l[[1]]$children[[1]]$text, l[[1]]$children[[2]]$text),
    names(l[[2]]$children) == c(l[[2]]$children[[1]]$text, l[[2]]$children[[2]]$text)
) # TRUE


6

This recursive function seems to work

names_text <- function(x) {
  if (is.list(x)) {
    if (is.null(names(x))) {
      nn <- sapply(x, function(x) if(is.list(x) & "text" %in% names(x)) x[["text"]])
      x <- lapply(x, names_text)
      setNames(x, nn)
    } else {
      lapply(x, names_text)
    }
  } else {
    x
  }
}

Testing with the sample data we get

names_text(nodes) |> str()
List of 2
 $ RootA:List of 2
  ..$ text    : chr "RootA"
  ..$ children:List of 2
  .. ..$ ChildA1:List of 1
  .. .. ..$ text: chr "ChildA1"
  .. ..$ ChildA2:List of 1
  .. .. ..$ text: chr "ChildA2"
 $ RootB:List of 2
  ..$ text    : chr "RootB"
  ..$ children:List of 2
  .. ..$ ChildB1:List of 1
  .. .. ..$ text: chr "ChildB1"
  .. ..$ ChildB2:List of 1
  .. .. ..$ text: chr "ChildB2"

The idea is we look for unnamed lists, and then try to extract the "text" value from each child in that list and use that as the names.

There’s not a lot of error handling here and there are a lot of assumptions about the data structure, but it works with the test data.


4

This was fun. I went for something very interpretable. This function loops through every item of the list provided, and calls on itself if an item is itself another list. Should be able to handle unnamed list items as well.

list_rename_recursive <- function(x) {

    # If not a list, return the item
    if (!is.list(x)) {

        return(x)

    } else {

        # If a list, iterate through the items of the list
        for (i in seq_along(x)) {

            # If the list item i itself is a list, call
            # the function again. The list item is updated
            # with the returned value with proper name
            # $text if found
            if (is.list(x[[i]])) {

                name_item <- NA
                if (!is.null(x[[i]]$text)) name_item <- x[[i]]$text
                x[[i]] <- list_rename_recursive(x[[i]])
                if (!is.na(name_item)) names(x)[i] <- name_item

            }

        }

        return(x)

    }

}

nodes_new <- list_rename_recursive(nodes)
str(nodes_new)
List of 2
 $ RootA:List of 2
  ..$ text    : chr "RootA"
  ..$ children:List of 2
  .. ..$ ChildA1:List of 1
  .. .. ..$ text: chr "ChildA1"
  .. ..$ ChildA2:List of 1
  .. .. ..$ text: chr "ChildA2"
 $ RootB:List of 2
  ..$ text    : chr "RootB"
  ..$ children:List of 2
  .. ..$ ChildB1:List of 1
  .. .. ..$ text: chr "ChildB1"
  .. ..$ ChildB2:List of 1
  .. .. ..$ text: chr "ChildB2"



Leave a Reply

Your email address will not be published. Required fields are marked *