File size: 11,260 Bytes
9cee884 39181ba 9cee884 39181ba 9cee884 39181ba 9cee884 39181ba 9cee884 39181ba 9cee884 39181ba 9cee884 39181ba 9cee884 39181ba 9cee884 39181ba 9cee884 39181ba 9cee884 39181ba 9cee884 39181ba 9cee884 39181ba 9cee884 39181ba 9cee884 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 | # R code of a run: the text that the visitor copies, downloads or finds in the
# bundle. It uses only functions of decisions and base R (dplyr only for the
# subset of the injuries sample). Every string of the visitor goes through
# encodeString(), so quotes, backticks and new lines stay valid R.
# An R string with double quotes
r_string <- function(x) encodeString(x, quote = '"')
# A name for the left side of `name = value`: bare when R allows it, else quoted
r_name <- function(x) ifelse(make.names(x) == x, x, r_string(x))
# Folder names for the answers: a name with one of `/ \ : * ? " < > |`, a
# control character or a leading dot is cleaned, and the clean names stay
# unique. The names of the result are the answers.
answer_folders <- function(answers) {
folder <- answers |>
stringr::str_replace_all('[/\\\\:*?"<>|[:cntrl:]]', "_") |>
stringr::str_remove("^\\.+") |>
stringr::str_squish()
folder[folder == ""] <- "answer"
stats::setNames(make.unique(folder, sep = "_"), answers)
}
# The reference files of a spec: one row for each file, with its answer, the
# folder of the answer, the source path and the file name in the folder.
# A score question has none (Jev attaches references to noul and choice only).
reference_files <- function(spec) {
empty <- tibble::tibble(
answer = character(), folder = character(), path = character(), file = character()
)
if (is.null(spec$references) || spec$question$type == "score") return(empty)
folders <- answer_folders(names(spec$question$answers))
purrr::imap(spec$references, \(paths, answer) {
file <- basename(paths)
twice <- duplicated(file) | duplicated(file, fromLast = TRUE)
file[twice] <- paste0(seq_along(file)[twice], "_", file[twice])
tibble::tibble(answer = answer, folder = folders[[answer]], path = paths, file = file)
}) |>
purrr::list_rbind()
}
# The name of a run for file names: "my parts" gives "my_parts"
run_name <- function(spec) {
name <- janitor::make_clean_names(spec$name, case = "snake")
if (name == "") "run" else name
}
# The variable that holds the question in the code
question_var <- function(spec) {
switch(spec$source, textiles = "weave", injuries = "injury", "question")
}
# Strings in lines of at most `width` characters, with a comma after each
# string but the last. Used for long lists of files.
pack_lines <- function(items, indent = " ", width = 80) {
lines <- character()
line <- ""
for (i in seq_along(items)) {
piece <- if (i < length(items)) paste0(items[i], ",") else items[i]
if (nzchar(line) && nchar(indent) + nchar(line) + 1 + nchar(piece) > width) {
lines <- c(lines, paste0(indent, line))
line <- piece
} else {
line <- stringr::str_trim(paste(line, piece))
}
}
c(lines, paste0(indent, line))
}
# c("a", "b") as code: on one line when short, else in packed lines
r_vector <- function(strings, indent = "") {
one <- paste0("c(", paste(r_string(strings), collapse = ", "), ")")
if (nchar(indent) + nchar(one) <= 80) return(one)
lines <- pack_lines(r_string(strings), paste0(indent, " "))
paste0("c(\n", paste(lines, collapse = "\n"), "\n", indent, ")")
}
# The six lines of the comment on paths
paths_comment <- function(spec, refs) {
folders <- if (nrow(refs) > 0) {
'the folder "images" and the folder "references"'
} else {
'the folder "images"'
}
working_directory <- c(
"working directory (RStudio: Session > Set Working Directory > To Source",
"File Location). getwd() shows where R looks."
)
lines <- if (spec$mode == "text" && spec$source == "injuries") {
c(
"Paths. This run used the sample texts of the package: the dataset",
"ohio_injuries comes with decisions, so no path needs a change.",
'Your own texts use a file next to this script: "data.csv".',
'Put this script and the file "data.csv" in one folder, and make it the',
working_directory
)
} else if (spec$mode == "text") {
column <- if (is.null(spec$text_column)) {
'The bundle holds your texts there, in the column "text".'
} else {
sprintf(
'Name your file "data.csv". The texts are in the column %s.',
r_string(spec$text_column)
)
}
c(
"Paths. This run used your own texts. The code reads them from the file",
'"data.csv" next to this script.',
column,
'Put this script and the file "data.csv" in one folder, and make it the',
working_directory
)
} else if (spec$source == "textiles") {
c(
"Paths. This run used the sample images of the package: system.file()",
"finds them on any computer, so no path needs a change.",
'Your own images use relative paths, such as "images/part_001.jpg".',
sprintf("Put this script, %s in one", folders),
"folder, and make it the working directory (RStudio: Session > Set Working",
"Directory > To Source File Location). getwd() shows where R looks."
)
} else {
c(
"Paths. This run used your own images. The code reads them with relative",
'paths, such as "images/part_001.jpg". The bundle holds no input image.',
sprintf("Put this script, %s in one", folders),
"folder, and make it the working directory (RStudio: Session > Set Working",
"Directory > To Source File Location). getwd() shows where R looks.",
'Put your images in the folder "images", as the code below lists them.'
)
}
paste0("# ", lines, collapse = "\n")
}
# The definition of `references` from relative paths (empty without references)
references_code <- function(refs) {
if (nrow(refs) == 0) return("")
by_answer <- refs |>
dplyr::summarise(
code = paste0(
" ", r_name(dplyr::first(answer)), " = ",
r_vector(paste0("references/", dplyr::first(folder), "/", file), indent = " ")
),
.by = answer
)
paste0("\nreferences <- list(\n", paste(by_answer$code, collapse = ",\n"), "\n)")
}
# The comment above the inputs of Try on 5
TRY_COMMENT <- "# The inputs of Try on 5 in the app"
# The lines that keep the rows of Try on 5: x[c(3, 17), ] for a data frame,
# x[c(3, 17)] for a vector. Empty with no rows.
keep_code <- function(var, rows, frame = FALSE) {
if (length(rows) == 0) return("")
sprintf(
"\n%s\n%s <- %s[c(%s)%s]",
TRY_COMMENT, var, var, paste(rows, collapse = ", "), if (frame) ", " else ""
)
}
# The lines that define `texts` (or `injuries`). With `rows` (Try on 5), one
# more line keeps those rows.
text_code <- function(spec, rows = NULL) {
if (spec$source == "injuries") {
return(paste0(paste(
"set.seed(2026)",
"# This line needs the package dplyr.",
"injuries <- ohio_injuries |>",
" dplyr::slice_sample(n = 20, by = collapsed_nature_title)",
sep = "\n"
), keep_code("injuries", rows, frame = TRUE)))
}
if (is.null(spec$text_column)) {
return(paste0('texts <- read.csv("data.csv")$text', keep_code("texts", rows)))
}
# read.csv() changes odd column names unless check.names is FALSE
read <- if (make.names(spec$text_column) == spec$text_column) {
'read.csv("data.csv")'
} else {
'read.csv("data.csv", check.names = FALSE)'
}
# A CSV file keeps its own row numbers ("Row 3"): the app drops empty texts
file_rows <- as.integer(stringr::str_extract(spec$inputs$file[rows], "[0-9]+$"))
paste0("texts <- ", read, "[[", r_string(spec$text_column), "]]", keep_code("texts", file_rows))
}
# The lines that define `images` and `references`. With `rows` (Try on 5),
# the code names only the images of those rows.
inputs_code <- function(spec, refs, rows = NULL) {
if (spec$mode == "text") return(text_code(spec, rows))
if (spec$source == "textiles") {
by_answer <- refs |>
dplyr::summarise(
code = paste0(
" ", r_name(dplyr::first(answer)), " = file.path(folder, ",
r_vector(file, indent = " "), ")"
),
.by = answer
)
images <- if (is.null(rows)) {
'images <- file.path(folder, textiles$file[textiles$role == "input"])'
} else {
paste0(TRY_COMMENT, "\nimages <- file.path(folder, ", r_vector(spec$inputs$file[rows]), ")")
}
return(paste0(
'folder <- system.file("extdata", "textiles", package = "decisions")\n',
images, "\n",
"references <- list(\n", paste(by_answer$code, collapse = ",\n"), "\n)"
))
}
inputs <- if (is.null(rows)) spec$inputs else spec$inputs[rows, ]
images <- if (nrow(inputs) == 0) {
'images <- list.files("images", full.names = TRUE)'
} else {
labels <- unique(stats::na.omit(inputs$label))
folders <- answer_folders(union(names(spec$question$answers), labels))
sub <- ifelse(is.na(inputs$label), "", paste0(folders[inputs$label], "/"))
files <- r_string(paste0("images/", sub, inputs$file))
paste0("images <- c(\n", paste(pack_lines(files), collapse = "\n"), "\n)")
}
paste0(images, references_code(refs))
}
# The call of the question constructor, with the exact text of the question
question_code <- function(question, var) {
answers <- question$answers
args <- r_string(question$instructions)
if (question$type == "noul") {
given <- answers[nzchar(answers)]
if (length(given) > 0) args <- c(args, paste0(names(given), " = ", r_string(given)))
} else {
names_r <- r_name(names(answers))
padded <- stringr::str_pad(names_r, max(nchar(names_r)), side = "right")
args <- c(args, paste0(
"c(\n", paste0(" ", padded, " = ", r_string(answers), collapse = ",\n"), "\n )"
))
}
paste0(
var, " <- question_", question$type, "(\n",
paste0(" ", args, collapse = ",\n"), "\n)"
)
}
# The code of a run, as one string. `date` is the day of the run. `rows` are
# the rows of Try on 5 (NULL for all inputs).
run_code <- function(spec, date = Sys.Date(), rows = NULL) {
refs <- reference_files(spec)
var <- question_var(spec)
data_arg <- if (spec$mode == "text") {
paste0("text = ", if (spec$source == "injuries") "injuries$final_narrative" else "texts")
} else {
"image = images"
}
if (nrow(refs) > 0) data_arg <- paste0(data_arg, ", references = references")
paste(
sprintf("# The run of the decisions Space on %s, in R. Run it with your own key.", format(date)),
'library(decisions) # install once: pak::pak("fmegahed/decisions")',
"",
paths_comment(spec, refs),
inputs_code(spec, refs, rows),
"",
question_code(spec$question, var),
"",
"answers <- decide(",
sprintf(' decider_typesafe("%s"), %s,', MODEL, var),
paste0(" ", data_arg, ","),
sprintf(
" max_size = %s, upscale = %s",
IMAGE_SETTINGS$max_size, toupper(IMAGE_SETTINGS$upscale)
),
")",
"answers",
sep = "\n"
)
}
# The number of lines of code, for the closed card: the top-level calls, so
# comments and blank lines do not count. Code that does not parse counts the
# lines that are not blank and not only a comment.
code_lines <- function(code) {
calls <- tryCatch(length(parse(text = code, keep.source = FALSE)), error = \(e) NULL)
if (!is.null(calls)) return(calls)
lines <- stringr::str_trim(stringr::str_split_1(code, "\n"))
sum(lines != "" & !startsWith(lines, "#"))
}
|