diff --git a/R/surveyFA.R b/R/surveyFA.R index f60fffd8..fe4306f4 100644 --- a/R/surveyFA.R +++ b/R/surveyFA.R @@ -1,3 +1,20 @@ +#' Select the first named minimum without sorting the full vector +#' +#' @description Returns the name attached to the first minimum value in a named +#' numeric vector. The helper intentionally uses a single linear scan so the +#' bounded recovery loop does not pay the cost of sorting every candidate. +#' @param values A named numeric vector whose missing-value policy has already +#' been applied by the caller. +#' @return The first minimum value's name, or `NA_character_` for an empty input. +#' @keywords internal +.minimum_named_value <- function(values) { + minimum_index <- which.min(values) + if (length(minimum_index) == 0L) { + return(NA_character_) + } + names(values)[minimum_index] +} + #' @title surveyFA #' @description Fallback calibration helper used when direct model estimation in #' `autoFIPC()` fails. @@ -232,7 +249,7 @@ surveyFA <- function( names(p_values) <- rownames(fit_df) if (any(!is.na(p_values))) { p_values[is.na(p_values)] <- 1 - candidate <- names(sort(p_values, decreasing = FALSE))[1L] + candidate <- .minimum_named_value(p_values) if (!is.na(candidate) && p_values[[candidate]] < pThreshold) { return(candidate) } diff --git a/tests/testthat/test-surveyFA.R b/tests/testthat/test-surveyFA.R index 060ae68e..9ed1c425 100644 --- a/tests/testthat/test-surveyFA.R +++ b/tests/testthat/test-surveyFA.R @@ -58,8 +58,32 @@ test_that("surveyFA validates boolean control flags before estimator dispatch", ) }) +test_that("minimum named selection preserves prior sort semantics", { + normalized_cases <- list( + c(item_a = 0.40, item_b = 0.10, item_c = 0.30), + c(item_a = 0.10, item_b = 0.10, item_c = 0.20), + c(item_a = -0.50, item_b = 0.00, item_c = 0.50) + ) + missing_case <- c(item_a = NA_real_, item_b = 0.20, item_c = 0.30) + missing_case[is.na(missing_case)] <- 1 + normalized_cases[[length(normalized_cases) + 1L]] <- missing_case + + for (p_values in normalized_cases) { + prior_candidate <- names(sort(p_values, decreasing = FALSE))[1L] + expect_identical( + aFIPC:::.minimum_named_value(p_values), + prior_candidate + ) + } + expect_identical( + aFIPC:::.minimum_named_value(setNames(numeric(), character())), + NA_character_ + ) +}) + test_that("surveyFA reports bounded recovery exhaustion when unrecoverable", { skip_if_not_installed("mirt") + set.seed(20260726) raw <- as.data.frame( matrix(