Files
uscogdata/R/categories.R
T
jared 488d03d74b feat: cog_categories
Discovery verb over the summary_categories view, grouped one row per
(category, subtype). Parallels cog_gov_search: analysts use it to
find the valid `category` values to pass into cog_spending(),
cog_revenue(), cog_geographic_rollup().

Columns: category, category_type, subtype, n_codes, item_codes
(comma-separated, alphabetical). Optional filters:

  type    = NULL | "spending" | "revenue"
  pattern = regex matched case-insensitively on category

The user-facing "spending" alias is translated internally to the
corpus-native "expenditure" so callers don't have to learn Census
vocabulary, while the returned category_type column preserves the
native value for auditability.

Also: fix @noRd placement in session.R so devtools::document() stops
warning.

Tests: +16 new / 181 total pass. check 0E/0W/0N.
2026-04-24 18:15:26 -04:00

61 lines
2.3 KiB
R

# R/categories.R
#' List available spending / revenue categories
#'
#' Returns the category taxonomy exposed by the corpus's
#' `summary_categories` view, grouped to one row per
#' `(category, subtype)` pair. Use this to discover valid `category`
#' values for [cog_spending()] / [cog_revenue()] /
#' [cog_geographic_rollup()] and to audit which Census item codes feed
#' each category.
#'
#' @param type Either `NULL` (default, return both spending and revenue
#' rows), `"spending"`, or `"revenue"`.
#' @param pattern Optional regex matched case-insensitively against the
#' `category` column (e.g. `"Police"` or `"Tax"`).
#' @return Tibble with columns `category`, `category_type`, `subtype`,
#' `n_codes`, `item_codes` (comma-separated, alphabetical). Sorted by
#' `category_type`, `category`, `subtype`.
#' @export
cog_categories <- function(type = NULL, pattern = NULL) {
if (!is.null(type)) {
if (!is.character(type) || length(type) != 1L ||
!type %in% c("spending", "revenue")) {
cli::cli_abort('`type` must be NULL, "spending", or "revenue".')
}
}
if (!is.null(pattern) &&
(!is.character(pattern) || length(pattern) != 1L)) {
cli::cli_abort("`pattern` must be a length-1 character string or NULL.")
}
con <- .ensure_session()
preds <- character(0)
if (!is.null(type)) {
# Translate user-facing "spending" to the corpus's native "expenditure"
# value so callers don't have to learn Census vocabulary. "revenue" is
# the same in both.
db_type <- if (type == "spending") "expenditure" else type
preds <- c(preds, sprintf("category_type = %s", .sql_lit_chr(db_type)))
}
if (!is.null(pattern)) {
preds <- c(preds,
sprintf("regexp_matches(category, %s, 'i')",
.sql_lit_chr(pattern)))
}
where <- if (length(preds) == 0L) "" else paste("WHERE", paste(preds, collapse = " AND "))
sql <- paste(
"SELECT category, category_type,
COALESCE(spend_subtype, revenue_subtype) AS subtype,
COUNT(DISTINCT item_code) AS n_codes,
string_agg(DISTINCT item_code, ',' ORDER BY item_code) AS item_codes
FROM summary_categories",
where,
"GROUP BY category, category_type, subtype
ORDER BY category_type, category, subtype"
)
tibble::as_tibble(DBI::dbGetQuery(con, sql))
}