rjstreer

Delete list items based on its nested content


This is basically a spin-off of my earlier question here.

Taking into account the following example data, I'd like to recursively delete all children (and parents which have no more children) which contain the item selected = FALSE or vice versa copy all children which contain the item selected = TRUE while maintaining the list structure (please see the list expected_output).

My attempts so far fail:

# data --------------------------------------------------------------------
nodes <- list(
  list(
    text = "RootA",
    state = list(loaded = TRUE, opened = TRUE, selected = TRUE, disabled = FALSE),
    children = list(
      list(
        text = "ChildA1",
        state = list(loaded = TRUE, opened = TRUE, selected = TRUE, disabled = FALSE)
      ),
      list(
        text = "ChildA2",
        state = list(loaded = TRUE, opened = TRUE, selected = FALSE, disabled = FALSE)
      )
    )
  ),
  list(
    text = "RootB",
    state = list(loaded = TRUE, opened = TRUE, selected = FALSE, disabled = FALSE),
    children = list(
      list(
        text = "ChildB1",
        state = list(loaded = TRUE, opened = TRUE, selected = FALSE, disabled = FALSE)
      ),
      list(
        text = "ChildB2",
        state = list(loaded = TRUE, opened = TRUE, selected = FALSE, disabled = FALSE)
      )
    )
  )
)

# Error in x[[i]] : subscript out of bounds -------------------------------
delete_unselected_nodes <- function(x) {
  y <- list()
  for (i in seq_along(x)) {
    value <- x[[i]]
    if("state" %in% names(x)){
      if(x[["state"]][["selected"]] == TRUE) {
        y <- c(y, x[[i]]) 
      }
    } else {
      x[[i]] <- delete_unselected_nodes(value)
    }
  }
}

delete_unselected_nodes(nodes)

# expected output ---------------------------------------------------------
expected_output <- list(
  list(
    text = "RootA",
    state = list(loaded = TRUE, opened = TRUE, selected = TRUE, disabled = FALSE),
    children = list(
      list(
        text = "ChildA1",
        state = list(loaded = TRUE, opened = TRUE, selected = TRUE, disabled = FALSE)
      )
    )
  )
)

Solution

  • I would use the functionality from purrr to deal with lists and define some (predicate) functions to keep it tidy:

    library(tidyverse)
    library(lobstr)
    
    # define sample data ------------------------------------------------------
    
    # ...
    
    # define functions --------------------------------------------------------
    
    # filter list of children to keep only where selected == TRUE
    delete_unselected_children <- function(children_list) {
      keep(
        children_list,
        .p = \(x) x$state$selected)
    }
    
    # replace list element "children" of root with filtered list of children
    delete_from_root <- function(root) {
      modify_in(
        root,
        .where = list("children"),
        .f = delete_unselected_children
      )
    }
    
    # define predicate function to drop roots without children
    has_no_children <- function(root) {
      is_empty(root$children)
    }
    
    
    # apply -------------------------------------------------------------------
    
    result <- nodes |> 
      map(delete_from_root) |> 
      discard(has_no_children)
    
    lobstr::tree(result)
    #> <list>
    #> └─<list>
    #>   ├─text: "RootA"
    #>   ├─state: <list>
    #>   │ ├─loaded: TRUE
    #>   │ ├─opened: TRUE
    #>   │ ├─selected: TRUE
    #>   │ └─disabled: FALSE
    #>   └─children: <list>
    #>     └─<list>
    #>       ├─text: "ChildA1"
    #>       └─state: <list>
    #>         ├─loaded: TRUE
    #>         ├─opened: TRUE
    #>         ├─selected: TRUE
    #>         └─disabled: FALSE
    

    Created on 2023-10-16 with reprex v2.0.2