Skip to content

Commit cf73a9b

Browse files
committed
Implement list_transpose()
1 parent f04a6fe commit cf73a9b

File tree

6 files changed

+532
-0
lines changed

6 files changed

+532
-0
lines changed

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ export(list_combine)
487487
export(list_drop_empty)
488488
export(list_of)
489489
export(list_sizes)
490+
export(list_transpose)
490491
export(list_unchop)
491492
export(maybe_lossy_cast)
492493
export(n_fields)

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# vctrs (development version)
22

3+
* New `list_transpose()` for transposing a list of vectors.
4+
35
* `vec_interleave()` gains new `.size` and `.error_call` arguments.
46

57
* `vec_interleave()` now reports the correct index in errors when `NULL`s are present.

R/list-transpose.R

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
#' Transpose a list of vectors
2+
#'
3+
#' @description
4+
#' `list_transpose()` takes a list of vectors, transposes it, and returns a new
5+
#' list of vectors.
6+
#'
7+
#' @inheritParams rlang::args_dots_empty
8+
#' @inheritParams rlang::args_error_context
9+
#'
10+
#' @param x A list.
11+
#'
12+
#' @param size The expected size of each element of `x`. If not provided,
13+
#' computed automatically by [vec_size_common()].
14+
#'
15+
#' @param ptype The expected type of each element of `x`. If not provided,
16+
#' computed automatically by [vec_ptype_common()].
17+
#'
18+
#' @param x_arg Argument name used in error messages.
19+
#'
20+
#' @returns
21+
#' A list of vectors with the following invariants:
22+
#'
23+
#' For the list:
24+
#'
25+
#' - `vec_ptype(list_transpose(x)) == vec_ptype(x)`
26+
#' - `vec_size(list_transpose(x)) == (size || vec_size_common(!!!x))`
27+
#'
28+
#' For the list elements:
29+
#'
30+
#' - `vec_ptype(list_transpose(x)[[i]]) == (ptype || vec_ptype_common(!!!x))`
31+
#' - `vec_size(list_transpose(x)[[i]]) == vec_size(x)`
32+
#'
33+
#' @export
34+
#' @examples
35+
#' # Input:
36+
#' # - List size 3
37+
#' # - Element size 2
38+
#' # Output:
39+
#' # - List size 2
40+
#' # - Element size 3
41+
#' list_transpose(list(1:2, 3:4, 5:6))
42+
#'
43+
#' # With data frames
44+
#' x <- data_frame(a = 1:2, b = letters[1:2])
45+
#' y <- data_frame(a = 3:4, b = letters[3:4])
46+
#' list_transpose(list(x, y))
47+
#'
48+
#' # Size 1 elements are recycled to the common size before transposing
49+
#' list_transpose(list(1, 2:4))
50+
#'
51+
#' # With all size 1 elements, you can use `size` if you want to force a known
52+
#' # common size other than size 1
53+
#' list_transpose(list(1, 2), size = 3)
54+
#'
55+
#' # With size 0 elements, the invariants are a bit tricky!
56+
#' # This must return a size 0 list, but then you lose expected
57+
#' # type (integer) and size (2) information about the elements.
58+
#' # Losing that information makes it difficult to reverse the
59+
#' # transposition.
60+
#' #
61+
#' # Input:
62+
#' # - List size 2
63+
#' # - Element size 0
64+
#' # Output:
65+
#' # - List size 0
66+
#' # - Element size 2
67+
#' x <- list(integer(), integer())
68+
#' out <- list_transpose(x)
69+
#' out
70+
#'
71+
#' # Note how transposing a second time doesn't recover the original list
72+
#' list_transpose(out)
73+
#'
74+
#' # To work around this, provide the lost `size` and `ptype` manually
75+
#' list_transpose(out, size = vec_size(x), ptype = vec_ptype_common(!!!x))
76+
#'
77+
#' # If you'd like to pad with a missing value rather than recycling or
78+
#' # erroring, you might do something like this, which left-pads
79+
#' x <- list(1, 2:5, 6:7)
80+
#' sizes <- list_sizes(x)
81+
#' size <- max(sizes)
82+
#' index <- which(sizes != size)
83+
#' x[index] <- lapply(
84+
#' index,
85+
#' function(i) vec_c(rep(NA, times = size - sizes[[i]]), x[[i]])
86+
#' )
87+
#' list_transpose(x)
88+
#'
89+
#' # `NULL` values aren't allowed in `list_transpose()`. If you'd like `NULL`s
90+
#' # to be treated as size 1 missing values, replace them with `NA` first.
91+
#' x <- list(1:3, NULL, 5:7, NULL)
92+
#' x <- vec_assign(x, vec_detect_missing(x), list(NA))
93+
#' list_transpose(x)
94+
list_transpose <- function(
95+
x,
96+
...,
97+
size = NULL,
98+
ptype = NULL,
99+
x_arg = caller_arg(x),
100+
error_call = current_env()
101+
) {
102+
check_dots_empty0(...)
103+
104+
# Disallow `NULL` entirely. These would break `vec_size()` invariants of
105+
# `list_transpose()` if we simply drop them via `list_interleave()`.
106+
#
107+
# For example:
108+
#
109+
# ```
110+
# list_transpose(list(1:4, NULL, 5:8))
111+
# ```
112+
#
113+
# Input:
114+
# - List size 3
115+
# - Element size 4
116+
# Output:
117+
# - List size 4
118+
# - Element size 3
119+
#
120+
# But if we drop `NULL` you'd get:
121+
# - List size 4
122+
# - Element size 2
123+
#
124+
# A reasonable thing for users to do would be to replace `NULL` with `NA`
125+
# ahead of time. This is similar to `keep_empty` in some tidyr functions.
126+
# But we force the caller to make that decision, and it's a fairly easy
127+
# replacement to make with `vec_detect_missing()` and `vec_assign()`.
128+
#
129+
# ```
130+
# list_transpose(list(1:4, NA, 5:8))
131+
# ```
132+
allow_null <- FALSE
133+
134+
obj_check_list(x, arg = x_arg, call = error_call)
135+
list_check_all_vectors(
136+
x,
137+
allow_null = allow_null,
138+
arg = x_arg,
139+
call = error_call
140+
)
141+
142+
flat <- list_interleave(
143+
x,
144+
size = size,
145+
ptype = ptype,
146+
name_spec = "inner",
147+
x_arg = x_arg,
148+
error_call = error_call
149+
)
150+
151+
x_size <- vec_size(x)
152+
153+
if (is_null(size)) {
154+
# Identical to `elt_size <- vec_size_common(!!!x)`, but faster.
155+
# Utilizes known info about the `list_interleave()` return value.
156+
if (x_size == 0L) {
157+
elt_size <- 0L
158+
} else {
159+
elt_size <- vec_size(flat) / x_size
160+
}
161+
} else {
162+
elt_size <- size
163+
}
164+
165+
sizes <- vec_rep(x_size, times = elt_size)
166+
167+
# Chop the one big vector into transposed pieces of size `x_size`
168+
out <- vec_chop(flat, sizes = sizes)
169+
170+
out
171+
}

man/list_transpose.Rd

Lines changed: 113 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# `x` must be a list
2+
3+
Code
4+
list_transpose(1)
5+
Condition
6+
Error in `list_transpose()`:
7+
! `1` must be a list, not the number 1.
8+
9+
---
10+
11+
Code
12+
list_transpose(1, x_arg = "x", error_call = quote(foo()))
13+
Condition
14+
Error in `foo()`:
15+
! `x` must be a list, not the number 1.
16+
17+
# `...` must be empty
18+
19+
Code
20+
list_transpose(1, 2)
21+
Condition
22+
Error in `list_transpose()`:
23+
! `...` must be empty.
24+
x Problematic argument:
25+
* ..1 = 2
26+
i Did you forget to name an argument?
27+
28+
# respects `size`
29+
30+
Code
31+
list_transpose(list(1:2), size = 3)
32+
Condition
33+
Error in `list_transpose()`:
34+
! Can't recycle `list(1:2)[[1]]` (size 2) to size 3.
35+
36+
# respects `ptype`
37+
38+
Code
39+
list_transpose(list(1, 2), ptype = character())
40+
Condition
41+
Error in `list_transpose()`:
42+
! Can't convert `list(1, 2)[[1]]` <double> to <character>.
43+
44+
---
45+
46+
Code
47+
list_transpose(list(1, 2), ptype = character(), x_arg = "x", error_call = quote(
48+
foo()))
49+
Condition
50+
Error in `foo()`:
51+
! Can't convert `x[[1]]` <double> to <character>.
52+
53+
# doesn't allow `NULL` elements
54+
55+
Code
56+
list_transpose(list(1:4, NULL, 5:8))
57+
Condition
58+
Error in `list_transpose()`:
59+
! `list(1:4, NULL, 5:8)[[2]]` must be a vector, not `NULL`.
60+
61+
# doesn't allow scalar elements
62+
63+
Code
64+
list_transpose(list(1:4, lm(1 ~ 1)))
65+
Condition
66+
Error in `list_transpose()`:
67+
! `list(1:4, lm(1 ~ 1))[[2]]` must be a vector, not a <lm> object.
68+
69+
---
70+
71+
Code
72+
list_transpose(list(1:4, lm(1 ~ 1)), x_arg = "x", error_call = quote(foo()))
73+
Condition
74+
Error in `foo()`:
75+
! `x[[2]]` must be a vector, not a <lm> object.
76+

0 commit comments

Comments
 (0)