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, "#"))
}