---
title: "Clamp, Test, and Wrap Numeric Values in R"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Clamp, Test, and Wrap Numeric Values in R}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

Learn how to clamp numeric values to limits, test whether values fall within
an inclusive range, and wrap angles or other periodic measurements in R with
the dependency-free numops package.

```{r}
library(numops)
```

## Choose the right operation

The bounds functions are related, but they answer different questions.

| Goal | Function | Interval |
|---|---|---|
| Replace values outside fixed limits | `clamp()` | `[lower, upper]` |
| Restrict probabilities or proportions | `clamp01()` | `[0, 1]` |
| Identify values inside fixed limits | `in_range()` | `[lower, upper]` |
| Map periodic values to one cycle | `wrap()` | `[lower, upper)` |

`clamp()` and `clamp01()` modify values. `in_range()` returns logical results
without changing its input. `wrap()` uses modular arithmetic rather than
replacing values at the nearest boundary.

## Clamp values in R

### Clamp values to a numeric interval

`clamp()` restricts each value to the closed interval `[lower, upper]`. Its
element-wise formula is

```text
min(max(x, lower), upper)
```

Values inside the interval remain unchanged. Values below or above it are
replaced by the nearest boundary.

```{r}
x <- c(-3, -1, 0.5, 4, 8)

clamp(x, lower = -1, upper = 4)
```

The equivalent base R expression requires nested parallel extrema.

```{r}
pmin(pmax(x, -1), 4)
```

Both boundaries are included, so values equal to `lower` or `upper` remain
unchanged.

```{r}
clamp(c(-1, 4), lower = -1, upper = 4)
```

### Preserve matrix and array shape

Clamping a matrix preserves its dimensions and dimnames.

```{r}
x_matrix <- matrix(
  c(-2, 0, 3, 8),
  nrow = 2,
  dimnames = list(c("a", "b"), c("x", "y"))
)

clamp(x_matrix, lower = 0, upper = 5)
```

The same rule applies to higher-dimensional arrays.

## Clamp probabilities to zero and one

`clamp01()` is equivalent to `clamp(x, 0, 1)`. It is convenient when small
numerical errors produce probabilities or proportions just outside their
valid interval.

```{r}
probabilities <- c(-0.02, 0.25, 0.8, 1.03)

clamp01(probabilities)
```

Clamping changes invalid values, while range testing only identifies them.

```{r}
in_range(probabilities, lower = 0, upper = 1)
clamp01(probabilities)
```

When unexpected values may indicate a data problem, test and investigate them
before deciding whether clamping is appropriate.

## Test whether values are within a range

`in_range()` evaluates the inclusive condition

```text
x >= lower & x <= upper
```

```{r}
x <- c(-2, 0, 3, 5, 8)

in_range(x, lower = 0, upper = 5)
```

The logical result can be used directly for filtering.

```{r}
x[in_range(x, lower = 0, upper = 5)]
```

Both endpoints belong to the interval.

```{r}
in_range(c(0, 5), lower = 0, upper = 5)
```

Missing inputs produce missing logical results rather than `TRUE` or `FALSE`.

```{r}
in_range(c(1, NA, 5), lower = 0, upper = 4)
```

## Wrap angles and periodic values in R

`wrap()` maps values to the half-open interval `[lower, upper)`. Its conceptual
formula is

```text
lower + (x - lower) %% (upper - lower)
```

The implementation uses an equivalent calculation that avoids unnecessary
overflow.

### Wrap angles to one rotation

Angles outside a canonical rotation can be wrapped to `[0, 360)`.

```{r}
angles <- c(-370, -10, 0, 360, 370, 725)

wrap(angles, lower = 0, upper = 360)
```

The lower boundary is included and the upper boundary is excluded. Therefore,
an angle of 360 degrees maps to zero rather than remaining 360.

This differs from `in_range()`, which always includes both boundaries. To test
whether an angle is already in the canonical half-open interval, use an
explicit upper comparison.

```{r}
angles >= 0 & angles < 360
```

### Wrap clock times and phases

The same operation applies to clock times.

```{r}
hours <- c(-2, 0, 12, 24, 27, 49)

wrap(hours, lower = 0, upper = 24)
```

A symmetric interval is often useful for phase angles.

```{r}
phases <- c(-2 * pi, -pi, 0, pi, 2 * pi)

wrap(phases, lower = -pi, upper = pi)
```

### Visualize periodic wrapping

Wrapping produces a repeating sawtooth pattern. Each complete cycle returns
the result to the lower boundary.

```{r wrap-plot, fig.alt = "Wrapped angle against original angle"}
angle_sequence <- seq(-720, 720, length.out = 500)

plot(
  angle_sequence,
  wrap(angle_sequence, lower = 0, upper = 360),
  type = "l",
  xlab = "Original angle",
  ylab = "Wrapped angle",
  main = "Wrapping angles to [0, 360)"
)
```

## Scalar recycling and vectorized bounds

Bounds may have length one or the same length as the values being processed.
Scalar bounds are recycled to the shared length.

```{r}
clamp(c(-2, 5, 20), lower = 0, upper = 10)
```

Vectorized bounds allow each position to use a different interval.

```{r}
x <- c(-2, 5, 20)
lower <- c(0, 0, 10)
upper <- c(1, 10, 15)

clamp(x, lower, upper)
in_range(x, lower, upper)
```

Every argument must have length one or a shared length. Other combinations are
errors rather than partial recycling. Names, dimensions, and dimnames come
from the first input already having the shared length.

## Missing and infinite values

The bounds functions use consistent rules for missing and non-finite values.

| Condition | Behavior |
|---|---|
| Missing value in `x` | Produces a missing result |
| Missing bound | Produces an error |
| `lower > upper` | Produces an error |
| Infinite bound in `clamp()` or `in_range()` | Allowed |
| Infinite bound in `wrap()` | Produces an error |
| Infinite value passed to `wrap()` | Produces `NaN` |
| Equal lower and upper bounds in `wrap()` | Produces an error |

An empty numeric input is returned with length zero, provided its bounds are
valid.

## A validation-and-correction workflow

Consider measurements containing probabilities and angles. First record which
probabilities are valid before applying any correction.

```{r}
measurements <- data.frame(
  probability = c(-0.02, 0.35, 1.04, NA),
  angle = c(-10, 45, 360, 725)
)

measurements$probability_valid <- in_range(
  measurements$probability,
  lower = 0,
  upper = 1
)

measurements
```

If the out-of-range probabilities are known numerical artifacts, clamp them to
the valid interval. Wrap the angles to a canonical rotation at the same time.

```{r}
measurements$probability <- clamp01(
  measurements$probability
)

measurements$angle <- wrap(
  measurements$angle,
  lower = 0,
  upper = 360
)

measurements
```

The validation column preserves which probabilities required attention, while
the transformed columns are ready for downstream calculations.

## Interval semantics at a glance

The most important distinction among these functions is whether the upper
boundary is included.

```text
clamp():    [lower, upper]
in_range(): [lower, upper]
wrap():     [lower, upper)
```

Use `clamp()` to enforce limits, `in_range()` to validate or filter values, and
`wrap()` to represent periodic values in a single cycle. Use `clamp01()` when
the required limits are specifically zero and one.
