Repository: nhs-england-patient-safety/patient-safety-data-request-template
Pull request #37: Translate filters
Author: login: ruth-keane-nhs
Created: 2025-03-10T16:25:10Z | Merged: 2025-03-24T12:15:13Z

setup code to improve summary sheet of output file.
mostly translating from A001==4 to involved agents = medications 


Diff:
diff --git a/connections.R b/connections.R
index d00b004..1807c7d 100644
--- a/connections.R
+++ b/connections.R
@@ -53,6 +53,18 @@ if (search_nrls) {
   
   codes <- bind_rows(codes_ex_rm04, codes_rm04)
   
+  
+  # download nrls colnames file, and save to data folder
+  site_url <- "https://nhs.sharepoint.com/sites/MED/ps2/it/mit"
+  site <- get_sharepoint_site(site_url = site_url, tenant = "nhs")
+  reslib <- site$get_drive("Restricted Library")
+  reslib$download_file("NRLS/nrls lookup.xlsx",dest="Data/nrls_lookup_file.xlsx", overwrite = TRUE)
+  
+  nrls_lookup<- read.xlsx("Data/nrls_lookup_file.xlsx") %>%
+    select(Code, Label)
+  
+  
+  
   } 
 
 # lfpse ####
diff --git a/formatter.R b/formatter.R
index 6755e37..7948974 100644
--- a/formatter.R
+++ b/formatter.R
@@ -37,10 +37,11 @@ metadata <- c(
   "",
   "LFPSE categorical criteria:",
   "",
-  "Free text filters:",
+  "Sampling strategy:",
   "",
+  "Free text filters:",
   "",
-  "Sampling strategy:"
+  ""
 )
 
 ref_no <- substr(title, 5, 8)
@@ -62,6 +63,22 @@ date_type_text <-
 
 date_range <- glue('Incidents {date_type_text} between {format(as.Date(start_date), "%d-%b-%y")} and {format(as.Date(end_date), "%d-%b-%y")}')
 
+
+text_terms_pretty <- text_terms
+for (group in 1:length(text_terms)){
+  for (term in 1:length(text_terms[[group]])){
+      prettier_term<-text_terms[[group]][term] |>
+      str_replace_all(pattern = "\\|", " OR ") |>
+      str_replace_all(pattern = "\\|", " OR ") |>
+      str_replace_all(pattern = fixed('\\b'), "%") |>
+
+      str_replace_all(pattern = fixed('(?i)'), "" ) |>
+
+      str_replace_all(pattern =  "\\(\\?:\\|\\\\W\\)", "~")
+    text_terms_pretty[[group]][term]<-prettier_term
+  }
+}
+
 metadata_answers <- c(
   ref_no,
   "",
@@ -71,22 +88,28 @@ metadata_answers <- c(
   "",
   date_range,
   "",
-  deparse(nrls_categorical,width.cutoff = 500),
+  expanded_categorical_filter_nrls,
   "",
-  deparse(steis_categorical, width.cutoff = 500),
+  expanded_categorical_filter_steis,
   "",
-  deparse(lfpse_categorical, width.cutoff = 500),
+  expanded_categorical_filter_lfpse,
   "",
-  deparse(text_terms,width.cutoff = 500),
+  deparse(sampling_strategy),
+  "",
+  "Free text search based the following terms (case insensitive):",
+  deparse(text_terms_pretty,width.cutoff = 500),
   deparse(text_filter, width.cutoff = 500),
   "",
-  deparse(sampling_strategy)
-)
+  "Notes:",
+  "'%' represents a boundary",
+  "'~' represents an optional space that can be filled by any character")
 
-addStyle(wb, sheet = cover_sheet_name, textStyle, rows = 2:24, cols = 2)
-writeData(wb, cover_sheet_name, metadata, startRow = 2, startCol = 2)
-writeData(wb, cover_sheet_name, metadata_answers, startRow = 2, startCol = 5)
+addStyle(wb, "Search strategy", textStyle, rows = 2:30, cols = 2)
+addStyle(wb, "Search strategy", textStyle, rows = 18, cols = 5)
+addStyle(wb, "Search strategy", textStyle, rows = 22, cols = 5)
 
+writeData(wb, "Search strategy", metadata, startRow = 2, startCol = 2)
+writeData(wb, "Search strategy", metadata_answers, startRow = 2, startCol = 5)
 
 # Add worksheets ----------------------------------------------------------
 
diff --git a/functions.R b/functions.R
index 9690c7c..3b70dcc 100644
--- a/functions.R
+++ b/functions.R
@@ -1,37 +1,37 @@
-#function to find the minimum number in a vector, and return NA if all values are NA
+# function to find the minimum number in a vector, and return NA if all values are NA
 # This is required to find the minimum physical or psychological harm level
-min_safe<- function(vec){
-   ifelse(length(vec[!is.na(vec)]) == 0, NA_real_, min(vec,na.rm=TRUE))
+min_safe <- function(vec) {
+  ifelse(length(vec[!is.na(vec)]) == 0, NA_real_, min(vec, na.rm = TRUE))
 }
 
 
-#this function adds the header section to the sheet- the contents depend on the database and what sheet is being made
-add_header_to_sheet<-function(wb, title, 
-                              database_name, 
-                              sheet, 
-                              summary_sheet, 
-                              number_of_rows_sampled,
-                              number_of_rows_unsampled){
-  
+# this function adds the header section to the sheet- the contents depend on the database and what sheet is being made
+add_header_to_sheet <- function(wb, title,
+                                database_name,
+                                sheet,
+                                summary_sheet,
+                                number_of_rows_sampled,
+                                number_of_rows_unsampled) {
   # Write title
-  writeData( wb, 
-             sheet, 
-             paste(sheet, "Confidential", sep = " - "),
-             startCol = 1,
-             startRow = 1
+  writeData(wb,
+    sheet,
+    paste(sheet, "Confidential", sep = " - "),
+    startCol = 1,
+    startRow = 1
   )
-  
-  #write subtitle
+
+  # write subtitle
   writeData(wb, sheet, title, startCol = 1, startRow = 3)
-  
-  content_start_row = 5
-  
-  #add caveats to lfpse summary tab
-  if (database_name == "LFPSE" & summary_sheet){
-    
-    note<- c("Note: The data here has been aggregated for the patients within an incident, selecting the largest physical harm level accross patients",
-             "Note: Where a question can have multiple answers, these have been separated out so will sum to a larger number than the number of incidents.")
-    
+
+  content_start_row <- 5
+
+  # add caveats to lfpse summary tab
+  if (database_name == "LFPSE" & summary_sheet) {
+    note <- c(
+      "Note: The data here has been aggregated for the patients within an incident, selecting the largest physical harm level accross patients",
+      "Note: Where a question can have multiple answers, these have been separated out so will sum to a larger number than the number of incidents."
+    )
+
     # write note
     writeData(
       wb,
@@ -40,13 +40,14 @@ add_header_to_sheet<-function(wb, title,
       startCol = 1,
       startRow = content_start_row,
     )
-    
+
     content_start_row <- content_start_row + 3
   }
-  
-  incident_or_pt_level<- case_when(summary_sheet & database_name=="LFPSE" ~ " (incident level)",
-                                   !summary_sheet & database_name=="LFPSE" ~ " (patient level)",
-                                   .default = "")
+
+  incident_or_pt_level <- case_when(summary_sheet & database_name == "LFPSE" ~ " (incident level)",
+    !summary_sheet & database_name == "LFPSE" ~ " (patient level)",
+    .default = ""
+  )
   # write number of incidents
   writeData(
     wb,
@@ -55,7 +56,7 @@ add_header_to_sheet<-function(wb, title,
     startCol = 1,
     startRow = content_start_row
   )
-  
+
   writeData(
     wb,
     sheet,
@@ -63,178 +64,186 @@ add_header_to_sheet<-function(wb, title,
     startCol = 1,
     startRow = content_start_row + 1
   )
-  
-  #set start row for summary tables
-  table_start_row = content_start_row + 3
-  
-  #Add text style
-  addStyle(wb, sheet = sheet,  textStyle, rows = 1:(table_start_row - 1), cols = 1)
-  
+
+  # set start row for summary tables
+  table_start_row <- content_start_row + 3
+
+  # Add text style
+  addStyle(wb, sheet = sheet, textStyle, rows = 1:(table_start_row - 1), cols = 1)
+
   return(table_start_row)
-  
 }
 
 
-#this function creates a summary table from a dataframe and a list containing the variables to tabulate by
-create_summary_table<-function(df_to_create_summary_table,
-                               variables_to_tabulate_by_list, 
-                               database_name){
-    #work out if table needs to have one or 2 variables. 
+# this function creates a summary table from a dataframe and a list containing the variables to tabulate by
+create_summary_table <- function(df_to_create_summary_table,
+                                 variables_to_tabulate_by_list,
+                                 database_name) {
+  # work out if table needs to have one or 2 variables.
   if (length(variables_to_tabulate_by_list) == 1) {
-    
     # extract the variable from list of variables
-    variable_to_tabulate_by_one<- unlist(variables_to_tabulate_by_list)[[1]]
-    
-    #convert the variable which data will be tabulated to a more human readable name, using lookup
-    renamed_variable_to_tabulate_by_one<-names(which(rename_lookup[[database_name]]==variable_to_tabulate_by_one))
-    
-    if (length(renamed_variable_to_tabulate_by_one)==0){
+    variable_to_tabulate_by_one <- unlist(variables_to_tabulate_by_list)[[1]]
+
+    # convert the variable which data will be tabulated to a more human readable name, using lookup
+    renamed_variable_to_tabulate_by_one <- names(which(rename_lookup[[database_name]] == variable_to_tabulate_by_one))
+
+    if (length(renamed_variable_to_tabulate_by_one) == 0) {
       message(str_glue("{variable_to_tabulate_by_one} does not exist. Table cannot be created. "))
-      return(tibble(`Table could not be made`= str_glue("{variable_to_tabulate_by_one} doesn't exist.")))
+      return(tibble(`Table could not be made` = str_glue("{variable_to_tabulate_by_one} doesn't exist.")))
     }
-    
-    #allow the variable to be used as a column name
-    renamed_variable_to_tabulate_by_one_col_name<- sym(renamed_variable_to_tabulate_by_one) 
+
+    # allow the variable to be used as a column name
+    renamed_variable_to_tabulate_by_one_col_name <- sym(renamed_variable_to_tabulate_by_one)
 
     summary_table <- df_to_create_summary_table |>
-      #seperate multi select values
-      separate_rows(!!renamed_variable_to_tabulate_by_one_col_name,sep = " {~@~} ")|>
+      # separate multi select values
+      separate_rows(!!renamed_variable_to_tabulate_by_one_col_name, sep = " {~@~} ") |>
       convert_columns_to_factors(database_name) |>
-      #use count to tabulate
-      tabyl(!!renamed_variable_to_tabulate_by_one_col_name, 
-            show_missing_levels =TRUE,
-            show_na=TRUE)|>
-      adorn_totals('row') |>
-      adorn_pct_formatting()|>
+      # use count to tabulate
+      tabyl(!!renamed_variable_to_tabulate_by_one_col_name,
+        show_missing_levels = TRUE,
+        show_na = TRUE
+      ) |>
+      adorn_totals("row") |>
+      adorn_pct_formatting() |>
       select(-any_of("valid_percent")) # remove additional percent column
-    
-  } else if ( length(variables_to_tabulate_by_list)==2){
-    
+  } else if (length(variables_to_tabulate_by_list) == 2) {
     # extract the variables from list of variables
-    variable_to_tabulate_by_one<- unlist(variables_to_tabulate_by_list)[[1]]
-    variable_to_tabulate_by_two<- unlist(variables_to_tabulate_by_list)[[2]]
-    
-    #convert the variables which data will be tabulated to a more human readable name, using lookup
-    renamed_variable_to_tabulate_by_one<-names(which(rename_lookup[[database_name]]==variable_to_tabulate_by_one))
-    renamed_variable_to_tabulate_by_two<-names(which(rename_lookup[[database_name]]==variable_to_tabulate_by_two))
-    if (length(renamed_variable_to_tabulate_by_one)==0){
+    variable_to_tabulate_by_one <- unlist(variables_to_tabulate_by_list)[[1]]
+    variable_to_tabulate_by_two <- unlist(variables_to_tabulate_by_list)[[2]]
+
+    # convert the variables which data will be tabulated to a more human readable name, using lookup
+    renamed_variable_to_tabulate_by_one <- names(which(rename_lookup[[database_name]] == variable_to_tabulate_by_one))
+    renamed_variable_to_tabulate_by_two <- names(which(rename_lookup[[database_name]] == variable_to_tabulate_by_two))
+    if (length(renamed_variable_to_tabulate_by_one) == 0) {
       message(str_glue("{variable_to_tabulate_by_one} does not exist. Table cannot be created. "))
-      return(tibble(`Table could not be made`= str_glue("{variable_to_tabulate_by_one} doesn't exist.")))
+      return(tibble(`Table could not be made` = str_glue("{variable_to_tabulate_by_one} doesn't exist.")))
     }
-    if (length(renamed_variable_to_tabulate_by_two)==0){
+    if (length(renamed_variable_to_tabulate_by_two) == 0) {
       message(str_glue("{renamed_variable_to_tabulate_by_two} does not exist. Table cannot be created. "))
-      return(tibble(`Table could not be made`= str_glue("{variable_to_tabulate_by_two} doesn't exist.")))
+      return(tibble(`Table could not be made` = str_glue("{variable_to_tabulate_by_two} doesn't exist.")))
     }
-    
-    #allow the variable to be used as a column name
-    renamed_variable_to_tabulate_by_one_col_name<- sym(renamed_variable_to_tabulate_by_one) 
-    renamed_variable_to_tabulate_by_two_col_name<- sym(renamed_variable_to_tabulate_by_two) 
-    
+
+    # allow the variable to be used as a column name
+    renamed_variable_to_tabulate_by_one_col_name <- sym(renamed_variable_to_tabulate_by_one)
+    renamed_variable_to_tabulate_by_two_col_name <- sym(renamed_variable_to_tabulate_by_two)
+
     summary_table <- df_to_create_summary_table |>
-      #seperate multi select values
-      separate_rows(!!renamed_variable_to_tabulate_by_one_col_name,sep = " {~@~} ") |>
-      separate_rows(!!renamed_variable_to_tabulate_by_two_col_name,sep = " {~@~} ") |>
-      convert_columns_to_factors(database_name) |> 
+      # separate multi select values
+      separate_rows(!!renamed_variable_to_tabulate_by_one_col_name, sep = " {~@~} ") |>
+      separate_rows(!!renamed_variable_to_tabulate_by_two_col_name, sep = " {~@~} ") |>
+      convert_columns_to_factors(database_name) |>
       # use count to get a table
       tabyl(!!renamed_variable_to_tabulate_by_one_col_name,
-            !!renamed_variable_to_tabulate_by_two_col_name,
-            show_missing_levels =TRUE,
-            show_na=TRUE)|> 
-      rename(any_of(c(`Not available`= "NA_"))) |>
-      #add row and column totals
-      adorn_totals('both')
+        !!renamed_variable_to_tabulate_by_two_col_name,
+        show_missing_levels = TRUE,
+        show_na = TRUE
+      ) |>
+      rename(any_of(c(`Not available` = "NA_"))) |>
+      # add row and column totals
+      adorn_totals("both")
   } else {
     message(str_glue("TOO MANY VARIABLES INCLUDED FOR {database_name}"))
     message(paste(variables_to_tabulate_by_list, collapse = ", "))
-    return(tibble(`Table could not be made`= "Too many variables"))
+    return(tibble(`Table could not be made` = "Too many variables"))
   }
-  
+
   return(summary_table)
-  
 }
 
-#function to convert month and level of harm columns to factors (depending on database)
-convert_columns_to_factors<-function(df_without_factors, database_name){
-
-  #convert month and harm level to ordered factors 
-  if (database_name=="LFPSE"){
-    #relevel factor of columns
-    df_with_factors<- df_without_factors |>
+# function to convert month and level of harm columns to factors (depending on database)
+convert_columns_to_factors <- function(df_without_factors, database_name) {
+  # convert month and harm level to ordered factors
+  if (database_name == "LFPSE") {
+    # relevel factor of columns
+    df_with_factors <- df_without_factors |>
       mutate(
-        `Largest physical harm (across all patients in incident)`= factor(
-          `Largest physical harm (across all patients in incident)`, 
-          levels= c("No physical harm", "Low physical harm",
-                    "Moderate physical harm","Severe physical harm", "Fatal")),
-        `Largest psychological harm (across all patients in incident)`= factor(
+        `Largest physical harm (across all patients in incident)` = factor(
+          `Largest physical harm (across all patients in incident)`,
+          levels = c(
+            "No physical harm", "Low physical harm",
+            "Moderate physical harm", "Severe physical harm", "Fatal"
+          )
+        ),
+        `Largest psychological harm (across all patients in incident)` = factor(
           `Largest psychological harm (across all patients in incident)`,
-          levels= c("No psychological harm",
-                    "Low psychological harm",
-                    "Moderate psychological harm",
-                    "Severe psychological harm")),
-        `Month`= factor(`Month`, levels=month.abb),
+          levels = c(
+            "No psychological harm",
+            "Low psychological harm",
+            "Moderate psychological harm",
+            "Severe psychological harm"
+          )
+        ),
+        `Month` = factor(`Month`, levels = month.abb),
         `Year` = factor(`Year`,
-                        levels = sort(unique(`Year`))),
-        `Month - Year` = factor(zoo::as.yearmon(`Month - Year`), 
-                                levels = sort(unique(`Month - Year`)))
+          levels = sort(unique(`Year`))
+        ),
+        `Month - Year` = factor(zoo::as.yearmon(`Month - Year`),
+          levels = sort(unique(`Month - Year`))
         )
-    
-  } else if (database_name=="NRLS"){
-    df_with_factors<- df_without_factors |>
-      mutate( `Month`= factor(`Month`, levels=month.abb),
-              `Year` = factor(`Year`,
-                              levels = sort(unique(`Year`))),
-             `PD09 Degree of harm (severity)` = 
-               factor(`PD09 Degree of harm (severity)`,
-                      levels= c("No Harm", "Low", "Moderate", "Severe","Death")),
-             `Month - Year` = factor(zoo::as.yearmon(`Month - Year`), 
-                                     levels = sort(unique(`Month - Year`)))
       )
-    
-  } else if (database_name=="STEIS"){
-    df_with_factors<- df_without_factors |>
-      mutate( `Month`= factor(`Month`, levels=month.abb),
-              `Year` = factor(`Year`,
-                              levels = sort(unique(`Year`))),
-             `Month - Year` = factor(zoo::as.yearmon(`Month - Year`), 
-                                     levels = sort(unique(`Month - Year`)))
+  } else if (database_name == "NRLS") {
+    df_with_factors <- df_without_factors |>
+      mutate(
+        `Month` = factor(`Month`, levels = month.abb),
+        `Year` = factor(`Year`,
+          levels = sort(unique(`Year`))
+        ),
+        `PD09 Degree of harm (severity)` =
+          factor(`PD09 Degree of harm (severity)`,
+            levels = c("No Harm", "Low", "Moderate", "Severe", "Death")
+          ),
+        `Month - Year` = factor(zoo::as.yearmon(`Month - Year`),
+          levels = sort(unique(`Month - Year`))
+        )
       )
-  }else{
+  } else if (database_name == "STEIS") {
+    df_with_factors <- df_without_factors |>
+      mutate(
+        `Month` = factor(`Month`, levels = month.abb),
+        `Year` = factor(`Year`,
+          levels = sort(unique(`Year`))
+        ),
+        `Month - Year` = factor(zoo::as.yearmon(`Month - Year`),
+          levels = sort(unique(`Month - Year`))
+        )
+      )
+  } else {
     print("database name not found")
   }
 
   return(df_with_factors)
-
 }
 
 
 
 
 
-#this function adds a summary table to a sheet
-add_summary_table_to_sheet<- function(wb,
-                                      sheet, 
-                                      summary_table,
-                                      table_start_row, 
-                                      table_start_col){
-  
-  #add summary table to sheet
+# this function adds a summary table to a sheet
+add_summary_table_to_sheet <- function(wb,
+                                       sheet,
+                                       summary_table,
+                                       table_start_row,
+                                       table_start_col) {
+  # add summary table to sheet
   writeData(wb, sheet, summary_table, startRow = table_start_row, startCol = table_start_col, keepNA = TRUE, na.string = "Not available")
-    
+
   setColWidths(wb,
-               sheet = sheet,
-               cols = table_start_col:(table_start_col + ncol(summary_table) - 1 ),
-               widths = 20)
-  
-  # style table - header 
+    sheet = sheet,
+    cols = table_start_col:(table_start_col + ncol(summary_table) - 1),
+    widths = 20
+  )
+
+  # style table - header
   addStyle(
     wb,
     sheet = sheet,
     summaryTableTopBottomStyle,
     rows = table_start_row,
-    cols = table_start_col:(table_start_col + ncol(summary_table) - 1 )
+    cols = table_start_col:(table_start_col + ncol(summary_table) - 1)
   )
-  
-  # style table- row titles 
+
+  # style table- row titles
   addStyle(
     wb,
     sheet = sheet,
@@ -242,58 +251,58 @@ add_summary_table_to_sheet<- function(wb,
     rows = (table_start_row + 1):(nrow(summary_table) + table_start_row - 1),
     cols = table_start_col
   )
-  
-  #style table - main body
+
+  # style table - main body
   addStyle(
     wb,
     sheet = sheet,
     bodyStyleNoBorder,
     rows = (table_start_row + 1):(nrow(summary_table) + table_start_row - 1),
-    cols = (table_start_col + 1):(table_start_col + ncol(summary_table) - 1 ),
+    cols = (table_start_col + 1):(table_start_col + ncol(summary_table) - 1),
     gridExpand = T
   )
-  
-  #style table- header and footer
+
+  # style table- header and footer
   addStyle(
     wb,
     sheet = sheet,
     summaryTableTopBottomStyle,
-    rows = nrow(summary_table) + table_start_row ,
-    cols = table_start_col:(table_start_col + ncol(summary_table) - 1 ),
+    rows = nrow(summary_table) + table_start_row,
+    cols = table_start_col:(table_start_col + ncol(summary_table) - 1),
     gridExpand = T
   )
-  
-
 }
 
-#this function adds a data table to a sheet and styles it 
-add_data_table_to_sheet<- function(wb,
-                                   sheet,
-                                   data_table,
-                                   table_start_row){
-  
+# this function adds a data table to a sheet and styles it
+add_data_table_to_sheet <- function(wb,
+                                    sheet,
+                                    data_table,
+                                    table_start_row) {
   # Write data
   writeData(wb, sheet, data_table, startRow = table_start_row)
-  
+
   # set column widths
   setColWidths(wb,
-               sheet = sheet,
-               cols = 1:ncol(data_table),
-               widths = 35)
-  
+    sheet = sheet,
+    cols = 1:ncol(data_table),
+    widths = 35
+  )
+
   # set row heights - header row
-  
+
   setRowHeights(wb,
-                sheet = sheet,
-                rows = table_start_row:table_start_row,
-                heights = 34)
-  
+    sheet = sheet,
+    rows = table_start_row:table_start_row,
+    heights = 34
+  )
+
   # set row heights - body
   setRowHeights(wb,
-                sheet = sheet,
-                rows = (table_start_row + 1):(nrow(data_table) + table_start_row),
-                heights = 150)
-  
+    sheet = sheet,
+    rows = (table_start_row + 1):(nrow(data_table) + table_start_row),
+    heights = 150
+  )
+
   # Add header style
   addStyle(
     wb,
@@ -302,18 +311,173 @@ add_data_table_to_sheet<- function(wb,
     rows = table_start_row,
     cols = 1:ncol(data_table)
   )
-  
+
   # Add body style
   addStyle(
     wb,
     sheet = sheet,
     bodyStyle,
-    rows = (table_start_row+1):(nrow(data_table) + table_start_row),
+    rows = (table_start_row + 1):(nrow(data_table) + table_start_row),
     cols = 1:ncol(data_table),
     gridExpand = T
   )
+}
+
+
+# function to get find the label for a column value from the column name, code and database name
+get_code_text <- function(column, code, database_name) {
+  if (database_name == "steis") {
+    code <- str_replace_all(code, "#", "")
+    return(code)
+  } else if (database_name == "nrls") {
+    code_text_df <- codes |>
+      filter(col_name == column, SASCODE == code) |>
+      select(OPTIONTEXT)
+  } else if (database_name == "lfpse") {
+    code <- str_replace_all(code, "#", "")
+    code_text_df <- ResponseReference |>
+      filter(QuestionId == column, ResponseCode == code) |>
+      filter(TaxonomyVersion == max(TaxonomyVersion)) |>
+      select(ResponseText) |>
+      distinct(ResponseText)
+  }
+  if (nrow(code_text_df) == 1) {
+    code_text <- pull(code_text_df)
+  } else {
+    code_text <- code
+    print(str_glue("{code} was not found in {column} column in lookup table for {database_name}. (or it was found with duplicates)"))
+  }
+  return(code_text)
+}
+
+# function to get find the column label for a column from the column name and database name
+get_column_text <- function(column, database_name) {
+  if (database_name == "steis") {
+    return(column)
+  } else if (database_name == "lfpse") {
+    column_text_df <- QuestionReference |>
+      filter(QuestionId == column) |>
+      filter(TaxonomyVersion == max(TaxonomyVersion)) |>
+      distinct(QuestionId, Property) |>
+      select(Property)
+  } else if (database_name == "nrls") {
+    column_text_df <- nrls_lookup |>
+      filter(Code == column) |>
+      select(Label)
+  }
+  if (nrow(column_text_df) == 1) {
+    column_new <- pull(column_text_df)
+  } else {
+    column_new <- column
+    print(str_glue("{column} column was not found in lookup table for {database_name}"))
+  }
+  return(column_new)
+}
+# function to translate an individual filter into a more human readable value, given the filter string and database name
+translate_individual_filter <- function(individual_filter, database_name) {
+  # different logic depending on what the filter is
+  if (str_detect(individual_filter, "(IS NOT NA)|(IS NA)")) {
+    split_string <- str_replace(individual_filter, "(IS NOT NA)|(IS NA)", "")
+    column <- split_string |>
+      str_replace_all(fixed("("), "") |>
+      str_replace_all(fixed(")"), "") |>
+      str_trim()
+    column_new <- get_column_text(column, database_name)
+
+    translated_filter <- individual_filter |>
+      # replace IS NOT NA
+      str_replace(str_glue("IS NOT NA\\({column}\\)"), str_glue("{column_new} IS NOT NA")) |>
+      # replace IS NA
+      str_replace(str_glue("IS NA\\({column}\\)"), str_glue("{column_new} IS NA"))
+  } else if (str_detect(individual_filter, "(CONTAINS)|(IS IN)|(IS)")) {
+    split_string <- str_split(individual_filter, "(CONTAINS)|(IS IN)|(IS)") |> unlist()
+    column <- split_string[1] |>
+      str_replace_all(fixed("("), "") |>
+      str_replace_all(fixed(")"), "") |>
+      str_trim()
+    value <- split_string[2] |>
+      str_replace_all(fixed("("), "") |>
+      str_replace_all(fixed(")"), "") |>
+      str_trim()
+    column_new <- get_column_text(column, database_name)
+
+
+    if (str_detect(individual_filter, "(IS IN)")) {
+      value_new <- c()
+      value_split <- str_split(value, ",") |> unlist()
+      for (each_value in value_split) {
+        # find the text corresponding to the code
+        code_text <- get_code_text(column, str_replace(each_value, " ", ""), database_name)
+        # append this value to a vector of values
+        value_new <- append(value_new, code_text)
+      }
+
+      value_new <- str_c(value_new, collapse = ", ")
+    } else {
+      value_new <- get_code_text(column, value, database_name)
+    }
+
+
+    translated_filter <- individual_filter |>
+      # if brackets around both sides of column name- replace brackets (this is relevant for CONTAINS)
+      # (this is a separate step because we only want to remove brackets where they occur on both sides)
+      str_replace(str_glue("\\( *{column} *\\)"), column_new) |>
+      # if brackets not around column name- replace column name
+      str_replace(str_glue("{column}"), column_new) |>
+      str_replace(value, value_new)
+  }
 
-  
+  return(translated_filter)
 }
 
 
+# function to translate a categorical filter (as an expression object) into a mure human readable string given a database name
+translate_categorical_string <- function(categorical_filter, database_name) {
+  if (categorical_filter == 0) {
+    message(str_glue("No {database_name} filter"))
+    return("No categorical filter")
+  }
+
+  # turn the categorical filter into a string
+  categorical_filter_string <- deparse(categorical_filter, width.cutoff = 500)
+
+  # the maximum width cutoff for deparse is 500-
+  # for strings above this length, it will automatically split up the string into a vector.
+  # if this happens, it may break up filters, so we need to combine the vectors back into one string.
+  if (length(categorical_filter_string) > 1) {
+    categorical_filter_string <- str_c(categorical_filter_string, collapse = "")
+  }
+
+  # translate filter into more human readable format, including removing speech marks
+  categorical_filter_copy <- categorical_filter_string |>
+    str_replace_all(fixed('"'), "") |>
+    str_replace_all("==", "IS") |>
+    str_replace_all(fixed("+"), "") |>
+    str_replace_all("!is.na", "IS NOT NA") |>
+    str_replace_all("is.na", "IS NA") |>
+    str_replace_all("(?i)%LIKE%", "CONTAINS") |>
+    str_replace_all("(?i)%IN%", "IS IN") |>
+    str_replace_all("%", "") |>
+    str_replace_all("&", "AND") |>
+    str_replace_all("c\\(", "(") |>
+    str_replace_all(fixed("|"), "OR") |>
+    str_replace_all(" +", " ") # this replaces multiple spaces with a single space
+
+  string_to_split_by <- "(OR|AND)"
+  # if the string contains AND  or OR, we'll need to separate and loop through
+  categorical_filter_copy_split <- str_split(categorical_filter_copy, string_to_split_by) |> unlist()
+  # pull out whether split by and or or
+  bracket_breaks <- str_extract_all(categorical_filter_copy, string_to_split_by) |> unlist()
+
+  translated_filters <- ""
+  for (filter_number in 1:length(categorical_filter_copy_split)) {
+    # translate the individual filter
+    individual_filter_translated <- translate_individual_filter(categorical_filter_copy_split[filter_number], database_name)
+    # extract the AND or OR between filters
+    break_between_filters <- if_else(is.na(bracket_breaks[filter_number]), "", bracket_breaks[filter_number])
+    # add the translated filter and AND or OR to the translated_filters string
+    translated_filters <- str_c(translated_filters, individual_filter_translated, break_between_filters)
+  }
+
+  return(translated_filters)
+}
diff --git a/params.R b/params.R
index 0b62ef2..850334a 100644
--- a/params.R
+++ b/params.R
@@ -20,24 +20,37 @@ source("styles.R")
 # date filter (type is occurring/reported)
 start_date <- "2024-01-01"
 end_date <- "2024-03-30"
+
 date_type <- "occurring"
 
 # TODO: cols to extract (all/default)
 cols_to_extract <- "default"
 
+# translate_categorical_string() can translate filters from the codes used to access the database, to more readable code.
+# it splits up filters by & or | and translates each filter one by one. some examples of filters it can translate are:
+#nrls_categorical <- expr((IN05_LVL1 == 10 | IN05_LVL2 == 3) & (IN04 == 1 | IN04 %in% c(97, 99) & !is.na(RP07) | is.na(IN04) & !is.na(RP07)))
+#lfpse_categorical <- expr((' ' + A001 + ' ') %LIKE% '% 4 %'| ((' ' + A001 + ' ') %LIKE% '% 3 %' & (' ' + A001 + ' ') %LIKE% '% 10 %'))
+
 # nrls categorical filters (wrap in expr() or set to 0)
-nrls_categorical <- expr(IN05_LVL1 == 10)
+nrls_categorical <- expr(IN05_LVL1 == 10 )
 # lfpse categorical filters (wrap in expr() or set to 0)
-lfpse_categorical <- expr(A001 == '4')
+lfpse_categorical <- expr((' ' + A001 + ' ') %LIKE% '% 4 %')
 # steis categorical filters (wrap in expr() or set to 0)
 steis_categorical <- expr(type_of_incident == 'Medication incident meeting SI criteria')
 steis_filename <- 'SUI_2_87360.csv'
 
+expanded_categorical_filter_lfpse<-translate_categorical_string(lfpse_categorical, "lfpse")
+expanded_categorical_filter_nrls<-translate_categorical_string(nrls_categorical, "nrls")
+expanded_categorical_filter_steis<-translate_categorical_string(steis_categorical, "steis")
+message(str_glue("LFPSE filter is: \n{expanded_categorical_filter_lfpse}"))
+message(str_glue("NRLS filter is: \n{expanded_categorical_filter_nrls}"))
+message(str_glue("StEIS filter is: \n{expanded_categorical_filter_steis}"))
+
 # text terms
 #example below- not real example
 text_terms <- list(
   group_A = c("(?i)\\bparacetamol", "(?i)\\bco(-)?codamol"),
-  group_B = c("(i)\\bibuprofen"),
+  group_B = c("(?i)\\bibuprofen"),
   group_C = c("(?i)\\bunwell")
 )
 


Reviews:
user: login: znogbes
body: Thanks for working on this.  In principle, I think it's a good approach to have `translate_categorical_string` as the only function we see in params.R. Where I have reservations is in regards to the current set up of calling a function within a function within a function. I think it can pose a challenge for maintenance as it's currently a 5 layer function call. I think it also adds complexity to testing and losing track of code is easy.  I wonder if the number of steps could be simplified by preparing the string for translation in a different way.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-14T10:10:19Z
user: login: ruth-keane-nhs
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T10:28:12Z
user: login: znogbes
body: Thanks for these changes. I agree code is easier to follow. I might have found little bugs related to brackets and additional white space. 

I do wonder if we can shorten code in  `translate_individual_filter`. I'm aware that IS IN is quite a different case, but some of the wrangling in the other scenarios could be combined with an approach like this:

`pattern <- str_extract(individual_filter, 'IS IN|CONTAINS|IS NA|IS NOT NA|IS')`

`# steps that can be streamlined / are true for all scenarios` 

`split_string<- str_split(individual_filter, pattern) |> unlist()`

`column<- split_string |> 
       str_trim() |>
       str_replace_all(fixed("("),"") |>
        str_replace_all(fixed(")"),"")`
 
`column_new <- get_column_text(column, database_name)`
 
`# special wrangling` 
`if (pattern == 'IS IN) { translated_filter <- ... }`
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-20T17:20:47Z
user: login: ruth-keane-nhs
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T11:24:16Z
user: login: ruth-keane-nhs
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T11:34:36Z
user: login: ruth-keane-nhs
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T11:51:03Z
user: login: znogbes
body: Great work! I think you've balanced out succinctness and code clarity very well in this version. Thanks for working on this. 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-24T11:58:15Z

Review comments:
diff_hunk: @@ -20,11 +20,18 @@ source("styles.R")
 # date filter (type is occurring/reported)
 start_date <- "2024-01-01"
 end_date <- "2024-03-30"
+
 date_type <- "occurring"
 
 # TODO: cols to extract (all/default)
 cols_to_extract <- "default"
 
+# Note- to allow the translate_categorical_string function to work properly, filters should be written in a specific way:
+# - the code uses |, & and brackets to split the long filter into individual filters
+# - filters can be combined using & or | - however | and & may not be used within a filter (i.e. A001== 3|4 would cause an error)
path: params.R
user: login: znogbes
body: Not sure what you mean here, wouldn't `A001== 3|4` be invalid filtering syntax. You'd always want to use  `A001== 3 | A001== 4` to  filter either 3 or 4, wouldn't you?
created_at: 2025-03-13T10:05:13Z
author_association: MEMBER
diff_hunk: @@ -33,6 +40,16 @@ lfpse_categorical <- expr(A001 == '4')
 steis_categorical <- expr(type_of_incident == 'Medication incident meeting SI criteria')
 steis_filename <- 'SUI_2_87360.csv'
 
+expanded_categorical_filter_lfpse<-translate_categorical_string(lfpse_categorical, "lfpse")
+expanded_categorical_filter_nrls<-translate_categorical_string(nrls_categorical, "nrls")
+expanded_categorical_filter_steis<-translate_categorical_string(steis_categorical, "steis")
+message("LFPSE filter is:")
+message(expanded_categorical_filter_lfpse)
+message("NRLS filter is:")
+message(expanded_categorical_filter_nrls)
+message("StEIS filter is:")
+message(expanded_categorical_filter_steis)
+
 # text terms
 #example below- not real example
 text_terms <- list(
path: params.R
user: login: znogbes
body: Caught a small error in regex while translating terms, regex should be `(?i)\\bibuprofen`
created_at: 2025-03-13T10:33:28Z
author_association: MEMBER
diff_hunk: @@ -62,6 +63,20 @@ date_type_text <-
 
 date_range <- glue('Incidents {date_type_text} between {format(as.Date(start_date), "%d-%b-%y")} and {format(as.Date(end_date), "%d-%b-%y")}')
 
+
+text_terms_pretty <- text_terms
path: formatter.R
user: login: znogbes
body: Code leaves some terms untranslated. Ibuprofen is due to incorrect regex, but co-codamol might be a matter of code logic.

`$group_A 
[1] "%paracetamol"         "(?i)\\bco(-)?codamol"`

`$group_B
[1] "(i)%ibuprofen"`

`$group_C
[1] "%unwell"`
created_at: 2025-03-13T10:39:58Z
author_association: MEMBER
diff_hunk: @@ -62,6 +63,20 @@ date_type_text <-
 
 date_range <- glue('Incidents {date_type_text} between {format(as.Date(start_date), "%d-%b-%y")} and {format(as.Date(end_date), "%d-%b-%y")}')
 
+
+text_terms_pretty <- text_terms
+for (group in 1:length(text_terms)){
+  for (term in 1:length(group)){
+    prettier_term<-text_terms[[group]][term] |>
+      str_replace_all(pattern = "\\|", " OR ") |>
+      str_replace_all(pattern = "\\|", " OR ") |>
+      str_replace_all(pattern = "\\\\b", "%") |>
path: formatter.R
user: login: znogbes
body: Consider using below. Not hat original is wrong, but fixed does make things more humanly readable 
```suggestion
      str_replace_all(pattern = fixed('\\b'), "%") |>

```
created_at: 2025-03-13T10:49:47Z
author_association: MEMBER
diff_hunk: @@ -62,6 +63,20 @@ date_type_text <-
 
 date_range <- glue('Incidents {date_type_text} between {format(as.Date(start_date), "%d-%b-%y")} and {format(as.Date(end_date), "%d-%b-%y")}')
 
+
+text_terms_pretty <- text_terms
+for (group in 1:length(text_terms)){
+  for (term in 1:length(group)){
+    prettier_term<-text_terms[[group]][term] |>
+      str_replace_all(pattern = "\\|", " OR ") |>
+      str_replace_all(pattern = "\\|", " OR ") |>
+      str_replace_all(pattern = "\\\\b", "%") |>
+      str_replace_all(pattern = "\\(\\?i\\)", "") |>
path: formatter.R
user: login: znogbes
body: ```suggestion
      str_replace_all(pattern = fixed('(?i)'), "" ) |>

```
created_at: 2025-03-13T10:50:37Z
author_association: MEMBER
diff_hunk: @@ -62,6 +63,20 @@ date_type_text <-
 
 date_range <- glue('Incidents {date_type_text} between {format(as.Date(start_date), "%d-%b-%y")} and {format(as.Date(end_date), "%d-%b-%y")}')
 
+
+text_terms_pretty <- text_terms
+for (group in 1:length(text_terms)){
+  for (term in 1:length(group)){
path: formatter.R
user: login: znogbes
body: This would loop through every term in the group, so co-codamol would be translated
```suggestion
  for (term in 1:length(text_terms[[group]])){

```
created_at: 2025-03-13T12:39:38Z
author_association: MEMBER
diff_hunk: @@ -33,6 +40,16 @@ lfpse_categorical <- expr(A001 == '4')
 steis_categorical <- expr(type_of_incident == 'Medication incident meeting SI criteria')
 steis_filename <- 'SUI_2_87360.csv'
 
+expanded_categorical_filter_lfpse<-translate_categorical_string(lfpse_categorical, "lfpse")
+expanded_categorical_filter_nrls<-translate_categorical_string(nrls_categorical, "nrls")
+expanded_categorical_filter_steis<-translate_categorical_string(steis_categorical, "steis")
+message("LFPSE filter is:")
path: params.R
user: login: znogbes
body: Code could be shortened by using:
```suggestion
message(str_glue("LFPSE filter is:\n {expanded_categorical_filter_lfpse}"))
```
created_at: 2025-03-13T12:43:57Z
author_association: MEMBER
diff_hunk: @@ -312,8 +312,264 @@ add_data_table_to_sheet<- function(wb,
     cols = 1:ncol(data_table),
     gridExpand = T
   )
+  
+  
+}
+
+
+# function to get find the label for a column value from the column name, code and database name
+get_code_text <-function(column, code, database_name){
+  if (database_name=="steis"){
+    code= str_replace_all(code,"#","")
+    return(code)
+  }else if(database_name=="nrls"){
+    code_text_df<-codes |> 
+      filter(col_name==column, SASCODE== code) |>
+      select(OPTIONTEXT)
+  } else if(database_name=="lfpse"){
+    code= str_replace_all(code,"#","")
+    code_text_df<-ResponseReference |> 
+      filter(QuestionId==column, ResponseCode==code) |>
+      filter(TaxonomyVersion==max(TaxonomyVersion)) |>
+      select(ResponseText) %>%
+      distinct(ResponseText)
+  }
+  if (nrow(code_text_df) == 1) {
+    code_text <- pull(code_text_df)
+  } else {
+    code_text<-  code
+    print(str_glue("{code} was not found in {column} column in lookup table for {database_name}. (or it was found with duplicates)"))
+  }
+  return(code_text)
+}
+
+# function to get find the column label for a column from the column name and database name
+get_column_text<-function(column, database_name){
+  if (database_name=="steis"){
+    return(column)
+  } else if (database_name=="lfpse"){
+    column_text_df<-QuestionReference |> 
+      filter(QuestionId==column) |>
+      filter(TaxonomyVersion==max(TaxonomyVersion)) |>
+      distinct(QuestionId, Property) |>
+      select(Property)
+  }else if (database_name=="nrls"){
+    column_text_df<- nrls_lookup %>% 
+      filter(Code==column) %>% 
+      select(Label)
+  }
+  if (nrow(column_text_df) == 1){
+    column_new <- pull(column_text_df)
+  } else{
+    column_new <- column
+    print(str_glue("{column} column was not found in lookup table for {database_name}"))
+  }
+  return(column_new)   
+}
 
+#function to identify the variable type from the features it includes
+find_filter_category<- function(i){
+  
+  like_present <-sum(str_detect(i, "%LIKE%")) == 1
+  space_number_space_present <- sum(str_detect(i, "% \\d+ %")) == 1 
+  space_colname_space_present <- sum(str_detect(i, '# # *% *\\w* *% *# #')) == 1
+  vector_present <- sum(str_detect(as.character(i), "c~")) == 1
+  in_present <- sum(str_detect(as.character(i), "%in%")) == 1
+  equals_present <- sum(str_detect(as.character(i), "==|!=")) == 1
+  
+  
+  type_na <- sum(str_detect(i, "na"))
+  
+  type_equals <- if_else(equals_present & !vector_present, 1, 0)
+  
+  type_in <- if_else(vector_present & in_present, 1, 0)
+  
+  type_multi <- if_else(like_present & space_number_space_present & space_colname_space_present, 1, 0)
   
+  filter_category=NA
+  
+  if (type_equals == 1  &  sum(type_in, type_na, type_multi)==0){
+    filter_category="equals"
+  }
+  else if (type_in == 1  &  sum(type_equals, type_na, type_multi)==0){
+    filter_category="in"
+  }
+  else if (type_na == 1  &  sum(type_in, type_equals, type_multi)==0){
+    filter_category="filter_na"
+  }
+  else if (type_multi== 1  &  sum(type_in, type_na, type_equals)==0){
+    filter_category="multi"
+  } else{
+    print("Filter category not found")
+  }
+  return(filter_category)
 }
 
+#function to translate a filter into a more human readable value, given the filter string and data
+translate_individual_filter <- function(one_filter, database_name){
+  
+  filter_category<-find_filter_category(one_filter) 
+  
+  if (filter_category=="in"){
+    #split each filter into column, value and operator
+    column <- str_trim(str_split(one_filter,"%in%")[[1]][1])
+    column_new <- get_column_text(column, database_name)  
+    value_old <- str_trim(str_split(one_filter,"%in%")[[1]][2])
+    value_old_no_spaces <- str_replace(value_old," ", "")
+    operator <- "IN"
+    
+    #create vector for value
+    value_new <- c()
+    
+    # get value descriptions
+    # loop through each element in the vector (so can handle 1 or c(1,2,3))
+    for (j in str_split(value_old_no_spaces,"")[[1]]) {
+      #this is required because value is of the type class when it is in c(1,2,3) form
+      if (! j %in% c("c", ",", " ", "~", "#")){
+        
+        #get text for this element of value 
+        code_text <- get_code_text(column, j, database_name)
+        #append this value to a vector of values
+        value_new <- append(value_new, code_text)
+      }
+    }
+    
+    #convert from vector to string
+    value_new_string <- str_c(value_new, collapse = ", ")
+    
+    # add brackets to either side of value_new_string- helpful for clarity
+    value_new_string <- str_c("(", value_new_string, ")")
+    
+    #recreate the filter by combining column, operator and value old
+    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+    
+  } else if (filter_category=="equals"){
+    
+    column <- str_trim(str_split(one_filter,"==")[[1]][1])
+    column_new <- get_column_text(column, database_name)  
+    value_old <-str_trim(str_split(one_filter,"==")[[1]][2])
+    operator <- "="
+    
+    
+    value_new_string <- get_code_text(column, value_old, database_name)
+    
+    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+    
+    
+  }else if (filter_category=="multi"){
+    column <- one_filter%>% 
+      str_extract("[a-zA-Z0-9_.-]+")
+    column_new <- get_column_text(column, database_name)  
+    value_old <- str_extract(one_filter,"% \\d %")
+    operator <- "CONTAINS"
+    
+    value_pretty <- value_old %>% 
+      str_replace_all("%","") %>% #get rid of "% "- which is present with a multi-select column
+      str_trim()
+    value_new_string <- get_code_text(column, value_pretty, database_name)
+    #recreate the filter by combining column, operator and value old
+    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+  }else if (filter_category=="filter_na"){
+    not_na<-!str_detect(one_filter,"!")
+    column<- str_extract(one_filter,"[a-zA-Z0-9_-]{3,}")
+    column_new <- get_column_text(column , database_name) 
+    if (not_na){
+      filter_nice <- str_glue("{column_new} IS NOT NA")
+    } else {
+      filter_nice <- str_glue("{column_new} IS NA")
+    }
+    
+  } 
+  return(filter_nice)
+}
 
+#tranlate a subset of a filter- specifically, a section that occurs within brackets, seperated by | or &
+translate_filter_subset<- function(filter_subset, database_name){
+  
+  #replace the brackets at start and end of the subset
+  filter_subset<- filter_subset %>%
+    str_replace_all("^~","") %>%
+    str_replace_all("~$","")
+  
+  # split into each filter using | and & 
+  filter_vector<- str_split(filter_subset, "\\&|\\|")[[1]]
+  
+  # loop through filters in vectors, translating them
+  for (one_filter in filter_vector){
+    
+    one_filter_translated<- translate_individual_filter(one_filter, database_name)
+    
+    #replace the old filter with the translated filter 
+    filter_subset<-str_replace_all(filter_subset,
+                                   one_filter, 
+                                   one_filter_translated) 
+  }
+  
+  #replace the symbols with AND and OR
+  filter_subset <- filter_subset %>%
+    str_replace_all("\\&", " AND ") %>%
+    str_replace_all("\\£", " OR ")
+  
+  return(filter_subset)
+}
+
+
+
+#function to translate a categorical filter (as an expression object) into a mure human readable string given a database name
+translate_categorical_string<- function(categorical_filter, database_name){
+  
+  if(categorical_filter==0){
+    message(str_glue("No {database_name} filter"))
+    return("No categorical filter")
+  }
+  
+  #turn the categorical filter into a string
+  categorical_filter_string <- deparse(categorical_filter, width.cutoff = 500)
+  
+  #the maximum width cutoff for deparse is 500- 
+  #for strings above this length, it will automatically split up the string into a vector.
+  #if this happens, it may break up filters, so we need to combine the vectors back into one string.
+  if (length(categorical_filter_string)> 1){
+    categorical_filter_string<- str_c(categorical_filter_string, collapse = "")
+  }
+  
+  # brackets, plus symbols and speech marks are regex special characters and working with them is awkward
+  # so we replace them with non protected characters 
+  categorical_filter_copy<- categorical_filter_string %>%
+    str_replace_all("\\(", "~") %>% # replace brackets with ~
+    str_replace_all("\\)", "~") %>% # replace brackets with ~
+    str_replace_all('\\"', "#") %>% # replace speech marks with #
+    str_replace_all('\\+', "%") %>% # replace + with %
+    str_replace_all('\\|', "£") # replace | with £
+  
+  #if the string contains and  or  or- split by brackets, we'll need to seperate and loop through  
+  categorical_filter_copy_split <- str_split(categorical_filter_copy, "~ *(£|&) *~")[[1]]
path: functions.R
user: login: znogbes
body: Below is clearer in my opinion 
```suggestion
  categorical_filter_copy_split <- str_split(categorical_filter_copy, "~ *(£|&) *~") |> unlist()
```
created_at: 2025-03-13T18:41:04Z
author_association: MEMBER
diff_hunk: @@ -312,8 +312,264 @@ add_data_table_to_sheet<- function(wb,
     cols = 1:ncol(data_table),
     gridExpand = T
   )
+  
+  
+}
+
+
+# function to get find the label for a column value from the column name, code and database name
+get_code_text <-function(column, code, database_name){
+  if (database_name=="steis"){
+    code= str_replace_all(code,"#","")
+    return(code)
+  }else if(database_name=="nrls"){
+    code_text_df<-codes |> 
+      filter(col_name==column, SASCODE== code) |>
+      select(OPTIONTEXT)
+  } else if(database_name=="lfpse"){
+    code= str_replace_all(code,"#","")
+    code_text_df<-ResponseReference |> 
+      filter(QuestionId==column, ResponseCode==code) |>
+      filter(TaxonomyVersion==max(TaxonomyVersion)) |>
+      select(ResponseText) %>%
+      distinct(ResponseText)
+  }
+  if (nrow(code_text_df) == 1) {
+    code_text <- pull(code_text_df)
+  } else {
+    code_text<-  code
+    print(str_glue("{code} was not found in {column} column in lookup table for {database_name}. (or it was found with duplicates)"))
+  }
+  return(code_text)
+}
+
+# function to get find the column label for a column from the column name and database name
+get_column_text<-function(column, database_name){
+  if (database_name=="steis"){
+    return(column)
+  } else if (database_name=="lfpse"){
+    column_text_df<-QuestionReference |> 
+      filter(QuestionId==column) |>
+      filter(TaxonomyVersion==max(TaxonomyVersion)) |>
+      distinct(QuestionId, Property) |>
+      select(Property)
+  }else if (database_name=="nrls"){
+    column_text_df<- nrls_lookup %>% 
+      filter(Code==column) %>% 
+      select(Label)
+  }
+  if (nrow(column_text_df) == 1){
+    column_new <- pull(column_text_df)
+  } else{
+    column_new <- column
+    print(str_glue("{column} column was not found in lookup table for {database_name}"))
+  }
+  return(column_new)   
+}
 
+#function to identify the variable type from the features it includes
+find_filter_category<- function(i){
+  
+  like_present <-sum(str_detect(i, "%LIKE%")) == 1
+  space_number_space_present <- sum(str_detect(i, "% \\d+ %")) == 1 
+  space_colname_space_present <- sum(str_detect(i, '# # *% *\\w* *% *# #')) == 1
+  vector_present <- sum(str_detect(as.character(i), "c~")) == 1
+  in_present <- sum(str_detect(as.character(i), "%in%")) == 1
+  equals_present <- sum(str_detect(as.character(i), "==|!=")) == 1
+  
+  
+  type_na <- sum(str_detect(i, "na"))
+  
+  type_equals <- if_else(equals_present & !vector_present, 1, 0)
+  
+  type_in <- if_else(vector_present & in_present, 1, 0)
+  
+  type_multi <- if_else(like_present & space_number_space_present & space_colname_space_present, 1, 0)
   
+  filter_category=NA
+  
+  if (type_equals == 1  &  sum(type_in, type_na, type_multi)==0){
+    filter_category="equals"
+  }
+  else if (type_in == 1  &  sum(type_equals, type_na, type_multi)==0){
+    filter_category="in"
+  }
+  else if (type_na == 1  &  sum(type_in, type_equals, type_multi)==0){
+    filter_category="filter_na"
+  }
+  else if (type_multi== 1  &  sum(type_in, type_na, type_equals)==0){
+    filter_category="multi"
+  } else{
+    print("Filter category not found")
+  }
+  return(filter_category)
 }
 
+#function to translate a filter into a more human readable value, given the filter string and data
+translate_individual_filter <- function(one_filter, database_name){
+  
+  filter_category<-find_filter_category(one_filter) 
+  
+  if (filter_category=="in"){
+    #split each filter into column, value and operator
+    column <- str_trim(str_split(one_filter,"%in%")[[1]][1])
+    column_new <- get_column_text(column, database_name)  
+    value_old <- str_trim(str_split(one_filter,"%in%")[[1]][2])
+    value_old_no_spaces <- str_replace(value_old," ", "")
+    operator <- "IN"
+    
+    #create vector for value
+    value_new <- c()
+    
+    # get value descriptions
+    # loop through each element in the vector (so can handle 1 or c(1,2,3))
+    for (j in str_split(value_old_no_spaces,"")[[1]]) {
+      #this is required because value is of the type class when it is in c(1,2,3) form
+      if (! j %in% c("c", ",", " ", "~", "#")){
+        
+        #get text for this element of value 
+        code_text <- get_code_text(column, j, database_name)
+        #append this value to a vector of values
+        value_new <- append(value_new, code_text)
+      }
+    }
+    
+    #convert from vector to string
+    value_new_string <- str_c(value_new, collapse = ", ")
+    
+    # add brackets to either side of value_new_string- helpful for clarity
+    value_new_string <- str_c("(", value_new_string, ")")
+    
+    #recreate the filter by combining column, operator and value old
+    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+    
+  } else if (filter_category=="equals"){
+    
+    column <- str_trim(str_split(one_filter,"==")[[1]][1])
+    column_new <- get_column_text(column, database_name)  
+    value_old <-str_trim(str_split(one_filter,"==")[[1]][2])
+    operator <- "="
+    
+    
+    value_new_string <- get_code_text(column, value_old, database_name)
+    
+    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+    
+    
+  }else if (filter_category=="multi"){
+    column <- one_filter%>% 
+      str_extract("[a-zA-Z0-9_.-]+")
+    column_new <- get_column_text(column, database_name)  
+    value_old <- str_extract(one_filter,"% \\d %")
+    operator <- "CONTAINS"
+    
+    value_pretty <- value_old %>% 
+      str_replace_all("%","") %>% #get rid of "% "- which is present with a multi-select column
+      str_trim()
+    value_new_string <- get_code_text(column, value_pretty, database_name)
+    #recreate the filter by combining column, operator and value old
+    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+  }else if (filter_category=="filter_na"){
+    not_na<-!str_detect(one_filter,"!")
+    column<- str_extract(one_filter,"[a-zA-Z0-9_-]{3,}")
+    column_new <- get_column_text(column , database_name) 
+    if (not_na){
+      filter_nice <- str_glue("{column_new} IS NOT NA")
+    } else {
+      filter_nice <- str_glue("{column_new} IS NA")
+    }
+    
+  } 
+  return(filter_nice)
+}
 
+#tranlate a subset of a filter- specifically, a section that occurs within brackets, seperated by | or &
+translate_filter_subset<- function(filter_subset, database_name){
+  
+  #replace the brackets at start and end of the subset
+  filter_subset<- filter_subset %>%
+    str_replace_all("^~","") %>%
+    str_replace_all("~$","")
+  
+  # split into each filter using | and & 
+  filter_vector<- str_split(filter_subset, "\\&|\\|")[[1]]
+  
+  # loop through filters in vectors, translating them
+  for (one_filter in filter_vector){
+    
+    one_filter_translated<- translate_individual_filter(one_filter, database_name)
+    
+    #replace the old filter with the translated filter 
+    filter_subset<-str_replace_all(filter_subset,
+                                   one_filter, 
+                                   one_filter_translated) 
+  }
+  
+  #replace the symbols with AND and OR
+  filter_subset <- filter_subset %>%
path: functions.R
user: login: znogbes
body: I would rename `filter_subset` to `filter_subset_translated` to help tracking code and see differences between objects (for easier testing)
created_at: 2025-03-13T18:59:05Z
author_association: MEMBER
diff_hunk: @@ -312,8 +312,264 @@ add_data_table_to_sheet<- function(wb,
     cols = 1:ncol(data_table),
     gridExpand = T
   )
+  
+  
+}
+
+
+# function to get find the label for a column value from the column name, code and database name
+get_code_text <-function(column, code, database_name){
+  if (database_name=="steis"){
+    code= str_replace_all(code,"#","")
+    return(code)
+  }else if(database_name=="nrls"){
+    code_text_df<-codes |> 
+      filter(col_name==column, SASCODE== code) |>
+      select(OPTIONTEXT)
+  } else if(database_name=="lfpse"){
+    code= str_replace_all(code,"#","")
+    code_text_df<-ResponseReference |> 
+      filter(QuestionId==column, ResponseCode==code) |>
+      filter(TaxonomyVersion==max(TaxonomyVersion)) |>
+      select(ResponseText) %>%
+      distinct(ResponseText)
+  }
+  if (nrow(code_text_df) == 1) {
+    code_text <- pull(code_text_df)
+  } else {
+    code_text<-  code
+    print(str_glue("{code} was not found in {column} column in lookup table for {database_name}. (or it was found with duplicates)"))
+  }
+  return(code_text)
+}
+
+# function to get find the column label for a column from the column name and database name
+get_column_text<-function(column, database_name){
+  if (database_name=="steis"){
+    return(column)
+  } else if (database_name=="lfpse"){
+    column_text_df<-QuestionReference |> 
+      filter(QuestionId==column) |>
+      filter(TaxonomyVersion==max(TaxonomyVersion)) |>
+      distinct(QuestionId, Property) |>
+      select(Property)
+  }else if (database_name=="nrls"){
+    column_text_df<- nrls_lookup %>% 
+      filter(Code==column) %>% 
+      select(Label)
+  }
+  if (nrow(column_text_df) == 1){
+    column_new <- pull(column_text_df)
+  } else{
+    column_new <- column
+    print(str_glue("{column} column was not found in lookup table for {database_name}"))
+  }
+  return(column_new)   
+}
 
+#function to identify the variable type from the features it includes
+find_filter_category<- function(i){
path: functions.R
user: login: znogbes
body: Seems this might have been in a previous look, hence the name i? Might be clearer to change to one_filter?
created_at: 2025-03-13T20:40:22Z
author_association: MEMBER
path: functions.R
user: login: znogbes
body: A couple of comments here: 

- I think `translate_filter_subset` could be merged into `translate_categorical_string`
- Same for `filter_category` into  translate_individual_filter, but I do wonder if  `filter_category`  could be avoided at all if the string is prepared differently? See example below.
- Did you explore working with did you explore working with `fixed()`. I get the conversions of protected regex characters might be necessary, but it does make reading the code less human friendly.

What I mean by reducing number of steps sort of looks like this:

`categorical_filter_string  = expr((A001 == "3") & (A008 %in% c("1","2") & is.na(A001) & !is.na(A001) & ' ' + A001 +  ' ' %LIKE% '% 3 %') |  (!is.na(A001)))`

`categorical_filter_string |> 
    str_replace_all(fixed('('), fixed('( ')) |> 
    str_replace_all(fixed(')'), fixed(' )')) |>
    str_replace_all(fixed('!is.na'), 'IS NOT MISSING ') |>
    str_replace_all(fixed('is.na'), 'IS MISSING ') |>
    str_replace_all(fixed('\"'), '') |>
    str_replace_all(fixed('%'), '') |>
    str_replace_all(fixed('+'), '') |>
    str_replace_all(fixed('in'), 'IN') |>
    str_replace_all(fixed('=='), '=') |>
    str_replace_all(fixed('&'), 'AND') |>
    str_replace_all(fixed('|'), 'OR') |>
    str_replace_all(fixed('LIKE'), 'CONTAINS') |>
    str_replace_all('  ', ' ') |>
    str_split('AND|OR') |>
    unlist()`

An approach like that would give you split strings that could go through functions `translate_individual_filter`, `get_code_text`, and `get_column_text`

so your process could be calling the main function `translate_categorical_string`, and inside you have 4 steps  `split string |> replace variable names |> replace code names |> paste string back `
created_at: 2025-03-14T10:26:58Z
author_association: MEMBER
diff_hunk: @@ -20,11 +20,18 @@ source("styles.R")
 # date filter (type is occurring/reported)
 start_date <- "2024-01-01"
 end_date <- "2024-03-30"
+
 date_type <- "occurring"
 
 # TODO: cols to extract (all/default)
 cols_to_extract <- "default"
 
+# Note- to allow the translate_categorical_string function to work properly, filters should be written in a specific way:
+# - the code uses |, & and brackets to split the long filter into individual filters
+# - filters can be combined using & or | - however | and & may not be used within a filter (i.e. A001== 3|4 would cause an error)
path: params.R
user: login: ruth-keane-nhs
body: I tested it out and it is valid syntax- but can remove the comment if you don't think its a risk
created_at: 2025-03-14T10:28:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -533,43 +491,37 @@ translate_categorical_string<- function(categorical_filter, database_name){
     categorical_filter_string<- str_c(categorical_filter_string, collapse = "")
   }
   
-  # brackets, plus symbols and speech marks are regex special characters and working with them is awkward
-  # so we replace them with non protected characters 
-  categorical_filter_copy<- categorical_filter_string %>%
-    str_replace_all("\\(", "~") %>% # replace brackets with ~
-    str_replace_all("\\)", "~") %>% # replace brackets with ~
-    str_replace_all('\\"', "#") %>% # replace speech marks with #
-    str_replace_all('\\+', "%") %>% # replace + with %
-    str_replace_all('\\|', "£") # replace | with £
-  
-  #if the string contains and  or  or- split by brackets, we'll need to seperate and loop through  
-  categorical_filter_copy_split <- str_split(categorical_filter_copy, "~ *(£|&) *~")[[1]]
+  #translate filter into more human readable format, including removing speech marks
+  categorical_filter_copy<- categorical_filter_string |>
+    str_replace_all(fixed('"'), "") |> 
+    str_replace_all('==', "IS") |>
+    str_replace_all(fixed('+'), "") |> 
+    str_replace_all('!is.na', "IS NOT NA") |>
+    str_replace_all('is.na', "IS NA") |> 
+    str_replace_all('(?i)%LIKE%', "CONTAINS") |> 
+    str_replace_all('(?i)%IN%', "IS IN") |> 
+    str_replace_all('%', "") |> 
+    str_replace_all('&', "AND") |> 
+    str_replace_all('c\\(', "(") |> 
+    str_replace_all(fixed('|'), "OR") |> 
+    str_replace_all(" +", " ")
path: functions.R
user: login: znogbes
body: Is this still needed given 498 `str_replace_all(fixed('+'), "")`?
created_at: 2025-03-20T16:23:35Z
author_association: MEMBER
diff_hunk: @@ -367,157 +367,115 @@ get_column_text<-function(column, database_name){
   return(column_new)   
 }
 
-#function to identify the variable type from the features it includes
-find_filter_category<- function(i){
-  
-  like_present <-sum(str_detect(i, "%LIKE%")) == 1
-  space_number_space_present <- sum(str_detect(i, "% \\d+ %")) == 1 
-  space_colname_space_present <- sum(str_detect(i, '# # *% *\\w* *% *# #')) == 1
-  vector_present <- sum(str_detect(as.character(i), "c~")) == 1
-  in_present <- sum(str_detect(as.character(i), "%in%")) == 1
-  equals_present <- sum(str_detect(as.character(i), "==|!=")) == 1
-  
-  
-  type_na <- sum(str_detect(i, "na"))
-  
-  type_equals <- if_else(equals_present & !vector_present, 1, 0)
-  
-  type_in <- if_else(vector_present & in_present, 1, 0)
-  
-  type_multi <- if_else(like_present & space_number_space_present & space_colname_space_present, 1, 0)
-  
-  filter_category=NA
-  
-  if (type_equals == 1  &  sum(type_in, type_na, type_multi)==0){
-    filter_category="equals"
-  }
-  else if (type_in == 1  &  sum(type_equals, type_na, type_multi)==0){
-    filter_category="in"
-  }
-  else if (type_na == 1  &  sum(type_in, type_equals, type_multi)==0){
-    filter_category="filter_na"
-  }
-  else if (type_multi== 1  &  sum(type_in, type_na, type_equals)==0){
-    filter_category="multi"
-  } else{
-    print("Filter category not found")
-  }
-  return(filter_category)
-}
 
-#function to translate a filter into a more human readable value, given the filter string and data
-translate_individual_filter <- function(one_filter, database_name){
-  
-  filter_category<-find_filter_category(one_filter) 
-  
-  if (filter_category=="in"){
-    #split each filter into column, value and operator
-    column <- str_trim(str_split(one_filter,"%in%")[[1]][1])
-    column_new <- get_column_text(column, database_name)  
-    value_old <- str_trim(str_split(one_filter,"%in%")[[1]][2])
-    value_old_no_spaces <- str_replace(value_old," ", "")
-    operator <- "IN"
-    
-    #create vector for value
-    value_new <- c()
-    
-    # get value descriptions
-    # loop through each element in the vector (so can handle 1 or c(1,2,3))
-    for (j in str_split(value_old_no_spaces,"")[[1]]) {
-      #this is required because value is of the type class when it is in c(1,2,3) form
-      if (! j %in% c("c", ",", " ", "~", "#")){
-        
-        #get text for this element of value 
-        code_text <- get_code_text(column, j, database_name)
-        #append this value to a vector of values
-        value_new <- append(value_new, code_text)
-      }
-    }
-    
-    #convert from vector to string
-    value_new_string <- str_c(value_new, collapse = ", ")
-    
-    # add brackets to either side of value_new_string- helpful for clarity
-    value_new_string <- str_c("(", value_new_string, ")")
-    
-    #recreate the filter by combining column, operator and value old
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+#function to translate an individual filter into a more human readable value, given the filter string and database name
+translate_individual_filter <- function(individual_filter, database_name){
+
+  #different logic depending on what the filter is
+  if (str_detect(individual_filter,"IS IN")){
     
-  } else if (filter_category=="equals"){
+    split_string<- str_split(individual_filter,"IS IN") |> unlist()
+    column<- split_string[1]|> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
+    value <- split_string[2]|> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
+    value_split <- str_split(value, ",") |> unlist()
     
-    column <- str_trim(str_split(one_filter,"==")[[1]][1])
     column_new <- get_column_text(column, database_name)  
-    value_old <-str_trim(str_split(one_filter,"==")[[1]][2])
-    operator <- "="
     
+    value_new<- c()
+    for (each_value in value_split){
+      #find the text corresponding to the code
+      code_text <- get_code_text(column, str_replace(each_value," ",""), database_name)
+      #append this value to a vector of values
+      value_new <- append(value_new, code_text)
+    }
     
-    value_new_string <- get_code_text(column, value_old, database_name)
+    value_new<-str_c(value_new, collapse=", ")
     
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+    translated_filter<- individual_filter |>
+      str_replace(column, column_new) |>
+      str_replace(value, value_new)
     
     
-  }else if (filter_category=="multi"){
-    column <- one_filter%>% 
-      str_extract("[a-zA-Z0-9_.-]+")
+  }else if (str_detect(individual_filter, "CONTAINS")){
+
+    split_string<- str_split(individual_filter,"CONTAINS") |> unlist()
+    column<- split_string[1] |> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim() 
+    value <- split_string[2] |> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
     column_new <- get_column_text(column, database_name)  
-    value_old <- str_extract(one_filter,"% \\d %")
-    operator <- "CONTAINS"
+    value_new <- get_code_text(column, value, database_name)
     
-    value_pretty <- value_old %>% 
-      str_replace_all("%","") %>% #get rid of "% "- which is present with a multi-select column
-      str_trim()
-    value_new_string <- get_code_text(column, value_pretty, database_name)
-    #recreate the filter by combining column, operator and value old
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
-  }else if (filter_category=="filter_na"){
-    not_na<-!str_detect(one_filter,"!")
-    column<- str_extract(one_filter,"[a-zA-Z0-9_-]{3,}")
-    column_new <- get_column_text(column , database_name) 
-    if (not_na){
-      filter_nice <- str_glue("{column_new} IS NOT NA")
-    } else {
-      filter_nice <- str_glue("{column_new} IS NA")
-    }
+    translated_filter<- individual_filter |>
+      str_replace(str_glue("\\( *{column} *\\)"), column_new) |>
+      str_replace(str_glue("{column}"), column_new) |>
+      str_replace(value, value_new)
     
-  } 
-  return(filter_nice)
-}
+   }else if (str_detect(individual_filter, "IS NA")){
path: functions.R
user: login: znogbes
body: `(IN05_LVL1 == 10 | IN05_LVL2 == 3) & (IN04 == 1 | IN04 %in% c(97, 
    99) & !is.na(RP07) | is.na(IN04) & !is.na(RP07))`

is being translated as 

`(Incident category - level 1  IS Medication OR Incident category - level 2  IS Access / admission - unexpected readmission / reattendance) AND (Country  IS England OR Country  IS IN (Other, Unknown) AND NHS organisation code  IS NOT NA) OR Country  IS NA) AND NHS organisation code  IS NOT NA))`

There seems to be a bug with the brackets when there is an IS/IS NOT NA: `Country  IS NA)` /   `NHS organisation code  IS NOT NA)`


created_at: 2025-03-20T16:30:04Z
author_association: MEMBER
diff_hunk: @@ -367,157 +367,115 @@ get_column_text<-function(column, database_name){
   return(column_new)   
 }
 
-#function to identify the variable type from the features it includes
-find_filter_category<- function(i){
-  
-  like_present <-sum(str_detect(i, "%LIKE%")) == 1
-  space_number_space_present <- sum(str_detect(i, "% \\d+ %")) == 1 
-  space_colname_space_present <- sum(str_detect(i, '# # *% *\\w* *% *# #')) == 1
-  vector_present <- sum(str_detect(as.character(i), "c~")) == 1
-  in_present <- sum(str_detect(as.character(i), "%in%")) == 1
-  equals_present <- sum(str_detect(as.character(i), "==|!=")) == 1
-  
-  
-  type_na <- sum(str_detect(i, "na"))
-  
-  type_equals <- if_else(equals_present & !vector_present, 1, 0)
-  
-  type_in <- if_else(vector_present & in_present, 1, 0)
-  
-  type_multi <- if_else(like_present & space_number_space_present & space_colname_space_present, 1, 0)
-  
-  filter_category=NA
-  
-  if (type_equals == 1  &  sum(type_in, type_na, type_multi)==0){
-    filter_category="equals"
-  }
-  else if (type_in == 1  &  sum(type_equals, type_na, type_multi)==0){
-    filter_category="in"
-  }
-  else if (type_na == 1  &  sum(type_in, type_equals, type_multi)==0){
-    filter_category="filter_na"
-  }
-  else if (type_multi== 1  &  sum(type_in, type_na, type_equals)==0){
-    filter_category="multi"
-  } else{
-    print("Filter category not found")
-  }
-  return(filter_category)
-}
 
-#function to translate a filter into a more human readable value, given the filter string and data
-translate_individual_filter <- function(one_filter, database_name){
-  
-  filter_category<-find_filter_category(one_filter) 
-  
-  if (filter_category=="in"){
-    #split each filter into column, value and operator
-    column <- str_trim(str_split(one_filter,"%in%")[[1]][1])
-    column_new <- get_column_text(column, database_name)  
-    value_old <- str_trim(str_split(one_filter,"%in%")[[1]][2])
-    value_old_no_spaces <- str_replace(value_old," ", "")
-    operator <- "IN"
-    
-    #create vector for value
-    value_new <- c()
-    
-    # get value descriptions
-    # loop through each element in the vector (so can handle 1 or c(1,2,3))
-    for (j in str_split(value_old_no_spaces,"")[[1]]) {
-      #this is required because value is of the type class when it is in c(1,2,3) form
-      if (! j %in% c("c", ",", " ", "~", "#")){
-        
-        #get text for this element of value 
-        code_text <- get_code_text(column, j, database_name)
-        #append this value to a vector of values
-        value_new <- append(value_new, code_text)
-      }
-    }
-    
-    #convert from vector to string
-    value_new_string <- str_c(value_new, collapse = ", ")
-    
-    # add brackets to either side of value_new_string- helpful for clarity
-    value_new_string <- str_c("(", value_new_string, ")")
-    
-    #recreate the filter by combining column, operator and value old
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+#function to translate an individual filter into a more human readable value, given the filter string and database name
+translate_individual_filter <- function(individual_filter, database_name){
+
+  #different logic depending on what the filter is
+  if (str_detect(individual_filter,"IS IN")){
     
-  } else if (filter_category=="equals"){
+    split_string<- str_split(individual_filter,"IS IN") |> unlist()
+    column<- split_string[1]|> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
+    value <- split_string[2]|> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
+    value_split <- str_split(value, ",") |> unlist()
     
-    column <- str_trim(str_split(one_filter,"==")[[1]][1])
     column_new <- get_column_text(column, database_name)  
-    value_old <-str_trim(str_split(one_filter,"==")[[1]][2])
-    operator <- "="
     
+    value_new<- c()
+    for (each_value in value_split){
+      #find the text corresponding to the code
+      code_text <- get_code_text(column, str_replace(each_value," ",""), database_name)
+      #append this value to a vector of values
+      value_new <- append(value_new, code_text)
+    }
     
-    value_new_string <- get_code_text(column, value_old, database_name)
+    value_new<-str_c(value_new, collapse=", ")
     
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+    translated_filter<- individual_filter |>
+      str_replace(column, column_new) |>
+      str_replace(value, value_new)
     
     
-  }else if (filter_category=="multi"){
-    column <- one_filter%>% 
-      str_extract("[a-zA-Z0-9_.-]+")
+  }else if (str_detect(individual_filter, "CONTAINS")){
+
+    split_string<- str_split(individual_filter,"CONTAINS") |> unlist()
+    column<- split_string[1] |> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim() 
+    value <- split_string[2] |> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
     column_new <- get_column_text(column, database_name)  
-    value_old <- str_extract(one_filter,"% \\d %")
-    operator <- "CONTAINS"
+    value_new <- get_code_text(column, value, database_name)
     
-    value_pretty <- value_old %>% 
-      str_replace_all("%","") %>% #get rid of "% "- which is present with a multi-select column
-      str_trim()
-    value_new_string <- get_code_text(column, value_pretty, database_name)
-    #recreate the filter by combining column, operator and value old
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
-  }else if (filter_category=="filter_na"){
-    not_na<-!str_detect(one_filter,"!")
-    column<- str_extract(one_filter,"[a-zA-Z0-9_-]{3,}")
-    column_new <- get_column_text(column , database_name) 
-    if (not_na){
-      filter_nice <- str_glue("{column_new} IS NOT NA")
-    } else {
-      filter_nice <- str_glue("{column_new} IS NA")
-    }
+    translated_filter<- individual_filter |>
+      str_replace(str_glue("\\( *{column} *\\)"), column_new) |>
+      str_replace(str_glue("{column}"), column_new) |>
+      str_replace(value, value_new)
     
-  } 
-  return(filter_nice)
-}
+   }else if (str_detect(individual_filter, "IS NA")){
 
-#tranlate a subset of a filter- specifically, a section that occurs within brackets, seperated by | or &
-translate_filter_subset<- function(filter_subset, database_name){
-  
-  #replace the brackets at start and end of the subset
-  filter_subset<- filter_subset %>%
-    str_replace_all("^~","") %>%
-    str_replace_all("~$","")
-  
-  # split into each filter using | and & 
-  filter_vector<- str_split(filter_subset, "\\&|\\|")[[1]]
-  
-  # loop through filters in vectors, translating them
-  for (one_filter in filter_vector){
-    
-    one_filter_translated<- translate_individual_filter(one_filter, database_name)
+     split_string<- str_replace(individual_filter,"IS NA", "") 
+     column<- split_string |> 
+       str_replace_all(fixed("("),"") |>
+       str_replace_all(fixed(")"),"") |>
+       str_trim()
+     column_new <- get_column_text(column, database_name)  
+    
+     translated_filter<-   individual_filter |>
+       str_replace(str_glue("IS NA\\({column}"), str_glue("{column_new} IS NA")) 
path: functions.R
user: login: znogbes
body: I think bracket bug could be fixed with below? 
```suggestion
       str_replace(str_glue("IS NA\\({column}\\)"), str_glue("{column_new} IS NA"))
```
created_at: 2025-03-20T16:32:38Z
author_association: MEMBER
diff_hunk: @@ -367,157 +367,115 @@ get_column_text<-function(column, database_name){
   return(column_new)   
 }
 
-#function to identify the variable type from the features it includes
-find_filter_category<- function(i){
-  
-  like_present <-sum(str_detect(i, "%LIKE%")) == 1
-  space_number_space_present <- sum(str_detect(i, "% \\d+ %")) == 1 
-  space_colname_space_present <- sum(str_detect(i, '# # *% *\\w* *% *# #')) == 1
-  vector_present <- sum(str_detect(as.character(i), "c~")) == 1
-  in_present <- sum(str_detect(as.character(i), "%in%")) == 1
-  equals_present <- sum(str_detect(as.character(i), "==|!=")) == 1
-  
-  
-  type_na <- sum(str_detect(i, "na"))
-  
-  type_equals <- if_else(equals_present & !vector_present, 1, 0)
-  
-  type_in <- if_else(vector_present & in_present, 1, 0)
-  
-  type_multi <- if_else(like_present & space_number_space_present & space_colname_space_present, 1, 0)
-  
-  filter_category=NA
-  
-  if (type_equals == 1  &  sum(type_in, type_na, type_multi)==0){
-    filter_category="equals"
-  }
-  else if (type_in == 1  &  sum(type_equals, type_na, type_multi)==0){
-    filter_category="in"
-  }
-  else if (type_na == 1  &  sum(type_in, type_equals, type_multi)==0){
-    filter_category="filter_na"
-  }
-  else if (type_multi== 1  &  sum(type_in, type_na, type_equals)==0){
-    filter_category="multi"
-  } else{
-    print("Filter category not found")
-  }
-  return(filter_category)
-}
 
-#function to translate a filter into a more human readable value, given the filter string and data
-translate_individual_filter <- function(one_filter, database_name){
-  
-  filter_category<-find_filter_category(one_filter) 
-  
-  if (filter_category=="in"){
-    #split each filter into column, value and operator
-    column <- str_trim(str_split(one_filter,"%in%")[[1]][1])
-    column_new <- get_column_text(column, database_name)  
-    value_old <- str_trim(str_split(one_filter,"%in%")[[1]][2])
-    value_old_no_spaces <- str_replace(value_old," ", "")
-    operator <- "IN"
-    
-    #create vector for value
-    value_new <- c()
-    
-    # get value descriptions
-    # loop through each element in the vector (so can handle 1 or c(1,2,3))
-    for (j in str_split(value_old_no_spaces,"")[[1]]) {
-      #this is required because value is of the type class when it is in c(1,2,3) form
-      if (! j %in% c("c", ",", " ", "~", "#")){
-        
-        #get text for this element of value 
-        code_text <- get_code_text(column, j, database_name)
-        #append this value to a vector of values
-        value_new <- append(value_new, code_text)
-      }
-    }
-    
-    #convert from vector to string
-    value_new_string <- str_c(value_new, collapse = ", ")
-    
-    # add brackets to either side of value_new_string- helpful for clarity
-    value_new_string <- str_c("(", value_new_string, ")")
-    
-    #recreate the filter by combining column, operator and value old
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+#function to translate an individual filter into a more human readable value, given the filter string and database name
+translate_individual_filter <- function(individual_filter, database_name){
+
+  #different logic depending on what the filter is
+  if (str_detect(individual_filter,"IS IN")){
     
-  } else if (filter_category=="equals"){
+    split_string<- str_split(individual_filter,"IS IN") |> unlist()
+    column<- split_string[1]|> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
+    value <- split_string[2]|> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
+    value_split <- str_split(value, ",") |> unlist()
     
-    column <- str_trim(str_split(one_filter,"==")[[1]][1])
     column_new <- get_column_text(column, database_name)  
-    value_old <-str_trim(str_split(one_filter,"==")[[1]][2])
-    operator <- "="
     
+    value_new<- c()
+    for (each_value in value_split){
+      #find the text corresponding to the code
+      code_text <- get_code_text(column, str_replace(each_value," ",""), database_name)
+      #append this value to a vector of values
+      value_new <- append(value_new, code_text)
+    }
     
-    value_new_string <- get_code_text(column, value_old, database_name)
+    value_new<-str_c(value_new, collapse=", ")
     
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+    translated_filter<- individual_filter |>
+      str_replace(column, column_new) |>
+      str_replace(value, value_new)
     
     
-  }else if (filter_category=="multi"){
-    column <- one_filter%>% 
-      str_extract("[a-zA-Z0-9_.-]+")
+  }else if (str_detect(individual_filter, "CONTAINS")){
+
+    split_string<- str_split(individual_filter,"CONTAINS") |> unlist()
+    column<- split_string[1] |> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim() 
+    value <- split_string[2] |> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
     column_new <- get_column_text(column, database_name)  
-    value_old <- str_extract(one_filter,"% \\d %")
-    operator <- "CONTAINS"
+    value_new <- get_code_text(column, value, database_name)
     
-    value_pretty <- value_old %>% 
-      str_replace_all("%","") %>% #get rid of "% "- which is present with a multi-select column
-      str_trim()
-    value_new_string <- get_code_text(column, value_pretty, database_name)
-    #recreate the filter by combining column, operator and value old
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
-  }else if (filter_category=="filter_na"){
-    not_na<-!str_detect(one_filter,"!")
-    column<- str_extract(one_filter,"[a-zA-Z0-9_-]{3,}")
-    column_new <- get_column_text(column , database_name) 
-    if (not_na){
-      filter_nice <- str_glue("{column_new} IS NOT NA")
-    } else {
-      filter_nice <- str_glue("{column_new} IS NA")
-    }
+    translated_filter<- individual_filter |>
+      str_replace(str_glue("\\( *{column} *\\)"), column_new) |>
+      str_replace(str_glue("{column}"), column_new) |>
+      str_replace(value, value_new)
     
-  } 
-  return(filter_nice)
-}
+   }else if (str_detect(individual_filter, "IS NA")){
 
-#tranlate a subset of a filter- specifically, a section that occurs within brackets, seperated by | or &
-translate_filter_subset<- function(filter_subset, database_name){
-  
-  #replace the brackets at start and end of the subset
-  filter_subset<- filter_subset %>%
-    str_replace_all("^~","") %>%
-    str_replace_all("~$","")
-  
-  # split into each filter using | and & 
-  filter_vector<- str_split(filter_subset, "\\&|\\|")[[1]]
-  
-  # loop through filters in vectors, translating them
-  for (one_filter in filter_vector){
-    
-    one_filter_translated<- translate_individual_filter(one_filter, database_name)
+     split_string<- str_replace(individual_filter,"IS NA", "") 
+     column<- split_string |> 
+       str_replace_all(fixed("("),"") |>
+       str_replace_all(fixed(")"),"") |>
+       str_trim()
+     column_new <- get_column_text(column, database_name)  
path: functions.R
user: login: znogbes
body: I think the text from lookups is unintentionally bringing empty space so we could trim here to avoid that (e.g., `NHS organisation code  IS NOT NA)`)
```suggestion
     column_new <- get_column_text(column, database_name) |> str_trim() 
```
created_at: 2025-03-20T16:37:51Z
author_association: MEMBER
diff_hunk: @@ -367,157 +367,115 @@ get_column_text<-function(column, database_name){
   return(column_new)   
 }
 
-#function to identify the variable type from the features it includes
-find_filter_category<- function(i){
-  
-  like_present <-sum(str_detect(i, "%LIKE%")) == 1
-  space_number_space_present <- sum(str_detect(i, "% \\d+ %")) == 1 
-  space_colname_space_present <- sum(str_detect(i, '# # *% *\\w* *% *# #')) == 1
-  vector_present <- sum(str_detect(as.character(i), "c~")) == 1
-  in_present <- sum(str_detect(as.character(i), "%in%")) == 1
-  equals_present <- sum(str_detect(as.character(i), "==|!=")) == 1
-  
-  
-  type_na <- sum(str_detect(i, "na"))
-  
-  type_equals <- if_else(equals_present & !vector_present, 1, 0)
-  
-  type_in <- if_else(vector_present & in_present, 1, 0)
-  
-  type_multi <- if_else(like_present & space_number_space_present & space_colname_space_present, 1, 0)
-  
-  filter_category=NA
-  
-  if (type_equals == 1  &  sum(type_in, type_na, type_multi)==0){
-    filter_category="equals"
-  }
-  else if (type_in == 1  &  sum(type_equals, type_na, type_multi)==0){
-    filter_category="in"
-  }
-  else if (type_na == 1  &  sum(type_in, type_equals, type_multi)==0){
-    filter_category="filter_na"
-  }
-  else if (type_multi== 1  &  sum(type_in, type_na, type_equals)==0){
-    filter_category="multi"
-  } else{
-    print("Filter category not found")
-  }
-  return(filter_category)
-}
 
-#function to translate a filter into a more human readable value, given the filter string and data
-translate_individual_filter <- function(one_filter, database_name){
-  
-  filter_category<-find_filter_category(one_filter) 
-  
-  if (filter_category=="in"){
-    #split each filter into column, value and operator
-    column <- str_trim(str_split(one_filter,"%in%")[[1]][1])
-    column_new <- get_column_text(column, database_name)  
-    value_old <- str_trim(str_split(one_filter,"%in%")[[1]][2])
-    value_old_no_spaces <- str_replace(value_old," ", "")
-    operator <- "IN"
-    
-    #create vector for value
-    value_new <- c()
-    
-    # get value descriptions
-    # loop through each element in the vector (so can handle 1 or c(1,2,3))
-    for (j in str_split(value_old_no_spaces,"")[[1]]) {
-      #this is required because value is of the type class when it is in c(1,2,3) form
-      if (! j %in% c("c", ",", " ", "~", "#")){
-        
-        #get text for this element of value 
-        code_text <- get_code_text(column, j, database_name)
-        #append this value to a vector of values
-        value_new <- append(value_new, code_text)
-      }
-    }
-    
-    #convert from vector to string
-    value_new_string <- str_c(value_new, collapse = ", ")
-    
-    # add brackets to either side of value_new_string- helpful for clarity
-    value_new_string <- str_c("(", value_new_string, ")")
-    
-    #recreate the filter by combining column, operator and value old
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+#function to translate an individual filter into a more human readable value, given the filter string and database name
+translate_individual_filter <- function(individual_filter, database_name){
+
+  #different logic depending on what the filter is
+  if (str_detect(individual_filter,"IS IN")){
     
-  } else if (filter_category=="equals"){
+    split_string<- str_split(individual_filter,"IS IN") |> unlist()
+    column<- split_string[1]|> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
+    value <- split_string[2]|> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
+    value_split <- str_split(value, ",") |> unlist()
     
-    column <- str_trim(str_split(one_filter,"==")[[1]][1])
     column_new <- get_column_text(column, database_name)  
-    value_old <-str_trim(str_split(one_filter,"==")[[1]][2])
-    operator <- "="
     
+    value_new<- c()
+    for (each_value in value_split){
+      #find the text corresponding to the code
+      code_text <- get_code_text(column, str_replace(each_value," ",""), database_name)
+      #append this value to a vector of values
+      value_new <- append(value_new, code_text)
+    }
     
-    value_new_string <- get_code_text(column, value_old, database_name)
+    value_new<-str_c(value_new, collapse=", ")
     
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+    translated_filter<- individual_filter |>
+      str_replace(column, column_new) |>
+      str_replace(value, value_new)
     
     
-  }else if (filter_category=="multi"){
-    column <- one_filter%>% 
-      str_extract("[a-zA-Z0-9_.-]+")
+  }else if (str_detect(individual_filter, "CONTAINS")){
+
+    split_string<- str_split(individual_filter,"CONTAINS") |> unlist()
+    column<- split_string[1] |> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim() 
+    value <- split_string[2] |> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
     column_new <- get_column_text(column, database_name)  
-    value_old <- str_extract(one_filter,"% \\d %")
-    operator <- "CONTAINS"
+    value_new <- get_code_text(column, value, database_name)
path: functions.R
user: login: znogbes
body: 407 to 417 are identical to IS scenario (455 to 465)
created_at: 2025-03-20T16:46:44Z
author_association: MEMBER
diff_hunk: @@ -367,157 +367,115 @@ get_column_text<-function(column, database_name){
   return(column_new)   
 }
 
-#function to identify the variable type from the features it includes
-find_filter_category<- function(i){
-  
-  like_present <-sum(str_detect(i, "%LIKE%")) == 1
-  space_number_space_present <- sum(str_detect(i, "% \\d+ %")) == 1 
-  space_colname_space_present <- sum(str_detect(i, '# # *% *\\w* *% *# #')) == 1
-  vector_present <- sum(str_detect(as.character(i), "c~")) == 1
-  in_present <- sum(str_detect(as.character(i), "%in%")) == 1
-  equals_present <- sum(str_detect(as.character(i), "==|!=")) == 1
-  
-  
-  type_na <- sum(str_detect(i, "na"))
-  
-  type_equals <- if_else(equals_present & !vector_present, 1, 0)
-  
-  type_in <- if_else(vector_present & in_present, 1, 0)
-  
-  type_multi <- if_else(like_present & space_number_space_present & space_colname_space_present, 1, 0)
-  
-  filter_category=NA
-  
-  if (type_equals == 1  &  sum(type_in, type_na, type_multi)==0){
-    filter_category="equals"
-  }
-  else if (type_in == 1  &  sum(type_equals, type_na, type_multi)==0){
-    filter_category="in"
-  }
-  else if (type_na == 1  &  sum(type_in, type_equals, type_multi)==0){
-    filter_category="filter_na"
-  }
-  else if (type_multi== 1  &  sum(type_in, type_na, type_equals)==0){
-    filter_category="multi"
-  } else{
-    print("Filter category not found")
-  }
-  return(filter_category)
-}
 
-#function to translate a filter into a more human readable value, given the filter string and data
-translate_individual_filter <- function(one_filter, database_name){
-  
-  filter_category<-find_filter_category(one_filter) 
-  
-  if (filter_category=="in"){
-    #split each filter into column, value and operator
-    column <- str_trim(str_split(one_filter,"%in%")[[1]][1])
-    column_new <- get_column_text(column, database_name)  
-    value_old <- str_trim(str_split(one_filter,"%in%")[[1]][2])
-    value_old_no_spaces <- str_replace(value_old," ", "")
-    operator <- "IN"
-    
-    #create vector for value
-    value_new <- c()
-    
-    # get value descriptions
-    # loop through each element in the vector (so can handle 1 or c(1,2,3))
-    for (j in str_split(value_old_no_spaces,"")[[1]]) {
-      #this is required because value is of the type class when it is in c(1,2,3) form
-      if (! j %in% c("c", ",", " ", "~", "#")){
-        
-        #get text for this element of value 
-        code_text <- get_code_text(column, j, database_name)
-        #append this value to a vector of values
-        value_new <- append(value_new, code_text)
-      }
-    }
-    
-    #convert from vector to string
-    value_new_string <- str_c(value_new, collapse = ", ")
-    
-    # add brackets to either side of value_new_string- helpful for clarity
-    value_new_string <- str_c("(", value_new_string, ")")
-    
-    #recreate the filter by combining column, operator and value old
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+#function to translate an individual filter into a more human readable value, given the filter string and database name
+translate_individual_filter <- function(individual_filter, database_name){
+
+  #different logic depending on what the filter is
+  if (str_detect(individual_filter,"IS IN")){
     
-  } else if (filter_category=="equals"){
+    split_string<- str_split(individual_filter,"IS IN") |> unlist()
+    column<- split_string[1]|> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
+    value <- split_string[2]|> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
+    value_split <- str_split(value, ",") |> unlist()
     
-    column <- str_trim(str_split(one_filter,"==")[[1]][1])
     column_new <- get_column_text(column, database_name)  
-    value_old <-str_trim(str_split(one_filter,"==")[[1]][2])
-    operator <- "="
     
+    value_new<- c()
+    for (each_value in value_split){
+      #find the text corresponding to the code
+      code_text <- get_code_text(column, str_replace(each_value," ",""), database_name)
+      #append this value to a vector of values
+      value_new <- append(value_new, code_text)
+    }
     
-    value_new_string <- get_code_text(column, value_old, database_name)
+    value_new<-str_c(value_new, collapse=", ")
     
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+    translated_filter<- individual_filter |>
+      str_replace(column, column_new) |>
+      str_replace(value, value_new)
     
     
-  }else if (filter_category=="multi"){
-    column <- one_filter%>% 
-      str_extract("[a-zA-Z0-9_.-]+")
+  }else if (str_detect(individual_filter, "CONTAINS")){
+
+    split_string<- str_split(individual_filter,"CONTAINS") |> unlist()
+    column<- split_string[1] |> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim() 
+    value <- split_string[2] |> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
     column_new <- get_column_text(column, database_name)  
-    value_old <- str_extract(one_filter,"% \\d %")
-    operator <- "CONTAINS"
+    value_new <- get_code_text(column, value, database_name)
     
-    value_pretty <- value_old %>% 
-      str_replace_all("%","") %>% #get rid of "% "- which is present with a multi-select column
-      str_trim()
-    value_new_string <- get_code_text(column, value_pretty, database_name)
-    #recreate the filter by combining column, operator and value old
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
-  }else if (filter_category=="filter_na"){
-    not_na<-!str_detect(one_filter,"!")
-    column<- str_extract(one_filter,"[a-zA-Z0-9_-]{3,}")
-    column_new <- get_column_text(column , database_name) 
-    if (not_na){
-      filter_nice <- str_glue("{column_new} IS NOT NA")
-    } else {
-      filter_nice <- str_glue("{column_new} IS NA")
-    }
+    translated_filter<- individual_filter |>
+      str_replace(str_glue("\\( *{column} *\\)"), column_new) |>
+      str_replace(str_glue("{column}"), column_new) |>
+      str_replace(value, value_new)
     
-  } 
-  return(filter_nice)
-}
+   }else if (str_detect(individual_filter, "IS NA")){
 
-#tranlate a subset of a filter- specifically, a section that occurs within brackets, seperated by | or &
-translate_filter_subset<- function(filter_subset, database_name){
-  
-  #replace the brackets at start and end of the subset
-  filter_subset<- filter_subset %>%
-    str_replace_all("^~","") %>%
-    str_replace_all("~$","")
-  
-  # split into each filter using | and & 
-  filter_vector<- str_split(filter_subset, "\\&|\\|")[[1]]
-  
-  # loop through filters in vectors, translating them
-  for (one_filter in filter_vector){
-    
-    one_filter_translated<- translate_individual_filter(one_filter, database_name)
+     split_string<- str_replace(individual_filter,"IS NA", "") 
+     column<- split_string |> 
+       str_replace_all(fixed("("),"") |>
+       str_replace_all(fixed(")"),"") |>
+       str_trim()
+     column_new <- get_column_text(column, database_name)  
+    
+     translated_filter<-   individual_filter |>
+       str_replace(str_glue("IS NA\\({column}"), str_glue("{column_new} IS NA")) 
+     
+     
+   }else if (str_detect(individual_filter, "IS NOT NA")){
+
+     split_string<- str_replace(individual_filter,"IS NOT NA", "") 
+     
+     column<- split_string |> 
+       str_trim() |>
+       str_replace_all(fixed("("),"") |>
+        str_replace_all(fixed(")"),"")
+     
+     column_new <- get_column_text(column, database_name)  
+     
+     translated_filter<-   individual_filter |>
+       str_replace(str_glue("IS NOT NA\\({column}"), 
+                   str_glue("{column_new} IS NOT NA")) 
+     
path: functions.R
user: login: znogbes
body: I see some duplication here in the wrangling steps of both IS and IS NOT NA. The only bit that seems to be changing is the presence of 'NOT' . 
created_at: 2025-03-20T16:52:31Z
author_association: MEMBER
diff_hunk: @@ -533,43 +491,37 @@ translate_categorical_string<- function(categorical_filter, database_name){
     categorical_filter_string<- str_c(categorical_filter_string, collapse = "")
   }
   
-  # brackets, plus symbols and speech marks are regex special characters and working with them is awkward
-  # so we replace them with non protected characters 
-  categorical_filter_copy<- categorical_filter_string %>%
-    str_replace_all("\\(", "~") %>% # replace brackets with ~
-    str_replace_all("\\)", "~") %>% # replace brackets with ~
-    str_replace_all('\\"', "#") %>% # replace speech marks with #
-    str_replace_all('\\+', "%") %>% # replace + with %
-    str_replace_all('\\|', "£") # replace | with £
-  
-  #if the string contains and  or  or- split by brackets, we'll need to seperate and loop through  
-  categorical_filter_copy_split <- str_split(categorical_filter_copy, "~ *(£|&) *~")[[1]]
+  #translate filter into more human readable format, including removing speech marks
+  categorical_filter_copy<- categorical_filter_string |>
+    str_replace_all(fixed('"'), "") |> 
+    str_replace_all('==', "IS") |>
+    str_replace_all(fixed('+'), "") |> 
+    str_replace_all('!is.na', "IS NOT NA") |>
+    str_replace_all('is.na', "IS NA") |> 
+    str_replace_all('(?i)%LIKE%', "CONTAINS") |> 
+    str_replace_all('(?i)%IN%', "IS IN") |> 
+    str_replace_all('%', "") |> 
+    str_replace_all('&', "AND") |> 
+    str_replace_all('c\\(', "(") |> 
+    str_replace_all(fixed('|'), "OR") |> 
+    str_replace_all(" +", " ")
+  
+  string_to_split_by<-"(OR|AND)"
+  #if the string contains AND  or OR, we'll need to seperate and loop through  
path: functions.R
user: login: znogbes
body: ```suggestion
  #if the string contains AND  or OR, we'll need to separate and loop through  
```
created_at: 2025-03-20T16:58:35Z
author_association: MEMBER
diff_hunk: @@ -367,157 +367,115 @@ get_column_text<-function(column, database_name){
   return(column_new)   
 }
 
-#function to identify the variable type from the features it includes
-find_filter_category<- function(i){
-  
-  like_present <-sum(str_detect(i, "%LIKE%")) == 1
-  space_number_space_present <- sum(str_detect(i, "% \\d+ %")) == 1 
-  space_colname_space_present <- sum(str_detect(i, '# # *% *\\w* *% *# #')) == 1
-  vector_present <- sum(str_detect(as.character(i), "c~")) == 1
-  in_present <- sum(str_detect(as.character(i), "%in%")) == 1
-  equals_present <- sum(str_detect(as.character(i), "==|!=")) == 1
-  
-  
-  type_na <- sum(str_detect(i, "na"))
-  
-  type_equals <- if_else(equals_present & !vector_present, 1, 0)
-  
-  type_in <- if_else(vector_present & in_present, 1, 0)
-  
-  type_multi <- if_else(like_present & space_number_space_present & space_colname_space_present, 1, 0)
-  
-  filter_category=NA
-  
-  if (type_equals == 1  &  sum(type_in, type_na, type_multi)==0){
-    filter_category="equals"
-  }
-  else if (type_in == 1  &  sum(type_equals, type_na, type_multi)==0){
-    filter_category="in"
-  }
-  else if (type_na == 1  &  sum(type_in, type_equals, type_multi)==0){
-    filter_category="filter_na"
-  }
-  else if (type_multi== 1  &  sum(type_in, type_na, type_equals)==0){
-    filter_category="multi"
-  } else{
-    print("Filter category not found")
-  }
-  return(filter_category)
-}
 
-#function to translate a filter into a more human readable value, given the filter string and data
-translate_individual_filter <- function(one_filter, database_name){
-  
-  filter_category<-find_filter_category(one_filter) 
-  
-  if (filter_category=="in"){
-    #split each filter into column, value and operator
-    column <- str_trim(str_split(one_filter,"%in%")[[1]][1])
-    column_new <- get_column_text(column, database_name)  
-    value_old <- str_trim(str_split(one_filter,"%in%")[[1]][2])
-    value_old_no_spaces <- str_replace(value_old," ", "")
-    operator <- "IN"
-    
-    #create vector for value
-    value_new <- c()
-    
-    # get value descriptions
-    # loop through each element in the vector (so can handle 1 or c(1,2,3))
-    for (j in str_split(value_old_no_spaces,"")[[1]]) {
-      #this is required because value is of the type class when it is in c(1,2,3) form
-      if (! j %in% c("c", ",", " ", "~", "#")){
-        
-        #get text for this element of value 
-        code_text <- get_code_text(column, j, database_name)
-        #append this value to a vector of values
-        value_new <- append(value_new, code_text)
-      }
-    }
-    
-    #convert from vector to string
-    value_new_string <- str_c(value_new, collapse = ", ")
-    
-    # add brackets to either side of value_new_string- helpful for clarity
-    value_new_string <- str_c("(", value_new_string, ")")
-    
-    #recreate the filter by combining column, operator and value old
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+#function to translate an individual filter into a more human readable value, given the filter string and database name
+translate_individual_filter <- function(individual_filter, database_name){
+
+  #different logic depending on what the filter is
+  if (str_detect(individual_filter,"IS IN")){
     
-  } else if (filter_category=="equals"){
+    split_string<- str_split(individual_filter,"IS IN") |> unlist()
+    column<- split_string[1]|> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
+    value <- split_string[2]|> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
+    value_split <- str_split(value, ",") |> unlist()
     
-    column <- str_trim(str_split(one_filter,"==")[[1]][1])
     column_new <- get_column_text(column, database_name)  
-    value_old <-str_trim(str_split(one_filter,"==")[[1]][2])
-    operator <- "="
     
+    value_new<- c()
+    for (each_value in value_split){
+      #find the text corresponding to the code
+      code_text <- get_code_text(column, str_replace(each_value," ",""), database_name)
+      #append this value to a vector of values
+      value_new <- append(value_new, code_text)
+    }
     
-    value_new_string <- get_code_text(column, value_old, database_name)
+    value_new<-str_c(value_new, collapse=", ")
     
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+    translated_filter<- individual_filter |>
+      str_replace(column, column_new) |>
+      str_replace(value, value_new)
     
     
-  }else if (filter_category=="multi"){
-    column <- one_filter%>% 
-      str_extract("[a-zA-Z0-9_.-]+")
+  }else if (str_detect(individual_filter, "CONTAINS")){
+
+    split_string<- str_split(individual_filter,"CONTAINS") |> unlist()
+    column<- split_string[1] |> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim() 
+    value <- split_string[2] |> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
     column_new <- get_column_text(column, database_name)  
-    value_old <- str_extract(one_filter,"% \\d %")
-    operator <- "CONTAINS"
+    value_new <- get_code_text(column, value, database_name)
     
-    value_pretty <- value_old %>% 
-      str_replace_all("%","") %>% #get rid of "% "- which is present with a multi-select column
-      str_trim()
-    value_new_string <- get_code_text(column, value_pretty, database_name)
-    #recreate the filter by combining column, operator and value old
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
-  }else if (filter_category=="filter_na"){
-    not_na<-!str_detect(one_filter,"!")
-    column<- str_extract(one_filter,"[a-zA-Z0-9_-]{3,}")
-    column_new <- get_column_text(column , database_name) 
-    if (not_na){
-      filter_nice <- str_glue("{column_new} IS NOT NA")
-    } else {
-      filter_nice <- str_glue("{column_new} IS NA")
-    }
+    translated_filter<- individual_filter |>
+      str_replace(str_glue("\\( *{column} *\\)"), column_new) |>
+      str_replace(str_glue("{column}"), column_new) |>
+      str_replace(value, value_new)
     
-  } 
-  return(filter_nice)
-}
+   }else if (str_detect(individual_filter, "IS NA")){
path: functions.R
user: login: ruth-keane-nhs
body: thanks for picking this up!
created_at: 2025-03-21T11:24:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -367,157 +367,115 @@ get_column_text<-function(column, database_name){
   return(column_new)   
 }
 
-#function to identify the variable type from the features it includes
-find_filter_category<- function(i){
-  
-  like_present <-sum(str_detect(i, "%LIKE%")) == 1
-  space_number_space_present <- sum(str_detect(i, "% \\d+ %")) == 1 
-  space_colname_space_present <- sum(str_detect(i, '# # *% *\\w* *% *# #')) == 1
-  vector_present <- sum(str_detect(as.character(i), "c~")) == 1
-  in_present <- sum(str_detect(as.character(i), "%in%")) == 1
-  equals_present <- sum(str_detect(as.character(i), "==|!=")) == 1
-  
-  
-  type_na <- sum(str_detect(i, "na"))
-  
-  type_equals <- if_else(equals_present & !vector_present, 1, 0)
-  
-  type_in <- if_else(vector_present & in_present, 1, 0)
-  
-  type_multi <- if_else(like_present & space_number_space_present & space_colname_space_present, 1, 0)
-  
-  filter_category=NA
-  
-  if (type_equals == 1  &  sum(type_in, type_na, type_multi)==0){
-    filter_category="equals"
-  }
-  else if (type_in == 1  &  sum(type_equals, type_na, type_multi)==0){
-    filter_category="in"
-  }
-  else if (type_na == 1  &  sum(type_in, type_equals, type_multi)==0){
-    filter_category="filter_na"
-  }
-  else if (type_multi== 1  &  sum(type_in, type_na, type_equals)==0){
-    filter_category="multi"
-  } else{
-    print("Filter category not found")
-  }
-  return(filter_category)
-}
 
-#function to translate a filter into a more human readable value, given the filter string and data
-translate_individual_filter <- function(one_filter, database_name){
-  
-  filter_category<-find_filter_category(one_filter) 
-  
-  if (filter_category=="in"){
-    #split each filter into column, value and operator
-    column <- str_trim(str_split(one_filter,"%in%")[[1]][1])
-    column_new <- get_column_text(column, database_name)  
-    value_old <- str_trim(str_split(one_filter,"%in%")[[1]][2])
-    value_old_no_spaces <- str_replace(value_old," ", "")
-    operator <- "IN"
-    
-    #create vector for value
-    value_new <- c()
-    
-    # get value descriptions
-    # loop through each element in the vector (so can handle 1 or c(1,2,3))
-    for (j in str_split(value_old_no_spaces,"")[[1]]) {
-      #this is required because value is of the type class when it is in c(1,2,3) form
-      if (! j %in% c("c", ",", " ", "~", "#")){
-        
-        #get text for this element of value 
-        code_text <- get_code_text(column, j, database_name)
-        #append this value to a vector of values
-        value_new <- append(value_new, code_text)
-      }
-    }
-    
-    #convert from vector to string
-    value_new_string <- str_c(value_new, collapse = ", ")
-    
-    # add brackets to either side of value_new_string- helpful for clarity
-    value_new_string <- str_c("(", value_new_string, ")")
-    
-    #recreate the filter by combining column, operator and value old
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+#function to translate an individual filter into a more human readable value, given the filter string and database name
+translate_individual_filter <- function(individual_filter, database_name){
+
+  #different logic depending on what the filter is
+  if (str_detect(individual_filter,"IS IN")){
     
-  } else if (filter_category=="equals"){
+    split_string<- str_split(individual_filter,"IS IN") |> unlist()
+    column<- split_string[1]|> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
+    value <- split_string[2]|> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
+    value_split <- str_split(value, ",") |> unlist()
     
-    column <- str_trim(str_split(one_filter,"==")[[1]][1])
     column_new <- get_column_text(column, database_name)  
-    value_old <-str_trim(str_split(one_filter,"==")[[1]][2])
-    operator <- "="
     
+    value_new<- c()
+    for (each_value in value_split){
+      #find the text corresponding to the code
+      code_text <- get_code_text(column, str_replace(each_value," ",""), database_name)
+      #append this value to a vector of values
+      value_new <- append(value_new, code_text)
+    }
     
-    value_new_string <- get_code_text(column, value_old, database_name)
+    value_new<-str_c(value_new, collapse=", ")
     
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
+    translated_filter<- individual_filter |>
+      str_replace(column, column_new) |>
+      str_replace(value, value_new)
     
     
-  }else if (filter_category=="multi"){
-    column <- one_filter%>% 
-      str_extract("[a-zA-Z0-9_.-]+")
+  }else if (str_detect(individual_filter, "CONTAINS")){
+
+    split_string<- str_split(individual_filter,"CONTAINS") |> unlist()
+    column<- split_string[1] |> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim() 
+    value <- split_string[2] |> 
+      str_replace_all(fixed("("),"") |>
+      str_replace_all(fixed(")"),"") |>
+      str_trim()
     column_new <- get_column_text(column, database_name)  
-    value_old <- str_extract(one_filter,"% \\d %")
-    operator <- "CONTAINS"
+    value_new <- get_code_text(column, value, database_name)
     
-    value_pretty <- value_old %>% 
-      str_replace_all("%","") %>% #get rid of "% "- which is present with a multi-select column
-      str_trim()
-    value_new_string <- get_code_text(column, value_pretty, database_name)
-    #recreate the filter by combining column, operator and value old
-    filter_nice <- str_c(column_new, operator, value_new_string, sep = " ")
-  }else if (filter_category=="filter_na"){
-    not_na<-!str_detect(one_filter,"!")
-    column<- str_extract(one_filter,"[a-zA-Z0-9_-]{3,}")
-    column_new <- get_column_text(column , database_name) 
-    if (not_na){
-      filter_nice <- str_glue("{column_new} IS NOT NA")
-    } else {
-      filter_nice <- str_glue("{column_new} IS NA")
-    }
+    translated_filter<- individual_filter |>
+      str_replace(str_glue("\\( *{column} *\\)"), column_new) |>
+      str_replace(str_glue("{column}"), column_new) |>
+      str_replace(value, value_new)
     
-  } 
-  return(filter_nice)
-}
+   }else if (str_detect(individual_filter, "IS NA")){
 
-#tranlate a subset of a filter- specifically, a section that occurs within brackets, seperated by | or &
-translate_filter_subset<- function(filter_subset, database_name){
-  
-  #replace the brackets at start and end of the subset
-  filter_subset<- filter_subset %>%
-    str_replace_all("^~","") %>%
-    str_replace_all("~$","")
-  
-  # split into each filter using | and & 
-  filter_vector<- str_split(filter_subset, "\\&|\\|")[[1]]
-  
-  # loop through filters in vectors, translating them
-  for (one_filter in filter_vector){
-    
-    one_filter_translated<- translate_individual_filter(one_filter, database_name)
+     split_string<- str_replace(individual_filter,"IS NA", "") 
+     column<- split_string |> 
+       str_replace_all(fixed("("),"") |>
+       str_replace_all(fixed(")"),"") |>
+       str_trim()
+     column_new <- get_column_text(column, database_name)  
path: functions.R
user: login: ruth-keane-nhs
body: good catch! This was actually from the NRLS lookup table, so I've fixed the labels there 
created_at: 2025-03-21T11:34:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -533,43 +491,37 @@ translate_categorical_string<- function(categorical_filter, database_name){
     categorical_filter_string<- str_c(categorical_filter_string, collapse = "")
   }
   
-  # brackets, plus symbols and speech marks are regex special characters and working with them is awkward
-  # so we replace them with non protected characters 
-  categorical_filter_copy<- categorical_filter_string %>%
-    str_replace_all("\\(", "~") %>% # replace brackets with ~
-    str_replace_all("\\)", "~") %>% # replace brackets with ~
-    str_replace_all('\\"', "#") %>% # replace speech marks with #
-    str_replace_all('\\+', "%") %>% # replace + with %
-    str_replace_all('\\|', "£") # replace | with £
-  
-  #if the string contains and  or  or- split by brackets, we'll need to seperate and loop through  
-  categorical_filter_copy_split <- str_split(categorical_filter_copy, "~ *(£|&) *~")[[1]]
+  #translate filter into more human readable format, including removing speech marks
+  categorical_filter_copy<- categorical_filter_string |>
+    str_replace_all(fixed('"'), "") |> 
+    str_replace_all('==', "IS") |>
+    str_replace_all(fixed('+'), "") |> 
+    str_replace_all('!is.na', "IS NOT NA") |>
+    str_replace_all('is.na', "IS NA") |> 
+    str_replace_all('(?i)%LIKE%', "CONTAINS") |> 
+    str_replace_all('(?i)%IN%', "IS IN") |> 
+    str_replace_all('%', "") |> 
+    str_replace_all('&', "AND") |> 
+    str_replace_all('c\\(', "(") |> 
+    str_replace_all(fixed('|'), "OR") |> 
+    str_replace_all(" +", " ")
path: functions.R
user: login: ruth-keane-nhs
body: this replaces multiple spaces with just one space- I've added a comment to explain because it isn't obvious! 
created_at: 2025-03-21T11:51:03Z
author_association: CONTRIBUTOR

PR comments:
user: login: ruth-keane-nhs
created_at: 2025-03-17T13:07:53Z
body: Thanks for your feedback, and discussion on this one, its been really helpful! I've managed to remove 2 of the functions, and it a lot easier to understand now. 
author_association: CONTRIBUTOR
user: login: ruth-keane-nhs
created_at: 2025-03-24T12:15:04Z
body: Thanks for your help reviewing this one- its made a big difference to how the code looks!
author_association: CONTRIBUTOR