-
Notifications
You must be signed in to change notification settings - Fork 292
Expand file tree
/
Copy pathlist-transpose.R
More file actions
172 lines (163 loc) · 5.24 KB
/
list-transpose.R
File metadata and controls
172 lines (163 loc) · 5.24 KB
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
#' Transpose a list
#'
#' @description
#' `list_transpose()` turns a list-of-lists "inside-out". For instance it turns a pair of
#' lists into a list of pairs, or a list of pairs into a pair of lists. For
#' example, if you had a list of length `n` where each component had values `a`
#' and `b`, `list_transpose()` would make a list with elements `a` and
#' `b` that contained lists of length `n`.
#'
#' It's called transpose because `x[["a"]][["b"]]` is equivalent to
#' `list_transpose(x)[["b"]][["a"]]`, i.e. transposing a list flips the order of
#' indices in a similar way to transposing a matrix.
#'
#' @param x A list of vectors to transpose.
#' @param template A "template" that describes the output list. Can either be
#' a character vector (where elements are extracted by name), or an integer
#' vector (where elements are extracted by position). Defaults to the union
#' of the names of the elements of `x`, or if they're not present, the
#' union of the integer indices.
#' @param simplify Should the result be [simplified][list_simplify]?
#' * `TRUE`: simplify or die trying.
#' * `NA`: simplify if possible.
#' * `FALSE`: never try to simplify, always leaving as a list.
#'
#' Alternatively, a named list specifying the simplification by output
#' element.
#' @param ptype An optional vector prototype used to control the simplification.
#' Alternatively, a named list specifying the prototype by output element.
#' @param default A default value to use if a value is absent or `NULL`.
#' Alternatively, a named list specifying the default by output element.
#' @inheritParams rlang::args_dots_empty
#' @export
#' @examples
#' # list_transpose() is useful in conjunction with safely()
#' x <- list("a", 1, 2)
#' y <- x |> map(safely(log))
#' y |> str()
#' # Put all the errors and results together
#' y |> list_transpose() |> str()
#' # Supply a default result to further simplify
#' y |> list_transpose(default = list(result = NA)) |> str()
#'
#' # list_transpose() will try to simplify by default:
#' x <- list(list(a = 1, b = 2), list(a = 3, b = 4), list(a = 5, b = 6))
#' x |> list_transpose()
#' # this makes list_tranpose() not completely symmetric
#' x |> list_transpose() |> list_transpose()
#'
#' # use simplify = FALSE to always return lists:
#' x |> list_transpose(simplify = FALSE) |> str()
#' x |>
#' list_transpose(simplify = FALSE) |>
#' list_transpose(simplify = FALSE) |> str()
#'
#' # Provide an explicit template if you know which elements you want to extract
#' ll <- list(
#' list(x = 1, y = "one"),
#' list(z = "deux", x = 2)
#' )
#' ll |> list_transpose()
#' ll |> list_transpose(template = c("x", "y", "z"))
#' ll |> list_transpose(template = 1)
#'
#' # And specify a default if you want to simplify
#' ll |> list_transpose(template = c("x", "y", "z"), default = NA)
list_transpose <- function(
x,
...,
template = NULL,
simplify = NA,
ptype = NULL,
default = NULL
) {
obj_check_list(x)
check_dots_empty()
if (length(x) == 0) {
template <- integer()
} else if (is.null(template)) {
indexes <- map(x, vec_index)
call <- current_env()
withCallingHandlers(
template <- reduce(indexes, vec_set_union),
vctrs_error_ptype2 = function(e) {
cli::cli_abort(
"Can't combine named and unnamed vectors.",
arg = template,
call = call
)
}
)
}
if (!is.character(template) && !is.numeric(template)) {
cli::cli_abort(
"{.arg template} must be a character or numeric vector, not {.obj_type_friendly {template}}.",
arg = template
)
}
simplify <- match_template(simplify, template)
default <- match_template(default, template)
ptype <- match_template(ptype, template)
out <- rep_along(template, list())
if (is.character(template)) {
names(out) <- template
}
for (i in seq_along(template)) {
idx <- template[[i]]
res <- map(x, idx, .default = default[[i]])
res <- list_simplify_internal(
res,
simplify = simplify[[i]] %||% NA,
ptype = ptype[[i]],
error_arg = result_index(idx)
)
out[[i]] <- res
}
out
}
result_index <- function(idx) {
if (is.character(idx)) {
paste0("result$", idx)
} else {
paste0("result[[", idx, "]]")
}
}
match_template <- function(
x,
template,
error_arg = caller_arg(x),
error_call = caller_env()
) {
if (is.character(template)) {
if (is_bare_list(x) && is_named(x)) {
extra_names <- setdiff(names(x), template)
if (length(extra_names)) {
cli::cli_abort(
"{.arg {error_arg}} contains unknown names: {.str {extra_names}}.",
arg = error_arg,
call = error_call
)
}
out <- rep_named(template, list(NULL))
out[names(x)] <- x
out
} else {
rep_named(template, list(x))
}
} else if (is.numeric(template)) {
if (is_bare_list(x) && length(x) > 0) {
if (length(x) != length(template)) {
cli::cli_abort(
"Length of {.arg {error_arg}} ({length(x)}) and {.arg template} ({length(template)}) must be the same when transposing by position.",
arg = error_arg,
call = error_call
)
}
x
} else {
rep_along(template, list(x))
}
} else {
cli::cli_abort("Invalid `template`", .internal = TRUE)
}
}