Menu

R Operators: Arithmetic, Comparison, Logical, %in% and %%

Every operator you need in R - arithmetic including %% and %/%, vectorized comparisons, the & vs && distinction, and %in% for membership testing.

This page includes runnable editors - edit, run, and see output instantly.

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 == 0 tests evenness.
  • Comparisons are vectorized - they return a logical vector you can filter with.
  • &/| combine vectors element by element; &&/|| are for single values in if() and short-circuit. Since R 4.3, && on a longer vector is an error.
  • x %in% table replaces chained == and never returns NA.
  • -2^2 is -4 and 1:n - 1 starts 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).

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED