Skip to content

Commit d9bf7d9

Browse files
committed
Add content-based targeting for stub row resolution
1 parent 9d55d57 commit d9bf7d9

File tree

1 file changed

+237
-41
lines changed

1 file changed

+237
-41
lines changed

R/resolver.R

Lines changed: 237 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -96,53 +96,52 @@ resolve_cells_stub <- function(data,
9696
resolved_columns <- character(0)
9797
} else if (!is.null(object$columns)) {
9898
# Only resolve columns if the columns parameter exists (new behavior)
99-
# First resolve columns normally with stub inclusion
100-
resolved_columns <-
101-
resolve_cols_c(
102-
expr = !!object$columns,
103-
data = data,
104-
excl_stub = FALSE,
105-
call = call
106-
)
10799

108-
# Validate that all requested columns are actually stub columns
109-
if (length(resolved_columns) > 0) {
110-
# Check if the requested columns are stub columns
111-
all_data_cols <- names(dt_data_get(data))
112-
requested_cols <- intersect(resolved_columns, all_data_cols)
100+
# Check if columns expression is everything() (default)
101+
expr_text <- rlang::quo_text(object$columns)
102+
if (expr_text == "everything()") {
103+
# Handle everything() case explicitly for stub columns
104+
resolved_columns <- stub_vars
105+
} else {
106+
# First resolve columns normally with stub inclusion
107+
resolved_columns <-
108+
resolve_cols_c(
109+
expr = !!object$columns,
110+
data = data,
111+
excl_stub = FALSE,
112+
call = call
113+
)
113114

114-
if (length(requested_cols) > 0) {
115-
non_stub_cols <- setdiff(requested_cols, stub_vars)
115+
# Validate that all requested columns are actually stub columns
116+
if (length(resolved_columns) > 0) {
117+
# Check if the requested columns are stub columns
118+
all_data_cols <- names(dt_data_get(data))
119+
requested_cols <- intersect(resolved_columns, all_data_cols)
116120

117-
if (length(non_stub_cols) > 0) {
118-
available_stub_cols <- if (length(stub_vars) > 0) {
119-
paste0("Available stub columns: ", paste(stub_vars, collapse = ", "))
120-
} else {
121-
"This table has no stub columns."
122-
}
121+
if (length(requested_cols) > 0) {
122+
non_stub_cols <- setdiff(requested_cols, stub_vars)
123123

124-
cli::cli_abort(
125-
c(
126-
"Column{?s} {.val {non_stub_cols}} {?is/are} not stub column{?s}.",
127-
"i" = "cells_stub() can only target columns that are part of the table stub.",
128-
"i" = available_stub_cols,
129-
"i" = "To target non-stub columns, use cells_body() instead."
124+
if (length(non_stub_cols) > 0) {
125+
available_stub_cols <- if (length(stub_vars) > 0) {
126+
paste0("Available stub columns: ", paste(stub_vars, collapse = ", "))
127+
} else {
128+
"This table has no stub columns."
129+
}
130+
131+
cli::cli_abort(
132+
c(
133+
"Column{?s} {.val {non_stub_cols}} {?is/are} not stub column{?s}.",
134+
"i" = "cells_stub() can only target columns that are part of the table stub.",
135+
"i" = available_stub_cols,
136+
"i" = "To target non-stub columns, use cells_body() instead."
137+
)
130138
)
131-
)
139+
}
132140
}
133141
}
134-
}
135-
136-
# Filter to only include actual stub variables
137-
resolved_columns <- intersect(resolved_columns, stub_vars)
138-
139-
# If no columns were resolved but we have stub vars, default to all stub vars
140-
if (length(resolved_columns) == 0) {
141-
# Check if columns expression is everything() (default)
142-
expr_text <- rlang::quo_text(object$columns)
143-
if (expr_text == "everything()") {
144-
resolved_columns <- stub_vars
145-
}
142+
143+
# Filter to only include actual stub variables
144+
resolved_columns <- intersect(resolved_columns, stub_vars)
146145
}
147146
} else {
148147
# Legacy behavior: no columns parameter provided
@@ -152,11 +151,13 @@ resolve_cells_stub <- function(data,
152151
#
153152
# Resolution of rows as integer vectors
154153
# providing the positions of the matched variables
154+
# Enhanced to support content-based targeting
155155
#
156156
resolved_rows_idx <-
157-
resolve_rows_i(
157+
resolve_stub_rows_enhanced(
158158
expr = !!object$rows,
159159
data = data,
160+
columns = resolved_columns,
160161
call = call
161162
)
162163

@@ -835,3 +836,198 @@ cap_first_letter <- function(x) {
835836
substr(x, 1, 1) <- toupper(substr(x, 1, 1))
836837
x
837838
}
839+
840+
#' Enhanced stub row resolution with content-based targeting
841+
#'
842+
#' @param expr The expression for row specification (can be numeric indices or content values)
843+
#' @param data The gt table data object
844+
#' @param columns The resolved stub columns for context
845+
#' @param call The calling environment for error reporting
846+
#' @noRd
847+
resolve_stub_rows_enhanced <- function(
848+
expr,
849+
data,
850+
columns = NULL,
851+
call = rlang::caller_env()
852+
) {
853+
854+
# Evaluate the expression to get the rows specification
855+
quo <- rlang::enquo(expr)
856+
857+
# Handle the special case of everything() which is the default
858+
if (identical(rlang::quo_get_expr(quo), quote(everything()))) {
859+
return(seq_len(nrow(dt_data_get(data))))
860+
}
861+
862+
# Try to evaluate the expression
863+
tryCatch({
864+
rows_spec <- rlang::eval_tidy(quo, data = dt_data_get(data))
865+
866+
# If NULL or missing, return all rows
867+
if (is.null(rows_spec)) {
868+
return(seq_len(nrow(dt_data_get(data))))
869+
}
870+
871+
# If TRUE, return all rows
872+
if (identical(rows_spec, TRUE)) {
873+
return(seq_len(nrow(dt_data_get(data))))
874+
}
875+
876+
# If logical vector, convert to row indices
877+
if (is.logical(rows_spec)) {
878+
return(which(rows_spec))
879+
}
880+
881+
# If character, use enhanced content-based targeting (do this BEFORE numeric check)
882+
if (is.character(rows_spec)) {
883+
return(resolve_stub_content_targeting(data, rows_spec, columns, call))
884+
}
885+
886+
# If numeric, use traditional resolution (backwards compatibility)
887+
if (is.numeric(rows_spec)) {
888+
return(resolve_rows_i(expr = {{ expr }}, data = data, call = call))
889+
}
890+
891+
# For other types, fall back to traditional resolution
892+
return(resolve_rows_i(expr = {{ expr }}, data = data, call = call))
893+
894+
}, error = function(e) {
895+
# If evaluation fails, fall back to traditional resolution
896+
return(resolve_rows_i(expr = {{ expr }}, data = data, call = call))
897+
})
898+
}
899+
900+
#' Resolve stub rows by content-based targeting
901+
#'
902+
#' @param data The gt table data object
903+
#' @param rows_spec Character vector of content values to target
904+
#' @param columns Specific stub columns to search in (optional)
905+
#' @param call The calling environment for error reporting
906+
#' @noRd
907+
resolve_stub_content_targeting <- function(data, rows_spec, columns = NULL, call = rlang::caller_env()) {
908+
909+
# Get the data table and stub variables
910+
data_tbl <- dt_data_get(data)
911+
stub_vars <- dt_boxhead_get_var_stub(data)
912+
913+
# Handle case where no stub exists
914+
if (length(stub_vars) == 0 || all(is.na(stub_vars))) {
915+
cli::cli_abort(
916+
c(
917+
"Cannot use content-based targeting: table has no stub columns.",
918+
"i" = "Use numeric row indices instead, or add stub columns with rowname_col in gt()."
919+
),
920+
call = call
921+
)
922+
}
923+
924+
# Create a comprehensive stub targeting map
925+
stub_map <- create_stub_targeting_map_internal(data_tbl, stub_vars)
926+
927+
resolved_rows <- c()
928+
929+
for (row_spec in rows_spec) {
930+
current_rows <- c()
931+
932+
# Strategy 1: Direct value match in specified columns (if provided)
933+
if (!is.null(columns) && length(columns) > 0) {
934+
for (col in columns) {
935+
key <- paste0(col, ":", row_spec)
936+
if (key %in% names(stub_map)) {
937+
current_rows <- c(current_rows, stub_map[[key]])
938+
}
939+
}
940+
}
941+
942+
# Strategy 2: Search all stub columns for the value (if no specific columns or no matches)
943+
if (length(current_rows) == 0) {
944+
for (col in stub_vars) {
945+
key <- paste0(col, ":", row_spec)
946+
if (key %in% names(stub_map)) {
947+
current_rows <- c(current_rows, stub_map[[key]])
948+
}
949+
}
950+
}
951+
952+
# Strategy 3: Hierarchical match (partial keys) - for complex hierarchical targeting
953+
if (length(current_rows) == 0) {
954+
# Look for hierarchical keys that contain this value
955+
matching_keys <- names(stub_map)[grepl(paste0(":", row_spec, "$"), names(stub_map))]
956+
for (key in matching_keys) {
957+
current_rows <- c(current_rows, stub_map[[key]])
958+
}
959+
}
960+
961+
# If still no matches found, provide helpful error
962+
if (length(current_rows) == 0) {
963+
# Get available values for error message
964+
available_values <- c()
965+
for (col in stub_vars) {
966+
col_values <- unique(data_tbl[[col]])
967+
available_values <- c(available_values, paste0(col, ": ", col_values))
968+
}
969+
970+
cli::cli_abort(
971+
c(
972+
"Cannot find '{row_spec}' in any stub column.",
973+
"i" = "Available values in stub columns:",
974+
set_names(paste0(" ", available_values), rep("*", length(available_values)))
975+
),
976+
call = call
977+
)
978+
}
979+
980+
resolved_rows <- c(resolved_rows, current_rows)
981+
}
982+
983+
# Return unique rows in original order
984+
unique(resolved_rows)
985+
}
986+
987+
#' Create an internal stub targeting map
988+
#'
989+
#' @param data_tbl The data table
990+
#' @param stub_vars The stub column variables
991+
#' @noRd
992+
create_stub_targeting_map_internal <- function(data_tbl, stub_vars) {
993+
994+
stub_map <- list()
995+
996+
# For each stub column, create mappings from values to row indices
997+
for (col_name in stub_vars) {
998+
col_values <- data_tbl[[col_name]]
999+
unique_values <- unique(col_values)
1000+
1001+
for (value in unique_values) {
1002+
rows_with_value <- which(col_values == value)
1003+
key <- paste0(col_name, ":", value)
1004+
stub_map[[key]] <- rows_with_value
1005+
}
1006+
}
1007+
1008+
# Add hierarchical mappings for multi-column stubs
1009+
if (length(stub_vars) > 1) {
1010+
for (row_idx in seq_len(nrow(data_tbl))) {
1011+
row_values <- sapply(stub_vars, function(col) data_tbl[[col]][row_idx])
1012+
1013+
# Add mappings for each level of hierarchy
1014+
for (level in seq_along(stub_vars)) {
1015+
partial_values <- row_values[1:level]
1016+
partial_key <- paste(stub_vars[1:level], partial_values, sep=":", collapse="|")
1017+
1018+
# Find all rows that match this partial hierarchy
1019+
matching_rows <- c()
1020+
for (check_row in seq_len(nrow(data_tbl))) {
1021+
check_values <- sapply(stub_vars[1:level], function(col) data_tbl[[col]][check_row])
1022+
if (all(check_values == partial_values)) {
1023+
matching_rows <- c(matching_rows, check_row)
1024+
}
1025+
}
1026+
1027+
stub_map[[partial_key]] <- matching_rows
1028+
}
1029+
}
1030+
}
1031+
1032+
stub_map
1033+
}

0 commit comments

Comments
 (0)