Your collaborator says to you “I have some code I’d like to distribute to people who will probably work in R most of the time. I don’t write R, but I write C. Can you package this up for me?” so you have a few options: re-write the code in R, package up the C code and make it available in R, or say no. I decided to try out the second of these, and this post details how I achieved that.
Before we even start, this is an excellent post summarising many of the finer points involved here - go read that! Then, read some of @coolbutuseless’ various repositories demonstrating how to wrap C code into R packages. These, and many others, go much deeper into how to achieve this, but I’m going to detail what I did because a) I’ll want to remember, later; b) I had enough trouble piecing together what I needed between these excellent posts and some older, possibly out of date posts; and c) I did build some functionality beyond what was done in those straightforward examples.
Those of you who know R really well probably know that the language itself is in no small part written in C. Many packages do the same, usually for performance reasons. This becomes most apparent if you install a package “from source” and see a lot of this mess fly past in your console
gcc -I"/usr/share/R/include" -DNDEBUG -I./pkg/ -fvisibility=hidden -fpic -g -O2 -ffile-prefix-map=/build/r-base-4A2Reg/r-base-4.1.2=. -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -g -c file1.c -o file1.o
gcc -I"/usr/share/R/include" -DNDEBUG -I./pkg/ -fvisibility=hidden -fpic -g -O2 -ffile-prefix-map=/build/r-base-4A2Reg/r-base-4.1.2=. -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -g -c file2.c -o file2.o
gcc -I"/usr/share/R/include" -DNDEBUG -I./pkg/ -fvisibility=hidden -fpic -g -O2 -ffile-prefix-map=/build/r-base-4A2Reg/r-base-4.1.2=. -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -g -c pkg.c -o pkg.o
gcc -shared -L/usr/lib/R/lib -Wl,-Bsymbolic-functions -flto=auto -ffat-lto-objects -flto=auto -Wl,-z,relro -o pkg.so file1.o file2.o pkg.o -L/usr/lib/R/lib -lR
Other languages are supported, including Fortran (yet to be superseded for numerical libraries), C++, Rust, and various others. You can usually dig into the source of these if you can track down where they come from. When debugging a function call, R is happy to step through individual lines of R code. Try the following
debugonce(seq.default)
seq(5)
and step through the lines of seq.default
until it reaches 1L:from
(yes, seq(from = x)
produces the values 1
to from
… sigh) where it returns that value as
exiting from: seq.default(5)
[1] 1 2 3 4 5
When the function involves C code, though, R can’t step through that because it’s
compiled. One of the most common ways to hit that limitation is when a function
calls either .Internal()
or .Primitive()
.
I went looking for a function containing one of these (there are plenty) and found .row_names_info
# number of rownames
.row_names_info(mtcars)
## [1] 32
# the rownames themselves
.row_names_info(mtcars, type = 0)
## [1] "Mazda RX4" "Mazda RX4 Wag" "Datsun 710"
## [4] "Hornet 4 Drive" "Hornet Sportabout" "Valiant"
## [7] "Duster 360" "Merc 240D" "Merc 230"
## [10] "Merc 280" "Merc 280C" "Merc 450SE"
## [13] "Merc 450SL" "Merc 450SLC" "Cadillac Fleetwood"
## [16] "Lincoln Continental" "Chrysler Imperial" "Fiat 128"
## [19] "Honda Civic" "Toyota Corolla" "Toyota Corona"
## [22] "Dodge Challenger" "AMC Javelin" "Camaro Z28"
## [25] "Pontiac Firebird" "Fiat X1-9" "Porsche 914-2"
## [28] "Lotus Europa" "Ford Pantera L" "Ferrari Dino"
## [31] "Maserati Bora" "Volvo 142E"
if we wanted to see what .row_names_info()
does
we would write
.row_names_info
## function (x, type = 1L)
## .Internal(shortRowNames(x, type))
## <bytecode: 0x563e6bf3c890>
## <environment: namespace:base>
but we can’t see any deeper unless we ask where that C code lives. I recommend using pryr::show_c_source()
(as I did in a previous post) to identify the C code for these, e.g.
pryr::show_c_source(.Internal(shortRowNames(mtcars)))
shortRowNames is implemented by do_shortRowNames with op = 0
which opens a GitHub search of a copy of the R source in a browser. The file we want is attrib.c
and contains the C code
SEXP do_shortRowNames(SEXP call, SEXP op, SEXP args, SEXP env)
{
/* return n if the data frame 'vec' has c(NA, n) rownames;
* nrow(.) otherwise; note that data frames with nrow(.) == 0
* have no row.names.
==> is also used in dim.data.frame() */
checkArity(op, args);
SEXP s = getAttrib0(CAR(args), R_RowNamesSymbol), ans = s;
int type = asInteger(CADR(args));
if( type < 0 || type > 2)
error(_("invalid '%s' argument"), "type");
if(type >= 1) {
int n = (isInteger(s) && LENGTH(s) == 2 && INTEGER(s)[0] == NA_INTEGER)
? INTEGER(s)[1] : (isNull(s) ? 0 : LENGTH(s));
ans = ScalarInteger((type == 1) ? n : abs(n));
}
return ans;
}
Fully interpreting this is beyond the scope of this post, but the links at the start of this post cover most of what’s not plain C code here.
I won’t share my collaborator’s exact code, but I can write enough C that I can create something with all the relevant features.
Let’s calculate Pythagorean Triples! These
are sets of 3 integers (whole numbers) a
, b
, and c
such that a triangle with sides of those
lengths will be a right-triangle (contains a 90 degree / right-angle). These have
the property that \[a^2 + b^2 = c^2\]
The smallest of these is 3, 4, 5
because \[3^2 + 4^2 = 9 + 16 = 25 = 5^2\]
Generating these just happens to fit the use-case I’m emulating, plus I have a whole other blog post coming up about these (stay tuned!).
Some C code to generate these up to some maximum side-length, written similar to the code I received, is
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main (int argc, char *argv[]) {
int a, b, c;
int maxval;
int ***triangles;
if ( argc != 2 ) {
printf("Usage: triangle max_side_length\n");
exit(EXIT_FAILURE);
}
maxval = atoi( argv[1] );
triangles = (int ***) malloc (maxval * sizeof(int **));
for (a = 0; a < maxval; ++a) {
triangles[a] = (int **) malloc (maxval * sizeof(int *));
for (b = 0; b < maxval; ++b) {
triangles[a][b] = (int *) malloc (maxval * sizeof(int));
for (c = 0; c < maxval; ++c) {
triangles[a][b][c] = 0;
}
}
}
for (c = 1; c <= maxval; c++) {
for (b = 1; b <= c; b++)
for (a = 1; a <= b; a++)
if ( ( pow ( a, 2 ) + pow ( b, 2 ) ) == pow ( c, 2 ) ) {
triangles[a][b][c] = a + b + c;
}
}
printf("%4s\t%4s\t%4s\t%4s\n", "a", "b", "c", "sum");
printf(" -------------------------\n");
for (c = 1; c <= maxval; c++) {
for (b = 1; b <= c; b++)
for (a = 1; a <= b; a++)
if ( ( pow ( a, 2 ) + pow ( b, 2 ) ) == pow ( c, 2 ) ) {
printf("%4i\t%4i\t%4i\t%4i\n", a, b, c, triangles[a][b][c]);
}
}
exit(EXIT_SUCCESS);
}
I won’t make this an entire C tutorial, but the main pieces are:
Load some libraries for printing to screen, doing math, …
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
Define an entrypoint function (the thing that will run when the code is run)
which takes some number of character arguments argv
, the first of which is the
compiled name of the program itself
int main (int argc, char *argv[]) {
Define some variables, the most significant being triangles
which is denoted as
a pointer to a pointer to a pointer (!)
int a, b, c;
int maxval;
int ***triangles;
That’s a lot of redirection, but it’s just creating a reference to a 3-dimensional array.
Side-note: 0-indexed languages actually make a bit more sense when working with pointer math
because a “vector” of memory addresses really only needs to “point” to the starting address,
then every element is some offset away from that, so the first element of some vector vec
might have some address x
, but you can access that with vec[0]
. You can access the next
element with vec[1]
which means “offset 1 position from x
, the starting address.” You can access
the fifth value by offsetting 4 positions, so vec[4]
.
One of my favourite bits of C trivia is that this syntactic sugar of using square brackets to identify positions actually translates to
vec[0] is at address x + 0 => vec + 0
vec[1] is at address x + 1 => vec + 1
vec[2] is at address x + 2 => vec + 2
...
vec[5] is at address x + 5 => vec + 5
but addition (+
) is symmetric (commutative) so we can just as easily write
vec + 0 => 0 + vec => 0 + x is at address 0[vec]
vec + 1 => 1 + vec => 1 + x is at address 1[vec]
vec + 2 => 2 + vec => 2 + x is at address 2[vec]
...
vec + 5 => 5 + vec => 5 + x is at address 5[vec]
and it all works out… 5[obj]
is valid, and corresponds to the same address as obj[5]
.
Back to our function. If only one argument is passed in (the name of the program) then the usage information is printed, otherwise the next argument is used to set the upper bound on the length of a
side of the triangle, converting it from a string to an int with atoi
if ( argc != 2 ) {
printf("Usage: triangle max_side_length\n");
exit(EXIT_FAILURE);
}
maxval = atoi( argv[1] );
Next, the array is allocated (and assigned a default value of 0)
triangles = (int ***) malloc (maxval * sizeof(int **));
for (a = 0; a < maxval; ++a) {
triangles[a] = (int **) malloc (maxval * sizeof(int *));
for (b = 0; b < maxval; ++b) {
triangles[a][b] = (int *) malloc (maxval * sizeof(int));
for (c = 0; c < maxval; ++c) {
triangles[a][b][c] = 0;
}
}
}
and then, finally, we do the ‘calculation’ which just involves stepping through
every value, and if our criteria of \[a^2 + b^2 == c^2\] is met, we
assign the sum of these to an element in triangles
indexed by a
, b
, and c
for (c = 1; c <= maxval; c++) {
for (b = 1; b <= c; b++)
for (a = 1; a <= b; a++)
if ( ( pow ( a, 2 ) + pow ( b, 2 ) ) == pow ( c, 2 ) ) {
triangles[a][b][c] = a + b + c;
}
}
This isn’t efficient at all - there will be lots of 0
values, but this is a
simple program.
The last section of the code just loops back through all of a
, b
, and c
and when
it finds a non-zero element, it prints it, along with the sum a + b + c
(the value in
triangles[a][b][c]
)
printf("%4s\t%4s\t%4s\t%4s\n", "a", "b", "c", "sum");
printf(" -------------------------\n");
for (c = 1; c <= maxval; c++) {
for (b = 1; b <= c; b++)
for (a = 1; a <= b; a++)
if ( ( pow ( a, 2 ) + pow ( b, 2 ) ) == pow ( c, 2 ) ) {
printf("%4i\t%4i\t%4i\t%4i\n", a, b, c, triangles[a][b][c]);
}
}
With all of this saved as triangles.c
we can compile and run this code in a terminal
$ cc -O -o triangle triangles.c
$ ./triangle
Usage: triangle max_side_length
$ ./triangle 16
a b c sum
-------------------------
3 4 5 12
6 8 10 24
5 12 13 30
9 12 15 36
Woot! You can even check that it has worked: \[9^2 + 12^2 = 81 + 144 = 225 = 15^2\]
Back to the goal of this post - how do we get R to run that? We have some C code, now what?
First, I created an R package. I like using RStudio for this as it auto-generates a lot of
the structure I want. It does, however, create an example R file R/hello.R
(and corresponding
man/hello.Rd
page) so I delete those. I also delete the NAMESPACE
because I’m going to
use {roxygen} to generate a new one. I check ‘Generate documentation with Roxygen’ in the
Build tools menu, making sure to select ‘Build & Reload’ (which should be a default, no?)
and build my empty package.
I love the {usethis} package for building packages, and there’s support there for what
we’re doing, too! usethis::use_c()
sets up some structure and adds the required boilerplate
so that Roxygen knows we’re using C code. This does add a src/code.c
file we can delete and
in its place we can put our own C code.
If you read the links at the start of this post, you’ll recognise that this C code isn’t quite
ready to be used in an R package, though - we need to be able to send an R object (a SEXP
) to this
C code, not a char
. More importantly, the functionality of the C code is all wrapped
up in the main()
entrypoint function - it would be great if that was refactored out to
another function that could be called from main()
but also from an R-facing function.
I communicated this to my colleague and they agreed we could refactor that, but they want to still run the C code from the command line, so we can’t just put everything in our R-facing function. The actual processing in the code could go to a new function that doesn’t return anything, but does update the 3-dimensional array passed by reference
void calculate_sum(int maxval, int ****tri_sum) {
int a, b, c;
for (c = 1; c <= maxval; c++) {
for (b = 1; b <= c; b++)
for (a = 1; a <= b; a++)
if ( ( pow ( a, 2 ) + pow ( b, 2 ) ) == pow ( c, 2 ) ) {
(*tri_sum)[a][b][c] = a + b + c;
}
}
}
[... in main()]
printf("calling external sum\n");
calculate_sum(maxval, &triangles);
Yes, that’s a pointer to a pointer to a pointer to a pointer (!!!!).
The gotchas I encountered here were that
*tri_sum[a][b][c]
would be a pointer to the indexed object, so I needed
(*tri_sum)[a][b][c]
and &triangles
sends a reference to the triangles
object.
Compiling and running this shows that we’ve successfully refactored out the main functionality
$ cc -O -o triangle1 triangles1.c
$ ./triangle1 20
calling external sum
a b c sum
-------------------------
3 4 5 12
6 8 10 24
5 12 13 30
9 12 15 36
8 15 17 40
12 16 20 48
But this still isn’t quite what we need for R… we need to pass and return SEXP
s.
Rather than disrupt the runnable C code, we can add some additional R-specific code. That requires the R-related libraries
#include <R.h>
#include <Rinternals.h>
(keeping in mind that these are required if the user is compiling all of this code - it’s possible, but perhaps we’ll comment these out when just using the C code standalone).
We need a function that takes a SEXP
(our maximum value) and returns a SEXP
- this is
required, but so far we’re just printing to screen. We’ll return something for now. A
function that meets these criteria and calls the new calculate_sum()
could be
SEXP C_triangles(SEXP maximum) {
int a, b, c;
int ***triangles;
int maxval = * INTEGER(maximum);
triangles = (int ***) malloc (maxval * sizeof(int **));
for (a = 0; a < maxval; ++a) {
triangles[a] = (int **) malloc (maxval * sizeof(int *));
for (b = 0; b < maxval; ++b) {
triangles[a][b] = (int *) malloc (maxval * sizeof(int));
for (c = 0; c < maxval; ++c) {
triangles[a][b][c] = 0;
}
}
}
printf("calling C function to calc sum\n");
calculate_sum(maxval, &triangles);
printf("%4s\t%4s\t%4s\t%4s\n", "a", "b", "c", "sum");
printf(" -------------------------\n");
for (c = 1; c < maxval; ++c) {
for (b = 1; b <= c; ++b)
for (a = 1; a <= b; ++a)
if ( ( pow ( a, 2 ) + pow ( b, 2 ) ) == pow ( c, 2 ) ) {
printf("%4i\t%4i\t%4i\t%4i\n", a, b, c, triangles[a][b][c]);
}
}
SEXP result = PROTECT(allocVector(LGLSXP, 1));
LOGICAL(result)[0] = 1;
UNPROTECT(1);
return(result);
}
This is very similar to what’s in main()
- it still performs the allocation then
calls out to the calculating code, then prints the result. The only new part is
creating a logical result
object (1
== TRUE
) so that there’s something to return.
You can read about PROTECT
which guards against garbage collection in the R-exts manual.
The new functions called here such as allocVector
come from the Rinternals library and
are macros for functions starting with Rf_
- i.e. Rf_allocVector
. I had some issues initially
because I followed some guides which used #define R_NO_REMAP
. Keep in mind that if you use
that (so that library functions aren’t remapped) you will need to use the Rf_
versions
of these functions. I ended up removing the #define
myself, but I’m not sure if that will
bite me later.
This also needs to convert the SEXP
input maximum
to a C int
via * INTEGER(maximum)
.
We now have something that should work in our R package! Saving this as src/triangles.c
we can add the R interface as R/triangles.R
containing just
#' triangles
#'
#' @export
triangles <- function(maxval) {
.Call("C_triangles", as.integer(maxval))
}
where we definitely only send an integer to the C function.
Build the package, which compiles the code, and load the package
library(triangles)
triangles(20)
calling C function to calc sum
a b c sum
-------------------------
3 4 5 12
6 8 10 24
5 12 13 30
9 12 15 36
8 15 17 40
[1] TRUE
We see the debug print statement, then the printed output, and finally our returned TRUE
. Success!
The original code was made to work in a command line pipeline where the values were read back in by a subsequent program, e.g.
$ ./triangle 16 | tail +3 | awk '{ sum += $4 } END { print sum }'
102
so printing to stdout
made sense there, but we want to use the values in R, so it
would be great to return an actual data.frame
. This repo
contains a great example of doing that but I want to return a data.frame
with a
variable number of rows, and I need to allocate data into that. ChatGPT actually got me close enough to a working version. Here’s what I ended up with
[...]
calculate_sum(maxval, &triangles);
/* count rows */
int nrows = 0;
for (c = 1; c < maxval; ++c) {
for (b = 1; b <= c; ++b)
for (a = 1; a <= b; ++a)
if (triangles[a][b][c] != 0) {
nrows += 1;
}
}
/* output a data.frame */
int ncols = 4;
SEXP col1, col2, col3, col4, df;
PROTECT(df = allocVector(VECSXP, ncols));
PROTECT(col1 = allocVector(INTSXP, nrows));
PROTECT(col2 = allocVector(INTSXP, nrows));
PROTECT(col3 = allocVector(INTSXP, nrows));
PROTECT(col4 = allocVector(INTSXP, nrows));
int j = 0;
for (c = 1; c < maxval; ++c) {
for (b = 1; b <= c; ++b) {
for (a = 1; a <= b; ++a) {
if (triangles[a][b][c] != 0) {
INTEGER(col1)[j] = a;
INTEGER(col2)[j] = b;
INTEGER(col3)[j] = c;
INTEGER(col4)[j] = triangles[a][b][c];
j += 1;
}
}
}
}
SET_VECTOR_ELT(df, 0, col1);
SET_VECTOR_ELT(df, 1, col2);
SET_VECTOR_ELT(df, 2, col3);
SET_VECTOR_ELT(df, 3, col4);
SEXP colNames;
PROTECT(colNames = allocVector(STRSXP, ncols));
SET_STRING_ELT(colNames, 0, mkChar("a"));
SET_STRING_ELT(colNames, 1, mkChar("b"));
SET_STRING_ELT(colNames, 2, mkChar("c"));
SET_STRING_ELT(colNames, 3, mkChar("sum"));
setAttrib(df, R_NamesSymbol, colNames);
SEXP rowNames;
PROTECT(rowNames = allocVector(STRSXP, nrows));
for (int i = 0; i < nrows; ++i) {
char rowName[11];
snprintf(rowName, sizeof(rowName), "%10d", i + 1
SET_STRING_ELT(rowNames, i, mkChar(rowName));
}
setAttrib(df, R_RowNamesSymbol, rowNames);
SEXP className;
PROTECT(className = allocVector(STRSXP, 1));
SET_STRING_ELT(className, 0, mkChar("data.frame"));
classgets(df, className);
UNPROTECT(8);
return df;
Going through the biggest parts of this: first we identify how many rows we want
to output by counting the nonzero elements of the passed-by-reference triangles
/* count rows */
int nrows = 0;
for (c = 1; c < maxval; ++c) {
for (b = 1; b <= c; ++b)
for (a = 1; a <= b; ++a)
if (triangles[a][b][c] != 0) {
nrows += 1;
}
}
then allocating vectors - first a list the length of the number of columns (4) then
one for each of the columns with length nrows
/* output a data.frame */
int ncols = 4;
SEXP col1, col2, col3, col4, df;
PROTECT(df = allocVector(VECSXP, ncols));
PROTECT(col1 = allocVector(INTSXP, nrows));
PROTECT(col2 = allocVector(INTSXP, nrows));
PROTECT(col3 = allocVector(INTSXP, nrows));
PROTECT(col4 = allocVector(INTSXP, nrows));
These are then populated in a loop with a new counter for the nonzero elements
int j = 0;
for (c = 1; c < maxval; ++c) {
for (b = 1; b <= c; ++b) {
for (a = 1; a <= b; ++a) {
if (triangles[a][b][c] != 0) {
INTEGER(col1)[j] = a;
INTEGER(col2)[j] = b;
INTEGER(col3)[j] = c;
INTEGER(col4)[j] = triangles[a][b][c];
j += 1;
}
}
}
}
and finally the vectors linked into the list
SET_VECTOR_ELT(df, 0, col1);
SET_VECTOR_ELT(df, 1, col2);
SET_VECTOR_ELT(df, 2, col3);
SET_VECTOR_ELT(df, 3, col4);
The rest is mostly boilerplate of setting up the data.frame
: assigning
column names
SEXP colNames;
PROTECT(colNames = allocVector(STRSXP, ncols));
SET_STRING_ELT(colNames, 0, mkChar("a"));
SET_STRING_ELT(colNames, 1, mkChar("b"));
SET_STRING_ELT(colNames, 2, mkChar("c"));
SET_STRING_ELT(colNames, 3, mkChar("sum"));
setAttrib(df, R_NamesSymbol, colNames);
and row names
SEXP rowNames;
PROTECT(rowNames = allocVector(STRSXP, nrows));
for (int i = 0; i < nrows; ++i) {
char rowName[11];
snprintf(rowName, sizeof(rowName), "%10d", i + 1
SET_STRING_ELT(rowNames, i, mkChar(rowName));
}
setAttrib(df, R_RowNamesSymbol, rowNames);
and the class itself
SEXP className;
PROTECT(className = allocVector(STRSXP, 1));
SET_STRING_ELT(className, 0, mkChar("data.frame"));
classgets(df, className);
then finally UNPROTECT
ing the PROTECTED
objects and returning the data.frame
UNPROTECT(8);
return df;
At one point, I had forgotten that I had modified an example and now had more PROTECT
wrappers around objects, but hadn’t updated the number in UNPROTECT
. It turns out this
leads to an error in R about a stack imbalance - not particularly meaningful if you don’t realise what that means, so FYI.
So, with this new code in src/triangles.c
we re-build and reload and try it out
library(triangles)
x <- triangles(16)
x
## a b c sum
## 1 3 4 5 12
## 2 6 8 10 24
## 3 5 12 13 30
## 4 9 12 15 36
str(x)
## 'data.frame': 4 obs. of 4 variables:
## $ a : int 3 6 5 9
## $ b : int 4 8 12 12
## $ c : int 5 10 13 15
## $ sum: int 12 24 30 36
Nothing printed when not expected, and the result is really a data.frame
! We can
even work with the data now
sum(x$sum)
## [1] 102
We still have the C code, and this can be updated as it evolves without affecting the R interface to it. With the R parts commented out, it can still be run as if it was just a regular C file. If we really want to compile it with the R parts still there we can include the R libraries (on a linux system, for example) with
$ cc -O -o triangle triangles.c -I/usr/share/R/include -L/usr/lib/R/lib -lR
Update 12-Aug-2023: I forgot to mention that in order to pass checks, R wants
to have the following, typically in a file src/init.c
#include <R.h>
#include <Rinternals.h>
#include <stdlib.h> // for NULL
#include <R_ext/Rdynload.h>
/* .Call calls */
extern SEXP C_triangles(SEXP maximum);
static const R_CallMethodDef CallEntries[] = {
{"C_triangles", (DL_FUNC) &C_triangles, 1},
{NULL, NULL, 0}
};
void R_init_addr(DllInfo *dll) {
R_registerRoutines(dll, NULL, CallEntries, NULL, NULL);
R_useDynamicSymbols(dll, FALSE);
}
I won’t say I understand this bit, but it is mentioned in this part of Davis’ post.
I also updated the snprintf
call in the rownames assignment since I got a warning about
truncation.
There are some valid concerns about the fact that I’m not explicitly free()
ing the
allocated memory, so I plan to add some code to do that.
As always, I’ve learned a lot messing with things outside of my comfort zone here. I wouldn’t say that I want to write a lot more C code, but at least now I feel somewhat comfortable bringing into R to work with.
The package I detailed building here is on GitHub: https://github.com/jonocarroll/triangles in case it’s useful to you.
There’s always more for me to learn, though, so if you have comments, feedback, suggestions for improvements, etc… I can be found on Mastodon or use the comments below.
devtools::session_info()
## ─ Session info ───────────────────────────────────────────────────────────────
## setting value
## version R version 4.1.2 (2021-11-01)
## 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
## tz Australia/Adelaide
## date 2023-08-12
## pandoc 3.1.1 @ /usr/lib/rstudio/resources/app/bin/quarto/bin/tools/ (via rmarkdown)
##
## ─ Packages ───────────────────────────────────────────────────────────────────
## package * version date (UTC) lib source
## blogdown 1.17 2023-05-16 [1] CRAN (R 4.1.2)
## bookdown 0.29 2022-09-12 [1] CRAN (R 4.1.2)
## bslib 0.5.0 2023-06-09 [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.1.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.21 2023-05-05 [3] CRAN (R 4.3.0)
## 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.5 2023-03-23 [3] CRAN (R 4.2.3)
## htmlwidgets 1.5.4 2021-09-08 [1] CRAN (R 4.1.2)
## httpuv 1.6.6 2022-09-08 [1] CRAN (R 4.1.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.43 2023-05-25 [3] CRAN (R 4.3.0)
## later 1.3.0 2021-08-18 [1] CRAN (R 4.1.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.1.2)
## pkgbuild 1.4.0 2022-11-27 [1] CRAN (R 4.1.2)
## pkgload 1.3.0 2022-06-27 [1] CRAN (R 4.1.2)
## prettyunits 1.1.1 2020-01-24 [3] CRAN (R 4.0.1)
## processx 3.8.2 2023-06-30 [3] CRAN (R 4.3.1)
## profvis 0.3.7 2020-11-02 [1] CRAN (R 4.1.2)
## promises 1.2.0.1 2021-02-11 [1] CRAN (R 4.1.2)
## ps 1.7.5 2023-04-18 [3] CRAN (R 4.3.0)
## purrr 1.0.1 2023-01-10 [1] CRAN (R 4.1.2)
## R6 2.5.1 2021-08-19 [3] CRAN (R 4.2.0)
## Rcpp 1.0.9 2022-07-08 [1] CRAN (R 4.1.2)
## remotes 2.4.2 2021-11-30 [1] CRAN (R 4.1.2)
## rlang 1.1.1 2023-04-28 [1] CRAN (R 4.1.2)
## rmarkdown 2.23 2023-07-01 [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.1.2)
## shiny 1.7.2 2022-07-19 [1] CRAN (R 4.1.2)
## stringi 1.7.12 2023-01-11 [3] CRAN (R 4.2.2)
## stringr 1.5.0 2022-12-02 [1] CRAN (R 4.1.2)
## triangles * 0.1.0 2023-08-11 [1] local
## urlchecker 1.0.1 2021-11-30 [1] CRAN (R 4.1.2)
## usethis 2.1.6 2022-05-25 [1] CRAN (R 4.1.2)
## vctrs 0.6.3 2023-06-14 [1] CRAN (R 4.1.2)
## xfun 0.39 2023-04-20 [3] CRAN (R 4.3.0)
## xtable 1.8-4 2019-04-21 [1] CRAN (R 4.1.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.1
## [2] /usr/local/lib/R/site-library
## [3] /usr/lib/R/site-library
## [4] /usr/lib/R/library
##
## ──────────────────────────────────────────────────────────────────────────────