36  Logical Operations

Author

Jarad Niemi

R Code Button

library("tidyverse")
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.2     ✔ readr     2.1.4
✔ forcats   1.0.0     ✔ stringr   1.5.0
✔ ggplot2   3.4.2     ✔ tibble    3.2.1
✔ lubridate 1.9.2     ✔ tidyr     1.3.0
✔ purrr     1.0.1     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors

Always use TRUE/FALSE and not T/F!

T
[1] TRUE
F
[1] FALSE
TRUE
[1] TRUE
FALSE
[1] FALSE

T and TRUE appear the same.

isTRUE(T)
[1] TRUE
isTRUE(TRUE)
[1] TRUE

But T/F can be reassigned.

T <- FALSE

isTRUE(T)
[1] FALSE

TRUE/FALSE cannot be reassigned.

TRUE <- FALSE
Error in TRUE <- FALSE: invalid (do_set) left-hand side to assignment

The reason is because R has a short list of reserved words

?Reserved

36.1 Logical operators

36.1.1 NOT (!)

!TRUE
[1] FALSE
!FALSE
[1] TRUE

36.1.2 OR (|)

TRUE | TRUE
[1] TRUE
TRUE | FALSE
[1] TRUE
FALSE | TRUE
[1] TRUE
FALSE | FALSE
[1] FALSE

36.1.3 AND (&)

TRUE & TRUE
[1] TRUE
TRUE & FALSE
[1] FALSE
FALSE & TRUE
[1] FALSE
FALSE & FALSE
[1] FALSE

36.1.4 XOR (xor)

xor(TRUE, TRUE)
[1] FALSE
xor(TRUE, FALSE)
[1] TRUE
xor(FALSE, TRUE)
[1] TRUE
xor(FALSE, FALSE)
[1] FALSE

36.2 Multiple comparisons

The above operators are all vector operators and perform element-wise operations.

36.2.1 Elementwise comparisons

The operations above perform element-wise comparisons.

# Create logical vectors
b <- c(TRUE,FALSE)
x <- rep(b, times = 2)
y <- rep(b, each = 2)
x
[1]  TRUE FALSE  TRUE FALSE
y
[1]  TRUE  TRUE FALSE FALSE
# Demonstrate element-wise operations
x | y
[1]  TRUE  TRUE  TRUE FALSE
x & y
[1]  TRUE FALSE FALSE FALSE
xor(x,y)
[1] FALSE  TRUE  TRUE FALSE
!x
[1] FALSE  TRUE FALSE  TRUE

36.2.2 Left-to-right comparison

If you want left-to-right comparisons, you can use && and ||.

TRUE && TRUE && TRUE
[1] TRUE
TRUE && TRUE && FALSE
[1] FALSE
TRUE && FALSE && TRUE
[1] FALSE
FALSE && FALSE && FALSE
[1] FALSE
TRUE || TRUE || TRUE
[1] TRUE
FALSE || FALSE || TRUE
[1] TRUE
FALSE || FALSE || FALSE
[1] FALSE

You will get a warning if you try to pass a vector to these left-to-right comparisons.

c(TRUE,FALSE) && c(TRUE,TRUE)
Error in c(TRUE, FALSE) && c(TRUE, TRUE): 'length = 2' in coercion to 'logical(1)'

36.2.3 any()

The any() function returns true if any element in the vector is TRUE and FALSE otherwise.

# Demonstrate any()
any(c(TRUE,   TRUE,  TRUE,  TRUE))
[1] TRUE
any(c(TRUE,  FALSE, FALSE, FALSE))
[1] TRUE
any(c(FALSE, FALSE, FALSE, FALSE))
[1] FALSE

36.2.4 all()

The all() function returns true if every element is TRUE and FALSE otherwise.

# Demonstrate all()
all(c(TRUE,   TRUE,  TRUE,  TRUE))
[1] TRUE
all(c(TRUE,  FALSE, FALSE, FALSE))
[1] FALSE
all(c(FALSE, FALSE, FALSE, FALSE))
[1] FALSE

36.3 Comparisons

We will often use logical operations to compare objects including character and numeric quantities.

36.3.1 Numeric

Comparison of numerical quantities is typically much more straight-forward than chracter comparisons. But you do need to be careful of the numerical accuracy of numbers especially when trying to obtain exact matches.

# Assign two numerics
a <- 1
b <- 2

# Inequalities
a <  b
[1] TRUE
a >  b
[1] FALSE
a <= b
[1] TRUE
a >= b
[1] FALSE

We can also compare for exact values, but we may obtain unexpected results.

# Compare exactly
a == 1
[1] TRUE
a == 2 
[1] FALSE
a != 1
[1] FALSE
a != 2 
[1] TRUE
a == b
[1] FALSE
a != b
[1] TRUE

Be careful with double precision

# sin() example
sin(0)    == 0
[1] TRUE
sin(2*pi) == 0
[1] FALSE

When compare doubles, you probably want to determine if two values are close enough.

# Use tolerance
abs(sin(2*pi) - 0) < .Machine$double.eps^0.5
[1] TRUE
all.equal(sin(2*pi), 0)
[1] TRUE

An alternative is the near() function in the dplyr package. Unlike all.equal() this function returns FALSE when the two values are not near each other.

# near() uses a tolerance
near(sin(2*pi), 0)
[1] TRUE
near(1, 2)
[1] FALSE

36.3.2 Character

There are a variety of ways to compare character objects:

  • exact match
  • ignore capitalization matching
  • match an element of a set
  • substring matching
# Exact comparisons
"a" == "a"
[1] TRUE
"a" != "a"
[1] FALSE
"a" == ""
[1] FALSE
"a" != "b"
[1] TRUE
"a" == "A"
[1] FALSE

If you want to ignore capitalization, you can use the tolower() (or toupper()) function to change the capitalization.

# Match ignoring capitalization
"a" == "A"
[1] FALSE
"a" == tolower("A")
[1] TRUE
"string" == "StRiNG"
[1] FALSE
"string" == tolower("StRiNG")
[1] TRUE

We may also have a set of strings and we are interested in whether our string is in that set

# Character in set
"a" %in% c("a", "b")
[1] TRUE
"a" %in% letters
[1] TRUE
"1" %in% letters
[1] FALSE
# Compare a vector
letters %in% c("a", "b")
 [1]  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[13] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[25] FALSE FALSE

If we want to see if a character is NOT in a set, we can wrap the entire comparison with the NOT operator.

# Not in set
!( "a"     %in% c("a", "b") )
[1] FALSE
!( "a"     %in% letters     )
[1] FALSE
!( "1"     %in% letters     )
[1] TRUE
!( letters %in% c("a", "b") )
 [1] FALSE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE
[13]  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE
[25]  TRUE  TRUE

With characters we may want to find whether a given string is contained within another string.

# Match string within string
grepl("a", "abcd")
[1] TRUE
grepl("a", letters)
 [1]  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[13] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[25] FALSE FALSE
grepl("a", LETTERS, ignore.case = TRUE)
 [1]  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[13] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[25] FALSE FALSE

If you want to test if the string has any collection of characters, you can use regular expression (regex) matching.

# Regular expression help
?regexp

R has its own version of regular expression matching. To use the built-in character classes, you will need to wrap them in an additional set of square brackets.

# R built-in character classes
grepl('[[:alnum:]]', c("a", "1", "a1", "$"))
[1]  TRUE  TRUE  TRUE FALSE
grepl('[[:digit:]]', c("123", "1a3", "my $"))
[1]  TRUE  TRUE FALSE
# Alternative
grepl('[0-9A-Za-z]', c("a", "1", "a1", "$"))
[1]  TRUE  TRUE  TRUE FALSE
grepl('[0-9]', c("123", "1a3", "my $"))
[1]  TRUE  TRUE FALSE

If you need to match the exact string, you can use fixed = TRUE.

# Match fixed string
grepl("[:alnum:]", 
      c("[:alnum:]", "[:alnum:]1", "123", "abc"),
      fixed = TRUE)
[1]  TRUE  TRUE FALSE FALSE

There is also a lot of functionality in the stringr package for string matching. Functions in this package may be more intuitive.

36.3.3 Force TRUE/FALSE

Not all seemingly logical functions return TRUE/FALSE.

# all.equal() examples
all.equal(1,1) # Returns a logical
[1] TRUE
all.equal(1,2) # Returns a character
[1] "Mean relative difference: 1"
class(all.equal(1,1))
[1] "logical"
class(all.equal(1,2))
[1] "character"
# all() example
all(c(TRUE, FALSE))
[1] FALSE
all(c(TRUE, FALSE, NA))
[1] FALSE
all(c(TRUE, TRUE,  NA))
[1] NA

This can cause issues in flow control

# if() fails due to NA
if (all(c(TRUE, TRUE,  NA))) {
  # Do something
}
Error in if (all(c(TRUE, TRUE, NA))) {: missing value where TRUE/FALSE needed

36.3.4 isTRUE()

The isTRUE() and isFALSE() functions ALWAYS resolve to TRUE/FALSE.

# isTRUE() examples
isTRUE(TRUE)
[1] TRUE
isTRUE(NA)
[1] FALSE
isTRUE(1)
[1] FALSE
isTRUE(as.logical(NA))
[1] FALSE
isTRUE(as.logical(1))  # all non-zero numeric values are coerced to TRUE
[1] TRUE
isTRUE(as.logical(2))  
[1] TRUE
isTRUE(as.logical(1e-16)) 
[1] TRUE
# isFALSE() examples
isFALSE(FALSE)
[1] TRUE
isFALSE(NA)
[1] FALSE
isFALSE(1)
[1] FALSE
isFALSE(as.logical(NA))
[1] FALSE
isFALSE(as.logical(0))  # only 0 is coerced to FALSE
[1] TRUE
isFALSE(as.logical(2)) 
[1] FALSE

You can combine isTRUE()/isFALSE() with all.equal() to (perhaps) obtain behavior you are expecting.

# Combine isTRUE and all.equal()
isTRUE( all.equal(1,2))
[1] FALSE
isFALSE(all.equal(1,2))
[1] FALSE
isFALSE(!all.equal(1,1))
[1] TRUE
isTRUE( NA)
[1] FALSE
isFALSE(NA)
[1] FALSE

36.4 Summary

The use of logical operations on vectors is quite common in a data pipeline. Here is an example.

# Filter the data using logical operations
d <- diamonds |>
  filter(
    # Implicitly there is an AND operator
    # connecting these logical operations
    cut %in% c("Ideal", "Premium"),
    price <= 1000,
    0.2 < carat & carat < 0.4, # 0.2 < carat < 0.4
    color == "G",
    !(clarity %in% c("I1", "IF"))
  )

# Plot filtered data
ggplot(d, 
       aes(x = carat,
           y = price,
           color = table,
           shape = cut)) +
  geom_point() +
  facet_wrap(~ clarity) +
  scale_y_log10()
Warning: Using shapes for an ordinal variable is not advised