22  Logical Operators in Conditional Statements

NoteWhat this chapter covers

Real conditions are rarely a single comparison. You combine them, passes the exam and has 75% attendance, region is North or South, not failed. R provides the operators &, |, !, &&, ||, xor(), isTRUE(), and isFALSE() to build these compound conditions, plus %in% for set membership and short-circuit evaluation rules that affect both speed and safety. This chapter explains each operator, the crucial difference between the single and double forms, and how NA propagates through logical expressions.

22.1 The three building blocks

R has three classical logical operators:

Operator Name Meaning
&, && AND TRUE only if both sides are TRUE
\|, \|\| OR TRUE if either side is TRUE
! NOT flips TRUE ↔︎ FALSE

Everything else is built from these three.

22.2 Element-wise vs scalar, the & vs && rule

This is the single most important distinction in R logic.

  • & and | are element-wise: they compare two vectors position by position and return a vector the same length.
  • && and || are scalar: they expect a single TRUE/FALSE on each side and return a single value.

The scalar forms are designed for if statements, where the condition must collapse to one value:

WarningDon’t use && with vectors

In R 4.2 and later, passing a vector to && or || raises an error. Older code may “work” by silently using only the first element. Inside if (…), prefer &&/||. Outside if (…), i.e. when filtering vectors, use &/|.

# wrong shape for filtering - uses only first element
df[df$pass && df$paid, ]
# right
df[df$pass &  df$paid, ]

A simple rule: inside if(…), double; everywhere else, single.

22.3 Short-circuit evaluation

&& and || are short-circuit: they stop evaluating as soon as the answer is decided.

  • FALSE && anythingFALSE (right side never evaluated)
  • TRUE || anythingTRUE (right side never evaluated)

This matters when the right-hand side might error or is expensive to compute. The classic safety pattern:

If you wrote & instead of &&, R would try to evaluate every clause, including x[1] > 5 on a NULL, and you’d get unexpected behaviour. The element-wise & is not short-circuit.

22.4 NOT, the ! operator

! flips a logical vector. It is element-wise and works on any length.

Use ! to negate a condition: !is.na(x), !duplicated(x), !(region %in% c("North", "South")).

TipDe Morgan’s laws

Two identities worth memorising, they let you flip a complicated !(…) into something more readable:

  • !(a & b) is the same as !a | !b
  • !(a | b) is the same as !a & !b

So !(marks >= 50 & attendance >= 0.75) is equivalent to marks < 50 | attendance < 0.75. Choose the form that reads more naturally.

22.5 xor(), isTRUE(), isFALSE()

Three small helpers that come up often:

xor(a, b) is exclusive-or, TRUE when exactly one side is TRUE.

isTRUE(x) is TRUE only when x is exactly TRUE, not NA, not a vector, not a 1-length numeric. Useful when guarding against unknown input:

isFALSE() is the mirror image. Both are safer than raw == comparisons inside if, where any non-scalar or NA can wreck the condition.

22.6 NA in logical expressions

NA means “unknown.” When R combines an unknown with a known value, the answer is sometimes determined and sometimes not.

R follows what philosophers call three-valued logic: TRUE, FALSE, NA. The result is NA whenever the unknown could change the answer.

This bites in if statements:

Two defensive patterns:

22.7 %in%, set membership

%in% answers “is each element of x somewhere in y?” It is the cleanest replacement for chains of == joined by |.

%in% returns a logical vector of length length(x), never NA. To exclude a set, negate it: !(region %in% c("North","South")).

22.8 Operator precedence

Without brackets, R applies a fixed precedence: comparisons first, then !, then & / &&, then | / ||. The full chain is:

arithmetic   →   comparisons (<, ==, >=, %in%)   →   !   →   &, &&   →   |, ||

So marks >= 50 & marks <= 90 works as written, because >= and <= are evaluated before &. But mixing & and | without brackets is a recipe for confusion:

R reads & before |, so the condition becomes the first interpretation. Always parenthesise mixed &/| expressions, even when you remember the precedence, the next reader may not.

22.9 Worked example, exam eligibility

Decide whether each student in a class is eligible for the final exam. Rule: at least 50 marks and 75% attendance, or an exemption flag, but not if they are on academic hold.

Three things to notice:

  1. & and | (element-wise), we are filtering a vector, not running an if.
  2. The brackets make the precedence explicit, readers shouldn’t have to memorise the rule.
  3. !class$on_hold cleanly excludes anyone on hold, regardless of the other conditions.

22.10 Summary

Summary of concepts introduced in this chapter
Concept Description
Element-wise Operators
& and | Element-wise AND and OR for filtering and classifying vectors
! Element-wise NOT, flips TRUE and FALSE and propagates NA
xor() Exclusive OR, TRUE when exactly one side is TRUE
Scalar Operators
&& and || Scalar AND and OR for inside if(), errors on vectors in R 4.2+
short-circuit evaluation && and || stop evaluating as soon as the answer is decided, useful for safety guards
Safe Tests and Membership
%in% Clean set-membership test, returns FALSE for NA and never NA itself
isTRUE() and isFALSE() Safe scalar checks that treat non-logical, vector, or NA input as FALSE
Pitfalls and Precedence
NA in logical ops Three-valued logic: NA propagates unless the known side forces the answer
operator precedence Order: arithmetic, comparisons, !, & and &&, then | and ||
parenthesise mixed & and | Always wrap mixed & and | expressions in brackets for readability