---
title: "basetable: A Complete Function Reference"
subtitle: "Every exported function, explained progressively with real examples"
output:
  rmarkdown::pdf_document:
    toc: true
    toc_depth: 2
    number_sections: true
    fig_width: 6
    fig_height: 4
vignette: >
  %\VignetteIndexEntry{basetable: A Complete Function Reference}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup}
#| include: false
knitr::opts_chunk$set(warning = FALSE)
library(basetable)
.old_opts <- options(width = 90, digits = 4)
```

# Introduction

`basetable` is for people who already know base R and want fast table
operations without learning `data.table`'s `[i, j, by]` syntax or dplyr's tidy
evaluation. It is aimed specifically at two audiences: **teaching** table
manipulation to people learning base R (without the extra cognitive load of
non-standard evaluation or terse `[i, j, by]` syntax), and **migrating** a
codebase already built on base-R table semantics (`subset()`, `merge()`,
`aggregate()`, and friends) onto a faster engine without a rewrite.
It is not trying to replace dplyr or data.table for a new project that's free
to pick any tool; both are more established choices with a larger ecosystem.

Design goals:

- **Base-style naming and semantics.** Functions read like `subset()`,
  `transform()`, `aggregate()`, `merge()`, and `split()`, the base R verbs
  most R users already know, rather than inventing a new grammar.
- **A native C++ execution engine.** Every operation that touches real data
  routes through basetable's own compiled kernels, so the package is fast
  without asking you to learn `data.table`'s `[i, j, by]` syntax and without a
  `data.table` dependency.
- **Explicit, standard-evaluation interfaces.** Column names are passed as
  character strings, not bare symbols captured by non-standard evaluation
  (with a few clearly-marked exceptions, like `subset()`'s and `transform()`'s
  expression arguments, which mirror base R's own `subset()`/`transform()`
  behavior).

`basetable` deliberately does **not** ship the dplyr-coined verbs
(`filter()`, `select()`, `mutate()`, `arrange()`, `summarise()`,
`distinct()`, `glimpse()`, `slice()`, `relocate()`, `bind_rows()`,
`bind_cols()`), so it can be attached alongside `dplyr` without shadowing
its grammar. The two names it does share with `dplyr` are `count()` and
`pick()`, kept because they read as base-style verbs. It also reuses true
base-R names like `subset()`, `merge()`, `transform()` and `split()`, which
`data.table` also defines. Whenever two attached packages export the same
name the one attached **last** wins and silently shadows the other: call
`basetable::fn()` explicitly (or use the
[`conflicted`](https://conflicted.r-lib.org/) package) if you need both
loaded at once.

This document walks through **every function `basetable` exports**, grouped
into sections that build on each other: start with the core verbs, move
through aggregation, joining, reshaping, and exploration, then into the
lower-level data-cleaning, string, date, and numeric utility functions. Read
it top to bottom to learn the package, or jump to a section as a reference.

Every example below is real, runnable code: the output shown is the actual
output `basetable` produces.

# Core verbs {#sec-core-verbs}

These are the everyday functions: subsetting rows, picking columns, and
creating new columns. If you know `mtcars`, you know enough to follow along.

## Subsetting rows and columns: `subset()`

`subset()` mirrors `base::subset()` exactly: a logical condition for rows, and
an optional `select` for columns.

```{r}
subset(mtcars, cyl == 6, select = c("mpg", "hp", "wt"))
```

Pass `by` to evaluate the condition **within each group**, so an aggregate
inside it is per group. This is basetable's grouped filter; there is no
stateful `group_by()`, the grouping is named at the call.

```{r}
subset(mtcars, mpg > mean(mpg), by = "cyl")
```

## Picking and dropping columns: `pick()`, `drop()`

`pick()` keeps only the named columns; `drop()` removes them.

```{r}
pick(mtcars, c("mpg", "hp"))
drop(mtcars, c("vs", "am"))
```

## Creating and modifying columns: `transform()`, `within()`

`transform()` adds or replaces columns using named expressions, exactly like
`base::transform()`. Later expressions in the same call can refer to columns
created earlier in that call.

```{r}
transform(mtcars, power = hp / wt, power_sq = power^2)
```

`within()` instead evaluates a whole block of code against the data's columns
and returns whatever new/changed variables come out: this is `basetable`'s
version of `base::within()`.

```{r}
within(mtcars, {
  power <- hp / wt
  heavy <- wt > 3.5
})
```

## Renaming columns: `renamecols()`

`renamecols()` renames columns using `new = old` pairs. Old column names may
be supplied as bare names or character strings.

```{r}
mtcars |>
  subset(cyl == 6 & mpg > 18, select = c("mpg", "cyl", "hp")) |>
  transform(ratio = hp / mpg) |>
  orderrows(by = "ratio", decreasing = TRUE)
```

```{r}
transform(mtcars, ratio = hp / mpg, .keep = FALSE)
renamecols(mtcars, horsepower = hp) |> pick(c("horsepower", "mpg"))
```

# Aggregation and grouping {#sec-aggregation}

## Grouped summaries: `aggregate()`

`aggregate()` computes one summary function over one or more value columns,
per group, a faster analogue of `stats::aggregate()`. Note it returns a
`bt_table` directly, since it's meant to be fast and composable with further
basetable operations.

```{r}
aggregate(mtcars, by = "cyl", value = c("mpg", "hp"), fun = mean)
```

## Counting rows: `count()`, `propcount()`

`count()` counts rows per group; `propcount()` adds proportions, optionally
computed within a `margin` of columns rather than over the whole table.

```{r}
count(iris, by = "Species")
propcount(mtcars, by = c("cyl", "am"), margin = "cyl")
```

## Named summaries with `summaries()`

`summaries()` takes named summary expressions directly, with `by` as an
argument, useful when you're calling it programmatically:

```{r}
summaries(mtcars, "cyl", mean_hp = mean(hp), sd_hp = sd(hp))
```

## Splitting into groups: `split()` and `applyby()`

`split()` breaks a table into a named list of `bt_table`s, one per group, using
the native grouping engine internally:

```{r}
pieces <- split(iris, by = "Species")
names(pieces)
pieces$setosa |> headtail(2)
```

`applyby()` applies a function to each group through the same grouping
split path and, if `bind = TRUE`, stitches the results back into one table:

```{r}
applyby(mtcars, by = "cyl", fun = function(d) data.frame(mean_mpg = mean(d$mpg)), bind = TRUE, id = "cyl_group")
```

## First/last row per group: `firstby()`, `lastby()`

```{r}
df <- data.frame(id = c(1, 1, 2, 2, 2), visit = c(1, 2, 1, 2, 3), value = c(10, 12, 5, 6, 7))
firstby(df, by = "id")
lastby(df, by = "id")
```

# Joining tables {#sec-joins}

`basetable` provides the full family of table joins, all engine-backed.

## Standard joins: `merge()`

`merge()` mirrors `base::merge()`'s interface exactly (`by`, `all`, `all.x`,
`all.y`, `suffixes`) while running on the native join engine internally.

```{r}
orders <- data.frame(id = c(1, 2, 3), customer = c("a", "b", "c"))
payments <- data.frame(id = c(2, 3, 4), amount = c(50, 75, 20))

merge(orders, payments, by = "id")
merge(orders, payments, by = "id", all.x = TRUE)
```

## Semi- and anti-joins: `semimerge()`, `antimerge()`

`semimerge()` keeps rows of `x` whose key exists in `y` (like `merge()` but
without adding `y`'s columns); `antimerge()` keeps rows of `x` whose key does
**not** exist in `y`.

```{r}
semimerge(orders, payments, by = "id")
antimerge(orders, payments, by = "id")
```

## Key-matching helpers: `matchedkeys()`, `unmatchedkeys()`, `joinrelationship()`

`matchedkeys()`/`unmatchedkeys()` return `x`'s distinct rows whose key is (or
isn't) present in `y`. `joinrelationship()` classifies the cardinality of a
join key between two tables.

```{r}
matchedkeys(orders, payments, by = "id")
unmatchedkeys(orders, payments, by = "id")
joinrelationship(orders, payments, by = "id")
```

## Cross joins: `crossmerge()`

`crossmerge()` returns the full Cartesian product of two tables.

```{r}
crossmerge(data.frame(size = c("S", "M")), data.frame(color = c("red", "blue")))
```

## Rolling and nearest-key joins: `rollingmerge()`, `nearestmerge()`

`rollingmerge()` matches each row of `x` to the nearest row of `y` **at or
before** it (`direction = "backward"`), at or after it (`"forward"`), or
whichever is numerically closest (`"nearest"`), the classic "as-of" join used
for time series and event logs. `nearestmerge()` is a convenience wrapper for
`direction = "nearest"`.

```{r}
trades <- data.frame(id = 1L, time = c(5, 10, 15))
quotes <- data.frame(id = 1L, time = c(3, 8, 14), price = c(100, 101, 99))

rollingmerge(trades, quotes, by = c("id", "time"), direction = "backward")
nearestmerge(trades, quotes, by = c("id", "time"))
```

## Interval overlap joins: `overlapmerge()`

`overlapmerge()` matches rows whose `[startx, endx]` interval overlaps a
`[starty, endy]` interval in `y` (optionally within an exact `by` key), for
example, matching events to the time windows they fall inside.

```{r}
events <- data.frame(id = 1L, start = c(1, 10), end = c(5, 15))
windows <- data.frame(id = 1L, start = c(0, 8), end = c(6, 20), label = c("early", "late"))

overlapmerge(events, windows, startx = "start", endx = "end", starty = "start", endy = "end", by = "id")
```

## Updating values from another table: `updatemerge()`

`updatemerge()` overwrites `x`'s values with `y`'s wherever the join key
matches (an in-place "upsert" of column values, not a column-adding merge).

```{r}
current <- data.frame(id = c(1, 2, 3), status = c("pending", "pending", "pending"))
updates <- data.frame(id = c(2, 3), status = c("shipped", "cancelled"))

updatemerge(current, updates, by = "id")
```

## Conditional joins: `nonequimerge()`, `rangemerge()`

`nonequimerge()` supports genuine inequality conditions in `by`, written as
`"xcol<op>ycol"`,
alongside any plain exact-match columns:

```{r}
events <- data.frame(id = 1, date = as.Date("2024-01-15"))
periods <- data.frame(
  id = 1,
  start_date = as.Date(c("2024-01-01", "2024-02-01")),
  end_date = as.Date(c("2024-01-31", "2024-02-28")),
  period = c("Jan", "Feb")
)
nonequimerge(events, periods, by = c("id", "date>=start_date", "date<=end_date"))
```

`rangemerge()` keeps every row of `x` (a table of ranges), attaching the row(s)
of `y` whose `value` column falls inside that range, matched within `by`. An
`x` row with no `y` row in range still appears once, with `NA` for `y`'s
columns.

```{r}
ranges <- data.frame(id = c(1, 1, 2), lower = c(0, 0, 0), upper = c(10, 10, 20))
points <- data.frame(id = c(1, 1, 2), val = c(5, 15, 5), label = c("a", "b", "c"))
rangemerge(ranges, points, by = "id", lower = "lower", upper = "upper", value = "val")
```

# Set operations and table comparison {#sec-sets}

## Row-level set operations: `unionrows()`, `intersectrows()`, `diffrows()`

```{r}
a <- data.frame(id = c(1, 2, 3))
b <- data.frame(id = c(2, 3, 4))

unionrows(a, b)
intersectrows(a, b, by = "id")
diffrows(a, b, by = "id")
```

## Comparing two tables: `equalrows()`, `equaldata()`, `sameschema()`, `compareschema()`, `changedcols()`

`equalrows()` aligns two tables by a key and compares every column
(order-insensitive, duplicate-sensitive), not just the key itself.
`equaldata()` compares full table content directly. `sameschema()` and
`compareschema()` (aliased for this purpose by `changedcols()`) compare column
names and types.

```{r}
equalrows(a, data.frame(id = c(3, 2, 1)), by = "id")
equaldata(a, a[nrow(a):1, , drop = FALSE], ignoreorder = TRUE)
sameschema(mtcars, mtcars)
compareschema(mtcars, iris)
```

## Comparing two snapshots of the same table: `addedrows()`, `removedrows()`, `changedrows()`

`changedrows()` returns only the rows whose key exists in both snapshots
*and* whose non-key values actually differ: a row whose key matches but
whose values are identical is not "changed".

```{r}
old <- data.frame(id = c(1, 2, 3), status = c("a", "b", "c"))
new <- data.frame(id = c(2, 3, 4), status = c("b", "c2", "d"))

addedrows(old, new, by = "id")
removedrows(old, new, by = "id")
changedrows(old, new, by = "id")
```

## Row/column binding: `rbindfill()`, `cbind()`

`rbindfill()` stacks tables, filling missing columns with `NA`. For binding
tables side by side, use base R's own `cbind()`.

```{r}
rbindfill(data.frame(x = 1), data.frame(x = 2, y = 3))
cbind(data.frame(a = 1:2), data.frame(b = 3:4))
```

# Reshaping {#sec-reshape}

## Long and wide formats: `tolong()`, `towide()`

```{r}
wide <- data.frame(id = 1:2, jan = c(10, 20), feb = c(11, 22))
long <- tolong(wide, cols = c("jan", "feb"), names = "month", values = "sales")
long
towide(long, names = "month", values = "sales", idcols = "id", fun = sum)
```

## Base R reshape, stack, and unstack: `reshape()`, `stack()`, `unstack()`

`reshape()`, `stack()`, and `unstack()` are `basetable`-native wrappers around
`stats::reshape()`, `utils::stack()`, and `utils::unstack()`, kept for
base-R compatibility rather than reimplemented on the engine.

```{r}
wide2 <- data.frame(id = 1:2, t1 = c(10, 20), t2 = c(11, 22))
reshape(wide2, varying = c("t1", "t2"), v.names = "value", timevar = "time", idvar = "id", direction = "long")

stack(data.frame(a = 1:2, b = 3:4))
```

## Splitting and combining text columns: `separate()`, `unite()`

```{r}
codes <- data.frame(code = c("A-1-red", "B-2-blue"))
parts <- separate(codes, column = "code", into = c("letter", "num", "color"), sep = "-")
parts
unite(parts, column = "code", cols = c("letter", "num", "color"), sep = "_")
```

## Transposing: `transpose()`

```{r}
transpose(data.frame(a = 1:2, b = 3:4))
```

# Exploration and EDA {#sec-eda}

## Shape and types: `dims()`, `types()`, `nrows()`, `ncols()`

```{r}
dims(iris)
types(iris)
```

## Peeking at data: `headtail()`, `preview()`

```{r}
headtail(iris, 2)
preview(iris)
```

## Descriptive statistics: `describe()`, `profile()`

`describe()` gives a per-column summary (mean, sd, quantiles for numeric
columns; top values for others). `profile()` is an alias.

```{r}
describe(iris)
```

## Frequency tables: `freq()`

```{r}
freq(iris, column = "Species")
freq(mtcars, column = "cyl", by = "am", prop = TRUE)
```

## Table 1-style summaries: `summarytab()`

```{r}
dat <- transform(mtcars, am = factor(am, labels = c("Automatic", "Manual")))
summarytab(dat, vars = c("mpg", "cyl"), by = "am", p_value = TRUE)
```

## Missingness: `missingness()`

```{r}
with_na <- transform(iris, Sepal.Length = ifelse(Sepal.Length > 7, NA, Sepal.Length))
missingness(with_na)
```

## Comparing two tables at a glance: `compare()`

```{r}
compare(iris, with_na, by = "Species")
```

# Data cleaning and validation {#sec-cleaning}

## Assertions that stop on failure: `assert*()`

Each `assert*()` function returns its input invisibly if the check passes, and
throws an informative error otherwise, useful as a pipeline guard.

```{r}
mtcars |>
  assertnames(c("mpg", "cyl")) |>
  assertrows(mpg > 0) |>
  assertrange("mpg", lower = 0, upper = 60) |>
  asserttype("cyl", "numeric") |>
  assertcomplete() |>
  headtail(2)
```

```{r}
tryCatch(assertkey(mtcars, "cyl"), error = function(e) conditionMessage(e))
tryCatch(assertunique(mtcars, "cyl"), error = function(e) conditionMessage(e))
tryCatch(assertvalues(mtcars, "am", allowed = c(0, 1)), error = function(e) "passes")
```

`assertcols()`/`assertkey()` are convenience aliases for the lower-level
`assert_cols()`/`assert_key()`, which do the same checks and are what you'd
call from code that doesn't need the `assert*` naming convention:

```{r}
assert_cols(mtcars, c("mpg", "cyl")) |> invisible()
tryCatch(assert_key(mtcars, "cyl"), error = function(e) conditionMessage(e))
```

## Finding rows/values that fail a check: `invalidrows()`, `invalidvalues()`, `outofrange()`

These are the non-throwing counterparts of the `assert*()` functions above:
instead of stopping, they return the offending rows.

```{r}
invalidrows(mtcars, mpg > 15)
outofrange(mtcars, "mpg", lower = 15, upper = 30)
```

## Missing-data helpers: `missingrows()`, `keepmissing()`, `omitmissing()`, `missingindicator()`

```{r}
d <- data.frame(a = c(1, NA, 3), b = c(NA, NA, 3))
missingrows(d, mode = "any")
keepmissing(d)
omitmissing(d, mode = "any")
missingindicator(d)
```

## Filling in missing combinations: `completegrid()`, `expandrows()`

`completegrid()` fills in every combination of the named columns that's
missing from the data (useful before time-series analysis). `expandrows()`
repeats each row a given number of times.

```{r}
sparse <- data.frame(site = c("A", "A", "B"), year = c(2020, 2021, 2020), value = c(1, 2, 3))
completegrid(sparse, cols = c("site", "year"), fill = list(value = 0))

expandrows(data.frame(x = c(1, 2)), times = c(2, 1))
```

## Recoding values: `naif()`, `nato()`, `blanktona()`, `natoblank()`, `replacevalues()`, `replacewhere()`, `replacecols()`

```{r}
naif(c(1, 99, 2), 99)
nato(c(1, NA, 2), 0)
blanktona(c("a", "", "b"))
replacevalues(c("a", "b", "c"), old = c("a", "b"), new = c("A", "B"))
replacewhere(mtcars, cyl == 4, cols = "mpg", value = NA) |> headtail(2)
```

## Deduplication: `uniquerows()`, `removeduplicates()`, `duplicaterows()`, `duplicatekeys()`, `duplicatenames()`

```{r}
dup_df <- data.frame(id = c(1, 1, 2), v = c("a", "b", "c"))
uniquerows(dup_df, cols = "id")
removeduplicates(dup_df, by = "id", keep = "first")
duplicaterows(data.frame(x = c(1, 1, 2)))
duplicatekeys(dup_df, "id")
```

`duplicatekeys()` is a convenience alias for the lower-level `duplicated_keys()`:

```{r}
duplicated_keys(dup_df, "id")
```

## Column-name hygiene: `cleannames()`, `repairnames()`, `renamewith()`, `commonnames()`

```{r}
messy <- data.frame(`First Name` = 1, `2nd_col` = 2, check.names = FALSE)
cleannames(messy)
commonnames(mtcars, iris)
```

`commonnames()` is a convenience alias for the lower-level `common_names()`:

```{r}
common_names(mtcars, iris)
```

## Filling in missing values within groups: `filldown()`, `fillup()`, `fillboth()`

`filldown()` carries the last non-missing value forward within each group
(last observation carried forward); `fillup()` carries the next non-missing
value backward; `fillboth()` does both, filling any remaining gaps from
either direction.

```{r}
visits <- data.frame(
  id = c(1, 1, 1, 2, 2),
  visit = c(1, 2, 3, 1, 2),
  treatment = c("A", NA, NA, NA, "B")
)
filldown(visits, cols = "treatment", by = "id")
fillup(visits, cols = "treatment", by = "id")
fillboth(visits, cols = "treatment", by = "id")
```

# Row and column utilities {#sec-row-col-utils}

## Column metadata: `colnames()`, `rownames()`, `classes()`, `uniques()`, `cardinality()`, `constants()`, `emptycols()`

```{r}
colnames(iris)
classes(iris)
uniques(iris)
cardinality(iris, cols = "Species")
constants(data.frame(a = 1, b = 1:2))
emptycols(data.frame(a = c(NA, NA), b = 1:2))
```

## Finding blank rows: `emptyrows()`

```{r}
emptyrows(data.frame(a = c(NA, "x", ""), b = c(NA, "y", NA)))
```

## Reordering columns: `move()`, `firstcols()`, `lastcols()`

```{r}
move(mtcars, "hp", before = "mpg") |> colnames()
firstcols(mtcars, "wt") |> colnames()
lastcols(mtcars, "mpg") |> colnames()
```

## Row subsets: `firstrows()`, `lastrows()`, `samplerows()`, `samplefrac()`, `reverse()`

```{r}
firstrows(mtcars, 3)
lastrows(mtcars, 2)
set.seed(1)
samplerows(mtcars, 2)
```

`samplerows()` and `samplefrac()` take `by` to sample within each group
(`n` is capped at the group size); sampled rows keep their original order.

```{r}
set.seed(1)
samplerows(mtcars, 2, by = "cyl")
```

For an arbitrary set of row positions, index the table directly, the same
way you would any base R data frame:

```{r}
firstrows(mtcars, c(1, 3, 5))
```

## Sorting rows: `orderrows()`

```{r}
orderrows(mtcars, by = "mpg", decreasing = TRUE) |> headtail(2)
```

## Row position helpers: `rownumber()`

```{r}
rownumber(c("a", "b", "c"))
```

## Row-wise reductions across columns: `rowmin()`, `rowmax()`, `rowany()`, `rowall()`, `rowcount()`, `rowfirst()`, `rowlast()`, `rowapply()`

```{r}
d <- data.frame(x = c(1, NA, 3), y = c(4, 5, NA), z = c(TRUE, FALSE, TRUE))
rowmin(d, cols = c("x", "y"), na.rm = TRUE)
rowmax(d, cols = c("x", "y"), na.rm = TRUE)
rowfirst(d, cols = c("x", "y"), na.rm = TRUE)
rowlast(d, cols = c("x", "y"), na.rm = TRUE)
rowapply(d, cols = c("x", "y"), fun = function(row) sum(row, na.rm = TRUE))
```

## Applying a function to selected columns: `applycols()`, `convertcols()`

```{r}
applycols(mtcars, cols = "mpg", fun = round) |> headtail(2)
convertcols(mtcars, cols = "cyl", fun = as.character) |> types()
```

# String manipulation {#sec-strings}

## Whitespace and case: `trim()`, `squish()`, `lower()`, `upper()`, `titlecase()`, `sentencecase()`, `textlen()`

```{r}
trim("  hello  ")
squish("  too   many   spaces ")
lower("HELLO"); upper("hello")
titlecase("hello world"); sentencecase("HELLO WORLD")
textlen(c("abc", NA))
```

## Substrings and padding: `left()`, `right()`, `middle()`, `truncate()`, `padleft()`, `padright()`, `padcenter()`

```{r}
left("hello", 3); right("hello", 3); middle("hello", 2, 4)
truncate("a long sentence", 10)
padleft("7", 3, pad = "0"); padright("7", 3, pad = "0"); padcenter("hi", 6, pad = "*")
```

## Pattern matching: `containstext()`, `matchestext()`, `startswith()`, `endswith()`, `countmatch()`, `locate()`, `locateall()`

```{r}
containstext(c("apple", "banana"), "an")
matchestext(c("cat", "cats"), "cat")
startswith(c("apple", "banana"), "a")
endswith(c("apple", "banana"), "a")
countmatch("banana", "a")
locate("banana", "an")
```

## Extraction: `extract()`, `extractall()`, `extractnum()`, `extractint()`, `extractbetween()`

```{r}
extract("order #4231", "[0-9]+")
extractall("a1b2c3", "[0-9]")
extractnum("total: -12.5 kg")
extractbetween("[important]", "\\[", "\\]")
```

## Replace and remove: `replacetext()`, `removetext()`, `removeall()`, `replaceall()`

```{r}
replacetext("a-b-c", "-", "_")
removetext("a-b-c", "-")
replaceall(c("a", "b"), old = "a", new = "A")
```

## Split and join: `splittext()`, `splitfirst()`, `splitlast()`, `jointext()`, `collapsetext()`

```{r}
splittext("a-b-c", "-")
splitfirst("a-b-c", "-"); splitlast("a-b-c", "-")
jointext("a", "b", "c")
collapsetext(c("a", "b", "c"), sep = ", ")
```

## Blank and encoding helpers: `isblank()`, `removeaccents()`, `normalizeunicode()`, `normalizeencoding()`, `transliterate()`

```{r}
isblank(c("", "  ", NA, "x"))
removeaccents("café")
```

## String distance and similarity: `textdist()`, `nearesttext()`, `similartext()`

```{r}
textdist("cat", c("cat", "bat", "dog"))
nearesttext("kat", c("cat", "dog", "bird"))
```

## Classification predicates: `isalpha()`, `isalphanumeric()`, `isnumerictext()`, `isintegertext()`, `isemail()`, `isurl()`

```{r}
isalpha(c("abc", "a1c"))
isnumerictext(c("12.5", "abc"))
isemail(c("a@b.com", "not-an-email"))
isurl(c("https://example.com", "example.com"))
```

## Recoding and grouping values: `recode()`, `collapsevalues()`, `casewhen()`, `collapselevels()`, `lump()`, `reorderlevels()`, `expandlevels()`

```{r}
recode(c("a", "b", "c"), old = "a", new = "A")
collapsevalues(c("cat", "dog", "bird"), groups = list(pet = c("cat", "dog")))

x <- rep(c("a", "b", "c", "d"), c(10, 5, 3, 1))
lump(x, n = 2, other = "Other") |> table()

f <- factor(c("low", "high", "mid"), levels = c("low", "mid", "high"))
reorderlevels(f, by = c("high", "mid", "low"))
expandlevels(f, "extra")
```

`casewhen()` maps conditions to a label, taking the same named-list shape as
`collapsevalues()` but with logical vectors: the first element that is
`TRUE` at a position gives its value there, and `default` covers the rest.
It composes inside `transform()`.

```{r}
transform(mtcars, size = casewhen(
  list(light = wt < 2.5, mid = wt < 3.5),
  default = "heavy"
)) |> pick(c("wt", "size")) |> headtail(2)
```

# Parsing text into typed values {#sec-parsing}

`parse*()` functions convert messy text into typed vectors, with an `na=` for
sentinel missing-value strings and a `strict=` to error instead of silently
returning `NA` on failure.

```{r}
parseint(c("1", "2", "NA"))
parsenum("1.234,56", decimal = ",", grouping = ".")
parselogical(c("TRUE", "false", "T", "0"))
parsedate(c("2024-01-15", "15/01/2024"), formats = c("%Y-%m-%d", "%d/%m/%Y"))
parsedatetime("2024-01-15 08:30:00", formats = "%Y-%m-%d %H:%M:%S")
parsepercent("42%")
parsecurrency("$1,234.50")
parsefailures(c("1", "x", "3"), parseint)
```

# Dates and times {#sec-dates}

## Extracting components

```{r}
d <- as.Date("2024-03-15")
year(d); month(d); day(d); weekday(d); yearday(d); week(d); quarter(d)

t <- as.POSIXct("2024-03-15 08:30:45", tz = "UTC")
hour(t); minute(t); second(t)
```

## Arithmetic and sequences: `adddays()`, `addweeks()`, `addmonths()`, `addyears()`, `datediff()`, `dateseq()`, `betweendates()`

`addmonths()`/`addyears()` handle end-of-month overflow explicitly via
`invalid = c("previous", "next", "missing", "error")`: e.g. what should
January 31st plus one month become, given February doesn't have 31 days?

```{r}
adddays(d, 10); addweeks(d, 2)
addmonths(as.Date("2024-01-31"), 1, invalid = "previous")
addmonths(as.Date("2024-01-31"), 1, invalid = "next")
datediff(as.Date("2024-01-10"), as.Date("2024-01-01"), units = "days")
dateseq(as.Date("2024-01-01"), as.Date("2024-01-05"))
betweendates(d, "2024-01-01", "2024-12-31")
```

## Rounding dates: `floordate()`, `ceilingdate()`, `rounddate()`

```{r}
floordate(d, "month"); ceilingdate(d, "month"); rounddate(d, "month")
floordate(d, "week")
```

# Numeric transforms and rolling windows {#sec-numeric}

## Rescaling and standardizing: `rescale()`, `standardize()`, `center()`, `winsorize()`

```{r}
x <- c(1, 2, 3, 4, 100)
rescale(x); standardize(x); center(x)
winsorize(x, probs = c(0.1, 0.9))
```

## Binning: `quantilegroup()`

```{r}
quantilegroup(1:100, n = 4) |> table()
```

## Period-over-period change and rank: `percentchange()`, `percentrank()`, `denserank()`

`denserank()` ranks values by sorted order with no gaps between ranks, tied
values sharing a rank (the same semantics as SQL's `DENSE_RANK()`).

```{r}
percentchange(c(100, 110, 99))
percentrank(c(10, 20, 20, 30))
denserank(c(30, 10, 20, 10, 30))
```

## Cumulative helpers: `cumcount()`, `cumedist()`, `cumavg()`

```{r}
cumcount(c("a", "b", "c"))
cumedist(c("a", "a", "b"))
cumavg(c(1, 2, 3))
```

## Lag/lead and differencing: `difference()`, `lagvalue()`, `leadvalue()`

```{r}
difference(c(1, 3, 6, 10))
lagvalue(1:5, n = 1)
leadvalue(1:5, n = 1)
```

## Rolling window functions: `rollmean()`, `rollsum()`, `rollmin()`, `rollmax()`, `rollmedian()`, `rollsd()`, `rollvar()`, `rollprod()`, `rollapply()`

All roll functions share the same interface: a `width`, an `align`
("right"/"left"/"center"), a `fill` for incomplete windows, `partial` to allow
incomplete boundary windows instead of `fill`, and (where numerically
meaningful) `na.rm`.

```{r}
v <- c(1, 2, NA, 4, 5)
rollmean(v, width = 3, na.rm = TRUE)
rollsum(v, width = 3, na.rm = TRUE, partial = TRUE)
rollmax(v, width = 2, na.rm = TRUE)
rollapply(1:5, width = 3, FUN = sum, partial = TRUE)
```

# Functional helpers {#sec-functional}

`map()`/`traverse()`/`foldr()` are lightweight base-R helpers for working with
plain vectors and lists (not tables), useful for the occasional
loop-replacement inside a larger `basetable` pipeline.

```{r}
map(1:3, function(x) x + 0.5)
traverse(list(a = 1:2, b = 10:11), function(a, b) a + b)
foldr(1:4, `+`)
```

# Closing notes

This document covers all of `basetable`'s exported functions. For the
performance story (how basetable compares to base R and dplyr, and where
its overhead was tracked down and fixed), see the companion `benchmarking`
vignette.

```{r teardown}
#| include: false
options(.old_opts)
```
