tests/testthat/test-stdlib-strings.R

# Comprehensive string operation tests

engine <- make_engine()

thin <- make_cran_thinner()

test_that("string-slice extracts string portions", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  expect_equal(get("string-slice", envir = env)("hello", 1, 4), "ell")
  expect_equal(get("string-slice", envir = env)("world", 0, 3), "wor")

  # Full string
  expect_equal(get("string-slice", envir = env)("test", 0, 4), "test")
})

test_that("string case conversion works", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  expect_equal(get("string-upcase", envir = env)("hello"), "HELLO")
  expect_equal(get("string-downcase", envir = env)("HELLO"), "hello")

  # Mixed case
  expect_equal(get("string-upcase", envir = env)("HeLLo"), "HELLO")
  expect_equal(get("string-downcase", envir = env)("HeLLo"), "hello")

  # Empty string
  expect_equal(get("string-upcase", envir = env)(""), "")
})

test_that("char-at and string-ref access characters", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  # char-at (0-indexed)
  expect_equal(get("char-at", envir = env)("hello", 0), "h")
  expect_equal(get("char-at", envir = env)("hello", 4), "o")

  # Out of bounds
  expect_error(get("char-at", envir = env)("hi", 5), "out of bounds")
})

test_that("string-length returns character count", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  expect_equal(get("string-length", envir = env)("hello"), 5)
  expect_equal(get("string-length", envir = env)(""), 0)
  expect_equal(get("string-length", envir = env)("a"), 1)
})

test_that("number->string converts with bases", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  # Decimal (default)
  expect_equal(get("number->string", envir = env)(42), "42")

  # Hexadecimal
  expect_equal(get("number->string", envir = env)(42, 16), "2a")

  # Binary
  expect_equal(get("number->string", envir = env)(8, 2), "1000")

  # Octal
  expect_equal(get("number->string", envir = env)(64, 8), "100")
})

test_that("string->number parses numbers", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  expect_equal(get("string->number", envir = env)("42"), 42)
  expect_equal(get("string->number", envir = env)("3.14"), 3.14)
  expect_equal(get("string->number", envir = env)("-10"), -10)

  # Invalid string returns #f
  expect_false(get("string->number", envir = env)("not-a-number"))
})

test_that("string comparison operators work", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  # string=?
  expect_true(get("string=?", envir = env)("hello", "hello"))
  expect_false(get("string=?", envir = env)("hello", "world"))

  # string<?
  expect_true(get("string<?", envir = env)("apple", "banana"))
  expect_false(get("string<?", envir = env)("banana", "apple"))

  # string>?
  expect_true(get("string>?", envir = env)("banana", "apple"))
  expect_false(get("string>?", envir = env)("apple", "banana"))

  # string<=?
  expect_true(get("string<=?", envir = env)("apple", "apple"))
  expect_true(get("string<=?", envir = env)("apple", "banana"))

  # string>=?
  expect_true(get("string>=?", envir = env)("banana", "banana"))
  expect_true(get("string>=?", envir = env)("banana", "apple"))
})

test_that("string->list and list->string convert between representations", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  # string->list
  result <- get("string->list", envir = env)("abc")
  expect_equal(length(result), 3)
  expect_equal(result[[1]], "a")
  expect_equal(result[[2]], "b")
  expect_equal(result[[3]], "c")

  # list->string
  result <- get("list->string", envir = env)(list("a", "b", "c"))
  expect_equal(result, "abc")

  # Round trip
  original <- "hello"
  chars <- get("string->list", envir = env)(original)
  reconstructed <- get("list->string", envir = env)(chars)
  expect_equal(reconstructed, original)
})

test_that("string-append concatenates strings", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  expect_equal(get("string-append", envir = env)("hello", " ", "world"), "hello world")
  expect_equal(get("string-append", envir = env)("a", "b", "c"), "abc")

  # Single string
  expect_equal(get("string-append", envir = env)("alone"), "alone")

  # Empty strings
  expect_equal(get("string-append", envir = env)("", "test", ""), "test")
})

# Note: string-copy may not be implemented
# test_that("string-copy creates copy of string", {
#   env <- new.env()
#   toplevel_env(engine, env = env)
#
#   original <- "hello"
#   copy <- get("string-copy", envir = env)(original)
#   expect_equal(copy, original)
#   expect_equal(copy, "hello")
# })

# ============================================================================
# String Helpers and I/O
# ============================================================================

test_that("string and io helpers work", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  expect_equal(get("string-concat", envir = env)("a", 1, "b"), "a1b")
  expect_equal(get("format-value", envir = env)(list(1, 2, 3)), "(1 2 3)")
  expect_equal(get("format-value", envir = env)(quote(f(a, b))), "(f a b)")
  expect_equal(get("string-join", envir = env)(list("a", "b", "c"), "-"), "a-b-c")
  expect_equal(get("string-split", envir = env)("a-b-c", "-"), list("a", "b", "c"))
  expect_equal(get("trim", envir = env)("  hi "), "hi")
  expect_equal(get("string-format", envir = env)("x=%s", "y"), "x=y")

  con <- textConnection("hello")
  old_opts <- options(arl.stdin = con)
  on.exit({
    options(old_opts)
    close(con)
  }, add = TRUE)
  expect_equal(get("read-line", envir = env)(), "hello")
})

test_that("string match helpers work", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  expect_true(get("string-contains?", envir = env)("hello", "ell"))
  expect_false(get("string-contains?", envir = env)("hello", "^ell"))
  expect_true(get("string-contains?", envir = env)("hello", "^he", fixed = FALSE))

  expect_true(get("string-match?", envir = env)("hello", "^he"))
  expect_false(get("string-match?", envir = env)("hello", "ELL"))
  expect_false(get("string-match?", envir = env)("hello", "ELL", fixed = TRUE))

  expect_equal(get("string-find", envir = env)("hello", "ll"), 2)
  expect_equal(get("string-find", envir = env)("hello", "nope"), NULL)
  expect_equal(get("string-find", envir = env)("hello", "^he", fixed = FALSE), 0)

  expect_equal(get("string-replace", envir = env)("hello", "l", "L"), "heLlo")
  expect_equal(get("string-replace-all", envir = env)("hello", "l", "L"), "heLLo")

  # Regex mode via :fixed #f
  expect_equal(get("string-replace", envir = env)("abc123", "[0-9]+", "NUM", fixed = FALSE), "abcNUM")
  expect_equal(get("string-replace-all", envir = env)("a1b2c3", "[0-9]", "X", fixed = FALSE), "aXbXcX")
})

# ============================================================================
# Edge Cases: String Operations
# ============================================================================

test_that("string operations handle edge cases", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  # string-concat with no arguments returns empty string
  expect_equal(get("string-concat", envir = env)(), "")

  # string-concat with single argument
  expect_equal(get("string-concat", envir = env)("hello"), "hello")

  # string-concat with NULL (NULLs are skipped)
  expect_equal(get("string-concat", envir = env)(NULL, "world"), "world")

  # string-concat with numbers
  expect_equal(get("string-concat", envir = env)(1, 2, 3), "123")

  # string-join with empty list
  expect_equal(get("string-join", envir = env)(list(), "-"), "")

  # string-join with single element
  expect_equal(get("string-join", envir = env)(list("a"), "-"), "a")

  # string-split with empty string (R's strsplit("", "-", fixed=TRUE) returns character(0))
  result <- get("string-split", envir = env)("", "-")
  expect_equal(length(result), 0)
  expect_true(is.list(result))

  # string-split with delimiter not present
  expect_equal(get("string-split", envir = env)("hello", "-"), list("hello"))

  # trim with already trimmed string
  expect_equal(get("trim", envir = env)("hello"), "hello")

  # trim with only whitespace
  expect_equal(get("trim", envir = env)("   "), "")
})

# ============================================================================
# Coverage: string-find not found, substring negative start, number->string base error
# ============================================================================

test_that("string-find returns #nil when pattern not found", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  result <- engine$eval(
    engine$read('(string-find "hello" "xyz")')[[1]], env = env)
  expect_null(result)
})

test_that("string-slice errors on negative start", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  expect_error(
    engine$eval(engine$read('(string-slice "hello" -1 3)')[[1]], env = env),
    "start index cannot be negative")
})

test_that("number->string errors on base out of range", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  expect_error(
    engine$eval(engine$read("(number->string 10 1)")[[1]], env = env),
    "base must be between 2 and 36")

  expect_error(
    engine$eval(engine$read("(number->string 10 37)")[[1]], env = env),
    "base must be between 2 and 36")
})

# ============================================================================
# string-prefix?
# ============================================================================

test_that("string-prefix? checks string prefix", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  result <- engine$eval(
    engine$read('(string-prefix? "he" "hello")')[[1]], env = env)
  expect_true(result)

  result <- engine$eval(
    engine$read('(string-prefix? "wo" "hello")')[[1]], env = env)
  expect_false(result)

  # Empty prefix always matches
  result <- engine$eval(
    engine$read('(string-prefix? "" "hello")')[[1]], env = env)
  expect_true(result)

  # Exact match
  result <- engine$eval(
    engine$read('(string-prefix? "hello" "hello")')[[1]], env = env)
  expect_true(result)

  # Prefix longer than string
  result <- engine$eval(
    engine$read('(string-prefix? "hello world" "hello")')[[1]], env = env)
  expect_false(result)
})

# ============================================================================
# string-suffix?
# ============================================================================

test_that("string-suffix? checks string suffix", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  result <- engine$eval(
    engine$read('(string-suffix? "lo" "hello")')[[1]], env = env)
  expect_true(result)

  result <- engine$eval(
    engine$read('(string-suffix? "he" "hello")')[[1]], env = env)
  expect_false(result)

  # Empty suffix always matches
  result <- engine$eval(
    engine$read('(string-suffix? "" "hello")')[[1]], env = env)
  expect_true(result)

  # Exact match
  result <- engine$eval(
    engine$read('(string-suffix? "hello" "hello")')[[1]], env = env)
  expect_true(result)
})

# ============================================================================
# string-empty?
# ============================================================================

test_that("string-empty? checks for empty string", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  result <- engine$eval(
    engine$read('(string-empty? "")')[[1]], env = env)
  expect_true(result)

  result <- engine$eval(
    engine$read('(string-empty? "a")')[[1]], env = env)
  expect_false(result)

  result <- engine$eval(
    engine$read('(string-empty? "hello")')[[1]], env = env)
  expect_false(result)

  # Whitespace is not empty
  result <- engine$eval(
    engine$read('(string-empty? " ")')[[1]], env = env)
  expect_false(result)
})

# ============================================================================
# string-repeat
# ============================================================================

test_that("string-repeat repeats string n times", {
  thin()
  env <- new.env()
  toplevel_env(engine, env = env)

  result <- engine$eval(
    engine$read('(string-repeat "ab" 3)')[[1]], env = env)
  expect_equal(result, "ababab")

  # 0 repetitions
  result <- engine$eval(
    engine$read('(string-repeat "hello" 0)')[[1]], env = env)
  expect_equal(result, "")

  # 1 repetition
  result <- engine$eval(
    engine$read('(string-repeat "hello" 1)')[[1]], env = env)
  expect_equal(result, "hello")

  # Empty string
  result <- engine$eval(
    engine$read('(string-repeat "" 5)')[[1]], env = env)
  expect_equal(result, "")
})

Try the arl package in your browser

Any scripts or data that you put into this service are public.

arl documentation built on March 19, 2026, 5:09 p.m.