The Operators R Gives You
R's operators fall into four working groups: arithmetic (+ - * / ^ %% %/%), comparison (== != < <= > >=), logical (& | && || ! xor), and the membership test %in%. Two things make them different from most languages: nearly all of them are vectorized - they operate on whole vectors at once, element by element - and R has two AND operators and two OR operators that are not interchangeable. This page covers all of them, plus the precedence traps that produce silently wrong answers.
Arithmetic: +, -, *, /, ^, %% and %/%
The first five do what you expect. The two percent-wrapped ones - %% (modulo, the remainder) and %/% (integer division, the quotient) - are the ones people search for:
Note that / always does floating-point division - 17 / 5 is 3.4, never 3. If you want the whole-number part, that's exactly what %/% is for.
The everyday job of %% is divisibility. A number is even when dividing by 2 leaves no remainder:
That second line is worth a pause: with a negative operand, R's %% returns a result with the sign of the divisor (-7 %% 3 is 2, because -7 = -3 * 3 + 2). Some languages do it the other way; if you're porting code, check.
Comparison Operators Are Vectorized
==, !=, <, <=, >, >= compare element by element and return a logical vector, not a single answer:
This is the engine behind almost all filtering in R - you compare a whole vector against a value, get a logical mask, and use the mask to subset. One caution with == on decimals: floating-point numbers are stored approximately, so 0.1 + 0.2 == 0.3 is FALSE. For decimal comparisons, prefer all.equal() or check abs(x - y) < 1e-9.
Logical Operators: & vs && (the Classic Confusion)
R has two AND operators and two OR operators, and picking the wrong one is a rite of passage.
& and | are vectorized. They combine two logical vectors element by element:
Use these whenever you're building a filter over data: df[df$age > 30 & df$city == "Lisbon", ].
&& and || are scalar and short-circuit. They take single values, return a single TRUE/FALSE, and skip the second operand entirely if the first one already settles the answer:
That second line demonstrates short-circuiting: because x < 0 is FALSE, R never evaluates the stop() call. This is exactly why && belongs in if() conditions - you can write if (!is.na(x) && x > 10) and the x > 10 part is safely skipped when x is NA.
The rule of thumb: &/| for vectors and data filtering, &&/|| for if() conditions. And since R 4.3, this is enforced with teeth: handing && or || a vector of length greater than 1 is an error (older R versions silently used just the first element, which hid real bugs for years).
xor(a, b) rounds out the set - TRUE when exactly one of the two is TRUE:
%in%: Membership Testing
x %in% table asks, for each element of x, "does this appear anywhere in table?" It's one of the most-used operators in real R code:
The side being tested can be a whole vector, which makes %in% the natural tool for filtering rows by a set of allowed values.
It also beats the alternative you'd otherwise write - a chain of == glued together with |:
With five allowed values, the == chain becomes a wall of repetition; the %in% version just grows the vector. There's a second, subtler advantage - %in% never returns NA:
NA == 1 is NA because R can't know whether an unknown value equals 1 - a behavior covered in depth in missing values. That NA then poisons any if() it reaches. %in% sidesteps the whole problem by always answering TRUE or FALSE, which is why it's the safer choice for conditions on possibly-missing data.
Precedence Gotchas
Two operator-precedence traps account for a disproportionate share of "R is broken" moments. First, ^ binds tighter than unary minus:
Second, the colon binds tighter than binary arithmetic:
When in doubt, add parentheses. They cost nothing and every reader (including future you) parses them the same way the interpreter does.
$ and [ ]: the Access Operators
You'll also constantly see $, [ ], and [[ ]] - technically operators too, used for pulling values out of structures rather than computing with them:
They get full coverage in the pages on vectors, lists, and data frames - here it's enough to recognize them as members of the same operator family.
What You Take Away
%%gives the remainder,%/%the whole-number quotient;x %% 2 == 0tests evenness.- Comparisons are vectorized - they return a logical vector you can filter with.
&/|combine vectors element by element;&&/||are for single values inif()and short-circuit. Since R 4.3,&&on a longer vector is an error.x %in% tablereplaces chained==and never returnsNA.-2^2is-4and1:n - 1starts at 0 - parenthesize when in doubt.
Next up: putting these operators to work inside if, else and ifelse() - R's conditional statements.
Frequently Asked Questions
What does %% do in R?
%% is the modulo operator - it returns the remainder of a division. 17 %% 5 is 2. Its companion %/% does integer division and returns the quotient without the remainder: 17 %/% 5 is 3. The classic use of %% is testing for even numbers: x %% 2 == 0.
What is the difference between & and && in R?
& is vectorized: it compares two vectors element by element and returns a vector of the same length. && works on single values only, short-circuits (skips the second operand if the first already decides the result), and returns exactly one TRUE or FALSE. Use & when filtering vectors, && inside if() conditions. Since R 4.3, giving && or || a vector longer than 1 is an error, not a warning.
What does %in% do in R?
x %in% table tests whether each element of x appears anywhere in table, returning TRUE or FALSE per element. "mango" %in% basket replaces a chain of == comparisons joined with |, and unlike == it never returns NA - a missing value on the side being tested simply yields FALSE.
Why does -2^2 return -4 in R?
Because ^ binds tighter than unary minus, R reads -2^2 as -(2^2), which is -4. If you mean the square of negative two, write (-2)^2, which is 4. The colon has a similar trap: 1:n - 1 means (1:n) - 1, not 1:(n - 1).