Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

I’m solving Advent of Code this year using a relaxed criteria compared to last year in that I’m allowing myself to use packages where they’re helpful, rather than strictly base R. Last year I re-solved half of the exercises using Rust which helped me learn a lot about Rust. This year I’m enamored with APL, and I wanted to share a particularly beautiful solution.

⚠⚠⚠⚠⚠

Spoilers ahead for Day 7, in case you haven’t yet completed it yourself.

⚠⚠⚠⚠⚠

I solved Day 7 of Advent of Code using base R by testing whether or not a given hand was of each type with an individual function, either returning 0 (if it was not of that type) or `N` + a score, where `N` was sufficiently different between each type that they would sort nicely. For the score, I initially tried offsetting each card in a poor-man’s base-15 as `15^(4:0)*card_score` but later improved on that by using hex digits (which automatically sort nicer). The large `N` values ensured that ‘type’ would be sorted before the first/second/etc.. card.

That was sufficient to do an `apply(strength, hands)`, calculate the `order` of those, and multiply by the relevant bids. Aside from a bug not caught by the test case (the difference between `bid*order(x)` and `bid[order(x)]*seq_along(x)`) it was an okay solution to the problem, and it worked.

After solving each day, I’ve been trying to re-solve using APL; in particular Dyalog APL. For those who don’t know, APL is an old language (circa 1960s) borne from a mathematical notation in which a single glyph (symbol) represents some operation or application of a function. This makes it look very different to more modern languages, partly because of the glyphs, but also because it requires no boilerplate whatsoever. As an array language, it deals with vectors and matrices without needing to “loop over columns” or “for i in values”. It looks scary at first, but it’s really not – once you’re familiar with the glyphs it’s actually beautiful!

Let’s say you have a matrix `m` which contains the values `1` through `9`

```    m
1 2 3
4 5 6
7 8 9```

and you want to sum the columns. Chances are, the language you normally use will require you to first calculate the size of the matrix, maybe even perform a loop. In APL, it’s

```    +⌿m
12 15 18```

`⌿` is the glyph for “reduce along first axis”, or perform some operation (supplied as its left argument) to its right argument. `+⌿` is therefore “sum columns”. No boilerplate, just a direct explanation (the glyphs themselves are better names than any word you could attach to them) of what needs to be done.

Sure, you need to learn the glyphs, and potentially even how to enter them; one option being a prefix key then a corresponding key. How committed am I to learning those, you ask? Well, here’s my laptop

My laptop with APL stickers on the keys

I considered using APL for my Day 7 solution, but it was so many functions defined, and fiddly if/else logic, I figured it was just ill-suited to APL. Then I saw a video recap of an APL solution for Day 7 and my mind was blown.

Meanwhile, I saw a post from Elias Mårtenson, creator of the Kap language, promoting some examples of Kap and was even more interested given that it can do some things that (Dyalog) APL can’t, like produce graphics.

```    chart:line mtcars
┌→──────────────────────────────────────────────────────────────┐
↓1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 1 1 1 1 1 1 1│
│4 4 4 3 3 3 3 4 4 4 4 3 3 3 3 3 3 4 4 4 3 3 3 3 3 4 5 5 5 5 5 4│
│4 4 1 1 2 1 4 2 2 4 4 3 3 3 4 4 4 1 2 1 1 2 2 4 2 1 2 2 4 6 8 2│
└───────────────────────────────────────────────────────────────┘```
chart:line mtcars in Kap

Kap is a fairly new APL-based language (written in Kotlin) that supports most of Dyalog APL, but adds some cool extensions and alterations like lazy evaluation and parallel execution.

Uiua is another new language on the scene (written in Rust) which also supports graphics; the Uiua logo itself is written in Uiua

```Xy ← °⍉⊞⊟. ÷÷2: -÷2,⇡.200
Rgb ← [:°⊟×.Xy ↯△⊢Xy0.5]
u ← ↥<0.2:>0.7.+×2 ×.:°⊟Xy
c ← <:⍜°√/+ Xy
⍉⊂:-¬u c1 +0.1 ↧¤c0.95Rgb```
Uiua logo, coded in Uiua

The online editor for Uiua uses colours to distinguish different functions/operators, and the author has the flexibility to do what they want with that, so it’s awesome to see what they’ve used for “all” (`⋔`) and “transpose” (`⍉`)…

Uiua coloured glyphs for ‘all’ and ‘transpose’

I figured I’d try to reproduce the APL solution in Kap as a way to learn more about that language. The APL/Kap solution is so elegant! Additionally, I tried writing equivalent R code. I’ll interleave all three in this post (a nice excuse to get tabsets working!).

To start with, get the data into the workspace - this reads in a vector with each element representing a row of input

• APL

Reading from a file is performed using `⎕NGET`

```    ⊃⎕NGET'p07.txt'1
32T3K 765  T55J5 684  KK677 28  KTJJT 220  QQQJA 483```
• Kap

Kap uses some namespaces, which makes reading in a bit nicer, and the output is boxed, with explicit quotes for strings

```p ← io:read "p07.txt"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃"32T3K 765" "T55J5 684" "KK677 28" "KTJJT 220" "QQQJA 483"┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛```
• R

`readLines` reads in each line as an element of a vector

```p <- readLines("example07.txt")
p
## [1] "32T3K 765" "T55J5 684" "KK677 28"  "KTJJT 220" "QQQJA 483"```

## Preprocessing

The input consists of hands of cards juxtaposed with a bid value, separated by a space. The approach here is not to treat them individually, but to create a matrix containing columns of hands and columns of bids.

• APL

Partition (`(≠⊆⊢)`) on spaces (`' '`) for each (`¨`) row

```    ' '(≠⊆⊢)¨⊃⎕NGET'p07.txt'1
32T3K  765    T55J5  684    KK677  28    KTJJT  220    QQQJA  483```

It’s not entirely clear from this layout, but this is a vector of length-2 vectors.

These are “mixed” (stacked; `↑`), and the result assigned (`←`) to `p`

```p←↑' '(≠⊆⊢)¨⊃⎕NGET'p07.txt'1
32T3K  765
T55J5  684
KK677  28
KTJJT  220
QQQJA  483 ```

This is now a matrix, where the first column contains the hands, the second (last) column contains the bids.

• Kap

Rather than the partition idiom, Kap has regex support, so splitting the components involes `regex:split` for each (`¨`) element of input

```p←⊃{" " regex:split ⍵}¨p
┌→────────────┐
↓"32T3K" "765"│
│"T55J5" "684"│
│"KK677"  "28"│
│"KTJJT" "220"│
│"QQQJA" "483"│
└─────────────┘```
• R

The boilerplate of R’s matrix construction takes a toll after using APL/Kap…

```p <- matrix(unlist(strsplit(p, " ")), ncol = 2, byrow = TRUE)
p
##      [,1]    [,2]
## [1,] "32T3K" "765"
## [2,] "T55J5" "684"
## [3,] "KK677" "28"
## [4,] "KTJJT" "220"
## [5,] "QQQJA" "483"```

## Extraction

The hands and bids can be extracted into their own variables.

• APL

This can be achieved several ways, but a clean way is by reducing (`/`) with either the ‘leftmost’ (`⊣`) or ‘rightmost’ (`⊢`) operator, and evaluating (executing `⍎`) each (`¨`) of the bids to convert from strings to numbers

```hands←⊣/p
bids←⍎¨⊢/p```
• Kap

Kap uses exactly the same approach as APL for this

```hands←⊣/p
bids←⍎¨⊢/p```
• R

R’s ‘subset by index’ works just fine, but if this was generalised I’d use something like `p[, ncol(p)]` to get to the last column

```hands <- p[,1]
hands
## [1] "32T3K" "T55J5" "KK677" "KTJJT" "QQQJA"
bids <- as.integer(p[,2])
bids
## [1] 765 684  28 220 483```

## Tabulate

Now comes the interesting part! Rather than deal with the types separately, one approach is to identify them by their relative counts; a five-of-a-kind has 5 of one card and nothing elese; a four-of-a-kind has four of one and one of another.

• APL

APL has a “key” (`⌸`) which takes a function as a left argument, which can be to count the occurrences of each element with “tally” (`≢`)

```      {⍺,≢⍵}⌸'TGGATAACTTGAAC'
T 4
G 3
A 5
C 2```

In this case, we can get just the tallied count of each card in the hand with

```    {⊢∘≢⍵}⌸¨hands
2 1 1 1  1 3 1  2 1 2  1 2 2  3 1 1```

We can then sort (`⍵[⍒⍵]`) these, take just the first two values (`2↑`), and decode (`⊥`) using base 10 to a single number. A nice feature of APL is that trying to take the “first N” elements of a single element pads to the full N with zeroes.

```    f←{10⊥2↑{⍵[⍒⍵]}⊢∘≢⌸⍵}
f¨hands
21 31 22 22 31```
• Kap

Kap doesn’t have the equivalent Key, but after some discussion with the creator, it’s entirely possible to get something that does the same

```  key⇐(⍪+⌿≡⌻)∘∪ ⍝ using outer product - see the R solution
key2⇐{u←∪⍵ ⋄ c←⍸˝∧u⍳⍵} ⍝ using inverse 'where' and 'index of'

key2¨hands
┌→────────────────────────────────────────┐
│┌→──────┐ ┌→────┐ ┌→────┐ ┌→────┐ ┌→────┐│
││2 1 1 1│ │1 3 1│ │2 1 2│ │1 2 2│ │3 1 1││
│└───────┘ └─────┘ └─────┘ └─────┘ └─────┘│
└─────────────────────────────────────────┘```

The rest is the same as APL, except Kap uses a dedicated sort (`∨`)

```    handrank⇐{10⊥2↑∨⊢/key ⍵}
handrank¨hands
┏━━━━━━━━━━━━━━┓
┃21 31 22 22 31┃
┗━━━━━━━━━━━━━━┛```
• R

I wanted to recreate the above approach in R, so this will take the long way ’round.

First, we need a ‘key’ function

```key <- function(x) {
l <- strsplit(x, "")[[1]]
setNames(colSums(outer(l, unique(l), "==")), unique(l))
}

sapply(hands, key)
## \$`32T3K`
## 3 2 T K
## 2 1 1 1
##
## \$T55J5
## T 5 J
## 1 3 1
##
## \$KK677
## K 6 7
## 2 1 2
##
## \$KTJJT
## K T J
## 1 2 2
##
## \$QQQJA
## Q J A
## 3 1 1```

The idea of this is to create an outer product between the set of unique letters in the string, and the individual letters, performing an `==` check on each combination

```y <- strsplit(hands[2], "")[[1]]
outer(y, unique(y), "==")
##       [,1]  [,2]  [,3]
## [1,]  TRUE FALSE FALSE
## [2,] FALSE  TRUE FALSE
## [3,] FALSE  TRUE FALSE
## [4,] FALSE FALSE  TRUE
## [5,] FALSE  TRUE FALSE```

This is, of course, unnecessary as R has a way to do this

```table(y)
## y
## 5 J T
## 3 1 1```

but I wanted to see how to do it from scratch.

Applying this over the hands, we can sort each of the counts again, but now taking the first two values fails for the five-of-a-kind which only has a `5`, so in that case I add the missing 0. Decoding as base 10 can be done a couple of ways, but pasting and converting seems to work fine.

```handrank <- function(x) {
rank <- sort(sapply(x, key), decreasing = TRUE)
if (length(rank) == 1) rank <- c(rank, 0)
as.integer(paste(rank[1:2], collapse = ""))
}

sapply(hands, handrank)
## 32T3K T55J5 KK677 KTJJT QQQJA
##    21    31    22    22    31```

Finally, the part where the ‘array’ approach shines! Rather than constructing some sortable number for each hand, we can just score each card and use an array.

• APL

Creating a vector of all the cards is aided by the ‘numbers as a string’ helper `⎕D`. Drop the first two of these (`2↓`) then append the ‘face’ cards

```    r←'TJQKA',⍨2↓⎕D
r
23456789TJQKA```

Stacking the hands into a matrix of cards

```    ↑hands
32T3K
T55J5
KK677
KTJJT
QQQJA```

we can ask for the index of matches to the individual cards with `⍳`

```    r⍳↑hands
2  1  9  2 12
9  4  4 10  4
12 12  5  6  6
12  9 10 10  9
11 11 11 10 13```

Prepending (`,`) each column with the tabulated type of each hand

```    r{⍵,⍺⍳↑hands}f¨hands
21  2  1  9  2 12
31  9  4  4 10  4
22 12 12  5  6  6
22 12  9 10 10  9
31 11 11 11 10 13```

Now, some real magic… APL support “total array ordering” which means we can just sort the entire thing - it will sort by the first column, using the second and subsequent columns for ties. Given that the first column is the ‘type’ of hand, and subsequent columns are values of each card in order, that’s precisely the sorting we need!

```    r{⍋⍋⍵,⍺⍳↑hands}f¨hands
1 4 3 2 5```

Finally, multiplying by the bids themselves, and sum-reducing gives the final answer

```  +/r{bids×⍋⍋⍵,⍺⍳↑hands}f¨hands
6440```
• Kap

This is mostly the same solution as APL, except I couldn’t find the ‘numbers as string’ so i just typed it out. Kap also uses ‘disclose’ (`⊃`) in place of mix (`↑`) (ref).

```    ranks←"23456789TJQKA"
+/ranks{bids×1+⍋⍋⍵,⍺⍳⊃hands}handrank¨hands
6440```
• R

R doesn’t support Total Array Ordering, but it does seem to have a way to do it, so say the documentation examples for `order`

```## or along 1st column, ties along 2nd, ... *arbitrary* no.{columns}:
dd[ do.call(order, dd), ]```

That only works for a `data.frame`, which is a `list` (per `do.call`’s requirement). We can still work with that. First, smoosh together all the hands and convert the individual cards to a matrix - again, a long line of commands for what is reasonably straightforward in APL… `3 3⍴'abcdefghi'` reshapes those 9 letters into a 3x3 matrix.

```m <- matrix(strsplit(paste0(hands, collapse = ""), "")[[1]], ncol = 5, byrow = TRUE)
m
##      [,1] [,2] [,3] [,4] [,5]
## [1,] "3"  "2"  "T"  "3"  "K"
## [2,] "T"  "5"  "5"  "J"  "5"
## [3,] "K"  "K"  "6"  "7"  "7"
## [4,] "K"  "T"  "J"  "J"  "T"
## [5,] "Q"  "Q"  "Q"  "J"  "A"```

The individual cards vector benefits from coercing the digits to characters

`ranks <- c(2:9, "T", "J", "Q", "K", "A")`

The index mapping does actually work nicely with `match`, except it returns a single vector, not a matrix, so we need to reshape yet again. Plus, this time, the matches went down columns not along rows, so we need to use `byrow = FALSE`

```mm <- matrix(match(m, ranks), ncol = 5, byrow = FALSE)
mm
##      [,1] [,2] [,3] [,4] [,5]
## [1,]    2    1    9    2   12
## [2,]    9    4    4   10    4
## [3,]   12   12    5    6    6
## [4,]   12    9   10   10    9
## [5,]   11   11   11   10   13```

Prepending with the type rankings does work nicely via `cbind`

```g <- cbind(sapply(hands, handrank), mm)
g
##       [,1] [,2] [,3] [,4] [,5] [,6]
## 32T3K   21    2    1    9    2   12
## T55J5   31    9    4    4   10    4
## KK677   22   12   12    5    6    6
## KTJJT   22   12    9   10   10    9
## QQQJA   31   11   11   11   10   13```

Then ordering with the `do.call` idiom

```gdf <- as.data.frame(g)
gdf[do.call(order, gdf), ]
##       V1 V2 V3 V4 V5 V6
## 32T3K 21  2  1  9  2 12
## KTJJT 22 12  9 10 10  9
## KK677 22 12 12  5  6  6
## T55J5 31  9  4  4 10  4
## QQQJA 31 11 11 11 10 13```

Putting this all together into a function

```sortrank <- function(x, y) {
m <- matrix(strsplit(paste0(y, collapse = ""), "")[[1]], ncol = 5, byrow = TRUE)
mm <- matrix(match(m, x), ncol = 5, byrow = FALSE)
g <- cbind(sapply(y, handrank), mm)
do.call(order, as.data.frame(g))
}

sortrank(ranks, hands)
## [1] 1 4 3 2 5```

This isn’t the double sorting that APL and Kap used, and that little difference is what held me up for all too long trying to figure out why my solution passed tests but gave the wrong answer. Annoyingly, this mistake doesn’t show up in the test case because the ranks only differ by a switched place. The true input was not so kind.

This result is the order in which we need to place the bids, so doing that, then multiplying by the position (since it’s sorted, this is just a vector from `1` to the number of elements) we get the answer

```sum(bids[sortrank(ranks, hands)]*seq_along(bids))
## [1] 6440```

## Summary

So, how do these solutions all look? I’ll stop with the tabsets for a side-by-side comparison.

Compacting the APL solution (which does involve some duplication) it’s as simple as

```p←↑' '(≠⊆⊢)¨⊃⎕NGET'p07.txt'1
+/('TJQKA',⍨2↓⎕D){(⍎¨⊢/p)×⍋⍋⍵,⍺⍳↑⊣/p}{10⊥2↑{⍵[⍒⍵]}⊢∘≢⌸⍵}¨⊣/p```

which, admittedly, requires a fair amount of unpacking to read. In full form, it’s

```p←↑' '(≠⊆⊢)¨⊃⎕NGET'p07.txt'1
hands←⊣/p
bids←⍎¨⊢/p
f←{10⊥2↑{⍵[⍒⍵]}⊢∘≢⌸⍵}
r←'TJQKA',⍨2↓⎕D
+/r{bids×⍋⍋⍵,⍺⍳↑hands}f¨hands```

which is still pretty nice, considering what it’s doing.

The R solution, somewhat minimally, and leveraging `table`, is

```handrank <- function(x) {
rank <- sort(sapply(strsplit(x, ""), table), decreasing = TRUE)
if (length(rank) == 1) rank <- c(rank, 0)
as.integer(paste(rank[1:2], collapse = ""))
}

sortrank <- function(x, y) {
m <- matrix(strsplit(paste0(y, collapse = ""), "")[[1]], ncol = 5, byrow = TRUE)
mm <- matrix(match(m, x), ncol = 5, byrow = FALSE)
g <- cbind(sapply(y, handrank), mm)
do.call(order, as.data.frame(g))
}

solve <- function(x) {
p <- matrix(unlist(strsplit(x, " ")), ncol = 2, byrow = TRUE)
hands <- p[,1]
bids <- as.integer(p[,2])
ranks <- c(2:9, "T", "J", "Q", "K", "A")
sum(bids[sortrank(ranks, hands)]*seq_along(bids))
}

Certainly more typing, but still a much shorter solution than the one I originally came up with.

## Takeaways

Both APL and Kap (and so many other languages) benefit greatly from treating a string as an array of characters. This always hurts in R, where `strsplit(x, "")` is needed.

The array approach here highlights how one can think differently about a problem, provided the tools are at hand.

Kap has a lot to offer - it’s (vastly) newer, which comes with both advantages (can do new things) and disadvantages (things need to be implemented, and they won’t necessarily carry over 1:1).

Advent of Code once again proves to be a useful exercise.

## One more thing

I saw a solution in Uiua on Mastodon and had to give it a go, too…

```Input ← ⊜(⊜□≠@ .)≠@\n.&fras"p07.txt"
Label ← ⇌"AKQJT98765432"
Bids ← ⋕⊢↘1⍉
Cards ← ⊐≡(⊗:Label)⊢⍉
Types ← 0_1_2_4_8_5_10_9_3_6_12_11_13_7_14_15⊚1_4_3_3_2_2_1
TypeStr ← ⊏⊗⊙Types≡(°⋯≡/=◫2⊏⍏.)
/+×+1⍏⍏/+×ⁿ⇌⇡6⧻Label⊂⊃TypeStr⍉⊃Cards Bids Input```

I think this is taking the same approach, though unpacking this is even trickier.

Comments and improvements most welcome. I can be found on Mastodon or use the comments below.

devtools::session_info()
```## ─ Session info ───────────────────────────────────────────────────────────────
##  setting  value
##  version  R version 4.3.2 (2023-10-31)
##  os       Pop!_OS 22.04 LTS
##  system   x86_64, linux-gnu
##  ui       X11
##  language (EN)
##  collate  en_AU.UTF-8
##  ctype    en_AU.UTF-8
##  date     2023-12-10
##  pandoc   3.1.1 @ /usr/lib/rstudio/resources/app/bin/quarto/bin/tools/ (via rmarkdown)
##
## ─ Packages ───────────────────────────────────────────────────────────────────
##  package     * version date (UTC) lib source
##  blogdown      1.18    2023-06-19 [1] CRAN (R 4.3.2)
##  bookdown      0.36    2023-10-16 [1] CRAN (R 4.3.2)
##  bslib         0.5.1   2023-08-11 [3] CRAN (R 4.3.1)
##  cachem        1.0.8   2023-05-01 [3] CRAN (R 4.3.0)
##  callr         3.7.3   2022-11-02 [3] CRAN (R 4.2.2)
##  cli           3.6.1   2023-03-23 [3] CRAN (R 4.2.3)
##  crayon        1.5.2   2022-09-29 [3] CRAN (R 4.2.1)
##  devtools      2.4.5   2022-10-11 [1] CRAN (R 4.3.2)
##  digest        0.6.33  2023-07-07 [3] CRAN (R 4.3.1)
##  ellipsis      0.3.2   2021-04-29 [3] CRAN (R 4.1.1)
##  evaluate      0.22    2023-09-29 [3] CRAN (R 4.3.1)
##  fastmap       1.1.1   2023-02-24 [3] CRAN (R 4.2.2)
##  fs            1.6.3   2023-07-20 [3] CRAN (R 4.3.1)
##  glue          1.6.2   2022-02-24 [3] CRAN (R 4.2.0)
##  htmltools     0.5.6.1 2023-10-06 [3] CRAN (R 4.3.1)
##  htmlwidgets   1.6.2   2023-03-17 [1] CRAN (R 4.3.2)
##  httpuv        1.6.12  2023-10-23 [1] CRAN (R 4.3.2)
##  icecream      0.2.1   2023-09-27 [1] CRAN (R 4.3.2)
##  jquerylib     0.1.4   2021-04-26 [3] CRAN (R 4.1.2)
##  jsonlite      1.8.7   2023-06-29 [3] CRAN (R 4.3.1)
##  knitr         1.44    2023-09-11 [3] CRAN (R 4.3.1)
##  later         1.3.1   2023-05-02 [1] CRAN (R 4.3.2)
##  lifecycle     1.0.3   2022-10-07 [3] CRAN (R 4.2.1)
##  magrittr      2.0.3   2022-03-30 [3] CRAN (R 4.2.0)
##  memoise       2.0.1   2021-11-26 [3] CRAN (R 4.2.0)
##  mime          0.12    2021-09-28 [3] CRAN (R 4.2.0)
##  miniUI        0.1.1.1 2018-05-18 [1] CRAN (R 4.3.2)
##  pkgbuild      1.4.2   2023-06-26 [1] CRAN (R 4.3.2)
##  pkgload       1.3.3   2023-09-22 [1] CRAN (R 4.3.2)
##  prettyunits   1.2.0   2023-09-24 [3] CRAN (R 4.3.1)
##  processx      3.8.2   2023-06-30 [3] CRAN (R 4.3.1)
##  profvis       0.3.8   2023-05-02 [1] CRAN (R 4.3.2)
##  promises      1.2.1   2023-08-10 [1] CRAN (R 4.3.2)
##  ps            1.7.5   2023-04-18 [3] CRAN (R 4.3.0)
##  purrr         1.0.2   2023-08-10 [3] CRAN (R 4.3.1)
##  R6            2.5.1   2021-08-19 [3] CRAN (R 4.2.0)
##  Rcpp          1.0.11  2023-07-06 [1] CRAN (R 4.3.2)
##  remotes       2.4.2.1 2023-07-18 [1] CRAN (R 4.3.2)
##  rlang         1.1.1   2023-04-28 [3] CRAN (R 4.3.0)
##  rmarkdown     2.25    2023-09-18 [3] CRAN (R 4.3.1)
##  rstudioapi    0.15.0  2023-07-07 [3] CRAN (R 4.3.1)
##  sass          0.4.7   2023-07-15 [3] CRAN (R 4.3.1)
##  sessioninfo   1.2.2   2021-12-06 [1] CRAN (R 4.3.2)
##  shiny         1.7.5.1 2023-10-14 [1] CRAN (R 4.3.2)
##  stringi       1.7.12  2023-01-11 [3] CRAN (R 4.2.2)
##  stringr       1.5.0   2022-12-02 [3] CRAN (R 4.3.0)
##  urlchecker    1.0.1   2021-11-30 [1] CRAN (R 4.3.2)
##  usethis       2.2.2   2023-07-06 [1] CRAN (R 4.3.2)
##  vctrs         0.6.4   2023-10-12 [3] CRAN (R 4.3.1)
##  xfun          0.40    2023-08-09 [3] CRAN (R 4.3.1)
##  xtable        1.8-4   2019-04-21 [1] CRAN (R 4.3.2)
##  yaml          2.3.7   2023-01-23 [3] CRAN (R 4.2.2)
##
##  [1] /home/jono/R/x86_64-pc-linux-gnu-library/4.3
##  [2] /usr/local/lib/R/site-library
##  [3] /usr/lib/R/site-library
##  [4] /usr/lib/R/library
##
## ──────────────────────────────────────────────────────────────────────────────```