Chapter 18 Control Flow

What You’ll Learn:

  • if/else statements
  • for, while, repeat loops
  • break and next
  • switch statements
  • Common control flow errors

Key Errors Covered: 15+ control flow errors

Difficulty: ⭐⭐ Intermediate

18.1 Introduction

Control flow directs code execution, but has pitfalls:

x <- 5

# Looks okay...
if (x > 3 & x < 10) {
  print("In range")
}
#> [1] "In range"
# Works, but what about this?
x <- c(5, 15)

if (x > 3 & x < 10) {  # Warning!
  print("In range")
}
#> Error in if (x > 3 & x < 10) {: the condition has length > 1

Let’s master control flow to avoid these issues.

18.2 if/else Basics

💡 Key Insight: if Requires Single Logical

# Correct: single TRUE/FALSE
if (TRUE) {
  print("Yes")
}
#> [1] "Yes"

if (5 > 3) {
  print("Five is greater")
}
#> [1] "Five is greater"

# With else
x <- 10
if (x > 5) {
  print("Large")
} else {
  print("Small")
}
#> [1] "Large"

# else if
x <- 5
if (x > 10) {
  print("Large")
} else if (x > 5) {
  print("Medium")
} else {
  print("Small")
}
#> [1] "Small"

# ifelse for vectors (different!)
x <- c(3, 7, 12)
ifelse(x > 5, "Large", "Small")
#> [1] "Small" "Large" "Large"

Key points: - if needs single logical value - ifelse() is vectorized (for vectors) - if can have else and else if - Braces {} recommended even for single lines

18.3 Error #1: the condition has length > 1

⭐ BEGINNER 🧠 LOGIC

18.3.1 The Error

x <- c(5, 15)

if (x > 10) {
  print("Greater than 10")
}
#> Error in if (x > 10) {: the condition has length > 1

🔴 ERROR (R >= 4.2)

Error in if (x > 10) { : the condition has length > 1

In older R versions, this gives a warning and uses only the first element.

18.3.2 What It Means

You’re using a vector in if when it expects a single TRUE/FALSE.

18.3.3 Common Causes

18.3.3.1 Cause 1: Testing Vector

ages <- c(15, 25, 35)

if (ages >= 18) {
  print("All adults")
}
#> Error in if (ages >= 18) {: the condition has length > 1

18.3.3.2 Cause 2: Multiple Conditions

x <- 5
y <- 10

# Wrong: creates vector of length 2
if (c(x > 3, y > 8)) {
  print("Both true")
}
#> Error in if (c(x > 3, y > 8)) {: the condition has length > 1

18.3.3.3 Cause 3: Using | instead of ||

x <- c(5, 15)

# & and | are vectorized
x > 3 & x < 10  # Vector of 2 elements
#> [1]  TRUE FALSE

# && and || use only first element
x > 3 && x < 10  # Single logical (from first element)
#> Error in x > 3 && x < 10: 'length = 2' in coercion to 'logical(1)'

18.3.4 Solutions

SOLUTION 1: Use && or || for Scalar Conditions

x <- 5
y <- 10

# Logical AND
if (x > 3 && y > 8) {
  print("Both conditions true")
}
#> [1] "Both conditions true"

# Logical OR  
if (x < 3 || y > 8) {
  print("At least one true")
}
#> [1] "At least one true"

SOLUTION 2: Use all() or any() for Vectors

ages <- c(15, 25, 35)

# Check if all are adults
if (all(ages >= 18)) {
  print("All adults")
} else {
  print("Some minors")
}
#> [1] "Some minors"

# Check if any are adults
if (any(ages >= 18)) {
  print("At least one adult")
}
#> [1] "At least one adult"

SOLUTION 3: Use ifelse() for Vectorized Operations

ages <- c(15, 25, 35)

# Vectorized: returns vector
status <- ifelse(ages >= 18, "Adult", "Minor")
status
#> [1] "Minor" "Adult" "Adult"

# Or with dplyr::case_when (cleaner for multiple conditions)
library(dplyr)
case_when(
  ages < 13 ~ "Child",
  ages < 18 ~ "Teen",
  ages < 65 ~ "Adult",
  TRUE ~ "Senior"
)
#> [1] "Teen"  "Adult" "Adult"

⚠️ Common Pitfall: & vs && and | vs ||

# For if statements (scalar)
x <- 5
y <- 10

# Use && and || (short-circuit evaluation)
if (x > 3 && y > 8) {
  print("Both true")
}
#> [1] "Both true"

# For vectors
v1 <- c(TRUE, FALSE, TRUE)
v2 <- c(TRUE, TRUE, FALSE)

# Use & and | (element-wise)
v1 & v2
#> [1]  TRUE FALSE FALSE
v1 | v2
#> [1] TRUE TRUE TRUE

# Never use && or || on vectors in if!

Key difference: - & and |: Vectorized, return vector - && and ||: Scalar, return single value, short-circuit

18.4 Error #2: argument is of length zero

⭐⭐ INTERMEDIATE 🧠 LOGIC

18.4.1 The Error

x <- numeric(0)  # Empty vector

if (x > 5) {
  print("Greater")
}
#> Error in if (x > 5) {: argument is of length zero

🔴 ERROR

Error in if (x > 5) { : argument is of length zero

18.4.2 What It Means

You’re testing an empty vector in if.

18.4.3 Common Causes

18.4.3.1 Cause 1: Empty Result from Operation

data <- c(1, 2, 3, 4)
filtered <- data[data > 10]  # Empty!

if (filtered > 0) {
  print("Has values")
}
#> Error in if (filtered > 0) {: argument is of length zero

18.4.3.2 Cause 2: Missing Data

value <- NA
result <- value[!is.na(value)]  # Empty if all NA

if (length(result) > 0) {  # Good
  # ...
}

if (result > 0) {  # Bad! Error if empty
  # ...
}
#> Error in if (result > 0) {: argument is of length zero

18.4.4 Solutions

SOLUTION 1: Check Length First

x <- numeric(0)

if (length(x) > 0 && x[1] > 5) {
  print("First element greater than 5")
}

# Or more robustly
safe_check <- function(x, threshold) {
  if (length(x) == 0) {
    return(FALSE)
  }
  x[1] > threshold
}

if (safe_check(x, 5)) {
  print("Greater")
}

SOLUTION 2: Provide Default

x <- numeric(0)

# Use default if empty
value <- if (length(x) > 0) x[1] else 0

if (value > 5) {
  print("Greater")
}

SOLUTION 3: Use any() or all()

x <- numeric(0)

# any() and all() handle empty vectors
any(x > 5)  # FALSE (no elements satisfy condition)
#> [1] FALSE
all(x > 5)  # TRUE (all zero elements satisfy condition!)
#> [1] TRUE

if (any(x > 5)) {
  print("At least one greater")
}

18.5 for Loops

💡 Key Insight: for Loop Patterns

# 1. Loop over vector
for (i in 1:5) {
  print(i)
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5

# 2. Loop over elements
fruits <- c("apple", "banana", "cherry")
for (fruit in fruits) {
  print(fruit)
}
#> [1] "apple"
#> [1] "banana"
#> [1] "cherry"

# 3. Loop with indices
for (i in seq_along(fruits)) {
  cat(i, ":", fruits[i], "\n")
}
#> 1 : apple 
#> 2 : banana 
#> 3 : cherry

# 4. Nested loops
for (i in 1:3) {
  for (j in 1:2) {
    cat("(", i, ",", j, ") ")
  }
  cat("\n")
}
#> ( 1 , 1 ) ( 1 , 2 ) 
#> ( 2 , 1 ) ( 2 , 2 ) 
#> ( 3 , 1 ) ( 3 , 2 )

# 5. Pre-allocate results (important for performance!)
n <- 1000
result <- numeric(n)  # Pre-allocate

for (i in 1:n) {
  result[i] <- i^2
}

Best practices: - Use seq_along() instead of 1:length() - Pre-allocate result vectors - Consider vectorization instead - Use for for side effects (plots, files)

18.6 Error #3: object not found in loops

⭐ BEGINNER 🔍 SCOPE

18.6.1 The Error

# Empty vector
values <- numeric(0)

for (i in 1:length(values)) {
  print(values[i])
}
#> [1] NA
#> numeric(0)

🔴 ERROR

Error in 1:length(values) : argument of length 0

Wait, that’s different. Let me show the real issue:

# This actually works (badly)
values <- numeric(0)
for (i in 1:length(values)) {  # 1:0 creates c(1, 0)
  print(i)
}
#> [1] 1
#> [1] 0

18.6.2 The Real Problem

# 1:length() is dangerous with empty vectors
values <- numeric(0)
1:length(values)  # c(1, 0) - not what you want!
#> [1] 1 0

# Safer: seq_along()
seq_along(values)  # integer(0) - correct!
#> integer(0)

18.6.3 Solutions

SOLUTION 1: Use seq_along()

values <- numeric(0)

# Safe with empty vectors
for (i in seq_along(values)) {
  print(values[i])
}
# Doesn't iterate (correct behavior)

# Works with non-empty too
values <- c(10, 20, 30)
for (i in seq_along(values)) {
  print(values[i])
}
#> [1] 10
#> [1] 20
#> [1] 30

SOLUTION 2: Check Length First

values <- numeric(0)

if (length(values) > 0) {
  for (i in 1:length(values)) {
    print(values[i])
  }
} else {
  message("No values to process")
}
#> No values to process

SOLUTION 3: Use seq_len()

n <- 0

# seq_len() handles zero correctly
for (i in seq_len(n)) {
  print(i)
}
# Doesn't iterate

n <- 5
for (i in seq_len(n)) {
  print(i)
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
# Iterates 1 to 5

18.7 while Loops

💡 Key Insight: while vs for

# for: known number of iterations
for (i in 1:5) {
  print(i)
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5

# while: iterate until condition false
count <- 1
while (count <= 5) {
  print(count)
  count <- count + 1
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5

# while with break
count <- 1
while (TRUE) {
  print(count)
  count <- count + 1
  if (count > 5) break
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5

# Infinite loop danger!
# while (TRUE) {
#   # Never breaks!
# }

When to use while: - Unknown number of iterations - Convergence checking - Reading until end - Waiting for condition

18.8 break and next

💡 Key Insight: Loop Control

# break: exit loop immediately
for (i in 1:10) {
  if (i > 5) break
  print(i)
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5
# Prints 1-5, then stops

# next: skip to next iteration
for (i in 1:10) {
  if (i %% 2 == 0) next  # Skip even numbers
  print(i)
}
#> [1] 1
#> [1] 3
#> [1] 5
#> [1] 7
#> [1] 9
# Prints only odd numbers

# Combined
for (i in 1:20) {
  if (i > 15) break       # Stop at 15
  if (i %% 2 == 0) next   # Skip evens
  print(i)
}
#> [1] 1
#> [1] 3
#> [1] 5
#> [1] 7
#> [1] 9
#> [1] 11
#> [1] 13
#> [1] 15
# Prints odd numbers up to 15

# In nested loops
for (i in 1:3) {
  for (j in 1:3) {
    if (i == j) next  # Skip diagonal
    cat("(", i, ",", j, ") ")
  }
  cat("\n")
}
#> ( 1 , 2 ) ( 1 , 3 ) 
#> ( 2 , 1 ) ( 2 , 3 ) 
#> ( 3 , 1 ) ( 3 , 2 )

18.9 repeat Loops

💡 Key Insight: repeat Loop

# repeat: infinite loop with break
count <- 0
repeat {
  count <- count + 1
  print(count)
  if (count >= 5) break
}
#> [1] 1
#> [1] 2
#> [1] 3
#> [1] 4
#> [1] 5

# Common pattern: read until done
# repeat {
#   line <- readLines(connection, n = 1)
#   if (length(line) == 0) break
#   process(line)
# }

# Convergence checking
tolerance <- 0.001
value <- 10
repeat {
  old_value <- value
  value <- value / 2 + 1
  
  if (abs(value - old_value) < tolerance) {
    break
  }
}
cat("Converged to:", value, "\n")
#> Converged to: 2.000977

18.10 switch Statement

🎯 Best Practice: switch()

# Cleaner than multiple if/else
operation <- "add"

result <- switch(operation,
  add = 10 + 5,
  subtract = 10 - 5,
  multiply = 10 * 5,
  divide = 10 / 5,
  "Unknown operation"  # Default
)
result
#> [1] 15

# With functions
calculate <- function(op, x, y) {
  switch(op,
    "+" = x + y,
    "-" = x - y,
    "*" = x * y,
    "/" = x / y,
    stop("Unknown operation: ", op)
  )
}

calculate("+", 10, 5)
#> [1] 15
calculate("*", 10, 5)
#> [1] 50

# Numeric switch (uses position)
type <- 2
switch(type,
  "First",    # 1
  "Second",   # 2
  "Third"     # 3
)
#> [1] "Second"

# Multiple cases to same result
grade <- "B"
message <- switch(grade,
  "A" = ,
  "B" = "Good job!",
  "C" = ,
  "D" = "Need improvement",
  "F" = "Failed",
  "Invalid grade"
)
message
#> [1] "Good job!"

18.11 Common Loop Patterns

🎯 Best Practice: Loop Patterns

# 1. Accumulation
total <- 0
for (i in 1:10) {
  total <- total + i
}
total
#> [1] 55

# Better: use sum()
sum(1:10)
#> [1] 55

# 2. Building a result vector
n <- 5
squares <- numeric(n)  # Pre-allocate!
for (i in 1:n) {
  squares[i] <- i^2
}
squares
#> [1]  1  4  9 16 25

# Better: vectorize
(1:5)^2
#> [1]  1  4  9 16 25

# 3. Processing with indices
data <- c(10, 20, 30, 40)
for (i in seq_along(data)) {
  cat("Element", i, "is", data[i], "\n")
}
#> Element 1 is 10 
#> Element 2 is 20 
#> Element 3 is 30 
#> Element 4 is 40

# 4. Conditional accumulation
values <- c(1, 5, 3, 8, 2, 9, 4)
count <- 0
for (val in values) {
  if (val > 5) {
    count <- count + 1
  }
}
count
#> [1] 2

# Better: use sum()
sum(values > 5)
#> [1] 2

# 5. Early exit
find_first <- function(x, threshold) {
  for (i in seq_along(x)) {
    if (x[i] > threshold) {
      return(i)
    }
  }
  return(NA)
}

find_first(c(1, 3, 7, 2, 9), 5)
#> [1] 3

# 6. Nested iteration
matrix_data <- matrix(1:9, nrow = 3)
for (i in 1:nrow(matrix_data)) {
  for (j in 1:ncol(matrix_data)) {
    cat(matrix_data[i, j], " ")
  }
  cat("\n")
}
#> 1  4  7  
#> 2  5  8  
#> 3  6  9

# Better: often use apply family or vectorization

18.12 Vectorization vs Loops

⚠️ Performance: Vectorization Usually Better

n <- 10000

# Loop (slow)
system.time({
  result <- numeric(n)
  for (i in 1:n) {
    result[i] <- sqrt(i)
  }
})
#>    user  system elapsed 
#>   0.003   0.000   0.003

# Vectorized (fast)
system.time({
  result <- sqrt(1:n)
})
#>    user  system elapsed 
#>       0       0       0

# When to use loops:
# 1. Sequential dependencies
fibonacci <- function(n) {
  fib <- numeric(n)
  fib[1] <- 1
  fib[2] <- 1
  for (i in 3:n) {
    fib[i] <- fib[i-1] + fib[i-2]  # Depends on previous
  }
  fib
}

# 2. Side effects (printing, plotting, file I/O)
for (i in 1:3) {
  plot(1:10, main = paste("Plot", i))
  Sys.sleep(0.1)
}

# 3. Complex logic that can't be vectorized
# 4. Early termination conditions

18.13 ifelse() Details

💡 Key Insight: ifelse() Behavior

# Basic ifelse
x <- c(1, 5, 3, 8, 2)
ifelse(x > 4, "High", "Low")
#> [1] "Low"  "High" "Low"  "High" "Low"

# Nested ifelse
ifelse(x < 3, "Low",
       ifelse(x < 7, "Medium", "High"))
#> [1] "Low"    "Medium" "Medium" "High"   "Low"

# With NAs
x_na <- c(1, 5, NA, 8, 2)
ifelse(x_na > 4, "High", "Low")  # NA stays NA
#> [1] "Low"  "High" NA     "High" "Low"

# Type coercion in ifelse
ifelse(c(TRUE, FALSE, TRUE), 1, "No")  # Coerces to character!
#> [1] "1"  "No" "1"

# More control with dplyr::case_when
library(dplyr)
case_when(
  x < 3 ~ "Low",
  x < 7 ~ "Medium",
  TRUE ~ "High"  # Default
)
#> [1] "Low"    "Medium" "Medium" "High"   "Low"

# Maintains types better
case_when(
  c(TRUE, FALSE, TRUE) ~ 1L,
  TRUE ~ NA_integer_
)
#> [1]  1 NA  1

Prefer case_when() for: - Multiple conditions - Type preservation - Clearer code

18.14 Summary

Key Takeaways:

  1. if needs single logical - Use &&/|| not &/|
  2. Check length first - Avoid length-zero errors
  3. Use seq_along() - Not 1:length() in loops
  4. Pre-allocate vectors - Important for performance
  5. break exits loop - next skips iteration
  6. Vectorize when possible - Usually faster than loops
  7. ifelse() is vectorized - Different from if
  8. Use case_when() - Cleaner than nested ifelse

Quick Reference:

Error Cause Fix
condition has length > 1 Vector in if Use all(), any(), or &&/||
argument is of length zero Empty vector in if Check length() first
Infinite loop No break condition Add break or fix condition
Wrong 1:length() Empty vector Use seq_along()

Control Flow:

# if/else
if (condition) {
  # code
} else if (other_condition) {
  # code  
} else {
  # code
}

# ifelse (vectorized)
ifelse(test, yes, no)

# for loop
for (i in seq_along(x)) {
  # code
}

# while loop
while (condition) {
  # code
}

# repeat loop
repeat {
  # code
  if (condition) break
}

# switch
switch(value,
  case1 = result1,
  case2 = result2,
  default
)

Best Practices:

# ✅ Good
if (length(x) > 0 && x[1] > 5)     # Check length
for (i in seq_along(x))             # Safe indexing
result <- numeric(n); for...       # Pre-allocate
result <- sqrt(x)                   # Vectorize when possible

# ❌ Avoid
if (x > 5)                          # Vector in if
for (i in 1:length(x))              # Fails on empty
for... result <- c(result, new)     # Growing vector (slow)
for (i in 1:n) result[i] <- x[i]^2  # Loop when vectorization works

18.15 Exercises

📝 Exercise 1: Safe Condition Checker

Write safe_if(condition, true_val, false_val) that: 1. Checks if condition is single logical 2. Handles NA in condition 3. Returns appropriate value 4. Gives helpful errors

📝 Exercise 2: Loop Converter

Convert this loop to vectorized code:

x <- 1:1000
result <- numeric(length(x))
for (i in seq_along(x)) {
  if (x[i] %% 2 == 0) {
    result[i] <- x[i]^2
  } else {
    result[i] <- x[i]^3
  }
}

📝 Exercise 3: Find First

Write find_first(x, condition) that: 1. Finds first element satisfying condition 2. Returns index and value 3. Handles case where none match 4. Uses early exit for efficiency

📝 Exercise 4: Grade Classifier

Write classify_grade(score) using switch() that: 1. Converts numeric score to letter grade 2. Handles vectorized input 3. Validates input range (0-100) 4. Returns appropriate grade

18.16 Exercise Answers

Click to see answers

Exercise 1:

safe_if <- function(condition, true_val, false_val) {
  # Check if condition is logical
  if (!is.logical(condition)) {
    stop("Condition must be logical, got ", class(condition)[1])
  }
  
  # Check length
  if (length(condition) == 0) {
    stop("Condition has length zero")
  }
  
  if (length(condition) > 1) {
    warning("Condition has length ", length(condition), 
            ", using first element only")
    condition <- condition[1]
  }
  
  # Handle NA
  if (is.na(condition)) {
    warning("Condition is NA, returning NA")
    return(NA)
  }
  
  # Return appropriate value
  if (condition) {
    true_val
  } else {
    false_val
  }
}

# Test
safe_if(TRUE, "yes", "no")
#> [1] "yes"
safe_if(5 > 3, "greater", "less")
#> [1] "greater"
safe_if(NA, "yes", "no")  # Warning
#> Warning in safe_if(NA, "yes", "no"): Condition is NA, returning NA
#> [1] NA
safe_if(c(TRUE, FALSE), "yes", "no")  # Warning
#> Warning in safe_if(c(TRUE, FALSE), "yes", "no"): Condition has length 2, using
#> first element only
#> [1] "yes"
safe_if("not logical", "yes", "no")  # Error
#> Error in safe_if("not logical", "yes", "no"): Condition must be logical, got character

Exercise 2:

# Original loop
x <- 1:1000
result_loop <- numeric(length(x))
for (i in seq_along(x)) {
  if (x[i] %% 2 == 0) {
    result_loop[i] <- x[i]^2
  } else {
    result_loop[i] <- x[i]^3
  }
}

# Vectorized version 1: ifelse
result_vec1 <- ifelse(x %% 2 == 0, x^2, x^3)

# Vectorized version 2: case_when
library(dplyr)
result_vec2 <- case_when(
  x %% 2 == 0 ~ x^2,
  TRUE ~ x^3
)

# Vectorized version 3: logical indexing
result_vec3 <- numeric(length(x))
even <- x %% 2 == 0
result_vec3[even] <- x[even]^2
result_vec3[!even] <- x[!even]^3

# Verify all give same result
all.equal(result_loop, result_vec1)
#> [1] TRUE
all.equal(result_loop, result_vec2)
#> [1] TRUE
all.equal(result_loop, result_vec3)
#> [1] TRUE

# Compare performance
library(microbenchmark)
microbenchmark(
  loop = {
    result <- numeric(length(x))
    for (i in seq_along(x)) {
      if (x[i] %% 2 == 0) result[i] <- x[i]^2
      else result[i] <- x[i]^3
    }
  },
  ifelse = ifelse(x %% 2 == 0, x^2, x^3),
  case_when = case_when(x %% 2 == 0 ~ x^2, TRUE ~ x^3),
  logical_index = {
    result <- numeric(length(x))
    even <- x %% 2 == 0
    result[even] <- x[even]^2
    result[!even] <- x[!even]^3
  },
  times = 100
)
#> Unit: microseconds
#>           expr      min        lq       mean    median        uq       max
#>           loop 4252.890 4729.1255 5265.68637 4931.9740 5408.7665 23887.811
#>         ifelse   46.541   53.3485   59.63407   56.2125   60.6575   151.563
#>      case_when  210.291  255.8740  319.65934  305.8085  372.7440   558.738
#>  logical_index   33.349   39.0780   50.10506   43.2585   46.7335   488.479
#>  neval
#>    100
#>    100
#>    100
#>    100

Exercise 3:

find_first <- function(x, condition) {
  # Validate inputs
  if (!is.function(condition)) {
    stop("condition must be a function")
  }
  
  if (length(x) == 0) {
    return(list(index = NA, value = NA, found = FALSE))
  }
  
  # Search with early exit
  for (i in seq_along(x)) {
    if (condition(x[i])) {
      return(list(
        index = i,
        value = x[i],
        found = TRUE
      ))
    }
  }
  
  # None found
  list(index = NA, value = NA, found = FALSE)
}

# Test
data <- c(1, 3, 5, 8, 2, 9, 4)

# Find first > 5
find_first(data, function(x) x > 5)
#> $index
#> [1] 4
#> 
#> $value
#> [1] 8
#> 
#> $found
#> [1] TRUE

# Find first even
find_first(data, function(x) x %% 2 == 0)
#> $index
#> [1] 4
#> 
#> $value
#> [1] 8
#> 
#> $found
#> [1] TRUE

# Find first > 100 (none)
find_first(data, function(x) x > 100)
#> $index
#> [1] NA
#> 
#> $value
#> [1] NA
#> 
#> $found
#> [1] FALSE

# Empty vector
find_first(numeric(0), function(x) x > 5)
#> $index
#> [1] NA
#> 
#> $value
#> [1] NA
#> 
#> $found
#> [1] FALSE

# More complex condition
find_first(c("apple", "banana", "cherry"), 
          function(x) nchar(x) > 5)
#> $index
#> [1] 2
#> 
#> $value
#> [1] "banana"
#> 
#> $found
#> [1] TRUE

Exercise 4:

classify_grade <- function(score) {
  # Validate input
  if (!is.numeric(score)) {
    stop("Score must be numeric")
  }
  
  # Vectorized function
  sapply(score, function(s) {
    # Check range
    if (is.na(s)) {
      return(NA_character_)
    }
    
    if (s < 0 || s > 100) {
      warning("Score ", s, " is out of range (0-100)")
      return("Invalid")
    }
    
    # Classify using switch on integer ranges
    grade_num <- cut(s, 
                    breaks = c(-Inf, 60, 70, 80, 90, Inf),
                    labels = FALSE)
    
    switch(grade_num,
      "F",  # 1: 0-59
      "D",  # 2: 60-69
      "C",  # 3: 70-79
      "B",  # 4: 80-89
      "A"   # 5: 90-100
    )
  })
}

# Test
classify_grade(85)
#> [1] "B"
classify_grade(c(45, 65, 75, 85, 95))
#> [1] "F" "D" "C" "B" "A"
classify_grade(c(95, NA, 105, 65))
#> Warning in FUN(X[[i]], ...): Score 105 is out of range (0-100)
#> [1] "A"       NA        "Invalid" "D"

# More detailed version with +/-
classify_grade_detailed <- function(score) {
  sapply(score, function(s) {
    if (is.na(s)) return(NA_character_)
    if (s < 0 || s > 100) return("Invalid")
    
    if (s >= 97) return("A+")
    if (s >= 93) return("A")
    if (s >= 90) return("A-")
    if (s >= 87) return("B+")
    if (s >= 83) return("B")
    if (s >= 80) return("B-")
    if (s >= 77) return("C+")
    if (s >= 73) return("C")
    if (s >= 70) return("C-")
    if (s >= 67) return("D+")
    if (s >= 63) return("D")
    if (s >= 60) return("D-")
    return("F")
  })
}

classify_grade_detailed(c(98, 88, 78, 68, 58))
#> [1] "A+" "B+" "C+" "D+" "F"