@@ -96,53 +96,52 @@ resolve_cells_stub <- function(data,
96
96
resolved_columns <- character (0 )
97
97
} else if (! is.null(object $ columns )) {
98
98
# 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
- )
107
99
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
+ )
113
114
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 )
116
120
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 )
123
123
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
+ )
130
138
)
131
- )
139
+ }
132
140
}
133
141
}
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 )
146
145
}
147
146
} else {
148
147
# Legacy behavior: no columns parameter provided
@@ -152,11 +151,13 @@ resolve_cells_stub <- function(data,
152
151
#
153
152
# Resolution of rows as integer vectors
154
153
# providing the positions of the matched variables
154
+ # Enhanced to support content-based targeting
155
155
#
156
156
resolved_rows_idx <-
157
- resolve_rows_i (
157
+ resolve_stub_rows_enhanced (
158
158
expr = !! object $ rows ,
159
159
data = data ,
160
+ columns = resolved_columns ,
160
161
call = call
161
162
)
162
163
@@ -835,3 +836,198 @@ cap_first_letter <- function(x) {
835
836
substr(x , 1 , 1 ) <- toupper(substr(x , 1 , 1 ))
836
837
x
837
838
}
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