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
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 withleaf
, whilepost
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
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.
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"