roperators adds the
small things you keep wishing base R had — string arithmetic, in-place
modifiers, comparisons that don’t flinch at NA or floating
point, and a little drawer of everyday helpers. It’s pure base R, with
nothing heavy underneath, and it tries to be especially kind to people
arriving from Python and other languages.
Think of this as an unhurried tour — pour a coffee. But if you only want the highlights, here they are:
"foo" %+% "bar" # string addition
#> [1] "foobar"
(0.1 + 0.1 + 0.1) %~=% 0.3 # floating-point equality that just works
#> [1] TRUE
c(1, NA) %==% c(1, NA) # NA == NA is treated as TRUE here
#> [1] TRUE TRUE
name <- "you"
f("hello {name}, 2 + 2 = {2 + 2}") # f-strings!
#> [1] "hello you, 2 + 2 = 4"Let’s start with the one nearly everyone misses coming from other
languages — gluing strings together with a +. So we added
it:
my_string <- "using infix (%) operators " %+% "lets R do string addition"
my_string
#> [1] "using infix (%) operators lets R do string addition"
# subtraction removes a pattern
my_string %-% "lets R do string addition"
#> [1] "using infix (%) operators "
# multiplication repeats (%*% was already taken, so it's %s*%)
"ha" %s*% 3
#> ha
#> "hahaha"And something you can’t do in Python — string division, which simply counts how many times a pattern turns up (regular expressions are welcome):
+=)How many times have you written something like
df$x[long$condition] <- df$x[long$condition] + 1? The
line barely fits on the page. Let’s make it kinder:
x <- 1
x %+=% 2
x
#> [1] 3
d <- iris
# add 1 to setosa sepal lengths, in place
d$Sepal.Length[d$Species == "setosa"] %+=% 1The full set is %+=%, %-=%,
%*=%, %/=%, %^=%,
%root=%, and %log=%. %+=% and
%-=% are happy with strings, too:
NA == NA ought to be TRUEAn NA doesn’t technically equal another NA
— but most of the time, for what you’re actually doing, you’d like it
to. How many if statements have quietly broken on exactly
this?
a <- c(NA, "foo", "foo", NA)
b <- c(NA, "foo", "bar", "bar")
a == b # base R: the NA leaks through
#> [1] NA TRUE FALSE NA
a %==% b # roperators: NA == NA is treated as TRUE
#> [1] TRUE TRUE FALSE FALSE%>=% and %<=% carry the same gentle
NA-handling.
0.1 + 0.1 + 0.1 ought to equal
0.3This one catches almost everyone, and it really isn’t your fault — it’s just how computers hold decimals:
5 %><% c(1, 10) # strictly between
#> [1] TRUE
1 %>=<% c(1, 10) # inclusive
#> [1] TRUE
5 %><% c(10, 1) # reversed bounds are fine too — no need to worry about order
#> [1] TRUE
# %===% is strict value-AND-class equality, like JavaScript's ===
x <- int(2)
x == 2 # TRUE
#> [1] TRUE
x %===% 2 # FALSE (different class)
#> [1] FALSE
x %===% int(2)
#> [1] TRUE"z" %ni% c("a", "b", "c") # not in
#> [1] TRUE
TRUE %xor% FALSE # exclusive or
#> [1] TRUE
TRUE %aon% TRUE # all-or-nothing: both TRUE, or both FALSE
#> [1] TRUE
# SQL-style LIKE
c("FOO", "bar", "fizz") %rlike% "foo" # case-insensitive
#> [1] TRUE FALSE FALSE
c("dOe", "doe") %perl% "[a-z]O" # case-sensitive, Perl regex
#> [1] TRUE FALSEA few new friends, added in this release.
f() — string interpolation (R’s
f-strings). Anything inside { } is evaluated right
where you call it:
who <- "Ben"; n <- 2
f("Hi {who}, you have {n} new message{if (n != 1) 's'}")
#> [1] "Hi Ben, you have 2 new messages"
f("today's first letters: {head(LETTERS, n)}") # vectors are tidied up for you
#> [1] "today's first letters: A, B"%else% — a calm fallback for when an
expression might error (the fallback only runs if it’s actually
needed):
sqrt("not a number") %else% NA_real_
#> [1] NA
(1:3)[[99]] %else% "out of range"
#> [1] "out of range"%/0% — safe division that returns
NA rather than letting an Inf or
NaN wander into your next sum() or
mean():
%+-% — a tolerance interval that drops
straight into the between operators:
%~% — forgiving string equality that
ignores case and stray whitespace — the string cousin of
%~=%:
as.percent() — proportions, dressed
up:
R’s conversion syntax is a touch wordy. These trim it down:
chr(42) # as.character()
#> [1] "42"
int(42.9) # as.integer()
#> [1] 42
num("4.2") # as.numeric()
#> [1] 4.2
bool("TRUE") # as.logical()
#> [1] TRUE
# the famous factor-to-number stumble, smoothed over:
fac <- factor(c(11, 22, 33))
as.numeric(fac) # 1 2 3 -- almost never what you wanted
#> [1] 1 2 3
f.as.numeric(fac) # 11 22 33
#> [1] 11 22 33
# and convert to a class chosen at run time
as.class(255, "roman")
#> [1] CCLVRather than chaining five conditions, you can ask one calm question:
# would any of these break a calculation?
is.bad_for_calcs(c(1, NA, Inf, NaN, 5))
#> [1] FALSE TRUE TRUE TRUE FALSE
is.scalar(1)
#> [1] TRUE
is.constant(c(1, 1, 1))
#> [1] TRUE
is.binary(c("a", "b", "a"))
#> [1] TRUEThere’s a whole family of is.*_or_null() predicates too,
lovely for checking optional function arguments without fuss.
# pulling pieces out of vectors and strings
get_1st_word("Ada Lovelace")
#> [1] "Ada"
get_last_word("Ada Lovelace")
#> [1] "Lovelace"
get_most_frequent(c("a", "b", "b", "c", "b"))
#> [1] "b"
# Oxford-comma joining, done for you
paste_oxford("Tom", "Dick", "Harry")
#> [1] "Tom, Dick, and Harry"
# complete-cases stats: just add _cc for na.rm = TRUE
mean_cc(c(1, 2, NA))
#> [1] 1.5
sd_cc(c(1, 2, 3, NA))
#> [1] 1
# little environment checks
get_os()
#> [1] "linux"
get_R_version()
#> [1] "4.6.0"
# and file-extension checks
is_csv_file(c("a.csv", "b.txt"))
#> [1] TRUE FALSE| You want… | Reach for |
|---|---|
| String concat / subtract | %+% / %-% |
| String repeat / count | %s*% / %s/% |
| In-place maths | %+=% %-=% %*=%
%/=% %^=% |
| Fill NAs / regex edit in place | %na<-% / %regex=% /
%regex<-% |
| NA-aware (in)equality | %==% %>=% %<=% |
| Floating-point equality | %~=% %>~% %<~% |
| Strict (value + class) equality | %===% |
| Between (excl / incl) | %><% / %>=<% |
| Not-in / xor / all-or-nothing | %ni% / %xor% / %aon% |
| SQL-style LIKE | %rlike% / %perl% |
| String interpolation | f() |
| Inline error fallback | %else% |
| Safe divide / tolerance | %/0% / %+-% |
| Fuzzy string match | %~% |
A few names are shared on purpose with the wider world —
%+% with ggplot2, and %like%-style matching
with data.table. If you’ve got those loaded as well, just reach for the
namespaced form (roperators::%+%) where it matters, and
everyone gets along fine.