|
| 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 | +} |
0 commit comments