# DO NOT EDIT THIS FILE BY HAND! Instead edit the R Markdown source file `Rmd/fokus.Rmd` and run `pkgpurl::purl_rmd()`.
# See `README.md#r-markdown-format` for more information on the literate programming approach used applying the R Markdown format.
# fokus: Provides an API around the FOKUS Post-voting Surveys
# Copyright (C) 2024 Centre for Democracy Studies Aarau (ZDA)
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
.onLoad <- function(libname, pkgname) {
# clear pkgpins cache
tryCatch(expr = pkgpins::clear_cache(board = pkgpins::board(pkg = pkgname),
max_age = pal::pkg_config_val(key = "global_max_cache_age",
pkg = pkgname)),
error = function(e) cli::cli_alert_warning(text = "Failed to clear pkgpins cache on load of {.pkg {pkgname}}. Error message: {e$message}"))
}
utils::globalVariables(names = c(".",
# tidyselect fns
"all_of",
"any_of",
"ends_with",
"everything",
"starts_with",
"where",
# other
"alignment",
"allowed_at",
"block",
"date_begin",
"date_end",
"enumerator",
"enumerator_base",
"full_expressions",
"Geschlecht",
"has_fallback",
"has_same_length",
"Haushaltsgr\u00f6sse Anzahl Personen Total",
"Haushaltsgr\u00f6sse Anzahl Personen \u00fcber 18 Jahren",
"household_size_official",
"i",
"id",
"ID-Nummer",
"is_likely_default",
"j",
"Jahrgang",
"length_response_options",
"length_value_labels",
"length_variable_values",
"lvl",
"marital_status_official",
"matches_length",
"max_age",
"min_age",
"n_adults_in_household_official",
"n_cantonal_majoritarian_elections",
"n_cantonal_proportional_elections",
"n_cantonal_proposals",
"n_char_short",
"n_federal_majoritarian_elections",
"n_federal_proportional_elections",
"n_federal_proposals",
"n_kids_in_household_official",
"name",
"name.de.qstnr",
"nr",
"question",
"question_full",
"question_intro_i",
"question_intro_j",
"receives_print",
"response_options",
"sep",
"sex_official",
"string",
"term",
"topic",
"value_labels",
"variable_label",
"variable_name",
"variable_values",
"who",
"width",
"year_of_birth_official",
"Zivilstand"))
#' Raw FOKUS questionnaire data
#'
#' A structured list of the raw questionnaire data of the FOKUS surveys.
#'
#' @format `r pkgsnip::return_lbl("strict_list")`
#' @docType data
#' @family qstnr_raw
#' @keywords internal
#'
#' @name raw_qstnr
NULL
#' Raw supplemental date-specific FOKUS questionnaire data
#'
#' A structured list of raw supplemental date-specific questionnaire data of the FOKUS surveys.
#'
#' @format `r pkgsnip::return_lbl("strict_list")`
#' @docType data
#' @family qstnr_raw
#' @keywords internal
#'
#' @name raw_qstnrs_suppl
#'
#' @examples
#' fokus:::raw_qstnrs_suppl[["2018-09-23"]]$mode
NULL
#' Get raw supplemental date-specific FOKUS questionnaire data
#'
#' Returns a structured list of the [raw supplemental date-specific FOKUS questionnaire data][raw_qstnrs_suppl] for the specified ballot date.
#'
#' @inheritParams cantons
#'
#' @return `r pkgsnip::return_lbl("strict_list")`
#' @family qstnr_raw
#' @keywords internal
#'
#' @examples
#' fokus:::raw_qstnr_suppl(ballot_date = "2018-09-23") |> _$mode
raw_qstnr_suppl <- function(ballot_date = pal::pkg_config_val("ballot_date")) {
ballot_date %<>% as_ballot_date_chr()
raw_qstnrs_suppl[[ballot_date]]
}
#' Get a political level's raw supplemental date-specific FOKUS questionnaire data
#'
#' Returns a structured list of the [raw supplemental date-specific FOKUS questionnaire data][raw_qstnr_suppl] for the specified ballot date and political
#' level.
#'
#' @inheritParams cantons
#' @param lvl Political level. One of `r pal::enum_fn_param_defaults(param = "lvl", fn = raw_qstnr_suppl_lvl)`.
#'
#' @inherit raw_qstnr_suppl return seealso
#' @family qstnr_raw
#' @keywords internal
#'
#' @examples
#' fokus:::raw_qstnr_suppl_lvl(ballot_date = "2018-09-23",
#' lvl = "cantonal") |>
#' names()
raw_qstnr_suppl_lvl <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = all_lvls) {
lvl <- rlang::arg_match(lvl)
result <- raw_qstnr_suppl(ballot_date = ballot_date) |> purrr::pluck(lvl)
if (is.null(result)) {
# reduce to proper arg values for error msg
ballot_date %<>% as_ballot_date()
cli::cli_abort("No {.val {lvl}} level present in the supplemental {.val {ballot_date}} FOKUS questionnaire data.")
}
result
}
#' Get a canton's political-level-specific raw supplemental date-specific FOKUS questionnaire data
#'
#' Returns a structured list of the [raw supplemental date-specific FOKUS questionnaire data for the specified ballot date and political
#' level][raw_qstnr_suppl_lvl] that applies for the specified canton only.
#'
#' @inheritParams raw_qstnr_suppl_lvl
#' @inheritParams lvls
#'
#' @inherit raw_qstnr_suppl return
#' @family qstnr_raw
#' @keywords internal
#'
#' @examples
#' fokus:::raw_qstnr_suppl_lvl_canton(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau") |>
#' names()
raw_qstnr_suppl_lvl_canton <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = all_lvls,
canton = pal::pkg_config_val("canton")) {
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
result <-
raw_qstnr_suppl_lvl(ballot_date = ballot_date,
lvl = lvl) |>
purrr::pluck(canton)
if (is.null(result)) {
# reduce to proper arg values for error msg
ballot_date %<>% as_ballot_date()
lvl <- rlang::arg_match(lvl)
cli::cli_abort("No {.val {lvl}} supplemental {.val {ballot_date}} FOKUS questionnaire data present for canton {.val {canton}}.")
}
result
}
#' Get raw proposal supplemental questionnaire data
#'
#' Returns a structured list of a proposal's data from the [supplemental date-specific FOKUS questionnaire data][raw_qstnr_suppl].
#'
#' @inheritParams raw_qstnr_suppl_lvl_canton
#' @param proposal_nr Proposal number. A positive integerish scalar.
#'
#' @inherit raw_qstnr_suppl return
#' @family qstnr_raw
#' @keywords internal
#'
#' @examples
#' fokus:::raw_qstnr_suppl_proposal(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1) |>
#' _$name
raw_qstnr_suppl_proposal <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = all_lvls,
canton = pal::pkg_config_val("canton"),
proposal_nr = 1L) {
lvl <- rlang::arg_match(lvl)
checkmate::assert_count(proposal_nr,
positive = TRUE)
proposals <-
lvl |>
pal::when(. == "federal" ~
raw_qstnr_suppl_lvl(ballot_date = ballot_date,
lvl = .),
~ raw_qstnr_suppl_lvl_canton(ballot_date = ballot_date,
lvl = .,
canton = canton)) |>
purrr::pluck("proposal")
if (is.null(proposals)) {
# reduce to proper arg values for error msg
ballot_date %<>% as_ballot_date()
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
cli::cli_abort(paste0("No {.val {lvl}} proposals present",
" for {.val {canton}}"[lvl == "cantonal"],
" in the supplemental {.val {ballot_date}} FOKUS questionnaire data."))
}
proposal <- proposals |> purrr::pluck(as.character(proposal_nr))
if (is.null(proposal)) {
# reduce to proper arg values for error msg
ballot_date %<>% as_ballot_date()
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
cli::cli_abort(paste0("No {.val {lvl}} proposal {.val {proposal_nr}} present",
" for {.val {canton}}"[lvl == "cantonal"],
" in the supplemental {.val {ballot_date}} FOKUS questionnaire data."))
}
proposal
}
#' Safely get raw proposal supplemental questionnaire data
#'
#' Returns a structured list of a proposal's data from the [supplemental date-specific FOKUS questionnaire data][raw_qstnr_suppl], or `NULL` if no referendums
#' proposals were covered.
#'
#' @inheritParams raw_qstnr_suppl_proposal
#' @param proposal_nr Proposal number. Either a positive integerish scalar or `NULL` to return the data for all proposals.
#'
#' @return A [strict list][xfun::strict_list()], or `NULL` if no referendums proposals were covered.
#' @family qstnr_raw
#' @keywords internal
#'
#' @examples
#' fokus:::raw_qstnr_suppl_proposal_safe(ballot_date = "2019-10-20",
#' lvl = "cantonal",
#' canton = "aargau")
raw_qstnr_suppl_proposal_safe <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = all_lvls,
canton = pal::pkg_config_val("canton"),
proposal_nr = NULL) {
lvl <- rlang::arg_match(lvl)
checkmate::assert_count(proposal_nr,
positive = TRUE,
null.ok = TRUE)
if (lvl == "cantonal") {
canton <- rlang::arg_match(arg = canton,
values = cantons(ballot_date))
}
raw_qstnr_suppl(ballot_date = ballot_date) |>
purrr::pluck(lvl) |>
pal::when(lvl == "cantonal" ~ purrr::pluck(., canton),
~ .) |>
purrr::pluck("proposal") |>
pal::when(is.null(proposal_nr) ~ .,
~ purrr::pluck(., proposal_nr))
}
#' Get a proposal's raw name supplemental questionnaire data
#'
#' Returns a structured list of proposal name data from the [proposal-specific raw supplemental date-specific FOKUS questionnaire
#' data][raw_qstnr_suppl_proposal].
#'
#' @inheritParams raw_qstnr_suppl_proposal
#'
#' @inherit raw_qstnr_suppl return
#' @family qstnr_raw
#' @keywords internal
#'
#' @examples
#' fokus:::raw_qstnr_suppl_proposal_name(ballot_date = "2018-11-25",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 2)
raw_qstnr_suppl_proposal_name <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = all_lvls,
canton = pal::pkg_config_val("canton"),
proposal_nr = 1L) {
result <-
raw_qstnr_suppl_proposal(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr) |>
purrr::pluck("name")
if (is.null(result)) {
# reduce to proper arg values for error msg
ballot_date %<>% as_ballot_date()
lvl <- rlang::arg_match(lvl)
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
cli::cli_abort(paste0("No names present for {.val {lvl}} proposal {.val {proposal_nr}} ",
"in {.val {canton}} "[lvl == "cantonal"],
"in the supplemental {.val {ballot_date}} FOKUS questionnaire data."))
}
result
}
#' Get a proposal's raw argument supplemental questionnaire data
#'
#' Returns a structured list of argument data from the [proposal-specific raw supplemental date-specific FOKUS questionnaire
#' data][raw_qstnr_suppl_proposal].
#'
#' @inheritParams raw_qstnr_suppl_proposal
#'
#' @inherit raw_qstnr_suppl return
#' @family qstnr_raw
#' @keywords internal
#'
#' @examples
#' fokus:::raw_qstnr_suppl_arguments(ballot_date = "2018-11-25",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 2)
raw_qstnr_suppl_arguments <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = all_lvls,
canton = pal::pkg_config_val("canton"),
proposal_nr = 1L) {
result <-
raw_qstnr_suppl_proposal(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr) |>
purrr::pluck("argument")
if (is.null(result)) {
# reduce to proper arg values for error msg
ballot_date %<>% as_ballot_date()
lvl <- rlang::arg_match(lvl)
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
cli::cli_abort(paste0("No arguments present for {.val {lvl}} proposal {.val {proposal_nr}} ",
"in {.val {canton}} "[lvl == "cantonal"],
"in the supplemental {.val {ballot_date}} FOKUS questionnaire data."))
}
result
}
#' Get a proposal argument's raw supplemental questionnaire data
#'
#' Returns a structured list of a proposal argument's data from the [proposal-specific raw supplemental date-specific FOKUS questionnaire
#' data][raw_qstnr_suppl_proposal].
#'
#' @inheritParams raw_qstnr_suppl_arguments
#' @inheritParams proposal_argument
#'
#' @inherit raw_qstnr_suppl return
#' @family qstnr_raw
#' @keywords internal
#'
#' @examples
#' fokus:::raw_qstnr_suppl_argument(ballot_date = "2018-11-25",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 2,
#' side = "pro",
#' argument_nr = 2)
raw_qstnr_suppl_argument <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = all_lvls,
canton = pal::pkg_config_val("canton"),
proposal_nr = 1L,
side = all_argument_sides,
argument_nr = 1L) {
side <- rlang::arg_match(side)
checkmate::assert_count(argument_nr,
positive = TRUE)
result <-
raw_qstnr_suppl_arguments(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr) |>
purrr::keep(\(x) x$side %in% side && x$nr %in% argument_nr) |>
dplyr::first()
if (is.null(result)) {
# reduce to proper arg values for error msg
ballot_date %<>% as_ballot_date()
lvl <- rlang::arg_match(lvl)
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
cli::cli_abort(paste0("No {.val {side}} argument {.val {argument_nr}} present for {.val {lvl}} proposal {.val {proposal_nr}} ",
"in {.val {canton}} "[lvl == "cantonal"],
"in the supplemental {.val {ballot_date}} FOKUS questionnaire data."))
}
result
}
#' Get a proposal's raw main motive supplemental questionnaire data
#'
#' Returns a structured list of main motive data from the [proposal-specific raw supplemental date-specific FOKUS questionnaire
#' data][raw_qstnr_suppl_proposal].
#'
#' @inheritParams raw_qstnr_suppl_proposal
#'
#' @inherit raw_qstnr_suppl return
#' @family qstnr_raw
#' @keywords internal
#'
#' @examples
#' fokus:::raw_qstnr_suppl_main_motives(ballot_date = "2018-11-25",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 2)
raw_qstnr_suppl_main_motives <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = all_lvls,
canton = pal::pkg_config_val("canton"),
proposal_nr = 1L) {
result <-
raw_qstnr_suppl_proposal(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr) |>
purrr::pluck("main_motive")
if (is.null(result)) {
# reduce to proper arg values for error msg
ballot_date %<>% as_ballot_date()
lvl <- rlang::arg_match(lvl)
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
cli::cli_abort(paste0("No main motives present for {.val {lvl}} proposal {.val {proposal_nr}} ",
"in {.val {canton}} "[lvl == "cantonal"],
"in the supplemental {.val {ballot_date}} FOKUS questionnaire data."))
}
result
}
#' Get raw supplemental election questionnaire data
#'
#' Returns a structured list of election data from the [canton's political-level-specific raw supplemental date-specific FOKUS questionnaire
#' data][raw_qstnr_suppl_lvl_canton].
#'
#' @inheritParams raw_qstnr_suppl_lvl_canton
#'
#' @inherit raw_qstnr_suppl return
#' @family qstnr_raw
#' @keywords internal
#'
#' @examples
#' fokus:::raw_qstnr_suppl_elections(ballot_date = "2019-10-20",
#' lvl = "cantonal",
#' canton = "aargau") |>
#' _$skill_questions_source
raw_qstnr_suppl_elections <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = all_lvls,
canton = pal::pkg_config_val("canton")) {
result <-
raw_qstnr_suppl_lvl_canton(ballot_date = ballot_date,
lvl = lvl,
canton = canton) |>
purrr::pluck("election")
if (is.null(result)) {
# reduce to proper arg values for error msg
ballot_date %<>% as_ballot_date()
lvl <- rlang::arg_match(lvl)
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
cli::cli_abort("No {.val {lvl}} elections for canton {.val {canton}} present in the supplemental {.val {ballot_date}} FOKUS questionnaire data.")
}
result
}
#' Get an election's raw supplemental questionnaire data
#'
#' Returns a structured list of an election's data from the [canton's political-level-specific raw supplemental date-specific FOKUS questionnaire
#' data][raw_qstnr_suppl_lvl_canton].
#'
#' @inheritParams raw_qstnr_suppl_lvl_canton
#' @inheritParams election_name
#'
#' @inherit raw_qstnr_suppl return
#' @family qstnr_raw
#' @keywords internal
#'
#' @examples
#' fokus:::raw_qstnr_suppl_election(ballot_date = "2019-10-20",
#' lvl = "cantonal",
#' canton = "aargau",
#' prcd = "majoritarian") |>
#' _$n_seats
raw_qstnr_suppl_election <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = all_lvls,
canton = pal::pkg_config_val("canton"),
prcd = all_prcds,
election_nr = 1L) {
prcd <- rlang::arg_match(prcd)
checkmate::assert_count(election_nr,
positive = TRUE)
result <-
raw_qstnr_suppl_elections(ballot_date = ballot_date,
lvl = lvl,
canton = canton) |>
purrr::pluck(prcd)
if (is.null(result)) {
# reduce to proper arg values for error msg
ballot_date %<>% as_ballot_date()
lvl <- rlang::arg_match(lvl)
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
cli::cli_abort(paste0("No {.val {lvl}} {.val {prcd}} elections for canton {.val {canton}} present in the supplemental {.val {ballot_date}} FOKUS ",
"questionnaire data."))
}
result %<>% purrr::pluck(as.character(election_nr))
if (is.null(result)) {
# reduce to proper arg values for error msg
ballot_date %<>% as_ballot_date()
lvl <- rlang::arg_match(lvl)
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
cli::cli_abort(paste0("No {.val {lvl}} {.val {prcd}} election {.val {election_nr}} for canton {.val {canton}} present in the supplemental ",
"{.val {ballot_date}} FOKUS questionnaire data."))
}
result
}
#' Get raw supplemental election name questionnaire data
#'
#' Returns a structured list of an election's name data from the [political-level-specific raw supplemental date-specific FOKUS questionnaire
#' data][raw_qstnr_suppl_lvl].
#'
#' @inheritParams raw_qstnr_suppl_election
#'
#' @inherit raw_qstnr_suppl return
#' @family qstnr_raw
#' @keywords internal
#'
#' @examples
#' fokus:::raw_qstnr_suppl_election_name(ballot_date = "2019-10-20",
#' lvl = "cantonal",
#' canton = "aargau",
#' prcd = "majoritarian")
raw_qstnr_suppl_election_name <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = all_lvls,
canton = pal::pkg_config_val("canton"),
prcd = all_prcds,
election_nr = 1L) {
lvl <- rlang::arg_match(lvl)
prcd <- rlang::arg_match(prcd)
checkmate::assert_count(election_nr,
positive = TRUE)
# federal proportional election names are defined once for all cantons together
if (lvl == "federal" && prcd == "proportional") {
result <-
raw_qstnr_suppl_lvl(ballot_date = ballot_date,
lvl = lvl) |>
purrr::chuck("election", prcd, as.character(election_nr))
} else {
result <- raw_qstnr_suppl_election(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
prcd = prcd,
election_nr = election_nr)
}
result %<>% purrr::pluck("name")
if (is.null(result)) {
# reduce to proper arg values for error msg
ballot_date %<>% as_ballot_date()
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
cli::cli_abort(paste0("No names present for {.val {lvl}} {.val {prcd}} election {.val {election_nr}} in canton {.val {canton}} in the supplemental ",
"{.val {ballot_date}} FOKUS questionnaire data."))
}
result
}
#' Get raw supplemental survey mode questionnaire data
#'
#' Returns a structured list of survey mode data from the [raw supplemental date-specific FOKUS questionnaire data][raw_qstnr_suppl].
#'
#' @inheritParams raw_qstnr_suppl_lvl_canton
#'
#' @inherit raw_qstnr_suppl return
#' @family qstnr_raw
#' @keywords internal
#'
#' @examples
#' fokus:::raw_qstnr_suppl_mode(ballot_date = "2018-09-23",
#' canton = "aargau")
raw_qstnr_suppl_mode <- function(ballot_date = pal::pkg_config_val("ballot_date"),
canton = pal::pkg_config_val("canton")) {
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
result <-
raw_qstnr_suppl(ballot_date = ballot_date) |>
purrr::pluck("mode", canton)
if (is.null(result)) {
# reduce to proper arg values for error msg
ballot_date %<>% as_ballot_date()
cli::cli_abort("No survey mode data present for canton {.val {canton}} in supplemental {.val {ballot_date}} FOKUS questionnaire data.")
}
result
}
#' Get raw supplemental skill question questionnaire data
#'
#' Returns a structured list of skill question data from the [raw supplemental date-specific FOKUS questionnaire data][raw_qstnr_suppl].
#'
#' @inheritParams raw_qstnr_suppl_proposal
#' @param proposal_nr Proposal number. A positive integerish scalar or `NULL`. If `NULL`, non-proposal-specific skill question data is returned.
#'
#' @inherit raw_qstnr_suppl return
#' @family qstnr_raw
#' @keywords internal
#'
#' @examples
#' fokus:::raw_qstnr_suppl_skill_questions(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1) |>
#' purrr::map_depth(1L, "de") |>
#' purrr::list_c(ptype = character())
raw_qstnr_suppl_skill_questions <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = all_lvls,
canton = pal::pkg_config_val("canton"),
proposal_nr = NULL) {
lvl <- rlang::arg_match(lvl)
result <-
lvl |>
pal::when(
# federal non-proposal-specific skill questions (e.g. at federal elections)
length(proposal_nr) == 0L && . == "federal" ~
raw_qstnr_suppl_lvl(ballot_date = ballot_date,
lvl = .),
# cantonal non-proposal-specific skill questions (e.g. at cantonal elections)
length(proposal_nr) == 0L && . == "cantonal" ~
raw_qstnr_suppl_lvl_canton(ballot_date = ballot_date,
lvl = .,
canton = canton),
# federal or cantonal proposal-specific skill questions
~ raw_qstnr_suppl_proposal(ballot_date = ballot_date,
lvl = .,
canton = canton,
proposal_nr = proposal_nr)) |>
purrr::pluck("skill_question")
if (is.null(result)) {
# reduce to proper arg values for error msg
ballot_date %<>% as_ballot_date()
lvl <- rlang::arg_match(lvl)
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
cli::cli_abort(paste0("No skill questions present",
ifelse(length(proposal_nr) == 0L,
paste0(" on the {.val {lvl}} level",
" for {.val {canton}}"[lvl == "cantonal"]),
" for {.val {lvl}} proposal {.val {proposal_nr}}"),
" in the supplemental {.val {ballot_date}} FOKUS questionnaire data."))
}
result
}
#' Get a single skill question's raw supplemental questionnaire data
#'
#' Returns a structured list of a single skill question's data from the [raw supplemental date-specific FOKUS questionnaire data][raw_qstnr_suppl].
#'
#' @inheritParams raw_qstnr_suppl_lvl_canton
#' @inheritParams skill_question
#'
#' @inherit raw_qstnr_suppl return
#' @family qstnr_raw
#' @keywords internal
#'
#' @examples
#' fokus:::raw_qstnr_suppl_skill_question(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1,
#' skill_question_nr = 1)
raw_qstnr_suppl_skill_question <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = all_lvls,
canton = pal::pkg_config_val("canton"),
proposal_nr = NULL,
skill_question_nr = 1L) {
checkmate::assert_count(skill_question_nr,
positive = TRUE)
result <-
raw_qstnr_suppl_skill_questions(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr) |>
purrr::pluck(skill_question_nr)
if (is.null(result)) {
# reduce to proper arg values for error msg
ballot_date %<>% as_ballot_date()
lvl <- rlang::arg_match(lvl)
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
cli::cli_abort(paste0("No skill question {.val {skill_question_nr}} present",
ifelse(length(proposal_nr) == 0L,
paste0(" on the {.val {lvl}} level", " for {.val {canton}}"[lvl == "cantonal"]),
" for {.val {lvl}} proposal {.val {proposal_nr}}"),
" in the supplemental {.val {ballot_date}} FOKUS questionnaire data. Only {length(skill_questions)} skill questions included."))
}
result
}
#' Pick right raw questionnaire value
#'
#' Picks the right value of a certain raw questionnaire key based on ballot date and canton (recursively).
#'
#' @inheritParams raw_qstnr_suppl_lvl_canton
#' @param x Questionnaire key. A list object.
#' @param key Questionnaire key name, used to determine the correct default value fallback. A character scalar or `NULL`. If `NULL`, no fallback is used (and an
#' error is thrown in case none of the subkeys matches).
#'
#' @return Value of `x` that corresponds to `canton` and `ballot_date`.
#' @family qstnr_raw
#' @keywords internal
raw_pick_right <- function(x,
key = NULL,
ballot_date,
canton) {
# force evaluation of `ballot_date`, `canton` and `key` to ensure immediate error (with better msg) in case they are missing
ballot_date %<>% as_ballot_date_chr()
canton
key
if (purrr::pluck_depth(x) > 1L) {
x <-
pick_right_helper(x = x,
key = key,
ballot_date = ballot_date,
canton = canton) |>
raw_pick_right(key = key,
ballot_date = ballot_date,
canton = canton)
}
x
}
pick_right_helper <- function(x,
ballot_date,
canton,
key) {
if (purrr::is_list(x) && (length(x) > 1L || purrr::pluck_depth(x) > 1L)) {
# create plain ballot date as in subkeys
ballot_date_squeezed <- stringr::str_remove_all(string = ballot_date,
pattern = stringr::fixed("-"))
# convert ballot date to type date
ballot_date %<>% clock::date_parse()
# handle begin-end date subkeys
begin_end_subkeys <-
names(x) |>
stringr::str_subset(pattern = "^\\d+_\\d+$")
matches_begin_end_subkeys <-
begin_end_subkeys |>
purrr::map_lgl(function(x) {
begin <- x |> stringr::str_extract(pattern = "^\\d+") |> lubridate::as_date()
end <- x |> stringr::str_extract(pattern = "\\d+$") |> lubridate::as_date()
begin <= ballot_date && ballot_date <= end
})
# integrity check: ensure there aren't any overlapping intervals
if (length(which(matches_begin_end_subkeys)) > 1L) {
cli::cli_abort(c("Illegal overlapping interval subkeys found: {.var {begin_end_subkeys[matches_begin_end_subkeys]}}",
">" = "Please fix this and run again."),
.internal = TRUE)
}
ballot_types <- ballot_types(ballot_date = ballot_date,
canton = canton)
x <- names(x) |> pal::when(
# canton and ballot date
## consider overrides for binary keys
canton %in% x[["false"]] || ballot_date %in% x[["false"]] ~ FALSE,
canton %in% x[["true"]] || ballot_date %in% x[["true"]] ~ TRUE,
## consider overrides for non-binary keys
### single canton subkey
canton %in% . ~ x[[canton]],
### single date subkey
ballot_date_squeezed %in% . ~ x[[ballot_date_squeezed]],
### begin-end date subkey
any(matches_begin_end_subkeys) ~ x[[begin_end_subkeys[matches_begin_end_subkeys]]],
# consider overrides for ballot types (we take the first one in case of ambiguity)
any(ballot_types %in% .) ~ x[[intersect(., ballot_types)[1L]]],
# return default value if defined
"default" %in% . ~ x[["default"]],
# fall back on key's default value if no subkey matches canton and ballot date
key %in% qstnr_item_keys$key ~ unlist(qstnr_item_keys$default_val[qstnr_item_keys$key == key]),
# abort in any remaining case
~ cli::cli_abort("Undefined behavior, please debug. {.arg {key}} is {.val {key}}, {.arg {x}} is {.field {x}}.",
.internal = TRUE)
)
}
x
}
init_heritable_map <- function(block) {
xfun::strict_list(lvl = "?",
i = NA_integer_,
j = NA_integer_,
block = block,
variable_name = "???",
topic = NULL,
who = "alle",
question_intro_i = NULL,
question_intro_j = NULL,
question = NULL,
question_full = NULL,
question_common = NULL,
allow_multiple_answers = FALSE,
variable_label = "???",
variable_label_common = NULL,
response_options = NULL,
variable_values = NULL,
value_labels = NULL,
value_scale = "nominal",
randomize_response_options = FALSE,
is_mandatory = FALSE,
ballot_types = all_ballot_types,
include = TRUE)
}
resolve_qstnr_val <- function(x,
ballot_date,
canton,
key,
lvl,
i,
j,
...) {
ballot_date %<>% as_ballot_date_chr()
checkmate::assert_string(lvl,
na.ok = TRUE,
null.ok = TRUE)
checkmate::assert_count(i,
na.ok = TRUE,
null.ok = TRUE)
checkmate::assert_count(j,
na.ok = TRUE,
null.ok = TRUE)
x |>
raw_pick_right(key = key,
ballot_date = ballot_date,
canton = canton) |>
pal::when(is.character(.) ~ interpolate_qstnr_val(.,
ballot_date = ballot_date,
canton = canton,
key = key,
lvl = lvl,
i = i,
j = j,
... = ...),
~ .) |>
# convert to proper type
pal::when(
key %in% qstnr_item_keys$key[qstnr_item_keys$type == "character"] ~
as.character(.),
key %in% qstnr_item_keys$key[qstnr_item_keys$type == "logical"] ~
as.logical(.),
key %in% qstnr_item_keys$key[qstnr_item_keys$type == "integer"] ~
as.integer(.),
key %in% qstnr_item_keys$key[qstnr_item_keys$type == "double"] ~
as.double(.),
# undefined behaviour
~ cli::cli_abort("Undefined behaviour in {.fun resolve_qstnr_val}. Please debug.",
.internal = TRUE)
)
}
interpolate_qstnr_val <- function(x,
ballot_date,
canton,
key,
lvl,
i,
j,
...) {
# assign objects in dots to current env ensuring glue/cli fns respect them
rlang::env_bind(.env = rlang::current_env(),
...)
if (key %in% qstnr_item_keys$key[qstnr_item_keys$is_scalar]) {
result <- cli::pluralize(x,
.na = NULL,
.null = NA_character_,
.trim = FALSE)
} else {
result <-
x |>
purrr::map(\(x) glue::glue(x,
.envir = rlang::current_env(),
.na = NULL,
.null = NA_character_,
.trim = FALSE)) |>
unlist()
}
result
}
#' Generate questionnaire tibble
#'
#' @inheritParams ballot_title
#' @param verbose Whether or not to print detailed progress information during questionnaire generation. Note that it will take considerably more time when this
#' is set to `TRUE`.
#'
#' @return `r pkgsnip::return_lbl("tibble")`
#' @family qstnr_gen
#' @keywords internal
gen_qstnr_tibble <- function(ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date),
verbose = FALSE) {
ballot_date %<>% as_ballot_date()
canton <- rlang::arg_match(arg = canton,
values = cantons(ballot_date))
checkmate::assert_flag(verbose)
cli::start_app(theme = cli_theme)
pal::cli_progress_step_quick(msg = "Generating {.val {canton}} @ {.val {ballot_date}} questionnaire tibble")
purrr::map2_dfr(.x = raw_qstnr,
.y = names(raw_qstnr),
.f = ~ {
if (verbose && !(.y %in% qstnr_non_item_lvls)) cli::cli_h1("BLOCK: {.val {.y}}")
assemble_qstnr_tibble(ballot_date = ballot_date,
canton = canton,
raw_qstnr_branch = .x,
qstnr_lvl = .y,
heritable_map = init_heritable_map(block = .y),
verbose = verbose)
}) |>
# add ballot date and canton
dplyr::mutate(ballot_date = !!ballot_date,
canton = !!canton,
.before = 1L)
}
assemble_qstnr_tibble <- function(ballot_date,
canton,
raw_qstnr_branch,
qstnr_lvl,
heritable_map,
verbose) {
if (verbose && stringr::str_detect(qstnr_lvl, "\\..+\\.item$")) {
cli::cli_h2("BRANCH PATH: {.val {stringr::str_remove(qstnr_lvl, '\\\\.item$')}}")
}
map <- heritable_map
# only complement with non-table-array qstnr lvls (would be completely wrong to complement with the `item` table arrays, which are also *unnamed* lists)
if (rlang::is_named(raw_qstnr_branch)) {
map %<>%
purrr::list_modify(!!!raw_qstnr_branch) %>%
purrr::keep_at(at = qstnr_item_keys$key)
}
result <- NULL
if ("variable_name" %in% names(raw_qstnr_branch)) {
result <- assemble_qstnr_item_tibble(ballot_date = ballot_date,
canton = canton,
item_map = map,
verbose = verbose)
} else if (purrr::pluck_depth(raw_qstnr_branch) > 2L) {
result <- purrr::map2_dfr(.x = raw_qstnr_branch,
.y = names(raw_qstnr_branch) %||% seq_along(raw_qstnr_branch),
.f = ~ assemble_qstnr_tibble(ballot_date = ballot_date,
canton = canton,
raw_qstnr_branch = .x,
qstnr_lvl = paste(qstnr_lvl, .y,
sep = "."),
heritable_map = map,
verbose = verbose))
}
result
}
assemble_qstnr_item_tibble <- function(ballot_date,
canton,
item_map,
verbose) {
# iterate over `lvl`...
resolve_qstnr_val(x = item_map$lvl,
ballot_date = ballot_date,
canton = canton,
key = "lvl",
lvl = "",
i = NA_integer_,
j = NA_integer_) |>
purrr::map(
.f = function(lvl) {
# ...`i`...
resolve_qstnr_val(x = item_map$i,
ballot_date = ballot_date,
canton = canton,
key = "i",
lvl = lvl,
i = NA_integer_,
j = NA_integer_) |>
purrr::map(
lvl = lvl,
.f = function(i,
lvl) {
# ...and `j`
resolve_qstnr_val(x = item_map$j,
ballot_date = ballot_date,
canton = canton,
key = "j",
lvl = lvl,
i = i,
j = NA_integer_) |>
purrr::map(
i = i,
lvl = lvl,
.f = function(j,
i,
lvl) {
# respect keys `include` and `ballot_types`
is_incl <- resolve_qstnr_val(x = item_map$include,
ballot_date = ballot_date,
canton = canton,
key = "include",
lvl = lvl,
i = i,
j = j)
has_ballot_type <-
resolve_qstnr_val(x = item_map$ballot_types,
ballot_date = ballot_date,
canton = canton,
key = "ballot_types",
lvl = lvl,
i = i,
j = j) |>
intersect(ballot_types(ballot_date = ballot_date,
canton = canton)) |>
rlang::is_empty() |>
magrittr::not()
result <- NULL
if (is_incl && has_ballot_type) {
if (verbose) cli::cli_h3("ITEM: {.var {item_map$variable_name}}")
# resolve all keys in item map
result <-
names(item_map) |>
setdiff(c("lvl",
"i",
"j",
"ballot_types",
"include")) %>%
magrittr::set_names(x = .,
value = .) |>
purrr::map(~ {
if (verbose) cli::cli_progress_step("KEY: {.field {.x}}")
# pre-resolve `question` for dependent `question_full` resolution
if (.x == "question_full") {
question <- resolve_qstnr_val(x = item_map[["question"]],
ballot_date = ballot_date,
canton = canton,
key = "question",
lvl = lvl,
i = i,
j = j)
} else {
question <- NA_character_
}
resolve_qstnr_val(x = item_map[[.x]],
ballot_date = ballot_date,
canton = canton,
key = .x,
lvl = lvl,
i = i,
j = j,
question = question) |>
pal::when(
# replace empty scalars with NA
length(.) == 0L && .x %in% qstnr_item_keys$key[qstnr_item_keys$is_scalar] ~
.[NA],
# wrap vectors in list
.x %in% qstnr_item_keys$key[!qstnr_item_keys$is_scalar] ~
list(.),
~ .
)
}) |>
# add iterators
c(lvl = lvl,
i = i,
j = j)
# handle special cases
## 1: fill empty non-iterator multi-value keys with NA(s) if all other non-empty multi-value keys have the same length
lengths_multi_val_keys <-
qstnr_item_keys_multival %>%
magrittr::set_names(., .) |>
purrr::map_int(\(x) length(result[[x]][[1L]]))
positive_lengths_multi_val_keys <-
lengths_multi_val_keys %>%
magrittr::extract(. > 0L)
if (any(lengths_multi_val_keys == 0L) && length(unique(positive_lengths_multi_val_keys)) == 1L) {
for (k in (lengths_multi_val_keys %>%
magrittr::extract(. == 0L) |>
names())) {
result[[k]] <- list(rep(result[[k]][[1L]][NA],
times = length(result[[names(positive_lengths_multi_val_keys)[1L]]][[1L]])))
}
}
## 2: if no `question_common` is defined, fall back on
## a) `question_full.default`
## b) `question.default`
## c) `question_full`
## if either exists and actually differs from `question`
if (is.null(result$question_common)) {
question_common_fallback <-
item_map |>
pal::when("default" %in% names(.$question_full) ~
resolve_qstnr_val(x = item_map$question_full$default,
ballot_date = ballot_date,
canton = canton,
key = "question_full",
lvl = lvl,
i = i,
j = j),
"default" %in% names(.$question) ~
resolve_qstnr_val(x = item_map$question$default,
ballot_date = ballot_date,
canton = canton,
key = "question",
lvl = lvl,
i = i,
j = j),
~ result$question_full)
if (isTRUE(question_common_fallback != result$question)) {
result$question_common <- question_common_fallback
}
}
## 3: if no `variable_label_common` is defined, fall back on `variable_label.default` if it exists and actually differs from
## variable_label`
if (is.null(result$variable_label_common) && "default" %in% names(item_map$variable_label)) {
default_variable_label <- resolve_qstnr_val(x = item_map$variable_label$default,
ballot_date = ballot_date,
canton = canton,
key = "variable_label",
lvl = lvl,
i = i,
j = j)
if (isTRUE(default_variable_label != result$variable_label)) {
result$variable_label_common <- default_variable_label
}
}
## 4: add `who`-constraint to `variable_label` and `variable_label_common`
### get English `who` value
who_en <-
raw_qstnr$who |>
purrr::detect(\(x) result$who == cli::pluralize(x$value$de,
.null = NA_character_,
.trim = FALSE)) |>
purrr::chuck("value", "en") |>
cli::pluralize(.null = NA_character_,
.trim = FALSE)
### add who-constraints
if (!is.null(result$variable_label_common)
&& !is.na(result$variable_label_common)
&& !has_who_constraint(result$variable_label_common)) {
# ensure `who` doesn't vary over time
if (length(item_map$who) > 1L) {
cli::cli_abort(paste0("{.field who} of variable {.var {result$variable_name}} has changed over time. Thus an explicit {.field who}-",
"constraint has to be specified at the end of {.field variable_label_common} in the raw ",
"{.file questionnaire.toml}."),
.internal = TRUE)
}
result$variable_label_common %<>% add_who_constraint(who = who_en)
}
result$variable_label %<>% add_who_constraint(who = who_en)
}
# convert result to tibble
tibble::as_tibble(result)
}) |>
purrr::list_rbind()
}) |>
purrr::list_rbind()
}) |>
purrr::list_rbind()
}
#' Expand questionnaire tibble to long format
#'
#' Expands a [questionnaire tibble][gen_qstnr_tibble] to [long format](https://en.wikipedia.org/wiki/Wide_and_narrow_data).
#'
#' @param qstnr_tibble Questionnaire tibble as returned by [gen_qstnr_tibble()].
#'
#' @return `r pkgsnip::return_lbl("tibble")`
#' @family qstnr_gen
#' @keywords internal
expand_qstnr_tibble <- function(qstnr_tibble) {
# run integrity checks...
validate_qstnr_tibble(qstnr_tibble) |>
# ...expand questionnaire data to long format...
tidyr::unnest(cols = any_of(qstnr_item_keys_multival))
}
clean_qstnr_tibble <- function(qstnr_tibble) {
qstnr_tibble |>
# remove (single) placeholders
dplyr::mutate(dplyr::across(any_of(qstnr_item_keys_multival) & where(\(x) is.character(x[[1L]])),
\(col) purrr::map(col,
\(x) {
if (length(x) == 1L && isTRUE(stringr::str_detect(x, "^_.+_$"))) {
character()
} else {
x
}
}))) |>
# strip MD
dplyr::mutate(dplyr::across(where(is.character),
pal::strip_md),
dplyr::across(where(is.list) & where(\(x) is.character(x[[1L]])),
\(x) purrr::map(x,
pal::strip_md)))
}
validate_qstnr_tibble <- function(qstnr_tibble) {
# integrity check 1: ensure there are no duplicated topics, variable names and variable labels
c("topic",
"variable_name",
"variable_label") |>
purrr::walk(.f = function(var) {
is_dup <- duplicated(qstnr_tibble[[var]])
if (any(is_dup)) {
dup_indices <- which(is_dup)
for (i in dup_indices) {
dup_var <- qstnr_tibble[[var]][i]
cli::cli_alert_warning(paste0("{.var {var}} {.val {dup_var}} is included more than once in the questionnaire."))
}
}
})
# integrity check 2: ensure all multi-value columns have the same length or alternatively are empty, and if not, tell which ones don't
multi_val_var_lengths <-
qstnr_tibble |>
dplyr::mutate(dplyr::across(where(is.list),
lengths),
.keep = "none") |>
dplyr::rename_with(\(x) paste0("length_", x)) |>
dplyr::mutate(matches_length = length_variable_values == 0L | length_value_labels == 0L | length_variable_values == length_value_labels) |>
dplyr::mutate(matches_length =
matches_length & (length_response_options == 0L | length_value_labels == 0L | length_response_options == length_value_labels),
matches_length =
matches_length & (length_response_options == 0L | length_variable_values == 0L | length_response_options == length_variable_values))
i_violated <- which(!multi_val_var_lengths$matches_length)
if (length(i_violated)) {
multi_val_var_names <-
qstnr_tibble |>
dplyr::select(where(is.list)) |>
colnames()
# print affected variable names first since long error msg gets truncated
cli::cli({
cli::cli_alert_danger("Affected variable names:")
cli::cli_ul(items = qstnr_tibble$variable_name[i_violated])
})
bullets <-
qstnr_tibble$variable_name[i_violated] |>
purrr::map_chr(\(x) glue::glue("{{.var {x}}}")) |>
rlang::set_names("*")
cli::cli_abort(c("The number of {.var {multi_val_var_names}} differs for the following variable names:",
bullets),
.internal = TRUE)
}
invisible(qstnr_tibble)
}
has_who_constraint <- function(x) {
isTRUE(stringr::str_detect(string = x,
pattern = "(\\(|; )(\\d{4}-\\d{2}-\\d{2} )?only [^\\)]+\\)$"))
}
add_who_constraint <- function(x,
who) {
if (who != "all") {
result <- who %>% pal::when(endsWith(x, ")") ~ stringr::str_replace(string = x,
pattern = "\\)$",
replacement = paste0("; only *", ., "*)")),
~ paste0(x, " (only *", ., "*)"))
} else {
result <- x
}
result
}
#' Generate Markdown questionnaire
#'
#' @inheritParams expand_qstnr_tibble
#' @param incl_title Whether or not to generate an `<h1>` questionnaire title at the beginning of the document. If the result is intended to be fed to Pandoc,
#' it's recommended to set this to `FALSE` and provide the title via [Pandoc's `--metadata` option](https://pandoc.org/MANUAL.html#option--metadata) instead.
#'
#' @return A character vector.
#' @family qstnr_gen
#' @keywords internal
gen_qstnr_md <- function(qstnr_tibble,
incl_title = FALSE) {
# ensure we have a single ballot date and canton
ballot_date <-
qstnr_tibble |>
dplyr::pull("ballot_date") |>
unique() |>
as_ballot_date()
canton <-
qstnr_tibble |>
dplyr::pull("canton") |>
unique() |>
rlang::arg_match0(values = all_cantons,
arg_nm = "unique(qstnr_tibble$canton)")
pal::cli_progress_step_quick(msg = "Generating {.val {canton}} @ {.val {ballot_date}} Markdown questionnaire")
block_lines <-
qstnr_tibble |>
# add block title and across-block item enumerator base/group
dplyr::mutate(enumerator_base =
block |>
purrr::map_int(\(x) raw_qstnr |> purrr::pluck(x, "prefix",
.default = 0L))) |>
# add across-block item enumerator
dplyr::group_by(enumerator_base) |>
dplyr::group_modify(\(d, k) tibble::rowid_to_column(d,
var = "enumerator")) |>
dplyr::ungroup() |>
dplyr::mutate(enumerator = enumerator_base + enumerator) |>
dplyr::arrange(block, enumerator) |>
# iterate over every block and generate block header plus table
dplyr::group_by(block) |>
dplyr::group_map(~ {
block <- .y$block
block_nr <- block_name_to_nr(block)
block_title <- raw_qstnr |> purrr::chuck(block, "title")
block_intro <-
raw_qstnr |>
purrr::pluck(block, "intro") |>
raw_pick_right(key = "intro",
ballot_date = ballot_date,
canton = canton) |>
cli::pluralize(.trim = FALSE)
c(glue::glue("## Block {block_nr}: {block_title}",
.null = NA_character_),
"",
block_intro,
""[length(block_intro)],
qstnr_md_table_header,
qstnr_md_table_body(qstnr_tibble_block = .x,
block = block),
"",
"")
}) |>
purrr::list_c(ptype = character())
# add title, technical notes, introduction, footnotes and link references
title <- glue::glue("# FOKUS-{ stringr::str_to_sentence(canton) }-Fragebogen f\u00fcr den ",
ballot_title(ballot_date = ballot_date,
canton = canton),
"\n",
.trim = FALSE)
technical_notes <-
raw_qstnr$who |>
# assemble who lines
purrr::map_depth(.depth = 1L,
.f = function(who_map) {
lvl <- resolve_qstnr_val(x = who_map$lvl %||% "",
ballot_date = ballot_date,
canton = canton,
key = "lvl",
lvl = "",
i = 1L,
j = 1L)
i <- resolve_qstnr_val(x = who_map$i %||% 1L,
ballot_date = ballot_date,
canton = canton,
key = "i",
lvl = "",
i = 1L,
j = 1L)
j <- resolve_qstnr_val(x = who_map$j %||% 1L,
ballot_date = ballot_date,
canton = canton,
key = "j",
lvl = "",
i = 1L,
j = 1L)
value <-
lvl |>
purrr::map(\(lvl) {
i |>
purrr::map(lvl = lvl,
\(lvl, i) {
j |>
purrr::map_chr(lvl = lvl,
i = i,
.f = \(lvl, i, j) {
glue::glue(who_map$value$de,
.null = NA_character_,
.trim = FALSE)
})
})
}) |>
purrr::flatten() |>
purrr::list_c(ptype = character())
description <-
lvl |>
purrr::map(\(lvl) {
i |>
purrr::map(lvl = lvl,
\(lvl, i) {
j |>
purrr::map_chr(lvl = lvl,
i = i,
.f = \(lvl, i, j) {
# hack to support desc variation @ 2024-10-20
if ("de" %in% names(who_map$description)) {
result <- glue::glue(who_map$description$de,
.null = NA_character_,
.trim = FALSE)
} else {
result <-
who_map$description[[stringr::str_remove_all(ballot_date, "-")]] %||%
who_map$description$default |>
purrr::chuck("de") |>
glue::glue(.null = NA_character_,
.trim = FALSE)
}
result
})
})
}) |>
purrr::flatten() |>
purrr::list_c(ptype = character())
# reduce to who's that actually occur in data
ix_keep <- which(value %in% qstnr_tibble$who)
value %<>% magrittr::extract(ix_keep)
description %<>% magrittr::extract(ix_keep)
glue::glue("- { md_emphasize(value) }: { description }")
}) |>
purrr::list_c(ptype = character()) %>%
c("## Technische Vorbemerkungen",
"",
"### `Wer`",
"",
"Die Spalte `Wer` dient dem Fragebogen-Routing. Sie kennt folgende Werte:",
"",
.,
"",
md_snippets$qstnr_technical_notes_multiple_responses,
md_snippets$qstnr_technical_notes_free_text_fields)
footnotes <-
raw_qstnr$footnote |>
# reduce to footnotes that actually occur in table body
purrr::keep(~ any(stringr::str_detect(block_lines, glue::glue("\\[\\^{.x$id}\\]",
.null = NA_character_)))) |>
purrr::map(~ c(glue::glue("[^{.x$id}]: ", glue::glue(.x$text,
.null = NA_character_,
.trim = FALSE),
.null = NA_character_,
.trim = FALSE),
"")) |>
purrr::list_c(ptype = character())
link_refs <-
raw_qstnr$link |>
# reduce to link references that actually occur in table body
purrr::keep(~ any(stringr::str_detect(block_lines, glue::glue("\\[[^]]+\\]\\[{.x$id}\\]",
.null = NA_character_)))) |>
purrr::map(~ c(glue::glue("[{.x$id}]: {.x$url}",
.null = NA_character_),
"")) |>
purrr::list_c(ptype = character())
c(title[incl_title],
technical_notes,
paste0(glue::glue(md_snippets$qstnr_introduction,
.envir = rlang::current_env(),
.na = NULL,
.null = NA_character_,
.trim = TRUE),
"\n"),
block_lines,
footnotes,
link_refs)
}
qstnr_md_table_body <- function(qstnr_tibble_block,
block) {
qstnr_tibble_block |>
# replace logicals by German ja/nein
dplyr::mutate(dplyr::across(where(is.logical),
\(x) ifelse(x,
"ja",
"nein"))) |>
purrr::pmap_chr(function(ballot_date,
canton,
enumerator,
variable_name,
topic,
who,
question_intro_i,
question_intro_j,
question,
allow_multiple_answers,
variable_label,
response_options,
variable_values,
value_labels,
randomize_response_options,
is_mandatory,
i,
j,
...) {
paste(enumerator,
tidyr::replace_na(topic,
"-"),
who,
question |> pal::when(is.na(.) ~ "-",
~ c(c(question_intro_i[isTRUE(i == 1L && j %in% c(1L, NA_integer_))],
question_intro_j[isTRUE(j == 1L)]) %>%
magrittr::extract(!is.na(.)) |>
pal::as_str(),
.) %>%
magrittr::extract(!is.na(.)) |>
pal::as_str(sep = " <br><br>")),
allow_multiple_answers,
pal::wrap_chr(variable_name,
wrap = "`"),
shorten_var_names(var_names = variable_name,
max_n_char = ifelse(block %in% c("x_polling_agency", "y_generated", "z_generated")
|| stringr::str_detect(string = variable_name,
pattern = paste0("^", pal::fuse_regex(c("agreement_contra_argument_",
"information_source_",
"reason_non_participation_",
"political_occasions_")))),
32L,
30L)) |>
pal::wrap_chr("`"),
variable_label,
response_options |>
pal::when(is_skill_question_var(variable_name) ~
format_md_multival_col(x = .,
collapse_break = FALSE) |>
md_emphasize(which = skill_question_answer_nr(ballot_date = ballot_date,
lvl = var_lvls(var_names = variable_name),
canton = canton,
proposal_nr =
var_proposal_nr(variable_name) |>
pal::when(is.na(.) ~ NULL,
~ .),
skill_question_nr = var_skill_question_nr(variable_name))) |>
collapse_break(),
~ format_md_multival_col(.)),
format_md_multival_col(variable_values),
format_md_multival_col(value_labels),
randomize_response_options,
is_mandatory,
sep = " | ")
})
}
format_md_multival_col <- function(x,
collapse_break = TRUE) {
result <- x
if (all(is.na(x))) {
result <- "-"
} else if (length(x) > 1L
&& all(stringr::str_detect(string = x,
pattern = "^_.+_$",
negate = TRUE),
na.rm = TRUE)) {
result <-
x |>
pal::wrap_chr(wrap = "`") |>
pal::when(collapse_break ~ collapse_break(.),
~ .)
}
result
}
block_name_to_nr <- function(x) {
x |>
stringr::str_extract("^.[^_]?(_\\d)?") |>
stringr::str_replace("_(\\d)", "-\\1") |>
stringr::str_remove("^0") |>
stringr::str_to_upper()
}
#' Extract questionnaire item field value
#'
#' Extracts a single questionnaire item field value for every first- for every second-level iterator from the [raw FOKUS questionnaire data][raw_qstnr] based on
#' the specified variable name and branch path.
#'
#' When used in questionnaire item field values via [string interpolation][glue::glue], be careful to not create infinite loops via circular references.
#'
#' @inheritParams ballot_title
#' @param var_name Variable name, without resolved string interpolation, i.e. 1:1 as stated in the raw FOKUS questionnaire data. A character scalar.
#' @param branch_path Sequence of questionnaire table levels that lead to the `item` leaf node where `var_name` is defined. A character vector.
#' @param key Questionnaire item key. One of
#' `r pal::as_md_val_list(qstnr_item_keys$key)`
#' @param lvl Political-level loop iterator that can be referred to in field value via [string interpolation][glue::glue]. A character vector.
#' @param i Second-level loop iterator that can be referred to in field value via [string interpolation][glue::glue]. An integerish vector.
#' @param j Third-level loop iterator that can be referred to in field value via [string interpolation][glue::glue]. An integerish vector.
#'
#' @return A vector of the resolved item field values. Type and length of resolved values depend on `key`.
#' @family qstnr_internal
#' @keywords internal
#'
#' @examples
#' fokus:::qstnr_item_val(ballot_date = "2018-09-23",
#' canton = "aargau",
#' branch_path = c("03_proposal_specific", "004_participants"),
#' var_name = "appeal_federal_proposals",
#' key = "include")
#'
#' fokus:::qstnr_item_val(
#' ballot_date = "2019-10-20",
#' canton = "aargau",
#' branch_path = c("02_political_start", "002_non_participation",
#' "003_election", "z01_hypothetical"),
#' var_name = "hypothetical_voting_decision_federal_majoritarian_election_{i}_seat_{j}",
#' key = "topic",
#' i = 1:2,
#' j = 1:3
#' )
qstnr_item_val <- function(ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date),
branch_path,
var_name,
key = qstnr_item_keys$key,
lvl = "?",
i = NA_integer_,
j = NA_integer_) {
ballot_date %<>% as_ballot_date_chr()
canton <- rlang::arg_match(arg = canton,
values = cantons(ballot_date))
checkmate::assert_character(branch_path,
any.missing = FALSE,
min.len = 1L)
if ("item" %in% branch_path) {
cli::cli_abort("{.arg branch_path} must be specified {.emph without} the {.val item} leaf node.")
}
checkmate::assert_string(var_name)
key <- rlang::arg_match(key)
item_map <-
raw_qstnr |>
purrr::chuck(!!!branch_path, "item") |>
purrr::keep(.p = \(x) isTRUE(x$variable_name == var_name)) |>
purrr::list_flatten()
if (!rlang::has_name(x = item_map, name = "variable_name")) {
cli::cli_abort(paste0("There is no item with {.arg variable_name} {.val {var_name}} defined under the questionnaire branch path {.field ",
cli::ansi_collapse(x = branch_path,
x = branch_path,
sep = " -> ",
sep2 = " -> ",
last = " -> "),
"}."))
}
# traverse questionnaire branch path and complement heritable map
parent_map <- init_heritable_map(block = branch_path[1L])
for (branch_depth in purrr::accumulate(branch_path, c)) {
parent_map <-
raw_qstnr |>
purrr::chuck(!!!branch_depth) %>%
purrr::list_modify(.x = parent_map,
!!!.) |>
purrr::keep_at(at = qstnr_item_keys$key)
}
# evaluate requested item value
raw_val <-
parent_map |>
purrr::list_modify(!!!item_map) |>
purrr::chuck(key)
lvl |>
purrr::map(function(lvl) {
i |>
purrr::map(lvl = lvl,
.f = function(i,
lvl) {
j |>
purrr::map(i = i,
lvl = lvl,
.f = function(j,
i,
lvl) {
resolve_qstnr_val(x = raw_val,
ballot_date = ballot_date,
canton = canton,
key = key,
lvl = lvl,
i = i,
j = j)
})
})
}) |>
unlist()
}
#' Get political parties
#'
#' Returns a tibble of ballot-date-specific political party metadata defined in the [raw FOKUS questionnaire data][raw_qstnr].
#'
#' @return `r pkgsnip::return_lbl("tibble")`
#' @family qstnr_internal
#' @keywords internal
qstnr_parties <- function(ballot_date = pal::pkg_config_val("ballot_date")) {
ballot_date <- as_ballot_date(ballot_date)
raw_qstnr |>
purrr::chuck("party") |>
purrr::map(~ {
tibble::tibble_row(code = .x$code,
de.long = .x$de$long,
de.short = .x$de$short,
en.short = .x$en$short %||% .x$de$short,
date_begin = .x$date_begin %||% clock::date_build(year = 1970L,
month = 1L,
day = 1L),
date_end = .x$date_end %||% as.Date(Inf))
}) |>
purrr::list_rbind() |>
dplyr::filter(date_begin <= ballot_date & date_end >= ballot_date)
}
#' Get response option codes
#'
#' Extracts response option codes of the specified type(s) from the [raw FOKUS questionnaire data][raw_qstnr].
#'
#' Note that only codes of recurring response options are returned which are defined under the `response_options` top-level key in the file
#' `data-raw/questionnaire/questionnaire.toml`.
#'
#' @param types Response option types. One or more of
#' `r pal::as_md_val_list(all_response_option_types)`
#'
#' @return An integer vector.
#' @family qstnr_internal
#' @keywords internal
#'
#' @examples
#' fokus:::qstnr_response_option_codes(types = "abstain")
#'
#' fokus:::qstnr_response_option_codes(types = c("dunno",
#' "custom",
#' "abstain"))
qstnr_response_option_codes <- function(types = all_response_option_types) {
types <- rlang::arg_match(arg = types,
multiple = TRUE)
types |> purrr::map_int(\(x) raw_qstnr |> purrr::chuck("response_options", x, "code"))
}
#' Determine questionnaire data's value label column
#'
#' Determines the [questionnaire data][qstnrs] column that holds variable value labels, returned as a [symbol][as.symbol].
#'
#' The questionnaire data column that holds variable value labels is language-dependent. While the column
#' `r qstnr_lbl_col_sym("de") |> as.character() |> pal::wrap_chr("\x60")` holds the German value labels, their English counterpars are found in the column
#' `r qstnr_lbl_col_sym("en") |> as.character() |> pal::wrap_chr("\x60")`. `qstnr_lbl_col_sym()` is intended to ease language-agnostic questionnaire data
#' programming.
#'
#' @inheritParams lang_to_locale
#'
#' @return `r pkgsnip::return_lbl("sym")`
#' @family qstnr_predicate
#' @keywords internal
#'
#' @examples
#' fokus:::qstnr_lbl_col_sym(lang = "de")
#'
#' fokus:::qstnr_lbl_col_sym(lang = "en")
qstnr_lbl_col_sym <- function(lang = pal::pkg_config_val("lang")) {
lang <- rlang::arg_match(arg = lang,
values = all_langs)
as.symbol(ifelse(lang == "de",
"response_options",
"value_labels"))
}
#' Print structure of the private FOKUS repository
#'
#' Returns a textual representation of the structure of the private FOKUS repository, formatted as a Markdown [fenced code
#' block](https://pandoc.org/MANUAL.html#extension-fenced_code_blocks).
#'
#' @includeRmd data-raw/snippets/fokus_private_description.Rmd
#'
#' @return A character scalar.
#' @family private
#' @keywords internal
print_private_repo_structure <- function() {
pal::cat_lines("``` default")
fokus_private_structure |> pal::flatten_path_tree() |> pal::draw_path_tree()
pal::cat_lines("```",
"",
"The following placeholders are used in the schema above:",
"",
"- `...` for further files and/or folders",
"- `*` for a variable character sequence",
"- `#` for a count starting with `1`",
"- `{canton}` for the name of the FOKUS-covered canton (in lower case), e.g. `aargau`",
"- `{ballot_date}` for the FOKUS-covered ballot date (in the format `YYYY-MM-DD`), e.g. `2018-09-23`",
paste0("- `{date_delivery_statistical_office}` for the delivery date of the voting register data provided by the cantonal statistical ",
"office (in the format `YYYY-MM-DD`), e.g. `2019-09-11`"))
}
private_file_hash <- function(path,
auth_token = pal::pkg_config_val("token_repo_private")) {
req_private_file(path = path,
method = "HEAD",
auth_token = auth_token) |>
httr2::req_perform() |>
httr2::resp_header(header = "X-Gitlab-Content-Sha256")
}
req_private_file <- function(path,
method,
max_tries = 3L,
auth_token = pal::pkg_config_val("token_repo_private")) {
checkmate::assert_string(auth_token)
httr2::request(base_url = glue::glue("https://gitlab.com/api/v4/projects/{repo_private_proj_id}/repository/files/", utils::URLencode(path,
reserved = TRUE))) |>
httr2::req_url_query(ref = repo_private_default_branch) |>
httr2::req_method(method = method) |>
httr2::req_headers(`PRIVATE-TOKEN` = auth_token,
.redact = "PRIVATE-TOKEN") |>
httr2::req_retry(max_tries = max_tries) |>
httr2::req_error(body = \(resp) {
if (httr2::resp_has_body(resp)) {
return(httr2::resp_body_json(resp)$message)
}
NULL
})
}
#' Assemble private FOKUS repository URL
#'
#' @param ... Optional path components added to the base URL.
#' @param .branch Git branch name to use in URL. A character scalar. Only relevant if `...` is non-empty.
#'
#' @return A character scalar.
#' @family private
#' @keywords internal
#'
#' @examples
#' fokus:::url_repo_private("generated")
url_repo_private <- function(...,
.branch = repo_private_default_branch) {
result <- "https://gitlab.com/zdaarau/private/fokus_private/"
if (...length() > 0L) {
checkmate::assert_string(.branch)
result <- paste0(result, fs::path("-/tree", .branch, ..., "?ref_type=heads"))
}
result
}
assert_countish <- function(x,
positive = TRUE,
null_ok = FALSE) {
if (null_ok && is.null(x)) {
x
} else {
checkmate::assert_count(as.integer(x),
positive = positive)
}
}
assert_integerish <- function(x,
lower = -Inf,
upper = Inf,
any_missing = FALSE,
all_missing = FALSE,
null_ok = FALSE) {
if (null_ok && is.null(x)) {
x
} else {
checkmate::assert_integerish(as.integer(x),
lower = lower,
upper = upper,
any.missing = any_missing,
all.missing = all_missing,
coerce = TRUE)
}
}
assert_var_names_present <- function(data,
var_names) {
checkmate::assert_data_frame(data)
var_names |> purrr::walk(\(var_name) {
if (!(var_name %in% colnames(data))) {
cli::cli_abort("{.arg data} must contain a column {.var {var_name}}")
}
})
invisible(data)
}
assert_var_names_valid <- function(var_names,
as_scalar = FALSE,
null_ok = FALSE) {
checkmate::assert_flag(as_scalar)
if (as_scalar) {
checkmate::assert_choice(var_names,
choices = unique(fokus::qstnrs$variable_name),
null.ok = null_ok)
} else {
purrr::map_chr(var_names,
checkmate::assert_choice,
choices = unique(fokus::qstnrs$variable_name),
null.ok = null_ok)
}
}
#' Abbreviations used in the **fokus** package
#'
#' Returns a [tibble][tibble::tbl_df] listing an opinionated set of abbreviations used in the \R code and documentation of the **fokus** package.
#'
#' @inheritParams pkgsnip::abbrs
#'
#' @return `r pkgsnip::param_lbl("tibble")`
#' @keywords internal
abbrs <- function(unnest = FALSE) {
rlang::check_installed("pkgsnip",
reason = pal::reason_pkg_required())
tibble::tibble(full_expressions = list("google"),
abbreviation = "g") |>
dplyr::bind_rows(pkgsnip::abbrs()) |>
dplyr::arrange(purrr::map_chr(full_expressions,
\(x) stringr::str_to_lower(dplyr::first(x)))) |>
pal::when(unnest ~ tidyr::unnest_longer(data = .,
col = full_expressions,
values_to = "full_expression"),
~ .)
}
as_ballot_date <- function(ballot_date,
error_call = rlang::caller_env()) {
result <- ballot_date[1L]
if (lubridate::is.Date(result)) {
if (!(result %in% all_ballot_dates)) {
cli::cli_abort("{.arg ballot_date} must be one of {.val all_ballot_dates}, not {.val {ballot_date}}.")
}
} else {
result %<>%
as_ballot_date_chr(error_call = error_call) %>%
clock::date_parse()
}
result
}
as_ballot_date_chr <- function(ballot_date,
error_call = rlang::caller_env()) {
checkmate::assert_atomic(ballot_date,
len = 1L)
rlang::arg_match0(arg = as.character(ballot_date),
values = as.character(all_ballot_dates),
arg_nm = "ballot_date",
error_call = error_call)
}
as_ballot_dates <- function(ballot_dates,
error_call = rlang::caller_env()) {
ballot_dates |>
purrr::map(\(x) as_ballot_date(ballot_date = x,
error_call = error_call)) |>
purrr::list_c(ptype = as.Date(NULL))
}
as_flat_list <- function(x) {
result <- x
depth <- purrr::pluck_depth(result)
# unlist until only a single list level remains
while (depth > 2L) {
result %<>% unlist(recursive = FALSE)
depth <- purrr::pluck_depth(result)
}
# wrap in list if necessary
if (depth < 2L && !is.list(x)) {
result <- list(x)
}
result
}
as_sym_part_regex <- function(x) {
paste0(sym_part_regex_start, x, sym_part_regex_end)
}
collapse_break <- function(s) {
paste0(s, collapse = "<br>")
}
#' Convert language code to country-specific locale ID
#'
#' Converts a language code as used in many of this package's functions to a country-specific locale identifier.
#'
#' @param lang Language. One of `r all_langs |> pal::as_md_vals() |> pal::enum_str(sep2 = " or ")`.
#'
#' @return A character scalar.
#' @keywords internal
#'
#' @examples
#' fokus:::lang_to_locale("de")
#' fokus:::lang_to_locale("en")
lang_to_locale <- function(lang = pal::pkg_config_val("lang")) {
lang <- rlang::arg_match(arg = lang,
values = all_langs)
switch(lang,
de = "de-CH",
en = "en-US")
}
var_predicate <- function(predicate,
var_name,
ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date)) {
predicate <- rlang::arg_match(predicate,
values = setdiff(colnames(fokus::qstnrs),
c("ballot_date",
"canton",
"variable_name")))
assert_var_names_valid(var_names = var_name,
as_scalar = TRUE)
ballot_date %<>% as_ballot_date()
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
fokus::qstnrs |>
dplyr::filter(canton == !!canton
& ballot_date == !!ballot_date
& variable_name == !!var_name) %$%
eval(as.symbol(predicate)) |>
unique()
}
wrap_backtick <- function(x) {
dplyr::if_else(x == "-" | stringr::str_detect(string = x,
pattern = "^(_.*_|\\*.*\\*)$"),
as.character(x),
paste0("`", x, "`"))
}
this_pkg <- utils::packageName()
cli_theme <-
cli::builtin_theme() |>
purrr::list_modify(h2 = list("margin-bottom" = 0.0),
h3 = list("margin-top" = 0.0))
global_max_cache_age <- "30 days"
#' Questionnaire item keys
#'
#' A tibble of item keys supported in the [raw FOKUS questionnaire data][raw_qstnr].
#'
#' @format `r pkgsnip::return_lbl("tibble")`
#' @docType data
#' @family metadata
#' @keywords internal
#'
#' @name qstnr_item_keys
#'
#' @examples
#' fokus:::qstnr_item_keys
NULL
qstnr_md_table_header <-
tibble::tribble(
~name, ~width, ~alignment,
"\\#", 2L, "left",
"Thema", 5L, "left",
"Wer", 3L, "left",
"Frage", 15L, "left",
"Mehrfachnennungen", 3L, "left",
"Variablenname", 5L, "left",
"Variablenname (gek\u00fcrzt auf 32 Zeichen)", 5L, "left",
"Variablenlabel", 15L, "left",
"Antwortoptionen", 5L, "left",
"Variablenauspr\u00e4gungen", 5L, "left",
"Auspr\u00e4gungslabels", 5L, "left",
"Antwortoptionen in Zufallsreihenfolge", 3L, "left",
"Antwort obligatorisch", 3L, "left"
) |>
dplyr::mutate(sep = purrr::map2_chr(.x = width,
.y = alignment,
.f = ~
rep(x = "-",
times = .x) |>
paste0(collapse = "") |>
pal::when(.y == "left" ~ stringr::str_replace(string = .,
pattern = "^.",
replacement = ":"),
.y == "right" ~ stringr::str_replace(string = .,
pattern = ".$",
replacement = ":"),
.y == "center" ~ stringr::str_replace_all(string = .,
pattern = "(^.|.$)",
replacement = ":"),
~ .))) %$%
c(paste0(name, collapse = " | "),
paste0(sep, collapse = " | "))
repo_private_proj_id <- 21325371L
sym_part_regex_start <- "(^|_|\\b)"
sym_part_regex_end <- "(\\b|_|$)"
unicode_checkmark <- "\u2705"
unicode_crossmark <- "\u274C"
unicode_ellipsis <- "\u2026"
url_qstnr <- list(aargau = "https://qstnr.fokus.ag")
url_survey_host <- list(aargau = "https://umfrage.fokus.ag")
url_parameter_survey <- list(aargau = "pw")
#' FOKUS-covered ballot dates
#'
#' A vector of ballot dates covered by FOKUS surveys up until `r max(all_ballot_dates)`.
#'
#' @format `r pkgsnip::return_lbl("dates")`
#' @family metadata
#' @export
#'
#' @examples
#' fokus::all_ballot_dates
"all_ballot_dates"
#' FOKUS-covered cantons
#'
#' A vector of all [cantons][cantons] covered by FOKUS surveys.
#'
#' @format A character vector.
#' @family metadata
#' @export
#'
#' @examples
#' fokus::all_cantons
"all_cantons"
#' Ballot types
#'
#' A vector of all possible [ballot types][ballot_types].
#'
#' @format A character vector.
#' @family metadata
#' @export
#'
#' @examples
#' fokus::all_ballot_types
"all_ballot_types"
#' Political levels
#'
#' A vector of all possible [political levels][lvls].
#'
#' @format A character vector.
#' @family metadata
#' @export
#'
#' @examples
#' fokus::all_lvls
"all_lvls"
#' Election procedures
#'
#' A vector of all possible [election procedures][prcds].
#'
#' @format A character vector.
#' @family metadata
#' @export
#'
#' @examples
#' fokus::all_prcds
"all_prcds"
#' Referendum proposal types
#'
#' A vector of all possible [referendum proposal types][proposal_type].
#'
#' @format A character vector.
#' @family metadata
#' @export
#'
#' @examples
#' fokus::all_proposal_types
"all_proposal_types"
#' Standardized referendum proposal question groups
#'
#' A vector of all possible referendum [proposal question groups][proposal_qstn_groups].
#'
#' @format A character vector.
#' @family metadata
#' @export
#'
#' @examples
#' fokus::all_qstn_groups
"all_qstn_groups"
#' Referendum proposal argument sides
#'
#' A vector of all possible referendum proposal argument sides.
#'
#' @format A character vector.
#' @family metadata
#' @export
#'
#' @examples
#' fokus::all_argument_sides
"all_argument_sides"
#' Referendum proposal main motive types
#'
#' A vector of all possible referendum proposal main motive types.
#'
#' @format A character vector.
#' @family metadata
#' @export
#'
#' @examples
#' fokus::all_main_motive_types
"all_main_motive_types"
#' Election seat types
#'
#' A vector of all possible election seat types.
#'
#' @format A character vector.
#' @family metadata
#' @export
#'
#' @examples
#' fokus::all_election_seat_types
"all_election_seat_types"
#' Response option types
#'
#' A vector of all possible response option types defined in the [raw FOKUS questionnaire data][raw_qstnr].
#'
#' @format A character vector.
#' @family metadata
#' @export
#'
#' @examples
#' fokus::all_response_option_types
"all_response_option_types"
#' Postal dispatch types
#'
#' A vector of all possible postal dispatch types.
#'
#' @format A character vector.
#' @family metadata
#' @export
#'
#' @examples
#' fokus::all_postal_dispatch_types
"all_postal_dispatch_types"
#' Postal dispatch ways
#'
#' A vector of all possible [postal dispatch ways][postal_dispatch_way].
#'
#' @format A character vector.
#' @family metadata
#' @export
#'
#' @examples
#' fokus::all_postal_dispatch_ways
"all_postal_dispatch_ways"
#' Languages
#'
#' A vector of all possible survey metadata languages.
#'
#' @format A character vector.
#' @family metadata
#' @export
#'
#' @examples
#' fokus::all_langs
"all_langs"
#' Name types
#'
#' A vector of all possible name types for various entities.
#'
#' @format A character vector.
#' @family metadata
#' @export
#'
#' @examples
#' fokus::all_name_types
"all_name_types"
#' Get cantons covered by FOKUS survey
#'
#' Determines the cantons covered by the FOKUS survey at the specified ballot date.
#'
#' @param ballot_date FOKUS-covered ballot date. One of
#' `r pal::as_md_val_list(as.character(all_ballot_dates))`
#'
#' @return A character vector.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::cantons(ballot_date = "2018-09-23")
#'
#' # determine all cantons covered per ballot date
#' library(magrittr)
#'
#' fokus::all_ballot_dates %>%
#' magrittr::set_names(., .) |>
#' purrr::map_chr(fokus::cantons)
cantons <- function(ballot_date = pal::pkg_config_val("ballot_date")) {
ballot_date %<>% as_ballot_date_chr()
cantons_at[[ballot_date]]
}
#' Get ballot's political levels
#'
#' Determines the [political levels][all_lvls] covered by the FOKUS survey for the specified canton of the specified ballot type at the specified ballot date.
#'
#' @inheritParams cantons
#' @param canton FOKUS-covered canton name. One of
#' `r pal::as_md_val_list(all_cantons)`
#' @param ballot_type Ballot type. One of `r all_ballot_types |> pal::as_md_vals() |> cli::ansi_collapse(sep2 = " or ", last = " or ")`.
#' @param prcds Election procedure(s). One or more of `r all_prcds |> pal::as_md_vals() |> cli::ansi_collapse(sep2 = " and ", last = " and ")`.
#'
#' Only relevant if `ballot_type = "election"`.
#'
#' @return A character vector.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::lvls(ballot_date = "2018-09-23",
#' canton = "aargau")
#'
#' fokus::lvls(ballot_date = "2018-09-23",
#' canton = "aargau",
#' ballot_type = "election")
#'
#' fokus::lvls(ballot_date = "2019-10-20",
#' canton = "aargau",
#' ballot_type = "election")
#'
#' fokus::lvls(ballot_date = "2019-10-20",
#' canton = "aargau",
#' ballot_type = "election",
#' prcds = "proportional")
lvls <- function(ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date),
ballot_type = ballot_types(ballot_date = ballot_date,
canton = canton),
prcds = all_prcds) {
ballot_date %<>% as_ballot_date()
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
ballot_type <- rlang::arg_match(arg = ballot_type,
values = all_ballot_types)
prcds <- unique(rlang::arg_match(arg = prcds,
multiple = TRUE))
raw <- raw_qstnr_suppl(ballot_date = ballot_date)
if (ballot_type == "referendum") {
result <- c("cantonal"[length(raw$cantonal[[canton]]$proposal) > 0L],
"federal"[length(raw$federal$proposal) > 0L])
} else {
result <- c("cantonal"[any(purrr::map_lgl(prcds,
~ length(raw$cantonal[[canton]]$election[[.x]]) > 0L))],
"federal"[any(purrr::map_lgl(prcds,
~ length(raw$federal[[canton]]$election[[.x]]) > 0L))])
}
result
}
#' Get ballot types
#'
#' Determines the [types of the ballot][all_ballot_types] covered by the FOKUS survey for the specified canton on the specified political level(s) at the
#' specified ballot date.
#'
#' @inheritParams lvls
#' @param lvls Political level(s). One or more of `r all_lvls |> pal::as_md_vals() |> cli::ansi_collapse(sep2 = " and ", last = " and ")`.
#'
#' @return A character vector.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::ballot_types(ballot_date = "2018-09-23",
#' canton = "aargau")
#'
#' # in case of no ballot types at lvl, an empty character vector is returned
#' fokus::ballot_types(ballot_date = "2020-10-18",
#' lvls = "federal",
#' canton = "aargau")
ballot_types <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvls = all_lvls,
canton = cantons(ballot_date)) {
lvls <- rlang::arg_match(arg = lvls,
multiple = TRUE)
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
result <- character()
raw <- raw_qstnr_suppl(ballot_date = ballot_date)
if ("federal" %in% lvls) {
result <- c("referendum"[length(raw$federal$proposal) > 0L],
"election"[any(purrr::map_lgl(all_prcds,
~ length(raw$federal[[canton]]$election[[.x]]) > 0L))])
}
if ("cantonal" %in% lvls) {
result %<>% c("referendum"[length(raw$cantonal[[canton]]$proposal) > 0L],
"election"[any(purrr::map_lgl(all_prcds,
~ length(raw$cantonal[[canton]]$election[[.x]]) > 0L))])
}
unique(result)
}
#' Get ballot's election procedures
#'
#' Determines the [election procedures][all_prcds] covered by the FOKUS survey for the specified canton on the specified political level at the specified ballot
#' date.
#'
#' @inheritParams lvls
#' @param lvl Political level. One of `r all_lvls |> pal::as_md_vals() |> cli::ansi_collapse(sep2 = " or ", last = " or ")`.
#'
#' @return A character vector.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::prcds(ballot_date = "2019-10-20",
#' lvl = "cantonal",
#' canton = "aargau")
#'
#' fokus::prcds(ballot_date = "2019-10-20",
#' lvl = "federal",
#' canton = "aargau")
#'
#' # in case of no elections, an empty character vector is returned
#' fokus::prcds(ballot_date = "2020-10-18",
#' lvl = "federal",
#' canton = "aargau")
prcds <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "election"),
canton = cantons(ballot_date)) {
if (length(lvl)) {
lvl <- rlang::arg_match(arg = lvl,
values = all_lvls)
}
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
result <- character()
raw <- raw_qstnr_suppl(ballot_date = ballot_date)
if (isTRUE(lvl == "federal")) {
result <-
names(raw$federal[[canton]]$election) |>
intersect(all_prcds) |>
as.character()
} else if (isTRUE(lvl == "cantonal")) {
result <-
names(raw$cantonal[[canton]]$election) |>
intersect(all_prcds) |>
as.character()
}
result
}
#' Get ballot's referendum proposal numbers
#'
#' Determines the referendum proposal numbers covered by the FOKUS survey for the specified canton at the specified ballot date on the specified political
#' level.
#'
#' @inheritParams prcds
#' @param canton FOKUS-covered canton name. One of
#' `r pal::as_md_val_list(all_cantons)`
#'
#' Only relevant if `lvl = "cantonal"`.
#'
#' @return An integer vector.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::proposal_nrs(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau")
#'
#' fokus::proposal_nrs(ballot_date = "2018-09-23",
#' lvl = "federal",
#' canton = "aargau")
#'
#' # in case of no referendum proposals, an empty integer vector is returned
#' fokus::proposal_nrs(ballot_date = "2019-10-20",
#' lvl = "cantonal",
#' canton = "aargau")
proposal_nrs <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "referendum"),
canton = cantons(ballot_date)) {
if (length(lvl) > 0L) {
lvl <- rlang::arg_match(arg = lvl,
values = all_lvls)
}
result <- integer()
raw <- raw_qstnr_suppl(ballot_date = ballot_date)
if (isTRUE(lvl == "federal")) {
result <-
raw$federal$proposal |>
names() |>
as.integer()
} else if (isTRUE(lvl == "cantonal")) {
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
result <-
raw$cantonal[[canton]]$proposal |>
names() |>
as.integer()
}
result
}
#' Get ballot's election numbers
#'
#' Determines the election numbers covered by the FOKUS survey for the specified canton at the specified ballot date on the specified political level and of the
#' specified election procedure(s).
#'
#' @inheritParams prcds
#' @param prcd Election procedure. One of `r all_prcds |> pal::as_md_vals() |> cli::ansi_collapse(sep2 = " or ", last = " or ")`.
#'
#' @return An integer vector.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::election_nrs(ballot_date = "2019-10-20",
#' lvl = "federal",
#' canton = "aargau",
#' prcd = "majoritarian")
#'
#' # in case of no (matching) elections, an empty integer vector is returned
#' fokus::election_nrs(ballot_date = "2019-10-20",
#' lvl = "cantonal",
#' canton = "aargau",
#' prcd = "proportional")
election_nrs <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "election"),
canton = cantons(ballot_date),
prcd = prcds(ballot_date = ballot_date,
lvl = lvl,
canton = canton)) {
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
result <- integer()
if (length(prcd) > 0L) {
lvl <- rlang::arg_match(arg = lvl,
values = all_lvls)
prcd <- rlang::arg_match(arg = prcd,
values = all_prcds)
raw <- raw_qstnr_suppl(ballot_date = ballot_date)
result <- as.integer(names(raw[[lvl]][[canton]]$election[[prcd]]))
}
result
}
#' Get ballot's election procedures
#'
#' Determines the election procedures covered by the FOKUS survey for the specified canton at the specified ballot date on the specified political level.
#'
#' @inheritParams prcds
#'
#' @return A character vector.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::election_prcds(ballot_date = "2019-10-20",
#' lvl = "federal",
#' canton = "aargau")
#'
#' # in case of no (matching) elections, an empty integer vector is returned
#' fokus::election_prcds(ballot_date = "2019-10-20",
#' lvl = "cantonal",
#' canton = "aargau")
election_prcds <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "election"),
canton = cantons(ballot_date)) {
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
result <- character()
if (length(lvl) > 0L) {
lvl <- rlang::arg_match(arg = lvl,
values = all_lvls)
raw <- raw_qstnr_suppl(ballot_date = ballot_date)
result <-
names(raw[[lvl]][[canton]]$election) |>
intersect(y = all_prcds)
}
result
}
#' Get referendum proposal question groups
#'
#' Determines the referendum proposal [question groups][all_qstn_groups] covered by the FOKUS survey for the specified canton on the specified political level
#' at the specified ballot date.
#'
#' @inheritParams proposal_nrs
#' @inheritParams raw_qstnr_suppl_proposal
#'
#' @return A character vector of question group identifiers.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::proposal_qstn_groups(ballot_date = "2023-06-18",
#' lvl = "federal",
#' canton = "aargau",
#' proposal_nr = 1)
#'
#' fokus::proposal_qstn_groups(ballot_date = "2023-06-18",
#' lvl = "federal",
#' canton = "aargau",
#' proposal_nr = 2)
proposal_qstn_groups <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = all_lvls,
canton = cantons(ballot_date),
proposal_nr = 1L) {
raw_qstnr_suppl_proposal_safe(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr) |>
names() |>
intersect(all_qstn_groups) %||%
character()
}
#' Get referendum proposal numbers with question group
#'
#' Determines the referendum proposal numbers covered by the FOKUS survey for the specified canton at the specified ballot date on the specified political
#' level for which at least one question of each of the specified standardized question groups has been asked.
#'
#' @inheritParams proposal_nrs
#' @param qstn_groups One or more of the standardized question groups `r pal::enum_fn_param_defaults(param = "qstn_groups", fn = qstn_groups_proposal_nrs)`.
#'
#' @return An integer vector.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::qstn_groups_proposal_nrs(ballot_date = "2023-06-18",
#' lvl = "federal",
#' canton = "aargau")
qstn_groups_proposal_nrs <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = all_lvls,
canton = cantons(ballot_date),
qstn_groups = all_qstn_groups) {
raw_qstnr_suppl_proposal_safe(ballot_date = ballot_date,
lvl = lvl,
canton = canton) |>
purrr::keep(\(x) all(qstn_groups %in% names(x))) |>
names() |>
as.integer()
}
#' Get survey channels
#'
#' Determines the channels the surevy was conducted over.
#'
#' @inheritParams lvls
#'
#' @return A character vector.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::survey_channels(ballot_date = "2023-06-18",
#' canton = "aargau")
survey_channels <- function(ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date)) {
raw_qstnr_suppl_mode(ballot_date = ballot_date,
canton = canton) |>
purrr::chuck("channels")
}
#' Determine whether survey is based on representative sample
#'
#' Determines whether or not the survey was conducted among a representative sample of the electorate (i.e. drawn randomly).
#'
#' @inheritParams lvls
#'
#' @return A logical scalar.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::is_representative(ballot_date = "2018-09-23",
#' canton = "aargau")
#'
#' fokus::is_representative(ballot_date = "2024-10-20",
#' canton = "aargau")
is_representative <- function(ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date)) {
raw_qstnr_suppl_mode(ballot_date = ballot_date,
canton = canton) |>
purrr::chuck("is_representative")
}
#' Get number of referendum proposals
#'
#' Determines the number of referendum proposals covered by the FOKUS survey for the specified canton at the specified ballot date on the specified political
#' level(s).
#'
#' The number of *federal* proposals is independent from the canton, i.e. the returned number of *federal* proposals at a specific ballot date is always the
#' same, thus `canton` is ignored if `!("cantonal" %in% lvls)`.
#'
#' @inheritParams ballot_types
#' @param canton FOKUS-covered canton name. One of
#' `r pal::as_md_val_list(all_cantons)`
#'
#' Only relevant if `lvls` includes `"cantonal"`.
#'
#' @return An integer vector of the same length as and named after `lvls`.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::n_proposals(ballot_date = "2018-09-23",
#' canton = "aargau")
#'
#' fokus::n_proposals(ballot_date = "2018-09-23",
#' lvls = "cantonal",
#' canton = "aargau")
#'
#' fokus::n_proposals(ballot_date = "2019-10-20")
n_proposals <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvls = all_lvls,
canton = cantons(ballot_date)) {
lvls <- rlang::arg_match(arg = lvls,
multiple = TRUE)
result <- integer()
raw <- raw_qstnr_suppl(ballot_date = ballot_date)
if ("cantonal" %in% lvls) {
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
result <- c(cantonal = length(raw$cantonal[[canton]]$proposal))
}
if ("federal" %in% lvls) {
result %<>% c(federal = length(raw$federal$proposal))
}
result
}
#' Get number of elections
#'
#' Determines the number of elections covered by the FOKUS survey for the specified canton at the specified ballot date on the specified political level(s) and
#' of the specified election procedure(s).
#'
#' @inheritParams ballot_types
#' @param prcds Election procedure(s). One or more of `r all_prcds |> pal::as_md_vals() |> cli::ansi_collapse(sep2 = " and ", last = " and ")`.
#'
#' @return An integer vector of length `lvls` × `prcds`, named after `lvls.prcds`.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::n_elections(ballot_date = "2018-09-23",
#' canton = "aargau")
#'
#' fokus::n_elections(ballot_date = "2018-09-23",
#' lvls = "federal",
#' canton = "aargau")
#'
#' fokus::n_elections(ballot_date = "2018-09-23",
#' lvls = "federal",
#' canton = "aargau",
#' prcds = "majoritarian")
n_elections <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvls = all_lvls,
canton = cantons(ballot_date),
prcds = all_prcds) {
lvls <- rlang::arg_match(arg = lvls,
multiple = TRUE)
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
prcds <- unique(rlang::arg_match(arg = prcds,
multiple = TRUE))
result <- integer()
raw <- raw_qstnr_suppl(ballot_date = ballot_date)
if ("federal" %in% lvls) {
result <-
prcds %>%
magrittr::set_names(., .) |>
purrr::map_int(\(x) length(raw$federal[[canton]]$election[[x]])) |>
list(federal = _) |>
unlist()
}
if ("cantonal" %in% lvls) {
result <-
prcds %>%
magrittr::set_names(., .) |>
purrr::map_int(\(x) length(raw$cantonal[[canton]]$election[[x]])) |>
list(cantonal = _) |>
unlist() |>
c(result)
}
result
}
#' Determine whether ballot includes a referendum
#'
#' Determines whether or not the FOKUS survey for the specified canton at the specified ballot date on the specified political level(s) covered a referendum.
#'
#' @inheritParams n_proposals
#'
#' @return A logical vector of the same length as and named after `lvls`.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::has_referendum(ballot_date = "2018-09-23",
#' canton = "aargau")
#'
#' fokus::has_referendum(ballot_date = "2018-09-23",
#' lvls = "federal",
#' canton = "aargau")
has_referendum <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvls = all_lvls,
canton = cantons(ballot_date)) {
n_proposals(ballot_date = ballot_date,
lvls = lvls,
canton = canton) > 0L
}
#' Determine whether ballot includes an election
#'
#' Determines whether or not the FOKUS survey for the specified canton at the specified ballot date on the specified political level(s) covered an election of
#' the specified election procedure(s).
#'
#' @inheritParams n_elections
#'
#' @return A logical vector of length `lvls` × `prcds`, named after `lvls.prcds`.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::has_election(ballot_date = "2019-10-20",
#' canton = "aargau")
#'
#' fokus::has_election(ballot_date = "2019-10-20",
#' lvls = "federal",
#' canton = "aargau")
#'
#' fokus::has_election(ballot_date = "2018-09-23",
#' lvls = "federal",
#' canton = "aargau",
#' prcds = "proportional")
has_election <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvls = all_lvls,
canton = cantons(ballot_date),
prcds = all_prcds) {
n_elections(ballot_date = ballot_date,
lvls = lvls,
canton = canton,
prcds = prcds) > 0L
}
#' Determine whether ballot includes type
#'
#' Determines whether or not the FOKUS survey for the specified canton at the specified ballot date on the specified political level(s) covered the specified
#' [ballot types][ballot_types].
#'
#' @inheritParams lvls
#' @inheritParams n_elections
#' @param canton FOKUS-covered canton name. One of
#' `r pal::as_md_val_list(all_cantons)`
#'
#' Only relevant if `lvls` includes `"cantonal"` or `ballot_type = "election"`.
#'
#' @return A logical vector of the same length as `lvl` (× `prcds`), named after `lvls(.prcds).ballot_type`.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::has_ballot_type(ballot_date = "2018-09-23",
#' canton = "aargau")
#'
#' fokus::has_ballot_type(ballot_date = "2018-09-23",
#' canton = "aargau",
#' ballot_type = "election")
#'
#' fokus::has_ballot_type(ballot_date = "2019-10-20",
#' canton = "aargau",
#' ballot_type = "election",
#' prcds = "proportional")
has_ballot_type <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvls = all_lvls,
canton = cantons(ballot_date),
ballot_type = ballot_types(ballot_date = ballot_date,
lvls = lvls,
canton = canton),
prcds = all_prcds) {
ballot_type <- rlang::arg_match(arg = ballot_type,
values = all_ballot_types)
if (isTRUE(ballot_type == "election")) {
result <-
has_election(ballot_date = ballot_date,
lvls = lvls,
canton = canton,
prcds = prcds) %>%
magrittr::set_names(paste(names(.), "election",
sep = "."))
} else if (isTRUE(ballot_type == "referendum")) {
result <-
has_referendum(ballot_date = ballot_date,
lvls = lvls,
canton = canton) %>%
magrittr::set_names(paste(names(.), "referendum",
sep = "."))
}
result
}
#' Determine whether ballot includes a political level
#'
#' Determines whether or not the FOKUS survey for the specified canton at the specified ballot date covered the specified political level.
#'
#' @inheritParams cantons
#' @param lvl Political level to test for. One of `r all_lvls |> pal::as_md_vals() |> cli::ansi_collapse(sep2 = " or ", last = " or ")`.
#' @param canton FOKUS-covered canton name. One of
#' `r pal::as_md_val_list(all_cantons)`
#'
#' Only relevant if `lvl = "cantonal"` or `ballot_types` includes `"election"`.
#' @param ballot_types Ballot type(s). One or more of `r pal::enum_fn_param_defaults(param = "ballot_types", fn = has_lvl)`
#' @param prcds Election procedure(s). One or more of `r all_prcds |> pal::as_md_vals() |> cli::ansi_collapse(sep2 = " and ", last = " and ")`.
#'
#' Only relevant if `ballot_types` includes `"election"`.
#'
#' @return A logical vector of the same length as (`prcds` ×) `ballot_types`, named after `lvl(.prcds).ballot_types`.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::has_lvl(ballot_date = "2018-09-23",
#' lvl = "federal",
#' canton = "aargau")
#'
#' fokus::has_lvl(ballot_date = "2018-09-23",
#' lvl = "federal",
#' canton = "aargau",
#' ballot_types = "election")
#'
#' fokus::has_lvl(ballot_date = "2019-10-20",
#' lvl = "federal",
#' canton = "aargau",
#' ballot_types = "election")
has_lvl <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton),
canton = cantons(ballot_date),
ballot_types = all_ballot_types,
prcds = all_prcds) {
if (length(lvl)) {
lvl <- rlang::arg_match(arg = lvl,
values = all_lvls)
}
ballot_types <- rlang::arg_match(arg = ballot_types,
multiple = TRUE)
result <- logical()
if ("election" %in% ballot_types) {
result <-
has_election(ballot_date = ballot_date,
lvls = lvl,
canton = canton,
prcds = prcds) %>%
magrittr::set_names(paste(names(.), "election",
sep = "."))
}
if ("referendum" %in% ballot_types) {
result <-
has_referendum(ballot_date = ballot_date,
lvls = lvl,
canton = canton) %>%
magrittr::set_names(paste(names(.), "referendum",
sep = ".")) |>
c(result)
}
result
}
#' Determine whether ballot includes referendum proposals
#'
#' Determines whether or not the FOKUS survey for the specified canton at the specified ballot date on the specified political levels covered the specified
#' referendum proposal numbers.
#'
#' @inheritParams n_proposals
#' @param proposal_nrs Proposals number(s) to test for. An integerish vector or `NULL`. If `NULL`, falls back to [`1:n_proposals()`][n_proposals] present
#' matching the specified parameters.
#'
#' @return Either
#' - **an unnamed logical scalar** if `proposal_nrs` is `NULL` and there was no FOKUS-covered referendum at `ballot_date` on any `lvls`, or
#' - **a logical vector** of the same length as `lvls` × `proposal_nrs`, **named** after `lvls.proposal_nrs`.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::has_proposal_nrs(ballot_date = "2018-09-23",
#' canton = "aargau")
#'
#' fokus::has_proposal_nrs(ballot_date = "2018-09-23",
#' lvls = "federal",
#' canton = "aargau")
#'
#' fokus::has_proposal_nrs(ballot_date = "2018-09-23",
#' canton = "aargau",
#' proposal_nrs = 1:5)
#'
#' fokus::has_proposal_nrs(ballot_date = "2019-10-20",
#' canton = "aargau")
#'
#' fokus::has_proposal_nrs(ballot_date = "2021-11-28",
#' lvls = "cantonal",
#' canton = "aargau")
has_proposal_nrs <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvls = all_lvls,
canton = cantons(ballot_date),
proposal_nrs = NULL) {
lvls <- rlang::arg_match(arg = lvls,
multiple = TRUE)
checkmate::assert_integerish(proposal_nrs,
lower = 1L,
any.missing = FALSE,
null.ok = TRUE)
lvls %>%
magrittr::set_names(., .) |>
purrr::map(~ {
present_proposal_nrs <- proposal_nrs(ballot_date = ballot_date,
lvl = .x,
canton = canton)
if (is.null(proposal_nrs)) {
proposal_nrs <- present_proposal_nrs
}
proposal_nrs %>%
magrittr::set_names(., .) |>
purrr::map(\(x) x %in% present_proposal_nrs)
}) |>
unlist() |>
pal::when(is.null(.) ~ FALSE,
~ .)
}
#' Determine whether ballot includes elections
#'
#' Determines whether or not the FOKUS survey for the specified canton at the specified ballot date on the specified political levels covered the specified
#' election numbers of the specified procedures.
#'
#' @inheritParams n_elections
#' @param election_nrs Election number(s) to test for. An integerish vector or `NULL`. If `NULL`, falls back to [`1:n_elections()`][n_elections] present
#' matching the specified parameters.
#'
#' @return Either
#' - **an unnamed logical scalar** if `election_nrs` is `NULL` and there was no FOKUS-covered election of any `prcds` at `ballot_date` on any `lvls`, or
#' - **a logical vector** of the same length as `lvls` × `prcds` × `election_nrs`, **named** after `lvls.prcds.election_nrs`.
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' fokus::has_election_nrs(ballot_date = "2019-10-20",
#' canton = "aargau")
#'
#' fokus::has_election_nrs(ballot_date = "2019-10-20",
#' lvls = "federal",
#' canton = "aargau",
#' prcds = "proportional")
#'
#' fokus::has_election_nrs(ballot_date = "2019-10-20",
#' canton = "aargau",
#' election_nrs = 1:2)
#'
#' fokus::has_election_nrs(ballot_date = "2018-09-23",
#' canton = "aargau")
#'
#' fokus::has_election_nrs(ballot_date = "2020-10-18",
#' lvls = "federal",
#' canton = "aargau")
has_election_nrs <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvls = all_lvls,
canton = cantons(ballot_date),
prcds = all_prcds,
election_nrs = NULL) {
lvls <- rlang::arg_match(arg = lvls,
multiple = TRUE)
checkmate::assert_integerish(election_nrs,
lower = 1L,
any.missing = FALSE,
null.ok = TRUE)
lvls %>%
magrittr::set_names(., .) |>
purrr::map(function(lvl) {
prcds %>%
magrittr::set_names(., .) |>
purrr::map(lvl = lvl,
.f = function(prcd,
lvl) {
present_election_nrs <- election_nrs(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
prcd = prcd)
if (is.null(election_nrs)) {
election_nrs <- present_election_nrs
}
election_nrs %>%
magrittr::set_names(., .) |>
purrr::map(\(x) x %in% present_election_nrs)
})
}) |>
unlist() |>
pal::when(is.null(.) ~ FALSE,
~ .)
}
#' Get ballot type combinations
#'
#' Returns a list with a metadata item per ballot type, ballot date, canton and optionally political level.
#'
#' @inheritParams ballot_types
#' @inheritParams has_lvl
#' @param ballot_dates FOKUS-covered ballot date(s). One or more of
#' `r pal::as_md_val_list(as.character(all_ballot_dates))`
#' @param cantons FOKUS-covered canton name(s). One or more of
#' `r pal::as_md_val_list(all_cantons)`
#' @param incl_lvl Whether or not to include the political levels in the resulting list. Setting this to `FALSE` potentially results in fewer combinations.
#'
#' @return A list with an element per ballot-type and optionally -political-level combination.
#' @family combo
#' @family predicate_fundamental
#' @export
#'
#' @examples
#' # of all covered ballots
#' fokus::combos_ballot_types()
#'
#' # only of 2023-06-18 ballot in aargau
#' fokus::combos_ballot_types(ballot_dates = "2023-06-18",
#' cantons = "aargau")
combos_ballot_types <- function(ballot_dates = all_ballot_dates,
lvls = all_lvls,
cantons = all_cantons,
ballot_types = all_ballot_types,
incl_lvl = TRUE) {
ballot_dates %<>% as_ballot_dates()
lvls <- rlang::arg_match(arg = lvls,
multiple = TRUE)
cantons <- rlang::arg_match(arg = cantons,
multiple = TRUE)
ballot_types <- rlang::arg_match(arg = ballot_types,
multiple = TRUE)
checkmate::assert_flag(incl_lvl)
result <-
ballot_dates |>
purrr::map(\(ballot_date) {
cantons(ballot_date = ballot_date) |>
intersect(y = cantons) |>
purrr::map(\(canton) {
if (incl_lvl) {
sub_result <-
lvls |>
purrr::map(\(lvl) {
ballot_types(ballot_date = ballot_date,
lvls = lvl,
canton = canton) |>
intersect(y = ballot_types) |>
purrr::map(\(x) list(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
ballot_type = x))
}) |>
purrr::list_flatten()
} else {
sub_result <-
ballot_types(ballot_date = ballot_date,
lvls = lvls,
canton = canton) |>
intersect(y = ballot_types) |>
purrr::map(\(x) list(ballot_date = ballot_date,
canton = canton,
ballot_type = x))
}
sub_result
}) |>
purrr::list_flatten()
}) |>
purrr::list_flatten()
result
}
#' Get referendum proposal type
#'
#' Returns the [type][all_proposal_types] of the specified referendum proposal.
#'
#' @inheritParams proposal_name
#'
#' @return Proposal type. One of
#' `r pal::as_md_val_list(all_proposal_types)`
#'
#' @family predicate_proposal
#' @export
#'
#' @examples
#' fokus::proposal_type(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1)
proposal_type <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "referendum"),
canton = cantons(ballot_date),
proposal_nr = 1L) {
raw_qstnr_suppl_proposal(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr) |>
purrr::chuck("type")
}
#' Get referendum proposal name
#'
#' Returns the name of the specified referendum proposal in the specified language.
#'
#' @inheritParams proposal_nrs
#' @inheritParams raw_qstnr_suppl_proposal
#' @inheritParams lang_to_locale
#' @param type Name type. One of `r all_name_types |> pal::as_md_vals() |> cli::ansi_collapse(sep2 = " or ", last = " or ")`.
#'
#' @return A character scalar.
#' @family predicate_proposal
#' @export
#'
#' @examples
#' fokus::proposal_name(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1,
#' type = "long")
proposal_name <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "referendum"),
canton = cantons(ballot_date),
proposal_nr = 1L,
lang = pal::pkg_config_val("lang"),
type = all_name_types) {
lang <- rlang::arg_match(arg = lang,
values = all_langs)
type <- rlang::arg_match(type)
raw_qstnr_suppl_proposal_name(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr) |>
purrr::chuck(lang, type, "text")
}
#' Get German referendum proposal name's grammatical gender
#'
#' Returns the grammatical gender of the German name of the specified referendum proposal.
#'
#' @inheritParams proposal_name
#'
#' @return A character scalar.
#' @family predicate_proposal
#' @export
#'
#' @examples
#' fokus::proposal_name_gender(ballot_date = "2018-09-23",
#' lvl = "federal",
#' proposal_nr = 1,
#' type = "short")
#'
#' fokus::proposal_name_gender(ballot_date = "2018-09-23",
#' lvl = "federal",
#' proposal_nr = 1,
#' type = "long")
proposal_name_gender <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "referendum"),
canton = cantons(ballot_date),
proposal_nr = 1L,
type = all_name_types) {
type <- rlang::arg_match(type)
raw_qstnr_suppl_proposal_name(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr) |>
purrr::chuck("de", type, "gender")
}
#' Determine whether German referendum proposal name is plural
#'
#' Determines whether or not the German name of the specified referendum proposal is a plural.
#'
#' @inheritParams proposal_name
#'
#' @return A logical scalar.
#' @family predicate_proposal
#' @export
#'
#' @examples
#' fokus::is_proposal_name_plural(ballot_date = "2020-09-27",
#' lvl = "federal",
#' proposal_nr = 3,
#' type = "short")
is_proposal_name_plural <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "referendum"),
canton = cantons(ballot_date),
proposal_nr = 1L,
type = all_name_types) {
type <- rlang::arg_match(type)
raw_qstnr_suppl_proposal_name(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr) |>
purrr::pluck("de", type, "is_plural",
.default = FALSE)
}
#' Get referendum proposal's arguments
#'
#' Returns text, side and number of all referendum arguments on the specified proposal.
#'
#' @inheritParams proposal_name
#'
#' @return `r pkgsnip::return_lbl("tibble")`
#' @family predicate_proposal
#' @export
#'
#' @examples
#' fokus::proposal_arguments(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1)
proposal_arguments <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "referendum"),
canton = cantons(ballot_date),
proposal_nr = 1L) {
raw_qstnr_suppl_arguments(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr) |>
purrr::map(as_flat_list) |>
purrr::map(tibble::as_tibble) |>
purrr::list_rbind() |>
dplyr::relocate(any_of(c("side", "nr")),
any_of("de.short"),
starts_with("de."),
any_of("en.short"),
starts_with("en."))
}
#' Get referendum proposal argument
#'
#' Returns the specified referendum proposal argument's text of the specified type in the specified language.
#'
#' @inheritParams proposal_name
#' @param side Proposal argument side. One of `r all_argument_sides |> pal::as_md_vals() |> cli::ansi_collapse(sep2 = " or ", last = " or ")`.
#' @param argument_nr Proposal argument number. A positive integerish scalar.
#'
#' @return A character scalar.
#' @family predicate_proposal
#' @export
#'
#' @examples
#' fokus::proposal_argument(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1,
#' side = "contra",
#' argument_nr = 3,
#' type = "long")
proposal_argument <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "referendum"),
canton = cantons(ballot_date),
proposal_nr = 1L,
side = all_argument_sides,
argument_nr = 1L,
lang = pal::pkg_config_val("lang"),
type = all_name_types) {
lang <- rlang::arg_match(arg = lang,
values = all_langs)
type <- rlang::arg_match(type)
raw_qstnr_suppl_argument(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr,
side = side,
argument_nr = argument_nr) |>
purrr::chuck(lang, type)
}
#' Get referendum proposal's main motives
#'
#' Returns text and code number of all main motives of the specified `type` for the specified referendum proposal.
#'
#' @inheritParams proposal_name
#' @param type Main motive type. One of `r pal::enum_fn_param_defaults(param = "type", fn = proposal_main_motives)`.
#'
#' @return `r pkgsnip::return_lbl("tibble")`
#' @family predicate_proposal
#' @export
#'
#' @examples
#' fokus::proposal_main_motives(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1,
#' type = "no")
proposal_main_motives <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "referendum"),
canton = cantons(ballot_date),
proposal_nr = 1L,
type = all_main_motive_types) {
type <- rlang::arg_match(type)
raw_qstnr_suppl_main_motives(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr) |>
purrr::chuck(type) |>
purrr::map(tibble::as_tibble) |>
purrr::list_rbind()
}
#' Get referendum proposal's number of arguments
#'
#' Determines the number of arguments on the specified referendum proposal of the specified sides.
#'
#' @inheritParams proposal_name
#' @param sides Proposal argument side(s). One or more of `r all_argument_sides |> pal::as_md_vals() |> cli::ansi_collapse(sep2 = " and ", last = " and ")`.
#'
#' @return An integer scalar.
#' @family predicate_proposal
#' @export
#'
#' @examples
#' fokus::n_proposal_arguments(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1)
#'
#' fokus::n_proposal_arguments(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1,
#' sides = "pro")
n_proposal_arguments <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "referendum"),
canton = cantons(ballot_date),
proposal_nr = 1L,
sides = all_argument_sides) {
sides <- rlang::arg_match(arg = sides,
multiple = TRUE)
raw_qstnr_suppl_proposal(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr) |>
purrr::pluck("argument") |>
purrr::keep(\(x) x$side %in% sides) |>
length()
}
#' Get referendum proposal's number of main motives
#'
#' Determines the number of main motives for the specified proposal and motive type.
#'
#' @inheritParams proposal_main_motives
#' @param types Main motive type(s). One or more of `r pal::enum_fn_param_defaults(param = "type", fn = proposal_main_motives, sep2 = " and ")`.
#'
#' @return An integer scalar.
#' @family predicate_proposal
#' @export
#'
#' @examples
#' # count all motive types
#' fokus::n_proposal_main_motives(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1)
#'
#' # count only motives of type "no"
#' fokus::n_proposal_main_motives(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1,
#' types = "no")
n_proposal_main_motives <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "referendum"),
canton = cantons(ballot_date),
proposal_nr = 1L,
types = all_main_motive_types) {
types <- rlang::arg_match(types,
multiple = TRUE)
raw_qstnr_suppl_proposal(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr) |>
purrr::pluck("main_motive") |>
purrr::keep_at(at = types) |>
pal::when(length(.) > 0L ~
purrr::map_depth(.,
.depth = 1L,
length) |>
purrr::list_c(ptype = integer()),
~ 0L) |>
sum()
}
#' Determine whether arguments have been queried for referendum proposal
#'
#' Determines whether or not arguments have been queried for the specified referendum proposal.
#'
#' @inheritParams n_proposal_arguments
#'
#' @return A logical scalar.
#' @family predicate_proposal
#' @export
#'
#' @examples
#' fokus::has_proposal_arguments(ballot_date = "2023-06-18",
#' lvl = "federal",
#' canton = "aargau")
#'
#' fokus::has_proposal_arguments(ballot_date = "2023-06-18",
#' lvl = "federal",
#' canton = "aargau",
#' proposal_nr = 2)
has_proposal_arguments <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "referendum"),
canton = cantons(ballot_date),
proposal_nr = 1L,
sides = all_argument_sides) {
n_proposal_arguments(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr,
sides = sides) > 0L
}
#' Determine whether main motives have been queried for referendum proposal
#'
#' Determines whether or not main motives have been queried for the specified referendum proposal.
#'
#' @inheritParams n_proposal_main_motives
#'
#' @return A logical scalar.
#' @family predicate_proposal
#' @export
#'
#' @examples
#' fokus::has_proposal_main_motives(ballot_date = "2023-06-18",
#' lvl = "federal",
#' canton = "aargau")
#'
#' fokus::has_proposal_main_motives(ballot_date = "2023-06-18",
#' lvl = "federal",
#' canton = "aargau",
#' proposal_nr = 2)
has_proposal_main_motives <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "referendum"),
canton = cantons(ballot_date),
proposal_nr = 1L,
types = all_main_motive_types) {
n_proposal_main_motives(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr,
types = types) > 0L
}
#' Get referendum proposal numbers with arguments
#'
#' Determines the referendum proposal numbers covered by the FOKUS survey for the specified canton at the specified ballot date on the specified political
#' level for which at least one pro/contra argument has been queried.
#'
#' @inheritParams proposal_nrs
#'
#' @return An integer vector.
#' @family predicate_proposal
#' @export
#'
#' @examples
#' fokus::argument_proposal_nrs(ballot_date = "2023-06-18",
#' lvl = "federal",
#' canton = "aargau")
argument_proposal_nrs <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "referendum"),
canton = cantons(ballot_date)) {
qstn_groups_proposal_nrs(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
qstn_groups = "argument")
}
#' Get referendum proposal numbers with arguments
#'
#' Determines the referendum proposal numbers covered by the FOKUS survey for the specified canton at the specified ballot date on the specified political
#' level for which at least one pro/contra argument has been queried.
#'
#' @inheritParams proposal_nrs
#'
#' @return An integer vector.
#' @family predicate_proposal
#' @export
#'
#' @examples
#' fokus::main_motive_proposal_nrs(ballot_date = "2023-06-18",
#' lvl = "federal",
#' canton = "aargau")
main_motive_proposal_nrs <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "referendum"),
canton = cantons(ballot_date)) {
qstn_groups_proposal_nrs(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
qstn_groups = "main_motive")
}
#' Get referendum proposal combinations
#'
#' Returns a list with a metadata item per ballot date, political level, canton and optionally proposal number.
#'
#' @inheritParams combos_ballot_types
#' @param incl_nr Whether or not to include proposal numbers in the resulting list. Setting this to `FALSE` potentially results in fewer combinations.
#'
#' @return A list with an element per political-level and optionally -proposal-number combination.
#' @family combo
#' @family predicate_proposal
#' @export
#'
#' @examples
#' fokus::combos_proposals(ballot_dates = "2023-06-18",
#' cantons = "aargau")
#'
#' # without proposal numbers
#' fokus::combos_proposals(ballot_dates = "2023-06-18",
#' cantons = "aargau",
#' incl_nr = FALSE)
combos_proposals <- function(ballot_dates = all_ballot_dates,
lvls = all_lvls,
cantons = all_cantons,
incl_nr = TRUE) {
ballot_dates %<>% as_ballot_dates()
lvls <- rlang::arg_match(arg = lvls,
multiple = TRUE)
cantons <- rlang::arg_match(arg = cantons,
multiple = TRUE)
checkmate::assert_flag(incl_nr)
ballot_dates |>
purrr::map(\(ballot_date) {
cantons(ballot_date = ballot_date) |>
intersect(y = cantons) |>
purrr::map(\(canton) {
lvls |>
purrr::map(\(lvl) {
if (incl_nr) {
result <-
proposal_nrs(ballot_date = ballot_date,
lvl = lvl,
canton = canton) |>
purrr::map(\(nr) list(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = nr))
} else if (has_lvl(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
ballot_types = "referendum")) {
result <- list(ballot_date = ballot_date,
lvl = lvl,
canton = canton)
} else {
result <- NULL
}
result
}) |>
pal::when(incl_nr ~ purrr::list_flatten(.),
~ .) |>
purrr::compact()
}) |>
purrr::list_flatten()
}) |>
purrr::list_flatten()
}
#' Get referendum proposal combinations for which arguments have been queried
#'
#' Returns a list with a metadata item per ballot date, political level, canton, proposal number and optionally proposal argument side (pro/contra) and number.
#'
#' @inheritParams combos_ballot_types
#' @param incl_side Whether or not to include argument sides (pro/contra) in the resulting list. Setting this to `FALSE` potentially results in fewer
#' combinations.
#' @param incl_argument_nr Whether or not to include argument numbers in the resulting list. Setting this to `FALSE` potentially results in fewer combinations.
#' Setting this to `TRUE` implies `incl_side = TRUE`.
#'
#' @return A list with an element per political-level-proposal-number and optionally -argument-side and -argument-number combination.
#' @family combo
#' @family predicate_proposal
#' @export
#'
#' @examples
#' fokus::combos_proposal_arguments(ballot_dates = "2023-06-18",
#' cantons = "aargau")
#' # without argument numbers
#' fokus::combos_proposal_arguments(ballot_dates = "2023-06-18",
#' cantons = "aargau",
#' incl_argument_nr = FALSE)
#' # without argument sides and numbers
#' fokus::combos_proposal_arguments(ballot_dates = "2023-06-18",
#' cantons = "aargau",
#' incl_side = FALSE,
#' incl_argument_nr = FALSE)
combos_proposal_arguments <- function(ballot_dates = all_ballot_dates,
lvls = all_lvls,
cantons = all_cantons,
incl_side = TRUE,
incl_argument_nr = incl_side) {
ballot_dates %<>% as_ballot_dates()
lvls <- rlang::arg_match(arg = lvls,
multiple = TRUE)
cantons <- rlang::arg_match(arg = cantons,
multiple = TRUE)
checkmate::assert_flag(incl_side)
checkmate::assert_flag(incl_argument_nr)
# ensure `incl_argument_nr` and `incl_side` do not conflict
if (incl_argument_nr && !incl_side) {
cli::cli_abort("{.arg incl_argument_nr} cannot be {.val {TRUE}} when {.arg incl_side} is {.val {FALSE}}.")
}
ballot_dates |>
purrr::map(\(ballot_date) {
cantons(ballot_date = ballot_date) |>
intersect(y = cantons) |>
purrr::map(\(canton) {
lvls |>
purrr::map(\(lvl) {
proposal_nrs(ballot_date = ballot_date,
lvl = lvl,
canton = canton) |>
purrr::map(\(proposal_nr) {
if (has_proposal_arguments(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr)) {
if (incl_side) {
result <-
raw_qstnr_suppl_arguments(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr) |>
purrr::map(\(arg) {
if (incl_argument_nr) {
return(list(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr,
side = arg$side,
argument_nr = arg$nr))
} else {
return(list(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr,
side = arg$side))
}
}) |>
unique()
} else {
result <- list(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr)
}
} else {
result <- NULL
}
result
}) |>
purrr::compact() |>
pal::when(incl_side ~ purrr::list_flatten(.),
~ .)
}) |>
purrr::list_flatten()
}) |>
purrr::list_flatten()
}) |>
purrr::list_flatten()
}
#' Get referendum proposal combinations for which main motives have been queried
#'
#' Returns a list with a metadata item per ballot date, political level, canton, proposal number and optionally motive type for which [main
#' motives][proposal_main_motives] have been queried in the respective post-voting survey.
#'
#' @inheritParams combos_ballot_types
#' @param incl_type Whether or not to include motive types (yes/no) in the resulting list. Setting this to `FALSE` potentially results in fewer combinations.
#'
#' @return A list with an element per political-level-proposal-number and optionally -type combination.
#' @family combo
#' @family predicate_proposal
#' @export
#'
#' @examples
#' fokus::combos_proposal_main_motives(ballot_dates = "2023-06-18",
#' cantons = "aargau")
#' # without types
#' fokus::combos_proposal_main_motives(ballot_dates = "2023-06-18",
#' cantons = "aargau",
#' incl_type = FALSE)
combos_proposal_main_motives <- function(ballot_dates = all_ballot_dates,
lvls = all_lvls,
cantons = all_cantons,
incl_type = TRUE) {
ballot_dates %<>% as_ballot_dates()
lvls <- rlang::arg_match(arg = lvls,
multiple = TRUE)
cantons <- rlang::arg_match(arg = cantons,
multiple = TRUE)
checkmate::assert_flag(incl_type)
ballot_dates |>
purrr::map(\(ballot_date) {
cantons(ballot_date = ballot_date) |>
intersect(y = cantons) |>
purrr::map(\(canton) {
lvls |>
purrr::map(\(lvl) {
proposal_nrs(ballot_date = ballot_date,
lvl = lvl,
canton = canton) |>
purrr::map(\(proposal_nr) {
if (incl_type) {
result <-
all_main_motive_types |>
purrr::map(\(type) {
if (has_proposal_main_motives(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr,
types = type)) {
list(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr,
type = type)
} else {
NULL
}
}) |>
purrr::compact()
} else if (has_proposal_main_motives(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr)) {
result <- list(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr)
} else {
result <- NULL
}
result
}) |>
purrr::compact() |>
pal::when(incl_type ~ purrr::list_flatten(.),
~ .)
}) |>
purrr::list_flatten()
}) |>
purrr::list_flatten()
}) |>
purrr::list_flatten()
}
#' Get election name
#'
#' Returns the name of the specified election in the specified language.
#'
#' @inheritParams election_nrs
#' @inheritParams proposal_name
#' @param election_nr Election number. A positive integerish scalar.
#' @param type Name type. One of `r pal::enum_fn_param_defaults(param = "type", fn = election_name)`.
#'
#' @return A character scalar.
#' @family predicate_election
#' @export
#'
#' @examples
#' fokus::election_name(ballot_date = "2019-10-20",
#' lvl = "cantonal",
#' canton = "aargau",
#' prcd = "majoritarian",
#' election_nr = 1,
#' type = "body")
election_name <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "election"),
canton = cantons(ballot_date),
prcd = prcds(ballot_date,
lvl,
canton),
election_nr = 1L,
lang = pal::pkg_config_val("lang"),
type = c("short", "long", "body", "body_alt")) {
lang <- rlang::arg_match(arg = lang,
values = all_langs)
type <- rlang::arg_match(type)
# this is required to trigger a proper error message in case `prcds` is not explicitly set and there are no elections (`prcd = character()`)
if (!length(prcd)) {
cli::cli_abort(paste0("{.arg prcd} must be one of ", cli::ansi_collapse(paste0("{.val ", all_prcds, "}"), sep2 = " or ", last = " or "), "."))
}
raw_qstnr_suppl_election_name(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
prcd = prcd,
election_nr = election_nr) |>
purrr::chuck(lang, type, "text")
}
#' Get combined elections name
#'
#' Returns the combined name of all elections at the specified date on the specified level(s) for the specified canton in the specified language.
#'
#' @inheritParams n_elections
#' @inheritParams lang_to_locale
#' @param federal_first Whether or not to list federal elections before cantonal ones. Only has an effect if `"federal" %in% lvls`.
#'
#' @return A character scalar, empty if no elections were held.
#' @family predicate_election
#' @export
#'
#' @examples
#' fokus::election_names_combined(ballot_date = "2019-10-20",
#' lvls = "federal",
#' canton = "aargau")
#'
#' fokus::election_names_combined(ballot_date = "2019-10-20",
#' canton = "aargau")
#'
#' fokus::election_names_combined(ballot_date = "2019-10-20",
#' canton = "aargau",
#' federal_first = FALSE)
election_names_combined <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvls = all_lvls,
canton = cantons(ballot_date),
lang = pal::pkg_config_val("lang"),
federal_first = TRUE) {
lvls <- unique(rlang::arg_match(arg = lvls,
multiple = TRUE))
lang <- rlang::arg_match(arg = lang,
values = all_langs)
checkmate::assert_flag(federal_first)
sep <- switch(EXPR = lang,
de = " sowie ",
en = " as well as ")
lvls |>
intersect(lvls(ballot_date = ballot_date,
canton = canton,
ballot_type = "election")) |>
purrr::map_chr(\(x) {
raw_qstnr_suppl_elections(ballot_date = ballot_date,
lvl = x,
canton = canton) |>
purrr::chuck("names_combined", lang, "short")
}) |>
pal::when(federal_first ~ .[sort(x = seq_along(.),
decreasing = TRUE)],
~ .) |>
cli::ansi_collapse(sep2 = sep,
last = sep)
}
#' Get majoritarian election's candidates
#'
#' Returns the name and party of all candidates running for the specified majoritarian election.
#'
#' @inheritParams n_election_seats
#'
#' @return `r pkgsnip::return_lbl("tibble")`
#' @family predicate_election
#' @export
#'
#' @examples
#' fokus::election_candidates(ballot_date = "2019-10-20",
#' lvl = "cantonal",
#' canton = "aargau")
election_candidates <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "election"),
canton = cantons(ballot_date),
election_nr = 1L) {
raw_qstnr_suppl_election(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
prcd = "majoritarian",
election_nr = election_nr) |>
purrr::chuck("candidate") |>
purrr::map(tibble::as_tibble) |>
purrr::list_rbind()
}
#' Get proportional election's political parties
#'
#' Returns the questionnaire code as well as different versions of the name of all parties for the specified proportional election.
#'
#' @inheritParams n_election_seats
#' @param past Whether to process the current or the predecessor election's parties.
#'
#' @return `r pkgsnip::return_lbl("tibble")`
#' @seealso [fct_relabel_election_parties()]
#' @family predicate_election
#' @export
#'
#' @examples
#' fokus::election_parties(ballot_date = "2019-10-20",
#' lvl = "federal",
#' canton = "aargau")
#'
#' fokus::election_parties(ballot_date = "2019-10-20",
#' lvl = "federal",
#' canton = "aargau",
#' past = TRUE)
election_parties <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "election",
prcds = "proportional"),
canton = cantons(ballot_date),
election_nr = 1L,
past = FALSE) {
checkmate::assert_flag(past)
if (past) {
# priority 1: from current election's metadata (past election was *not* covered by FOKUS survey)
result <-
raw_qstnr_suppl_election(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
prcd = "proportional",
election_nr = election_nr) |>
purrr::pluck("past_party")
# priority 2: from past election's metadata (past election was covered by FOKUS survey)
if (is.null(result)) {
result <-
raw_qstnr_suppl_election(ballot_date = past_election_date(ballot_date = ballot_date,
lvl = lvl,
canton = canton),
lvl = lvl,
canton = canton,
prcd = "proportional",
election_nr = election_nr) |>
purrr::chuck("party")
}
} else {
result <-
raw_qstnr_suppl_election(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
prcd = "proportional",
election_nr = election_nr) |>
purrr::chuck("party")
}
result |>
purrr::map(as_flat_list) |>
purrr::map(tibble::as_tibble) |>
purrr::list_rbind()
}
#' Get proportional election's tickets
#'
#' Returns the name, number and party of all tickets for the specified proportional election.
#'
#' @inheritParams n_election_seats
#'
#' @return `r pkgsnip::return_lbl("tibble")`
#' @family predicate_election
#' @export
#'
#' @examples
#' fokus::election_tickets(ballot_date = "2019-10-20",
#' lvl = "federal",
#' canton = "aargau")
election_tickets <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "election"),
canton = cantons(ballot_date),
election_nr = 1L) {
raw_qstnr_suppl_election(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
prcd = "proportional",
election_nr = election_nr) |>
purrr::chuck("ticket") |>
purrr::map(tibble::as_tibble) |>
purrr::list_rbind()
}
#' Get election's past date
#'
#' Determines the ordinary date (i.e. 1st round of voting) on which the specified election was *last* held (i.e. 4 years earlier than `ballot_date`).
#'
#' @inheritParams election_name
#'
#' @return `r pkgsnip::return_lbl("date")`
#' @family predicate_election
#' @export
#'
#' @examples
#' fokus::past_election_date(ballot_date = "2019-10-20")
#' fokus::past_election_date(ballot_date = "2020-10-18")
#' fokus::past_election_date(ballot_date = "2024-10-20")
#' fokus::past_election_date(ballot_date = "2019-10-20",
#' lvl = "federal")
past_election_date <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "election"),
canton = cantons(ballot_date)) {
ballot_date %<>% as_ballot_date()
if (length(lvl)) {
lvl <- rlang::arg_match(arg = lvl,
values = all_lvls)
}
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
# priority 1: from current election's metadata (past election was *not* covered by FOKUS survey)
result <-
lvl |>
pal::when(. == "federal" ~
raw_qstnr_suppl_lvl(ballot_date = ballot_date,
lvl = .),
~ raw_qstnr_suppl_lvl_canton(ballot_date = ballot_date,
lvl = .,
canton = canton)) |>
purrr::pluck("election", "past_ballot_date") |>
checkmate::assert_date(any.missing = FALSE,
null.ok = TRUE)
# priority 2: from past election's metadata (past election was covered by FOKUS survey)
if (is.null(result)) {
# NOTE: we assume the past election must be *4* years before `ballot_date`
past_election_year <- clock::get_year(ballot_date) - 4L
result <-
combos_elections(ballot_dates = all_ballot_dates,
lvls = lvl,
cantons = canton,
incl_prcd = FALSE,
incl_nr = FALSE) |>
purrr::keep(.p = \(x) {
clock::get_year(x$ballot_date) == past_election_year
}) |>
purrr::map_depth(.depth = 1L,
.f = \(x) x$ballot_date) |>
purrr::list_c(ptype = as.Date(NULL)) |>
pal::safe_min()
}
if (length(result) == 0L) {
cli::cli_abort(paste0("Past election date couldn't be determined. Please set `past_ballot_date` explicitly in the supplemental {.val {ballot_date}} FOKUS ",
"questionnaire data."))
}
result
}
#' Get number of majoritarian election seats
#'
#' Determines the number of election seats of the specified type for the specified majority election.
#'
#' @inheritParams ballot_types
#' @inheritParams election_name
#' @param seat_type Seat type. One of `r pal::enum_fn_param_defaults(param = "seat_type", fn = n_election_seats)`.
#'
#' @return An integer scalar.
#' @family predicate_election
#' @export
#'
#' @examples
#' fokus::n_election_seats(ballot_date = "2019-10-20",
#' lvl = "cantonal",
#' canton = "aargau",
#' seat_type = "total")
n_election_seats <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "election"),
canton = cantons(ballot_date),
election_nr = 1L,
seat_type = all_election_seat_types) {
seat_type <- rlang::arg_match(seat_type)
raw_qstnr_suppl_election(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
prcd = "majoritarian",
election_nr = election_nr) |>
purrr::chuck("n_seats", seat_type)
}
#' Get number of (officially registered) majoritarian election candidates
#'
#' Determines the number of (officially registered) candidates of a majority election at the specified ballot date on the specified political level.
#'
#' @inheritParams n_election_seats
#'
#' @return An integer scalar.
#' @family predicate_election
#' @export
#'
#' @examples
#' fokus::n_election_candidates(ballot_date = "2019-10-20",
#' lvl = "cantonal",
#' canton = "aargau")
n_election_candidates <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "election"),
canton = cantons(ballot_date),
election_nr = 1L) {
raw_qstnr_suppl_election(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
prcd = "majoritarian",
election_nr = election_nr) |>
purrr::chuck("candidate") |>
length()
}
#' Determine whether majoritarian election requires candidate registration
#'
#' Determines whether or not candidates must be officially registered prior to the specified majority election.
#'
#' The absence of a candidate registration requirement usually means that every eligible citizen can be elected, i.e. receive valid votes.
#'
#' @inheritParams n_election_seats
#'
#' @return A logical scalar.
#' @family predicate_election
#' @export
#'
#' @examples
#' fokus::requires_candidate_registration(ballot_date = "2019-10-20",
#' lvl = "federal",
#' canton = "aargau")
requires_candidate_registration <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "election"),
canton = cantons(ballot_date),
election_nr = 1L) {
raw_qstnr_suppl_election(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
prcd = "majoritarian",
election_nr = election_nr) |>
purrr::chuck("requires_candidate_registration")
}
#' Get election combinations
#'
#' Returns a list with a metadata item per ballot date, political level and optionally election procedure and number.
#'
#' @inheritParams combos_ballot_types
#' @inheritParams n_elections
#' @param incl_prcd Whether or not to include election procedures in the resulting list. Setting this to `FALSE` potentially results in fewer combinations.
#' @param incl_nr Whether or not to include election numbers in the resulting list. Setting this to `FALSE` potentially results in fewer combinations. Only
#' relevant if `incl_prcd = TRUE`.
#'
#' @return A list with an element per political-level and optionally -election-procedure and -election-number combination.
#' @family combo
#' @family predicate_election
#' @export
#'
#' @examples
#' fokus::combos_elections(ballot_dates = "2019-10-20",
#' cantons = "aargau")
#'
#' # without election numbers
#' fokus::combos_elections(ballot_dates = "2019-10-20",
#' cantons = "aargau",
#' incl_prcd = TRUE,
#' incl_nr = FALSE)
#'
#' # without election procedures and numbers
#' fokus::combos_elections(ballot_dates = "2019-10-20",
#' cantons = "aargau",
#' incl_prcd = FALSE,
#' incl_nr = FALSE)
combos_elections <- function(ballot_dates = all_ballot_dates,
lvls = all_lvls,
cantons = all_cantons,
prcds = all_prcds,
incl_prcd = TRUE,
incl_nr = incl_prcd) {
ballot_dates %<>% as_ballot_dates()
lvls <- rlang::arg_match(arg = lvls,
multiple = TRUE)
cantons <- rlang::arg_match(arg = cantons,
multiple = TRUE)
prcds <- rlang::arg_match(arg = prcds,
multiple = TRUE)
checkmate::assert_flag(incl_prcd)
checkmate::assert_flag(incl_nr)
ballot_dates |>
purrr::map(\(ballot_date) {
cantons(ballot_date = ballot_date) |>
intersect(y = cantons) |>
purrr::map(\(canton) {
lvls |>
purrr::map(\(lvl) {
if (incl_prcd) {
result <-
election_prcds(ballot_date = ballot_date,
lvl = lvl,
canton = canton) |>
intersect(y = prcds) |>
purrr::map(\(prcd) {
if (incl_nr) {
return(election_nrs(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
prcd = prcd) |>
purrr::map(\(nr) list(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
prcd = prcd,
election_nr = nr)))
} else {
return(list(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
prcd = prcd))
}
}) |>
pal::when(incl_nr ~ purrr::list_flatten(.),
~ .)
} else if (any(has_lvl(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
ballot_types = "election"))) {
result <- list(ballot_date = ballot_date,
lvl = lvl,
canton = canton)
} else {
result <- NULL
}
result
}) |>
pal::when(incl_prcd ~ purrr::list_flatten(.),
~ .) |>
purrr::compact()
}) |>
purrr::list_flatten()
}) |>
purrr::list_flatten()
}
#' Get skill question numbers
#'
#' Determines the skill questions numbers at the specified ballot date on the specified political level. Note that by default (`proposal_nr = NULL`),
#' non-proposal-specific skill question numbers are returned.
#'
#' @inheritParams proposal_nrs
#' @param proposal_nr Proposal number. A positive integerish scalar or `NULL`. If `NULL`, the numbers of non-proposal-specific skill questions are returned.
#'
#' @return An integer vector.
#' @family predicate_skill_question
#' @export
#'
#' @examples
#' fokus::skill_question_nrs(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1)
#'
#' # note that by default, non-proposal-specific skill question numbers are returned
#' fokus::skill_question_nrs(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau")
#'
#' fokus::skill_question_nrs(ballot_date = "2019-10-20",
#' lvl = "cantonal",
#' canton = "aargau")
skill_question_nrs <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton),
canton = cantons(ballot_date),
proposal_nr = NULL) {
pal::safe_seq_len(n_skill_questions(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr))
}
#' Get number of skill questions
#'
#' Determines the number of skill questions at the specified ballot date on the specified political level. Note that by default (`proposal_nr = NULL`), the
#' number of non-proposal-specific skill questions is returned.
#'
#' @inheritParams proposal_nrs
#' @param proposal_nr Proposal number. A positive integerish scalar or `NULL`. If `NULL`, the number of non-proposal-specific skill questions is returned.
#'
#' @return An integer scalar.
#' @family predicate_skill_question
#' @export
#'
#' @examples
#' fokus::n_skill_questions(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1)
#'
#' # note that by default, the number of non-proposal-specific skill questions is returned
#' fokus::n_skill_questions(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau")
#'
#' fokus::n_skill_questions(ballot_date = "2019-10-20",
#' lvl = "cantonal",
#' canton = "aargau")
n_skill_questions <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton),
canton = cantons(ballot_date),
proposal_nr = NULL) {
lvl <- rlang::arg_match(arg = lvl,
values = all_lvls)
if (lvl == "cantonal") {
canton <- rlang::arg_match(arg = canton,
values = cantons(ballot_date))
}
checkmate::assert_count(proposal_nr,
positive = TRUE,
null.ok = TRUE)
raw_qstnr_suppl(ballot_date = ballot_date) |>
purrr::pluck(lvl) |>
pal::when(lvl == "cantonal" ~ purrr::pluck(., canton),
~ .) |>
# get non-proposal-specific skill questions if `proposal_nr = NULL`
pal::when(length(proposal_nr) > 0L ~ purrr::pluck(., "proposal", proposal_nr),
~ .) |>
purrr::pluck("skill_question") |>
length()
}
#' Get skill question
#'
#' Returns the skill question text in the specified language. Note that by default (`proposal_nr = NULL`), only non-proposal-specific skill questions are
#' returned.
#'
#' @inheritParams proposal_name
#' @param proposal_nr Proposal number. A positive integerish scalar or `NULL`. If `NULL`, it is considered to be a non-proposal-specific skill question
#' (the case at elections).
#' @param skill_question_nr Skill question number. A positive integerish scalar.
#'
#' @return A character scalar.
#' @family predicate_skill_question
#' @export
#'
#' @examples
#' fokus::skill_question(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1,
#' skill_question_nr = 2,
#' lang = "en")
skill_question <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton),
canton = cantons(ballot_date),
proposal_nr = NULL,
skill_question_nr = 1L,
lang = pal::pkg_config_val("lang")) {
lang <- rlang::arg_match(arg = lang,
values = all_langs)
raw_qstnr_suppl_skill_question(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr,
skill_question_nr = skill_question_nr) |>
purrr::chuck(lang)
}
#' Get skill question response options
#'
#' Returns the response options of the specified skill question together with the information whether they are correct or not.
#'
#' @inheritParams skill_question
#'
#' @return `r pkgsnip::return_lbl("tibble")`
#' @family predicate_skill_question
#' @export
#'
#' @examples
#' fokus::skill_question_response_options(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1,
#' skill_question_nr = 2)
skill_question_response_options <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton),
canton = cantons(ballot_date),
proposal_nr = NULL,
skill_question_nr = 1L) {
raw_qstnr_suppl_skill_question(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr,
skill_question_nr = skill_question_nr) |>
purrr::chuck("response_option") |>
purrr::map(tibble::as_tibble) |>
purrr::list_rbind()
}
#' Get correct skill question answer number
#'
#' Returns the sequential number of the correct answer for the specified skill question.
#'
#' @inheritParams skill_question
#'
#' @return An integer scalar.
#' @family predicate_skill_question
#' @export
#'
#' @examples
#' fokus::skill_question_answer_nr(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1,
#' skill_question_nr = 2)
skill_question_answer_nr <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton),
canton = cantons(ballot_date),
proposal_nr = NULL,
skill_question_nr = 1L) {
raw_qstnr_suppl_skill_question(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr,
skill_question_nr = skill_question_nr) |>
purrr::chuck("response_option") |>
purrr::map_depth(.depth = 1L,
.f = \(x) x$is_correct) |>
purrr::list_c(ptype = logical()) |>
which()
}
#' Get referendum proposal numbers with skill questions
#'
#' Determines the referendum proposal numbers covered by the FOKUS survey for the specified canton at the specified ballot date on the specified political
#' level that have at least one skill question.
#'
#' @inheritParams proposal_nrs
#'
#' @return An integer vector.
#' @family predicate_skill_question
#' @export
#'
#' @examples
#' fokus::skill_question_proposal_nrs(ballot_date = "2018-09-23",
#' lvl = "cantonal",
#' canton = "aargau")
skill_question_proposal_nrs <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "referendum"),
canton = cantons(ballot_date)) {
qstn_groups_proposal_nrs(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
qstn_groups = "skill_question")
}
#' Get ballot title
#'
#' Returns the ballot title consisting of the [ballot type][ballot_types()] and the ballot date, phrased in German.
#'
#' @inheritParams lvls
#' @inheritParams lang_to_locale
#'
#' @return A character scalar.
#' @family predicate_other
#' @export
#'
#' @examples
#' fokus::ballot_title(ballot_date = "2019-10-20",
#' canton = "aargau")
ballot_title <- function(ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date),
lang = pal::pkg_config_val("lang")) {
ballot_date %<>% as_ballot_date()
lang <- rlang::arg_match(arg = lang,
values = all_langs)
ballot_types <- ballot_types(ballot_date = ballot_date,
canton = canton)
if (!length(ballot_types)) {
cli::cli_abort("No ballot type could be determined. Please debug.",
.internal = TRUE)
}
if (lang == "de") {
result <-
ballot_types |>
pal::when(length(.) > 1L ~ "Abstimmungs- und Wahl",
. == "referendum" ~ "Abstimmungs",
. == "election" ~ "Wahl") |>
paste0("termin vom ", stringi::stri_datetime_format(time = ballot_date,
format = "date_long",
locale = lang_to_locale(lang)))
} else if (lang == "en") {
result <-
ballot_types |>
pal::when(length(.) > 1L ~ "Referendum and election",
~ stringr::str_to_sentence(.)) |>
paste0(" date of ", stringi::stri_datetime_format(time = ballot_date,
format = "date_long",
locale = lang_to_locale(lang)))
}
result
}
#' Get political issues
#'
#' Returns the political issues in the specified language at the specified ballot date.
#'
#' @inheritParams proposal_name
#'
#' @return A character vector.
#' @family predicate_other
#' @export
#'
#' @examples
#' fokus::political_issues(ballot_date = "2019-10-20",
#' lang = "en")
political_issues <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lang = pal::pkg_config_val("lang")) {
lang <- rlang::arg_match(arg = lang,
values = all_langs)
result <-
raw_qstnr_suppl(ballot_date = ballot_date) |>
purrr::pluck("political_issues")
if (is.null(result)) {
# reduce to proper arg value for error msg
ballot_date %<>% as_ballot_date()
cli::cli_abort("No political issues present in the supplemental {.val {ballot_date}} FOKUS questionnaire data.")
}
result |>
purrr::chuck("issue") |>
purrr::map_depth(.depth = 1L,
.f = \(x) purrr::chuck(x, lang)) |>
purrr::list_c(ptype = character())
}
#' Get postal dispatch way
#'
#' Returns the FOKUS survey's postal dispatch way of the specified type for the specified canton at the specified ballot date.
#'
#' @inheritParams lvls
#' @param dispatch_type Postal dispatch type. One of `r pal::enum_fn_param_defaults(param = "dispatch_type", fn = postal_dispatch_way)`.
#'
#' @return A character scalar.
#' @family predicate_other
#' @family postal_dispatch
#' @export
#'
#' @examples
#' fokus::postal_dispatch_way(ballot_date = "2018-09-23",
#' canton = "aargau",
#' dispatch_type = "invitation")
postal_dispatch_way <- function(ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date),
dispatch_type = all_postal_dispatch_types) {
dispatch_type <- rlang::arg_match(dispatch_type)
raw_qstnr_suppl_mode(ballot_date = ballot_date,
canton = canton) |>
purrr::chuck("postal_dispatch", "type", dispatch_type)
}
#' Get response options
#'
#' Returns response options of the specified type and optionally subtype(s) from the [raw FOKUS questionnaire data][raw_qstnr].
#'
#' Note that only recurring response options are returned which are defined under the `response_options` top-level key in the file
#' `data-raw/questionnaire/questionnaire.toml`.
#'
#' @inheritParams lang_to_locale
#' @param type Response option type. One of
#' `r pal::as_md_val_list(all_response_option_types)`
#' @param subtypes Hierarchical response option subtypes as a character vector, or `NULL`. If `NULL`, all subtypes are returned.
#'
#' @return A character vector.
#' @family predicate_other
#' @export
#'
#' @examples
#' fokus:::response_options(type = "abstain",
#' lang = "de")
#'
#' fokus:::response_options(type = "abstain",
#' lang = "de",
#' subtypes = "election")
#'
#' fokus:::response_options(type = "abstain",
#' lang = "de",
#' subtypes = c("election", "proportional"))
response_options <- function(type = all_response_option_types,
lang = pal::pkg_config_val("lang"),
subtypes = NULL) {
type <- rlang::arg_match(type)
lang <- rlang::arg_match(arg = lang,
values = all_langs)
checkmate::assert_character(subtypes,
any.missing = FALSE,
min.len = 1L,
null.ok = TRUE)
raw_qstnr |>
purrr::chuck("response_options", type, lang, !!!subtypes) |>
pal::as_chr()
}
#' Questionnaire data
#'
#' A tibble containing all FOKUS questionnaires as structured data.
#'
#' `qstnrs` was generated based on the following steps:
#'
#' 1. [gen_qstnr_tibble()] was run for all valid combinations of `canton` and `ballot_date`, some validation checks were performed, and the results were merged
#' into a single tibble.
#' 2. List columns were [expanded][expand_qstnr_tibble] to [long format](https://en.wikipedia.org/wiki/Wide_and_narrow_data).
#' 3. Columns [`question_intro_i` and `question_intro_j`](https://fokus.rpkg.dev/articles/raw_qstnr_schema.html#supported-keys) were merged into the single
#' column `question_intro`.
#' 4. Column [`question_full`](https://fokus.rpkg.dev/articles/raw_qstnr_schema.html#supported-keys) was complemented, i.e. made to fall back on `question` if
#' `NA`.
#' 5. Columns [`question_common`](https://fokus.rpkg.dev/articles/raw_qstnr_schema.html#supported-keys) and
#' [`variable_label_common`](https://fokus.rpkg.dev/articles/raw_qstnr_schema.html#supported-keys) were complemented, i.e. made to fall back on
#' `question_full` and `variable_label` respectively if `NA`.
#' 6. Markdown formatting was [stripped][pal::strip_md] from all character columns.
#'
#' @format `r pkgsnip::return_lbl("tibble")`
#' @family qstnr_data
#'
#' @examples
#' fokus::qstnrs
"qstnrs"
#' Proposal data
#'
#' A tibble containing basic referendum proposal data of all FOKUS questionnaires.
#'
#' @format `r pkgsnip::return_lbl("tibble")`
#' @family qstnr_data
#'
#' @examples
#' fokus::proposals
"proposals"
#' Election data
#'
#' A tibble containing basic election data of all FOKUS questionnaires.
#'
#' @format `r pkgsnip::return_lbl("tibble")`
#' @family qstnr_data
#'
#' @examples
#' fokus::elections
"elections"
#' Ballot data
#'
#' A list of tibbles containing official statistical data about all FOKUS-covered ballots. The data includes i.a. the total electorate as well as participation
#' numbers per voting channel and per voting decision.
#'
#' @format `r pkgsnip::return_lbl("tibble")`
#' @family qstnr_data
#'
#' @examples
#' fokus::ballots
"ballots"
#' Read in easyvote municipality data
#'
#' Reads in the latest dataset of easyvote municipality information provided to us not earlier than 90 days before and up until 20 days after the `ballot_date`.
#'
#' If both columns `min_age` and `max_age` are `NA` in the data returned, it means that
#'
#' - the municipality did not provide easyvote with specific information on the target age range, and
#' - the municipality has subscribed to parcel mailing (instead of direct delivery to households) and delivers the brochures itself -- very likely to young
#' adults between 18--25 years.
#'
#' @inheritParams lvls
#' @inheritParams read_private_file
#'
#' @return `r pkgsnip::param_lbl("tibble")`
#' @family data_import
#' @export
#'
#' @examples
#' # GitLab PAT with access to the private FOKUS repository is required for this function to work
#' try(
#' fokus::read_easyvote_municipalities(ballot_date = "2020-09-27",
#' canton = "aargau") |>
#' colnames()
#' )
read_easyvote_municipalities <- function(ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date),
use_cache = TRUE,
auth_token = pal::pkg_config_val("token_repo_private")) {
ballot_date %<>% as_ballot_date()
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
# get date of latest dataset delivered *not earlier than 90 days before ballot date* and *up until 20 days after ballot date*
date_boundary_lower <- ballot_date - 90L
date_boundary_upper <- ballot_date + 20L
date_data <-
gitlab::dir_ls(ref = repo_private_default_branch,
path = "raw",
id_project = repo_private_proj_id,
token = auth_token) |>
dplyr::pull("path") |>
stringr::str_subset(pattern = stringr::fixed("raw/easyvote_municipalities_")) |>
stringr::str_extract(glue::glue("\\d{{4}}-\\d{{2}}-\\d{{2}}(?=_{canton}\\.csv$)")) |>
clock::date_parse() %>%
magrittr::extract(. >= date_boundary_lower & . <= date_boundary_upper) |>
pal::safe_max()
if (length(date_data) == 0L) {
cli::cli_abort(paste0("No easyvote municipality data present for canton {.val {canton}} with effective date at minimum 90 days before and at maximum 20 ",
"days after the ballot date {.val {ballot_date}}."))
}
read_private_file(path = glue::glue("raw/easyvote_municipalities_{date_data}_{canton}.csv"),
use_cache = use_cache,
auth_token = auth_token) |>
readr::read_csv(col_types = "ciii")
}
#' Read in online participation codes
#'
#' Reads in the online participation codes externally generated by the survey institute that are necessary to [generate QR codes with personalized survey
#' URLs][export_qr_codes].
#'
#' @inheritParams read_easyvote_municipalities
#'
#' @return A character vector.
#' @family data_import
#' @export
#'
#' @examples
#' # GitLab PAT with access to the private FOKUS repository is required for this function to work
#' try(
#' fokus::read_online_participation_codes(ballot_date = "2018-11-25",
#' canton = "aargau") |>
#' length()
#' )
read_online_participation_codes <- function(ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date),
use_cache = TRUE,
auth_token = pal::pkg_config_val("token_repo_private")) {
ballot_date %<>% as_ballot_date()
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
path <- glue::glue("raw/online_participation_codes_{ballot_date}_{canton}.txt")
if (!gitlab::file_exists(path = path,
id_project = repo_private_proj_id,
ref = repo_private_default_branch,
token = auth_token)) {
cli::cli_abort("No online participation codes present for canton {.val {canton}} @ {.val {ballot_date}}.")
}
read_private_file(path = path,
use_cache = use_cache,
auth_token = auth_token) |>
stringr::str_split_1(pattern = stringr::fixed("\n"))
}
#' Read in generated survey data
#'
#' @description
#' Reads in the FOKUS survey dataset exported by [export_survey_data()] for the specified ballot date in the specified canton and language, optionally merged
#' with all prior survey datasets for that canton and language.
#'
#' @includeRmd data-raw/snippets/merged_data.Rmd
#'
#' @inheritParams ballot_title
#' @inheritParams read_private_file
#' @param merged Whether or not to read in the *merged* dataset that includes the data from **all** ballot dates up until `ballot_date` that were covered by
#' FOKUS surveys in the specified `canton`. See section *Merged data* below for details. If `FALSE`, only the data for `ballot_date` itself is returned.
#'
#' @return `r pkgsnip::return_lbl("tibble")`
#' @family data_import
#' @export
#'
#' @examples
#' # GitLab PAT with access to the private FOKUS repository is required for this function to work
#' try(
#' fokus::read_survey_data(ballot_date = "2023-06-18",
#' canton = "aargau",
#' lang = "en") |>
#' nrow()
#' )
#'
#' try(
#' fokus::read_survey_data(ballot_date = "2023-06-18",
#' canton = "aargau",
#' lang = "en",
#' merged = TRUE) |>
#' nrow()
#' )
read_survey_data <- function(ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date),
lang = pal::pkg_config_val("lang"),
merged = FALSE,
use_cache = TRUE,
auth_token = pal::pkg_config_val("token_repo_private")) {
ballot_date %<>% as_ballot_date()
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
lang <- rlang::arg_match(arg = lang,
values = all_langs)
checkmate::assert_flag(merged)
read_private_file(path = glue::glue("generated/survey_data_", ifelse(merged, "merged_", ""), "{lang}_{ballot_date}_{canton}.rds"),
use_cache = use_cache,
auth_token = auth_token) |>
memDecompress() |>
unserialize()
}
#' Read in extra voting register data
#'
#' Reads in the raw extra voting register data provided by the statistical office, performs various integrity checks and returns it in tidy shape.
#'
#' @inheritParams read_easyvote_municipalities
#'
#' @return `r pkgsnip::return_lbl("tibble")`
#' @family data_import
#' @export
#'
#' @examples
#' # GitLab PAT with access to the private FOKUS repository is required for this function to work
#' try(
#' fokus::read_voting_register_data_extra(ballot_date = "2019-10-20",
#' canton = "aargau") |>
#' colnames()
#' )
read_voting_register_data_extra <- function(ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date),
use_cache = TRUE,
auth_token = pal::pkg_config_val("token_repo_private")) {
ballot_date %<>% as_ballot_date()
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
rlang::check_installed("readxl",
reason = pal::reason_pkg_required())
# get date of latest dataset delivered *before* ballot date
date_data <-
gitlab::dir_ls(ref = repo_private_default_branch,
path = "raw",
id_project = repo_private_proj_id,
token = auth_token) |>
dplyr::pull("path") |>
stringr::str_subset(pattern = stringr::fixed("raw/voting_register_data_extra_")) |>
stringr::str_extract(glue::glue("\\d{{4}}-\\d{{2}}-\\d{{2}}(?=_{canton}\\.xlsx$)")) |>
clock::date_parse() %>%
magrittr::extract(. < ballot_date) |>
pal::safe_max()
if (length(date_data) == 0L) {
cli::cli_abort("No voting register data present for canton {.val {canton}} with effective date before the ballot on {.val {ballot_date}}.")
}
# NOTE: `readxl::read_xlsx()` can only read from file, so we have to temporarily write the file to disk
tmp_file <- fs::file_temp(pattern = glue::glue("voting_register_data_extra_{date_data}_{canton}"),
ext = "xlsx")
read_private_file(path = glue::glue("raw/voting_register_data_extra_{date_data}_{canton}.xlsx"),
use_cache = use_cache,
auth_token = auth_token) |>
brio::write_file_raw(path = tmp_file)
data <-
readxl::read_xlsx(path = tmp_file,
col_types = "text") |>
# rename variables to our scheme
dplyr::rename(id = `ID-Nummer`,
sex_official = Geschlecht,
year_of_birth_official = Jahrgang,
marital_status_official = Zivilstand,
household_size_official = "Haushaltsgr\u00f6sse Anzahl Personen Total",
n_adults_in_household_official = "Haushaltsgr\u00f6sse Anzahl Personen \u00fcber 18 Jahren",
n_kids_in_household_official = "Haushaltsgr\u00f6sse Anzahl Personen unter 18 Jahren") |>
# convert numeric columns to type integer
dplyr::mutate(dplyr::across(c(id,
year_of_birth_official,
household_size_official,
n_adults_in_household_official,
n_kids_in_household_official),
as.integer)) |>
# transform variable values to our scheme
dplyr::mutate(dplyr::across(c(sex_official, marital_status_official),
stringr::str_to_lower)) |>
dplyr::mutate(marital_status_official = dplyr::case_match(.x = marital_status_official,
"eingetragene partnerschaft" ~ "in eingetragener Partnerschaft",
"aufgel\u00f6ste partnerschaft" ~ "aufgel\u00f6ste Partnerschaft",
"unverheiratet" ~ "ledig",
.default = marital_status_official))
# integrity check 1: ensure no unexpected columns occur
if (ncol(data) > 7L) {
unknown_colnames <-
colnames(data) |>
setdiff(c(id,
sex_official,
year_of_birth_official,
marital_status_official,
household_size_official,
n_adults_in_household_official,
n_kids_in_household_official))
cli::cli_abort("Unexpected column(s) detected in private file {.file raw/voting_register_data_extra_{date_data}_{canton}.xlsx}: {.val unknown_colnames}",
.internal = TRUE)
}
# integrity check 2: ensure no unexpected values occur
## in `sex_official`
unknown_sex_official_i <-
data$sex_official |>
magrittr::is_in(var_val_set(var_name = "sex_official",
ballot_date = ballot_date,
canton = canton,
lang = "de")) |>
magrittr::not() |>
which()
if (length(unknown_sex_official_i) > 0L) {
cli::cli_abort("{.var sex_official} in raw extra voting register data has unknown values: {.val {unique(data$sex_official[unknown_sex_official_i])}}")
}
## in `marital_status_official`
unknown_marital_status_official_i <-
data$marital_status_official |>
magrittr::is_in(var_val_set(var_name = "marital_status_official",
ballot_date = ballot_date,
canton = canton,
lang = "de")) |>
magrittr::not() |>
which()
if (length(unknown_marital_status_official_i)) {
cli::cli_abort(paste0("{.var marital_status_official} in raw extra voting register data has unknown values: ",
"{.val {unique(data$sex_official[unknown_marital_status_official_i])}}"))
}
data
}
#' Read in voting register identifiers
#'
#' Reads in the voting register identifiers of the population that was invited to participate in the FOKUS survey at the specified ballot date in the specified
#' canton.
#'
#' Note that this data is not available for all FOKUS surveys.
#'
#' @inheritParams read_easyvote_municipalities
#'
#' @return `r pkgsnip::return_lbl("tibble_cols", cols = "id_voting_register")`
#' @family data_import
#' @export
#'
#' @examples
#' # GitLab PAT with access to the private FOKUS repository is required for this function to work
#' try(
#' fokus::read_voting_register_ids(ballot_date = "2019-10-20",
#' canton = "aargau") |>
#' nrow()
#' )
read_voting_register_ids <- function(ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date),
use_cache = TRUE,
auth_token = pal::pkg_config_val("token_repo_private")) {
ballot_date %<>% as_ballot_date()
canton <- rlang::arg_match(arg = canton,
values = all_cantons)
path <- glue::glue("raw/voting_register_ids_{ballot_date}_{canton}.csv")
if (!gitlab::file_exists(path = path,
id_project = repo_private_proj_id,
ref = repo_private_default_branch,
token = auth_token)) {
cli::cli_abort("No voting register ID data present for canton {.val {canton}} @ {.val {ballot_date}}.")
}
read_private_file(path = glue::glue("raw/voting_register_ids_{ballot_date}_{canton}.csv"),
use_cache = use_cache,
auth_token = auth_token) |>
readr::read_csv(col_types = "i") |>
# integrity check
pal::when(ncol(.) > 1L ~ cli::cli_abort("More than one column present in {.file {path}}. Please debug.",
.internal = TRUE),
~ .) |>
dplyr::rename(id_voting_register = 1L)
}
#' Read in file from private FOKUS repository
#'
#' Downloads a file from the [private FOKUS repository](`r url_repo_private()`) and returns it as a character scalar in case of a text file, or a [raw] vector
#' otherwise.
#'
#' Files are downloaded via [GitLab's RESTful API (v4)](https://docs.gitlab.com/ee/api/rest/). If `use_cache = TRUE` (the default), a downloaded file is cached
#' on disk in this package's [user-cache pins board][pkgpins::board] and only newly fetched from the private FOKUS repository GitLab remote if is has changed
#' since being downloaded the last time. Caching saves a bit of time and (potentially) a lot of bandwidth.
#'
#' @param path File path relative to the repository root.
#' @param use_cache Whether or not to return cached results if possible. Caching is done based on file content hashing, so the file is only newly fetched if it
#' actually changed since the last download. If `FALSE`, the file is always newly fetched.
#' @param auth_token [Personal access token of a gitlab.com account](https://gitlab.com/-/user_settings/personal_access_tokens) with access to the [private
#' FOKUS repository](`r url_repo_private()`).
#'
#' @return The file content, as character scalar for text files, otherwise as a [raw vector][raw].
#' @family data_import
#' @family private
#' @export
#'
#' @examples
#' # GitLab PAT with access to the private FOKUS repository is required for this function to work
#' try(
#' fokus::read_private_file("raw/survey_data_2018-09-23_aargau.xlsx") |>
#' length()
#' )
read_private_file <- function(path,
use_cache = TRUE,
auth_token = pal::pkg_config_val("token_repo_private")) {
checkmate::assert_string(path)
pkgpins::with_cache(expr = gitlab::file_content(path = path,
id_project = repo_private_proj_id,
ref = repo_private_default_branch,
token = auth_token),
pkg = this_pkg,
from_fn = "read_private_file",
gitlab::file_meta(path = path,
attribute = "content_sha256",
id_project = repo_private_proj_id,
ref = repo_private_default_branch,
token = auth_token),
use_cache = use_cache,
max_cache_age = Inf)
}
#' Tidy columns
#'
#' Renames a data frame's columns and converts them to their proper types. Aimed at datasets published by cantonal statistical offices.
#'
#' @param data Data frame whose columns are to be converted.
#' @param default_type [readr column type][readr::col_character] to which columns are to be converted for which no explicit type is predefined.
#'
#' @return `data`
#' @family data_import
#' @export
tidy_cols <- function(data,
default_type = readr::col_character()) {
data |>
# rename cols
pal::rename_from(dict = dicts$colnames$ballots) |>
# remove `.0` suffix from numbers to avoid warnings from `readr::type_convert()` below
dplyr::mutate(dplyr::across(.cols = where(is.character),
.fns = \(x) stringr::str_replace(string = x,
pattern = "^(\\d+)\\.0$",
replacement = "\\1"))) %>%
# tidy col types
# NOTE: we must reduce `col_types` to the ones actually present in `data` to avoid warnings
readr::type_convert(col_types = readr::cols(!!!col_types[intersect(names(col_types),
colnames(.))],
.default = default_type))
}
#' Export questionnaire data
#'
#' Generates the [questionnaire tibble][gen_qstnr_tibble], the [Markdown questionnaire][gen_qstnr_md] and optionally a CSV, an HTML and an XLSX version of it,
#' and writes all of them to `path` and optionally uploads them to a Google Drive folder.
#'
#' The generated files are named according to the scheme `{ballot_date}_{canton}.{ext}`, so if the `qstnr_tibble` of the 2018-09-23 survey in the canton of
#' Aargau is input, the following files will be written to `path` by default:
#'
#' - `2018-09-23_aargau.csv`
#' - `2018-09-23_aargau.html`
#' - `2018-09-23_aargau.md`
#' - `2018-09-23_aargau.xlsx`
#'
#' @inheritParams expand_qstnr_tibble
#' @param path Path to the directory to write the generated questionnaire files to. A character scalar.
#' @param verbose Whether or not to print detailed progress information during questionnaire generation and Google Drive file upload. Note that questionnaire
#' generation takes considerably more time when this is set to `TRUE`.
#' @param incl_csv Whether or not to also generate and export a CSV version of the questionnaire.
#' @param incl_html Whether or not to also generate and export an HTML version of the questionnaire.
#' @param incl_xlsx Whether or not to also generate and export an XLSX version of the questionnaire.
#' @param upload_to_g_drive Whether or not to upload the generated files to the Google Drive folder `g_drive_folder`.
#' @param g_drive_folder Google Drive folder to upload the generated files to. Ignored if `upload_to_g_drive = FALSE`.
#'
#' @return `path`, invisibly.
#' @family data_export
#' @export
export_qstnr <- function(qstnr_tibble,
path,
verbose = FALSE,
incl_csv = TRUE,
incl_html = TRUE,
incl_xlsx = incl_html,
upload_to_g_drive = FALSE,
g_drive_folder = "fokus/aargau/Umfragen/Dateien f\u00fcr Umfrageinstitut/Fragebogen/") {
path <- fs::path_abs(path)
fs::dir_create(path)
checkmate::assert_flag(incl_csv)
checkmate::assert_flag(incl_html)
checkmate::assert_flag(incl_xlsx)
if (incl_xlsx && !incl_html) {
cli::cli_abort("{.arg incl_html} must be set to {.val TRUE} when {.code incl_xlsx = TRUE} because the XLSX file is generated from the HTML file.")
}
checkmate::assert_flag(upload_to_g_drive)
rlang::check_installed("rmarkdown",
reason = pal::reason_pkg_required())
rlang::check_installed("yay",
reason = pal::reason_pkg_required())
# extract ballot date and canton from `qstnr_tibble`
ballot_date <- as_ballot_date(unique(qstnr_tibble$ballot_date))
canton <- rlang::arg_match0(arg = unique(qstnr_tibble$canton),
values = all_cantons,
arg_nm = "unique(qstnr_tibble$canton)")
# Generate Markdown version
md_path <- fs::path(path, glue::glue("{ballot_date}_{canton}.md"))
qstnr_tibble |>
gen_qstnr_md() |>
brio::write_lines(path = md_path)
# create CSV version from tibble if requested
if (incl_csv) {
pal::cli_progress_step_quick(msg = "Converting {.val {canton}} @ {.val {ballot_date}} questionnaire tibble to CSV")
csv_path <- fs::path_ext_set(path = md_path,
ext = "csv")
qstnr_tibble |>
clean_qstnr_tibble() |>
dplyr::mutate(variable_name_32 =
purrr::map2_chr(.x = variable_name,
.y = dplyr::if_else(block %in% c("x_polling_agency", "y_generated", "z_generated")
| stringr::str_detect(string = variable_name,
pattern = paste0("^", pal::fuse_regex(c("agreement_contra_argument_",
"information_source_",
"reason_non_participation_",
"political_occasions_")))),
32L,
30L),
.f = ~ shorten_var_names(var_names = .x,
max_n_char = .y)),
.after = variable_name) |>
dplyr::select(-ends_with("_common"),
-c(lvl,
i,
j,
question_intro_i,
question_intro_j,
question_full)) |>
expand_qstnr_tibble() |>
readr::write_csv(file = csv_path,
na = "")
cli::cli_progress_done()
}
# create HTML version from Markdown questionnaire if requested
if (incl_html) {
html_path <- fs::path_ext_set(path = md_path,
ext = "html")
pal::cli_progress_step_quick(msg = "Converting {.val {canton}} @ {.val {ballot_date}} Markdown questionnaire to HTML using Pandoc")
rmarkdown::pandoc_convert(input = md_path,
to = "html5",
from = "markdown",
output = html_path,
options = c("--standalone",
"--css=github-pandoc.css",
glue::glue('--metadata=title:FOKUS-{ stringr::str_to_sentence(canton) }-Fragebogen f\u00fcr den ',
ballot_title(ballot_date = ballot_date,
canton = canton))),
verbose = FALSE)
cli::cli_progress_done()
}
# create XLSX version from HTML questionnaire if requested
if (incl_xlsx) {
pal::cli_progress_step_quick(msg = "Converting {.val {canton}} @ {.val {ballot_date}} HTML questionnaire to XLSX using LibreOffice")
system2(command = "flatpak",
args = glue::glue("run --command=libreoffice",
"org.libreoffice.LibreOffice",
"--calc",
"--headless",
"--convert-to xlsx",
"--outdir \"{path}\"",
"\"{html_path}\"",
.sep = " "),
stdout = ifelse(verbose, "", FALSE))
cli::cli_progress_done()
}
# upload files to Google Drive for polling agency if requested
if (upload_to_g_drive) {
upload_to_g_drive(filepaths = c(md_path,
csv_path[incl_csv],
html_path[incl_html],
fs::path(path, "github-pandoc.css")[incl_html],
fs::path_ext_set(path = html_path,
ext = "xlsx")[incl_xlsx]),
g_drive_folder = g_drive_folder,
quiet = !verbose)
}
invisible(path)
}
#' Export QR codes with personalized survey URL
#'
#' Exports a ZIP file, that contains a [QR code](https://en.wikipedia.org/wiki/QR_code) in SVG and in EPS format for each survey participant storing the
#' personalized survey URL, to the [private FOKUS repository][print_private_repo_structure].
#'
#' @inheritParams lvls
#' @inheritParams export_qstnr
#' @inheritParams upload_to_g_drive
#' @inheritParams read_online_participation_codes
#' @param quiet `r pkgsnip::param_lbl("quiet")`
#' @param verbose Whether or not to print detailed status output from [Google Drive file upload][upload_to_g_drive].
#'
#' @return A [tibble][tibble::tbl_df] containing metadata about the contents of the created ZIP archive, invisibly.
#' @family data_export
#' @export
export_qr_codes <- function(ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date),
upload_to_g_drive = TRUE,
g_drive_folder = glue::glue("fokus/{canton}/Umfragen/Dateien f\u00fcr Umfrageinstitut/QR-Codes/"),
auth_token = pal::pkg_config_val("token_repo_private"),
quiet = FALSE,
verbose = FALSE) {
ballot_date %<>% as_ballot_date()
canton <- rlang::arg_match(arg = canton,
values = cantons(ballot_date))
checkmate::assert_flag(upload_to_g_drive)
checkmate::assert_flag(quiet)
checkmate::assert_flag(verbose)
rlang::check_installed("archive",
reason = pal::reason_pkg_required())
rlang::check_installed("qrencoder",
reason = pal::reason_pkg_required())
rlang::check_installed("rsvg",
reason = pal::reason_pkg_required())
participation_codes <- read_online_participation_codes(ballot_date = ballot_date,
canton = canton,
auth_token = auth_token)
tmp_dir <-
glue::glue("fokus_qr_codes_{ballot_date}_{canton}") |>
fs::path_temp() |>
fs::dir_create()
on.exit(fs::dir_delete(tmp_dir),
add = TRUE,
after = FALSE)
tmp_dir_svg <-
fs::path(tmp_dir, "svg") |>
fs::dir_create()
tmp_dir_eps <-
fs::path(tmp_dir, "eps") |>
fs::dir_create()
# create SVG and EPS image files
if (!quiet) {
cli_id <- pal::cli_progress_step_quick(
msg = "Generating {length(participation_codes)} personalized QR code{?s} in SVG and EPS format for canton {.val {canton}} @ {.val {ballot_date}}"
)
}
participation_codes |>
purrr::walk2(.progress = !quiet,
.x = participation_codes,
.y = _,
.f = ~ {
path_svg <- fs::path(tmp_dir_svg, .x,
ext = "svg")
# create SVG file
url <- url_survey_host |> purrr::chuck(canton)
url_parameter <- url_parameter_survey |> purrr::chuck(canton)
qrencoder::qrencode_svg(to_encode = glue::glue("{url}?{url_parameter}={.x}"),
level = 3L) |>
brio::write_file(path = path_svg)
# create EPS file from SVG file
rsvg::rsvg_eps(svg = path_svg,
file = fs::path(tmp_dir_eps, .x,
ext = "eps"))
})
if (!quiet) {
cli::cli_progress_done(id = cli_id)
}
# create ZIP archive of SVG and EPS files
if (!quiet) {
cli_id <- pal::cli_progress_step_quick(msg = "Compressing SVG and EPS QR code files to ZIP archive")
}
filename_zip <- glue::glue("{ballot_date}_{canton}_qr_codes.zip")
tmp_path_zip <- fs::path(tmp_dir, filename_zip)
result <- archive::archive_write_dir(archive = tmp_path_zip,
dir = tmp_dir,
format = "zip")
if (!quiet) {
cli::cli_progress_done(id = cli_id)
}
# upload ZIP archive to private repo
write_private_file(path = fs::path("generated/for-polling-agency", filename_zip),
content = tmp_path_zip,
from_file = TRUE,
auth_token = auth_token,
quiet = quiet)
# upload ZIP archive to Google Drive for polling agency if requested
if (upload_to_g_drive) {
upload_to_g_drive(filepaths = tmp_path_zip,
g_drive_folder = g_drive_folder,
quiet = quiet || !verbose)
}
invisible(result)
}
#' Export print recipients data
#'
#' Exports a CSV dataset containing the two columns `id` and `receives_print` to the [private FOKUS repository][print_private_repo_structure].
#'
#' @inheritParams export_qr_codes
#'
#' @return `NULL` if no export for the specified ballot date is possible, otherwise a [tibble][tibble::tbl_df] of the exported data, invisibly.
#' @family data_export
#' @export
export_print_recipients <- function(ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date),
upload_to_g_drive = TRUE,
g_drive_folder = glue::glue("fokus/{canton}/Umfragen/Dateien f\u00fcr Umfrageinstitut/Print-Zielgruppen/"),
auth_token = pal::pkg_config_val("token_repo_private"),
quiet = FALSE,
verbose = FALSE) {
ballot_date %<>% as_ballot_date()
canton <- rlang::arg_match(arg = canton,
values = cantons(ballot_date))
checkmate::assert_flag(upload_to_g_drive)
checkmate::assert_flag(quiet)
checkmate::assert_flag(verbose)
# only export if `reminder_print_*` constraint present
if (raw_qstnr_suppl_mode(ballot_date = ballot_date,
canton = canton) |>
purrr::pluck("constraints") |>
stringr::str_detect("^reminder_print_.+") |>
any()) {
pal::cli_progress_step_quick(msg = "Exporting print recipients data for canton {.val {canton}} @ {.val {ballot_date}}")
# read in statistical office IDs used for current survey
ids <-
read_voting_register_ids(ballot_date = ballot_date,
canton = canton,
auth_token = auth_token) |>
dplyr::pull("id_voting_register")
# ensure output folder exists
filename <- glue::glue("{ballot_date}_{canton}_print_recipients.csv")
tmp_path <- fs::path_temp(filename)
on.exit(fs::file_delete(tmp_path),
add = TRUE,
after = FALSE)
# export data
result <-
read_voting_register_data_extra(ballot_date = ballot_date,
canton = canton,
auth_token = auth_token) |>
dplyr::filter(id %in% !!ids) |>
dplyr::mutate(receives_print = year_of_birth_official < 1970L) |>
dplyr::select(id, receives_print) |>
readr::write_csv(file = tmp_path)
gitlab::file_write(content = tmp_path,
path = fs::path("generated/for-polling-agency", filename),
id_project = repo_private_proj_id,
start_branch = repo_private_default_branch)
write_private_file(path = fs::path("generated/for-polling-agency", filename),
content = tmp_path,
from_file = TRUE,
auth_token = auth_token,
quiet = quiet)
# upload data to Google Drive for polling agency if requested
if (upload_to_g_drive) {
upload_to_g_drive(filepaths = tmp_path,
g_drive_folder = g_drive_folder,
quiet = quiet || !verbose)
}
} else {
cli::cli_alert_info("No print recipients data export sensible or possible for canton {.val {canton}} @ {.val {ballot_date}}.")
result <- NULL
}
invisible(result)
}
#' Export easyvote municipality details
#'
#' Exports a CSV dataset containing the columns `municipality`, `municipality_id`, `min_age` and `max_age` to the [private FOKUS
#' repository][print_private_repo_structure].
#'
#' The meaning of the individual columns is as follows:
#'
#' | **column name** | **description** |
#' | --------------- | --------------- |
#' | `municipality` | official name of the municipality |
#' | `municipality_id` | [official Swiss community identification number](https://en.wikipedia.org/wiki/Community_Identification_Number#Switzerland) (also called "GEOSTAT"/"BFS" number, see the [corresponding German Wikipedia article](https://de.wikipedia.org/wiki/Gemeindenummer)) |
#' | `min_age` | `r var_lbl("easyvote_municipality_min_age")` |
#' | `max_age` | `r var_lbl("easyvote_municipality_max_age")` |
#'
#' @inheritParams export_qr_codes
#'
#' @return A [tibble][tibble::tbl_df] of the exported data, invisibly.
#' @family data_export
#' @export
export_easyvote_municipalities <- function(ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date),
upload_to_g_drive = TRUE,
g_drive_folder = glue::glue("fokus/{canton}/Umfragen/Dateien f\u00fcr Umfrageinstitut/easyvote-Gemeinden/"),
auth_token = pal::pkg_config_val("token_repo_private"),
quiet = FALSE,
verbose = FALSE) {
ballot_date %<>% as_ballot_date()
canton <- rlang::arg_match(arg = canton,
values = cantons(ballot_date))
checkmate::assert_flag(upload_to_g_drive)
checkmate::assert_flag(quiet)
checkmate::assert_flag(verbose)
file_basename <- glue::glue("{ballot_date}_{canton}_easyvote_municipalities")
tmp_path <- fs::file_temp(pattern = file_basename,
ext = "csv")
on.exit(fs::file_delete(tmp_path),
add = TRUE,
after = FALSE)
result <-
read_easyvote_municipalities(ballot_date = ballot_date,
canton = canton,
auth_token = auth_token) |>
# assume 18-25 age range if both min/max age are NA
dplyr::mutate(is_likely_default = is.na(min_age) & is.na(max_age),
min_age = dplyr::if_else(is_likely_default,
18L,
min_age),
max_age = dplyr::if_else(is_likely_default,
25L,
max_age)) |>
dplyr::select(-is_likely_default) |>
readr::write_csv(file = tmp_path)
write_private_file(path = fs::path("generated/for-polling-agency", file_basename,
ext = "csv"),
content = tmp_path,
from_file = TRUE,
auth_token = auth_token,
quiet = quiet)
if (upload_to_g_drive) {
upload_to_g_drive(filepaths = tmp_path,
g_drive_folder = g_drive_folder,
quiet = quiet || !verbose)
}
invisible(result)
}
#' Export generated survey data
#'
#' @description
#' Exports a FOKUS survey dataset generated via *TODO* to the [private FOKUS repository][print_private_repo_structure].
#'
#' @includeRmd data-raw/snippets/merged_data.Rmd
#'
#' @inheritParams write_private_file
#' @param data FOKUS survey dataset. `r pkgsnip::param_lbl("tibble")`
#' @param lang Language. Either `NULL` to read the language from `data`'s `fokus_lang` attribute or one of
#' `r all_langs |> pal::as_md_vals() |> pal::enum_str(sep2 = " or ")`.
#' @param merged Whether or not `data` is a *merged* dataset, i.e. one that includes the data from **all** ballot dates that were covered by FOKUS surveys up
#' until the most recent `ballot_date` contained in `data`. See section *Merged data* below for details.
#'
#' If `NULL`, a merged dataset is assumed if `data` covers multiple `ballot_date`s.
#'
#' @return `data`, invisibly.
#' @family data_export
#' @export
export_survey_data <- function(data,
lang = NULL,
merged = NULL,
auth_token = pal::pkg_config_val("token_repo_private")) {
lang <-
lang %||%
attr(data, "fokus_lang") %||%
cli::cli_abort("{.arg lang} must be explicitly specified since {.arg data} lacks a {.field fokus_lang} attribute to detect the language from.")
lang <- rlang::arg_match(arg = lang,
values = all_langs)
checkmate::assert_flag(merged,
null.ok = TRUE)
assert_var_names_present(data = data,
var_names = c("ballot_date", "canton"))
if (is.null(merged)) {
merged <- length(unique(data$ballot_date)) > 1L
}
ballot_date <- checkmate::assert_date(unique(data$ballot_date),
any.missing = FALSE,
min.len = 1L)
canton <- checkmate::assert_character(unique(data$canton),
any.missing = FALSE,
min.len = 1L)
# assemble target path
path <- "generated/survey_data_"
if (!merged) {
if (!checkmate::test_scalar(ballot_date)) {
cli::cli_abort("Column {.var ballot_date} mustn't contain different dates when {.code merged = FALSE}.")
}
if (!checkmate::test_scalar(canton)) {
cli::cli_abort("Column {.var canton} mustn't contain different cantons when {.code merged = FALSE}.")
}
path %<>% paste0("{lang}_{ballot_date}_{canton}")
} else {
if (length(ballot_date) > 1L) {
ballot_date %<>% max()
}
if (length(canton) > 1L) {
path %<>% paste0("merged_{lang}_{ballot_date}")
} else {
path %<>% paste0("merged_{lang}_{ballot_date}_{canton}")
}
}
# write data
data |>
serialize(connection = NULL,
xdr = FALSE) |>
memCompress(type = "xz") |>
write_private_file(path = glue::glue(path, ".rds"),
auth_token = auth_token)
invisible(data)
}
#' Write file to private FOKUS repository
#'
#' Uploads a file to the [private FOKUS repository](`r url_repo_private()`) via [GitLab's RESTful API (v4)](https://docs.gitlab.com/ee/api/rest/).
#'
#' @inheritParams read_private_file
#' @param content File content, as a character scalar for text files, or a [raw vector][raw] for binary files. Or the path to a local file as a character scalar
#' if `from_file = TRUE`.
#' @param from_file Whether or not `content` indicates the path to a local file instead of the actual file content.
#' @param overwrite Whether or not to overwrite an already existing file.
#' @param commit_message Git commit message for file creation/update.
#' @param branch Git branch name to upload the file to.
#' @param quiet `r pkgsnip::param_lbl("quiet")`
#'
#' @return `path`, invisibly.
#' @family data_export
#' @family private
#' @export
write_private_file <- function(path,
content,
from_file = FALSE,
overwrite = TRUE,
commit_message = "auto: update file via fokus R pkg",
branch = repo_private_default_branch,
auth_token = pal::pkg_config_val("token_repo_private"),
quiet = FALSE) {
gitlab::file_write(content = content,
path = path,
id_project = repo_private_proj_id,
start_branch = branch,
from_file = from_file,
overwrite = overwrite,
commit_message = commit_message,
token = auth_token,
quiet = quiet)
}
#' Get variable description
#'
#' Extracts a variable's (common) description from the [questionnaire data][qstnrs].
#'
#' If no `ballot_date` and `canton` are specified or `var_name` is not included at `ballot_date`,
#' [`variable_label_common`](https://fokus.rpkg.dev/articles/raw_qstnr_schema.html#supported-keys) is returned, otherwise
#' [`variable_label`](https://fokus.rpkg.dev/articles/raw_qstnr_schema.html#supported-keys). Note that `ballot_date` and `canton` either must both be `NULL` or
#' set to a valid canton name and ballot date respectively.
#'
#' @param var_name Variable name. A character scalar.
#' @param ballot_date `NULL` or a FOKUS-covered ballot date, i.e. one of
#' `r pal::as_md_val_list(as.character(all_ballot_dates))`
#' @param canton `NULL` or a FOKUS-covered canton name, i.e. one of
#' `r pal::as_md_val_list(all_cantons)`
#'
#' @return A character scalar.
#' @family vars
#' @export
#'
#' @examples
#' # if `ballot_date` and `canton` are not supplied, `variable_label_common` is returned:
#' fokus::var_lbl("participation_federal")
#'
#' # otherwise `variable_label`:
#' fokus::var_lbl(var_name = "participation_federal",
#' ballot_date = "2018-09-23",
#' canton = "aargau")
#'
#' fokus::var_lbl(var_name = "participation_federal",
#' ballot_date = "2019-10-20",
#' canton = "aargau")
var_lbl <- function(var_name,
ballot_date = NULL,
canton = NULL) {
is_common <- is.null(ballot_date) && is.null(canton)
if (!is_common) {
is_arg_invalid <- purrr::map_lgl(list(ballot_date, canton),
is.null)
arg_names <- c("ballot_date", "canton")
if (any(is_arg_invalid)) {
cli::cli_abort(paste0("Either {.arg ballot_date} and {.arg canton} must both be {.val NULL} or set to a valid canton name and ballot date, but ",
"{.arg {arg_names[is_arg_invalid]}} is {.val NULL} while {.arg {arg_names[!is_arg_invalid]}} is ",
"{.val {get(arg_names[!is_arg_invalid])}}."))
}
result <- var_predicate(predicate = "variable_label",
var_name = var_name,
ballot_date = ballot_date,
canton = canton)
is_common <- length(result) == 0L
}
if (is_common) {
result <-
fokus::qstnrs |>
dplyr::filter(variable_name == !!var_name) |>
dplyr::pull("variable_label_common") |>
unique()
}
result
}
#' Determine variable's political level(s)
#'
#' Determines political level(s) of one or more variables.
#'
#' Note that the political levels are determined by simply parsing `var_names`.
#'
#' @inheritParams is_skill_question_var
#'
#' @return A character vector of political levels, of length 0 if no specific levels could be determined.
#' @family vars
#' @export
#'
#' @examples
#' fokus::qstnrs |>
#' dplyr::mutate(lvls = purrr::map(variable_name,
#' fokus::var_lvls)) |>
#' dplyr::filter(purrr::map_lgl(lvls,
#' \(x) length(x) > 0)) |>
#' dplyr::select(variable_name, lvls) |>
#' tidyr::unnest_longer(col = lvls,
#' values_to = "lvl") |>
#' unique()
var_lvls <- function(var_names) {
checkmate::assert_character(var_names)
is_cantonal <- var_names |> stringr::str_detect(pattern = as_sym_part_regex("cantonal")) |> any()
is_federal <- var_names |> stringr::str_detect(pattern = as_sym_part_regex("federal")) |> any()
c("cantonal"[is_cantonal], "federal"[is_federal])
}
#' Determine variable's election procedure
#'
#' Determines the election procedure each variable corresponds to. In case no election procedure could be determined for a variable, `NA_character` is returned.
#'
#' @inherit is_skill_question_var details
#'
#' @inheritParams is_skill_question_var
#'
#' @return A character vector of the same length as `var_names`.
#' @family vars
#' @export
#'
#' @examples
#' fokus::qstnrs |>
#' dplyr::mutate(prcd = fokus::var_prcd(variable_name)) |>
#' dplyr::filter(!is.na(prcd)) |>
#' dplyr::select(variable_name, prcd) |>
#' unique()
var_prcd <- function(var_names) {
var_names |>
checkmate::assert_character() |>
stringr::str_extract(pattern = paste0(pal::fuse_regex(all_prcds), glue::glue("(?=_election{sym_part_regex_end})")))
}
#' Determine variable's election number
#'
#' Determines the election number each variable corresponds to. In case no election number could be determined for a variable, `NA_character` is returned.
#'
#' @inherit is_skill_question_var details
#'
#' @inheritParams is_skill_question_var
#'
#' @return An integer vector of the same length as `var_names`.
#' @family vars
#' @export
#'
#' @examples
#' fokus::qstnrs |>
#' dplyr::mutate(election_nr = fokus::var_election_nr(variable_name)) |>
#' dplyr::filter(!is.na(election_nr)) |>
#' dplyr::select(variable_name, election_nr) |>
#' unique()
var_election_nr <- function(var_names) {
var_names |>
checkmate::assert_character() |>
stringr::str_extract(glue::glue("(?<={sym_part_regex_start}election_)\\d+")) |>
as.integer()
}
#' Determine variable's proposal number
#'
#' Determines the proposal number each variable corresponds to. In case no proposal number could be determined for a variable, `NA_character` is returned.
#'
#' @inherit is_skill_question_var details
#'
#' @inheritParams is_skill_question_var
#'
#' @return An integer vector of the same length as `var_names`.
#' @family vars
#' @export
#'
#' @examples
#' fokus::qstnrs |>
#' dplyr::mutate(proposal_nr = fokus::var_proposal_nr(variable_name)) |>
#' dplyr::filter(!is.na(proposal_nr)) |>
#' dplyr::select(variable_name, proposal_nr) |>
#' unique()
var_proposal_nr <- function(var_names) {
var_names |>
checkmate::assert_character() |>
stringr::str_extract(glue::glue("(?<={sym_part_regex_start}proposal_)\\d+")) |>
as.integer()
}
#' Determine variable's skill question number
#'
#' Determines the skill question number each variable corresponds to. In case no skill question number could be determined for a variable, `NA_character` is
#' returned.
#'
#' @inherit var_proposal_nr details return
#'
#' @inheritParams is_skill_question_var
#'
#' @family vars
#' @export
#'
#' @examples
#' fokus::qstnrs |>
#' dplyr::mutate(skill_question_nr = fokus::var_skill_question_nr(variable_name)) |>
#' dplyr::filter(!is.na(skill_question_nr)) |>
#' dplyr::select(variable_name, skill_question_nr) |>
#' unique()
var_skill_question_nr <- function(var_names) {
var_names |>
checkmate::assert_character() |>
stringr::str_extract(glue::glue("(?<={sym_part_regex_start}skill_question_)\\d+")) |>
as.integer()
}
#' Get variable title
#'
#' Extracts a variable's title from the [questionnaire data][qstnrs], adapted to the actual [election_name()] if applicable.
#'
#' @inheritParams lvls
#' @inheritParams var_lbl
#'
#' @return `NULL` if the variable is not present at the specified `ballot_date` and `canton`, otherwise a character scalar.
#' @family vars
#' @export
#'
#' @examples
#' fokus::var_title(var_name = "weight_decision",
#' ballot_date = "2018-11-25",
#' canton = "aargau")
#'
#' fokus::var_title(var_name = "weight_decision",
#' ballot_date = "2019-10-20",
#' canton = "aargau")
#'
#' fokus::var_title(var_name = "voting_decision_cantonal_proportional_election_1_party",
#' ballot_date = "2024-10-20",
#' canton = "aargau")
var_title <- function(var_name,
ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date)) {
ballot_date %<>% as_ballot_date()
lang <- "de"
result <- var_predicate(predicate = "topic",
var_name = var_name,
ballot_date = ballot_date,
canton = canton)
# replace generic with actual election name
prcd <- var_prcd(var_name)
if (!is.na(prcd)) {
lvl <- checkmate::assert_string(var_lvls(var_name))
election_nr <- var_election_nr(var_name)
regex_cut <-
phrased_terms |>
dplyr::filter(term == "lvl") |>
dplyr::pull(!!lang) |>
paste(collapse = "|") %>%
paste0("(?=", ., ")")
if (has_election(ballot_date = ballot_date,
lvls = lvl,
canton = canton,
prcds = prcd)) {
result %<>%
stringr::str_extract(pattern = paste0("^.+?", regex_cut)) %>%
paste0(election_name(ballot_date = ballot_date,
lvl = lvl, canton = canton,
prcd = prcd,
election_nr = election_nr,
lang = lang,
type = "short"),
"\u00a0",
clock::get_year(ballot_date))
}
}
# return `NULL` instead of empty chr vctr
if (length(result) == 0L) {
result <- NULL
}
result
}
#' Get variable's value set
#'
#' Extracts a variable's value set from the [questionnaire data][qstnrs] in the specified language (or its integer representation if `lang = "int"`).
#'
#' @inheritParams lvls
#' @inheritParams var_lbl
#' @param lang Language. One of `r c(all_langs, "int") |> pal::as_md_vals() |> pal::enum_str(sep2 = " or ")` for the value set's integer codes."
#'
#' @return A character scalar.
#' @family vars
#' @export
#'
#' @examples
#' fokus::var_val_set(var_name = "favored_party",
#' ballot_date = "2018-09-23",
#' canton = "aargau")
#'
#' fokus::var_val_set(var_name = "favored_party",
#' ballot_date = "2023-06-18",
#' canton = "aargau")
#'
#' fokus::var_val_set(var_name = "favored_party",
#' ballot_date = "2023-06-18",
#' canton = "aargau",
#' lang = "en")
#'
#' fokus::var_val_set(var_name = "favored_party",
#' ballot_date = "2023-06-18",
#' canton = "aargau",
#' lang = "int")
var_val_set <- function(var_name,
ballot_date = pal::pkg_config_val("ballot_date"),
canton = cantons(ballot_date),
lang = pal::pkg_config_val("lang")) {
lang <- rlang::arg_match(arg = lang,
values = c(all_langs, "int"))
var_predicate(predicate = switch(EXPR = lang,
de = "response_options",
en = "value_labels",
int = "variable_values",
cli::cli_abort("Not implemented. Please debug.",
.internal = TRUE)),
var_name = var_name,
ballot_date = ballot_date,
canton = canton)
}
#' Add variable names to combination list
#'
#' Adds one or more `var_name`s to a combination list.
#'
#' @inheritParams shorten_var_names
#' @param list A list as returned by the `combos_*()` functions like [combos_ballot_types()].
#'
#' @return A list with `list` × `var_names` elements.
#' @family combo
#' @family vars
#' @export
#'
#' @examples
#' fokus::combos_proposals(ballot_dates = "2023-06-18",
#' cantons = "aargau",
#' incl_nr = FALSE) |>
#' fokus::add_vars_to_combos(var_names = c("age_group", "favored_party"))
add_vars_to_combos <- function(list,
var_names) {
checkmate::assert_list(list,
any.missing = FALSE)
checkmate::assert_character(var_names,
min.chars = 1L,
any.missing = FALSE)
list |>
purrr::map(\(x) {
purrr::map(var_names,
\(var_name) purrr::assign_in(x = x,
where = "var_name",
value = var_name))
}) |>
purrr::list_flatten()
}
#' Determine whether variable is skill question
#'
#' Determines for each variable whether or not it is a skill question.
#'
#' Note that the determination is performed by simply parsing `var_names`.
#'
#' @param var_names A character vector of FOKUS variable names.
#'
#' @return A logical vector of the same length as `var_names`.
#' @family vars
#' @export
is_skill_question_var <- function(var_names) {
var_names |>
checkmate::assert_character() |>
stringr::str_detect(pattern = "^skill_question_\\d+_(cantonal|federal)(_proposal_\\d+)?$")
}
#' Relabel factor of election parties
#'
#' Relabels a [factor] that contains election parties from/to the different FOKUS election party name types.
#'
#' @inheritParams election_parties
#' @param fct Factor to be relabelled.
#' @param from_type Name type to convert from. One of `r pal::enum_fn_param_defaults(param = "from_type", fn = fct_relabel_election_parties)`.
#' @param to_type Name type to convert to. One of `r pal::enum_fn_param_defaults(param = "to_type", fn = fct_relabel_election_parties)`.
#' @param strict Whether or not to strictly ensure that all of `fct` levels are matched by [known election parties][election_parties].
#'
#' @return A factor.
#' @seealso [election_parties()]
#' @family fcts
#' @export
#'
#' @examples
#' d <- fokus::read_survey_data(ballot_date = "2024-10-20",
#' canton = "aargau",
#' lang = "de")
#' # before
#' levels(d$voting_decision_cantonal_proportional_election_1_party)
#'
#' fct_new <- fct_relabel_election_parties(
#' fct = d$voting_decision_cantonal_proportional_election_1_party,
#' to_type = "short",
#' ballot_date = "2024-10-20",
#' lvl = "cantonal",
#' canton = "aargau",
#' election_nr = 1L,
#' past = FALSE
#' )
#'
#' # after
#' levels(fct_new)
#'
#' # set `strict = FALSE` if your factor contains non-standard levels
#' d$voting_decision_cantonal_proportional_election_1_party |>
#' forcats::fct_lump_min(other_level = "Kleinparteien",
#' min = 5L) |>
#' fct_relabel_election_parties(to_type = "short",
#' ballot_date = "2024-10-20",
#' lvl = "cantonal",
#' canton = "aargau",
#' election_nr = 1L,
#' past = FALSE,
#' strict = FALSE) |>
#' levels()
fct_relabel_election_parties <- function(fct,
from_type = c("qstnr", "run", "short"),
to_type = c("short", "run", "qstnr"),
ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "election",
prcds = "proportional"),
canton = cantons(ballot_date),
election_nr = 1L,
past = FALSE,
strict = TRUE) {
from_type <- rlang::arg_match(from_type)
to_type <- rlang::arg_match(to_type)
checkmate::assert_flag(strict)
ref <-
election_parties(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
election_nr = election_nr,
past = past) |>
# TODO: determine below "none" val dynamically instead of hardcoding it
tibble::add_row(code = 0L,
name.de.qstnr = "keine (leer eingelegt oder nicht teilgenommen)",
name.de.run = name.de.qstnr,
name.de.short = "keine",
.before = 1L)
checkmate::assert_factor(fct,
levels = if (strict) ref[[paste0("name.de.", from_type)]])
forcats::fct_relabel(.f = fct,
.fun = \(fct_lvls) purrr::map_chr(fct_lvls,
\(old_val) {
new_val <-
ref |>
dplyr::filter(!!as.symbol(paste0("name.de.", from_type)) == old_val) |>
dplyr::pull(paste0("name.de.", to_type))
if (length(new_val) != 1L) {
new_val <- old_val
}
new_val
}))
}
#' Shorten variable names to a maximum length of 32 characters
#'
#' @param var_names A character vector of variable names.
#' @param reverse Whether to apply the inversion of the shortening logic, i.e. to restore original/unshortened variable names.
#' @param max_n_char Maximum allowed number of characters. It is ensured that the maximum resulting variable name length doesn't exceed this limit. Has no
#' influence on the applied shortening logic. An integerish scalar or `Inf` for no limit.
#'
#' @return A character vector of the same length as `var_names`.
#' @family var_name_shortening
#' @export
shorten_var_names <- function(var_names,
reverse = FALSE,
max_n_char = 32L) {
checkmate::assert_character(var_names,
any.missing = FALSE)
checkmate::assert_flag(reverse)
if (!isTRUE(is.infinite(max_n_char))) {
checkmate::assert_count(max_n_char)
}
rules <- shortening_rules
if (reverse) colnames(rules) %<>% .[c(2L, 1L, 3L)]
rules %<>% dplyr::mutate(pattern = purrr::map2_chr(
.x = string,
.y = allowed_at,
.f = ~ .y |> pal::when(. == "begin" ~
paste0("^", .x, "(?=_)"),
. == "middle" ~
paste0("(?<=_)", .x, "(?=_)"),
. == "end" ~
paste0("(?<=_)", .x, "$"),
. == "begin-middle" ~
paste0("(?<=(^|_))", .x, "(?=_)"),
. == "begin-end" ~
paste0("(^", .x, "(?=_)|(?<=_)", .x, "$)"),
. == "middle-end" ~
paste0("(?<=_)", .x, "(?=(_|$))"),
. == "begin-middle-end" ~
paste0("(?<=(^|_))", .x, "(?=(_|$))"),
~ cli::cli_abort("Unknown {.var allowed_at} type: {.val {.}}.",
.internal = TRUE))
))
pattern_replacement <- rules$replacement
names(pattern_replacement) <- rules$pattern
var_names_new <- var_names |> stringr::str_replace_all(pattern = pattern_replacement)
# ensure we did our job
if (!is.null(max_n_char) && !reverse && any(nchar(var_names_new) > max_n_char)) {
# NOTE: `cli::cli_abort()` doesn't properly print the output of `pal::capture_print()` because:
# - it just seems to ignore the output if it includes ANSI escape sequences (this can be worked around by an additional `cli::ansi_strip()`)
# - it normalizes whitespace chars **incl. tabs** to a single regular whitespace, thus breaking the formatting
rlang::abort(glue::glue("There are still variable names left of a length greater than {max_n_char} characters after applying `shorten_var_names()`. ",
"Affected are the following (shortened) variable names:\n\n",
tibble::tibble(var_name = var_names,
var_name_short = var_names_new,
n_char = nchar(var_names),
n_char_short = nchar(var_names_new)) |>
dplyr::filter(n_char_short > max_n_char) |>
pal::capture_print(collapse = "\n"),
.null = NA_character_,
.trim = FALSE))
}
var_names_new
}
#' Shorten column names to a maximum length of 32 characters
#'
#' This is useful for DTA export since Stata has a built-in variable name length limit of [32
#' characters](https://www.stata.com/manuals/r.pdf#rLimits) (see also
#' [here](https://www.statalist.org/forums/forum/general-stata-discussion/general/1452366-number-of-characters-in-variable-names)).
#'
#' @inheritParams shorten_var_names
#' @param x `r pkgsnip::param_lbl("tbl_data")`
#'
#' @return `x` with column names shortened to a maximum length of 32 characters.
#' @family var_name_shortening
#' @export
shorten_colnames <- function(x,
max_n_char = 32L) {
x |> magrittr::set_colnames(value = shorten_var_names(var_names = colnames(x),
max_n_char = max_n_char))
}
#' Restore original/unshortened column names
#'
#' Applies the inversion of [shorten_colnames()].
#'
#' @inheritParams shorten_colnames
#' @inheritParams shorten_var_names
#'
#' @return `x` with original/unshortened column names restored.
#' @family var_name_shortening
#' @export
restore_colnames <- function(x) {
x |> magrittr::set_colnames(value = shorten_var_names(var_names = colnames(x),
reverse = TRUE))
}
#' Phrase term
#'
#' Transforms a term's value into a phrased string representation.
#'
#' @inheritParams lang_to_locale
#' @param term Term to be phrased. A character scalar.
#' @param vals Value(s) of `term` to be phrased. A character vector.
#'
#' @return A character scalar.
#' @family phrase
#' @export
#'
#' @examples
#' fokus::phrase(term = "side",
#' vals = c("pro", "pro", "contra"),
#' lang = "de")
phrase <- function(term,
vals,
lang = pal::pkg_config_val("lang")) {
lang <- rlang::arg_match(arg = lang,
values = all_langs)
term <- rlang::arg_match(arg = term,
values = unique(phrased_terms_tidy$term))
data <- phrased_terms_tidy |> dplyr::filter(term == !!term & lang == !!lang)
vals <- rlang::arg_match(arg = vals,
values = unique(data$value),
multiple = TRUE)
vals |>
purrr::map_chr(\(val) {
data$phrase[data$value == val]
})
}
#' Phrase (ballot) date
#'
#' Phrases a date according to the specified `format` and `lang`.
#'
#' @inheritParams salim::phrase_datetime
#' @inheritParams lang_to_locale
#'
#' @return A character scalar.
#' @family phrase
#' @export
#'
#' @examples
#' fokus::phrase_date()
phrase_date <- function(x = pal::pkg_config_val("ballot_date"),
format = "date_long",
lang = pal::pkg_config_val("lang")) {
lang <- rlang::arg_match(arg = lang,
values = all_langs)
salim::phrase_datetime(x = x,
format = format,
locale = lang)
}
#' Phrase majoritarian election's candidate(s)
#'
#' Assembles one or more majoritarian election candidate strings consisting of the candidate's first name, last name and optionally political party (in
#' parentheses).
#'
#' @inheritParams n_election_seats
#' @param candidate_nrs Election candidate numbers to include. A vector of positive integers or `NULL`. If `NULL`, all candidates will be included.
#' @param incl_party Whether or not to include the candidate's political party in the resulting string (in parentheses).
#'
#' @return A character vector.
#' @family phrase
#' @export
#'
#' @examples
#' fokus::phrase_election_candidate(ballot_date = "2019-10-20",
#' lvl = "cantonal",
#' canton = "aargau",
#' candidate_nrs = 1:3)
phrase_election_candidate <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "election"),
canton = cantons(ballot_date),
election_nr = 1L,
candidate_nrs = NULL,
incl_party = TRUE) {
data_candidates <- election_candidates(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
election_nr = election_nr)
checkmate::assert_integerish(candidate_nrs,
lower = 1L,
upper = nrow(data_candidates),
any.missing = FALSE,
null.ok = TRUE)
checkmate::assert_flag(incl_party)
if (length(candidate_nrs)) {
data_candidates %<>% dplyr::filter(dplyr::row_number() %in% candidate_nrs)
}
data_candidates |> purrr::pmap_chr(\(first_name, last_name, party, ...) paste0(first_name, " ", last_name, paste0(" (", party, ")")[incl_party]))
}
#' Get declined German proposal name
#'
#' Declines a German [proposal name][proposal_name] to the specified grammatical `case`. Simple convenience function combining [proposal_name()],
#' [proposal_name_gender()] and [salim::decline_noun_de()].
#'
#' @inheritParams proposal_name
#' @inheritParams salim::decline_noun_de
#'
#' @return A character scalar.
#' @family phrase
#' @export
#'
#' @examples
#' fokus::phrase_proposal_name_de(ballot_date = "2023-06-18",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1,
#' type = "short",
#' case = "dative")
#'
#' fokus::phrase_proposal_name_de(ballot_date = "2023-06-18",
#' lvl = "cantonal",
#' canton = "aargau",
#' proposal_nr = 1,
#' type = "short",
#' case = "genitive")
phrase_proposal_name_de <- function(ballot_date = pal::pkg_config_val("ballot_date"),
lvl = lvls(ballot_date,
canton,
ballot_type = "referendum"),
canton = cantons(ballot_date),
proposal_nr = 1L,
type = all_name_types,
case = c("nominative", "genitive", "dative", "accusative")) {
rlang::check_installed("salim",
reason = pal::reason_pkg_required())
proposal_name(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr,
lang = "de",
type = type) |>
salim::decline_noun_de(gender = proposal_name_gender(ballot_date = ballot_date,
lvl = lvl,
canton = canton,
proposal_nr = proposal_nr,
type = type),
case = case)
}
#' Authorize googledrive using GCP Service Account Key
#'
#' Authorizes the googledrive package to access and manage files on your Google Drive via a [Google Cloud Platform (GCP) Service
#' Account Key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) file (in JSON format). See the [relevant
#' documentation](https://gargle.r-lib.org/articles/non-interactive-auth.html#provide-a-service-account-token-directly) for details.
#'
#' The recommended way to provide the filesystem path to the GCP Service Account Key file is to set the [environment
#' variable](https://en.wikipedia.org/wiki/Environment_variable) `PATH_GCP_KEY_ZDA` to that path, e.g. via the [`.Renviron`
#' file](https://rstats.wtf/r-startup.html#renviron).
#'
#' @param path_gcp_service_account_key Path to the GCP Service Account Key JSON file.
#'
#' @return `path_gcp_service_account_key`, invisibly.
#' @family g_apps
#' @keywords internal
auth_g_drive_gcp <- function(path_gcp_service_account_key = Sys.getenv("PATH_GCP_KEY_ZDA")) {
rlang::check_installed("googledrive",
reason = pal::reason_pkg_required())
is_file <- checkmate::test_file_exists(path_gcp_service_account_key,
access = "r")
if (is_file) {
googledrive::drive_auth(path = path_gcp_service_account_key,
use_oob = TRUE)
} else {
cli::cli_abort(paste0("No Google Cloud Platform service account key found under {.path {path_gcp_service_account_key}}. ",
"Instructions to store such a key can be found here: ",
"{.url https://gargle.r-lib.org/articles/non-interactive-auth.html#provide-a-service-account-token-directly}"))
}
invisible(path_gcp_service_account_key)
}
#' Authorize googlesheets4 using GCP Service Account Key
#'
#' Authorizes the googlesheets4 package to access and manage Google Sheets via a [Google Cloud Platform (GCP) Service
#' Account Key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) file (in JSON format). See the [relevant
#' documentation](https://gargle.r-lib.org/articles/non-interactive-auth.html#provide-a-service-account-token-directly) for details.
#'
#' The recommended way to provide the filesystem path to the GCP Service Account Key file is to set the [environment
#' variable](https://en.wikipedia.org/wiki/Environment_variable) `PATH_GCP_KEY_ZDA` to that path, e.g. via the [`.Renviron`
#' file](https://rstats.wtf/r-startup.html#renviron).
#'
#' @param path_gcp_service_account_key Path to the GCP Service Account Key JSON file.
#'
#' @return `path_gcp_service_account_key`, invisibly.
#' @family g_apps
#' @keywords internal
auth_g_sheets_gcp <- function(path_gcp_service_account_key = Sys.getenv("PATH_GCP_KEY_ZDA")) {
rlang::check_installed("googlesheets4",
reason = pal::reason_pkg_required())
is_file <- checkmate::test_file_exists(path_gcp_service_account_key,
access = "r")
if (is_file) {
googlesheets4::gs4_auth(path = path_gcp_service_account_key,
use_oob = TRUE)
} else {
cli::cli_abort(paste0("No Google Cloud Platform service account key found under {.path {path_gcp_service_account_key}} ",
"Instructions to store such a key can be found here: ",
"{.url https://gargle.r-lib.org/articles/non-interactive-auth.html#provide-a-service-account-token-directly}"))
}
invisible(path_gcp_service_account_key)
}
#' Backup Google Drive file locally
#'
#' Creates a local backup of a [Google Drive](https://en.wikipedia.org/wiki/Google_Drive) file.
#'
#' Essentially a convenience wrapper around [googledrive::drive_download()].
#'
#' If a [Google Sheet](https://en.wikipedia.org/wiki/Google_Sheets) that includes multiple worksheets is backed up to a file `type` that doesn't support
#' worksheets like `"csv"`, only the default (first) worksheet is written. In contrast, [backup_g_sheet()] allows to specify any worksheet to be backed up (but
#' always writes a single worksheet only regardless of the filetype).
#'
#' @inheritParams g_file_mod_time
#' @inheritParams upload_to_g_drive
#' @param path Path to the local file backup destination. A character scalar.
#' @param type Desired type of the Google Drive file to be backed up. Only consulted if `g_id` identifies a native Google Apps file. Will be processed via
#' [googledrive::drive_mime_type()], so it can either be a file extension like `"pdf"`, a full MIME type like `"application/pdf"`, or `NULL` to determine the
#' type based on the file extension of `path` (if none is specified, falls back on the default type determined by the [Google Drive
#' API](https://developers.google.com/drive/api/v3/)). Note that `type` takes precedence over a possible file extension of `path`, but specifying only the
#' latter should normally suffice.
#' @param overwrite Whether or not to overwrite an already existing file under `path`.
#' @param force Whether or not to force overwriting the file regardless whether it has changed since the last backup or not.
#'
#' @return An object of class [dribble][googledrive::dribble], a tibble with one row per file if local backup was (over)written, otherwise `NULL`, meaning
#' the remote file hasn't deviated from the local backup since the last run, invisibly.
#' @family g_apps
#' @export
backup_g_file <- function(g_id,
path,
type = NULL,
path_gcp_service_account_key = Sys.getenv("PATH_GCP_KEY_ZDA"),
overwrite = TRUE,
force = FALSE,
quiet = TRUE) {
checkmate::assert_string(g_id)
checkmate::assert_path_for_output(path,
overwrite = TRUE)
checkmate::assert_flag(overwrite)
checkmate::assert_flag(force)
checkmate::assert_flag(quiet)
rlang::check_installed("googledrive",
reason = pal::reason_pkg_required())
if (quiet) {
googledrive::local_drive_quiet()
}
result <- NULL
remote_mod <- g_file_mod_time(g_id,
path_gcp_service_account_key = path_gcp_service_account_key)
local_mod <- ifelse(fs::file_exists(path),
pal::path_mod_time(path),
lubridate::as_datetime(0L))
if (local_mod < remote_mod || force) {
result <- googledrive::drive_download(file = googledrive::as_id(g_id),
path = path,
type = type,
overwrite = overwrite)
}
invisible(result)
}
#' Backup Google Sheet locally
#'
#' Creates a local backup of a [Google Sheet](https://en.wikipedia.org/wiki/Google_Sheets). It is accessed via [googlesheets4::read_sheet()] and written via
#' [readr::write_csv()] or [writexl::write_xlsx()], depending on the file extension of `path`.
#'
#' `backup_g_sheet()` only backs up a single worksheet at once (specified by the optional `sheet` argument). If you intend to backup multiple worksheets of the
#' same Google Sheet, consider using [backup_g_file()] in combination with a file `type` that supports multiple worksheets like `"ods"` or `"xlsx"`.
#'
#' @inheritParams backup_g_file
#' @inheritParams googlesheets4::read_sheet
#' @param ... Further arguments passed on to [googlesheets4::read_sheet()].
#' @param quiet Whether or not to [suppress printing status output from googledrive][googledrive::local_drive_quiet] and [googlesheets4
#' operations][googlesheets4::local_gs4_quiet].
#'
#' @return A [tibble][tibble::tbl_df] if the local backup was (over)written, otherwise `NULL`, meaning the remote file hasn't deviated from the local backup
#' since the last run, invisibly.
#' @family g_apps
#' @export
backup_g_sheet <- function(g_id,
path,
...,
path_gcp_service_account_key = Sys.getenv("PATH_GCP_KEY_ZDA"),
overwrite = TRUE,
force = FALSE,
quiet = TRUE) {
checkmate::assert_string(g_id)
checkmate::assert_path_for_output(path,
overwrite = TRUE)
checkmate::assert_flag(overwrite)
checkmate::assert_flag(force)
checkmate::assert_flag(quiet)
rlang::check_installed("googledrive",
reason = pal::reason_pkg_required())
rlang::check_installed("googlesheets4",
reason = pal::reason_pkg_required())
pal::check_dots_named(...,
.fn = googlesheets4::read_sheet,
.forbidden = "ss")
if (quiet) {
googledrive::local_drive_quiet()
googlesheets4::local_gs4_quiet()
}
# authenticate Google account
auth_g_drive_gcp(path_gcp_service_account_key)
# ensure `g_id` refers to a spreadsheet
mime_type <-
googledrive::drive_get(id = g_id) |>
dplyr::pull("drive_resource") |>
dplyr::first() |>
purrr::chuck("mimeType")
if (mime_type != "application/vnd.google-apps.spreadsheet") {
cli::cli_abort("The supplied {.arg g_id} {.val {g_id}} doesn't seem to refer to a Google Sheet. Its Google Drive MIME type is {.val {mime_type}}.")
}
data <- NULL
remote_mod <- g_file_mod_time(g_id,
path_gcp_service_account_key = path_gcp_service_account_key)
local_mod <- ifelse(fs::file_exists(path),
pal::path_mod_time(path),
lubridate::as_datetime(0L))
if (local_mod < remote_mod || force) {
# authenticate Google account
auth_g_sheets_gcp(path_gcp_service_account_key)
filetype <- fs::path_ext(path)
data <- googlesheets4::read_sheet(ss = g_id,
...)
if (overwrite || !fs::file_exists(path)) {
filetype |>
pal::when(. == "csv" ~
readr::write_csv(x = data,
file = path,
na = ""),
. == "xlsx" ~ {
rlang::check_installed("writexl",
reason = pal::reason_pkg_required())
writexl::write_xlsx(x = data,
path = path)
},
. == "" ~
cli::cli_abort("{.arg path} must have a file extension ({.file .csv} or {.file .xlsx})."),
~ cli::cli_abort("Exporting filetype {.file {.}} is not yet implemented."))
} else {
cli::cli_alert_warning("A file already exists under {.arg path} {.file {path}} but {.arg overwrite} was set to {.val FALSE}. Nothing done.")
}
}
invisible(data)
}
#' Upload files to Google Drive
#'
#' Uploads one or more files to Google Drive.
#'
#' Essentially a convenience wrapper around [googledrive::drive_put()].
#'
#' @inheritParams g_file_mod_time
#' @param filepaths Local path(s) to the file(s) to be uploaded.
#' @param g_drive_folder Destination path on Google Drive where the files are to be uploaded to.
#' @param quiet Whether or not to [suppress printing status output from googledrive operations][googledrive::local_drive_quiet].
#'
#' @return `filepaths`, invisibly.
#' @family g_apps
#' @export
upload_to_g_drive <- function(filepaths,
g_drive_folder,
path_gcp_service_account_key = Sys.getenv("PATH_GCP_KEY_ZDA"),
quiet = FALSE) {
checkmate::assert_character(filepaths,
any.missing = FALSE)
checkmate::assert_string(g_drive_folder)
checkmate::assert_flag(quiet)
# mute googledrive msgs if requested
if (quiet) googledrive::local_drive_quiet()
# extract filenames
filenames <- fs::path_file(filepaths)
# authenticate Google account
auth_g_drive_gcp(path_gcp_service_account_key)
# upload files
pal::cli_progress_step_quick(msg = "Uploading {length(filepaths)} file{?s} to Google Drive folder {.field {g_drive_folder}}")
purrr::walk2(.x = filenames,
.y = filepaths,
.f = ~ {
# overwrite existing file if possible or create new one otherwise
googledrive::drive_put(media = .y,
path = g_drive_folder,
name = .x)
})
invisible(filepaths)
}
#' Get Google Drive file modification timestamp
#'
#' @param g_id Google Drive file ID. A character scalar.
#' @param path_gcp_service_account_key Path to the GCP Service Account Key JSON file. See [auth_g_drive_gcp()] for details.
#'
#' @return `r pkgsnip::return_lbl("datetime")`
#' @family g_apps
#' @export
g_file_mod_time <- function(g_id,
path_gcp_service_account_key = Sys.getenv("PATH_GCP_KEY_ZDA")) {
auth_g_drive_gcp(path_gcp_service_account_key = path_gcp_service_account_key)
googledrive::drive_get(id = g_id) |>
dplyr::pull("drive_resource") |>
dplyr::first() |>
purrr::chuck("modifiedTime") |>
lubridate::as_datetime()
}
#' Convert logical vector to Unicode symbols `r unicode_checkmark` and `r unicode_crossmark`
#'
#' @param x A logical vector.
#'
#' @return A character vector.
#' @export
#'
#' @examples
#' fokus::lgl_to_unicode(c(TRUE, TRUE, FALSE, NA))
lgl_to_unicode <- function(x) {
checkmate::assert_logical(x)
dplyr::if_else(x,
unicode_checkmark,
unicode_crossmark)
}
#' Emphasize xth element of character vector (Markdown)
#'
#' @param x Input as a character vector.
#' @param which Indices of the elements to be emphasized.
#' @param emph Character sequence used for emphasis.
#'
#' @return A character vector of the same length as `x`.
#' @export
md_emphasize <- function(x,
which = TRUE,
emph = "**") {
x[which] %<>% paste0(emph, ., emph)
x
}
#' `r this_pkg` package configuration metadata
#'
#' A [tibble][tibble::tbl_df] with metadata of all possible `r this_pkg` package configuration options. See [pal::pkg_config_val()] for more information.
#'
#' @format `r pkgsnip::return_lbl("tibble_cols", cols = colnames(pkg_config))`
#' @export
#'
#' @examples
#' fokus::pkg_config
"pkg_config"
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.