Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 57 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ The Clamp extension introduces range-clamping scalar functions to DuckDB. Initia
It currently provides:

- `clamp(value, min, max)` — clamp to an arbitrary range
- `clip(value, min, max)` - alias for `clamp`
- `saturate(value)` — clamp to the `[0, 1]` range
- `clamp01(value)` — alias for `saturate`, common in graphics and ML
- `wrap(value, min, max)` - Wrap a value x into the range [min_val, max_val) using modular arithmetic.

All functions are implemented as native DuckDB scalar functions with
vectorized execution for high performance.
Expand All @@ -28,15 +30,16 @@ vectorized execution for high performance.

- Native DuckDB scalar functions (`ScalarFunctionSet`)
- Vectorized execution via DuckDB executors
- `TernaryExecutor` for `clamp`
- `TernaryExecutor` for `clamp` / `clip` / `wrap`
- `UnaryExecutor` for `saturate` / `clamp01`
- Supports:
- `BIGINT` (`int64_t`) — `clamp`, `saturate1`, `clamp01`
- `DOUBLE` — `clamp`, `saturate`, `clamp01`
- `BIGINT` (`int64_t`) — `clamp`, `saturate1`, `clamp01`, `clip`, `wrap`
- `DOUBLE` — `clamp`, `saturate`, `clamp01`, `clip`, `wrap`
- NULL-safe:
- returns `NULL` if any input argument is `NULL`
- Strict validation:
- `clamp` throws an error if `min > max`
- `clamp` / `clip` throws an error if `min > max`
- `wrap` throws an error if `min >= max`

---

Expand Down Expand Up @@ -119,6 +122,26 @@ SELECT clamp(15, 20, 10);

---

### clip(value, min, max)

Alias for clamp - restricts a value to an inclusive minimum and maximum bound.

```sql
-- Value within bounds
SELECT clip(15, 10, 20);
-- 15

-- Below minimum
SELECT clip(5, 10, 20);
-- 10

-- Above maximum
SELECT clip(25, 10, 20);
-- 20
```

---

### saturate(value)

A specialized clamp that restricts a value to the `[0, 1]` range.
Expand Down Expand Up @@ -159,15 +182,42 @@ SELECT clamp01(0.25);
SELECT clamp01(2.0);
-- 1.0
```
---
### wrap(value, min, max)

Wrap a value x into the range [min_val, max_val) using modular arithmetic.

Definition:

WRAP(x, min_val, max_val) = min_val + ((x - min_val) % (max_val - min_val))

```sql
SELECT wrap(25, 10, 20);
--15

SELECT wrap(45, 10, 20);
--15

SELECT wrap(5, 10, 20);
--15

SELECT wrap(-15, 10, 20);
--15

SELECT wrap(370, 0, 360), wrap(-10, 0, 360), wrap(720, 0, 360);

-- 10.0 350.0 0.0
```
---

## Function Signatures

```
clamp(value, min, max)
clip(value, min, max)
saturate(value)
clamp01(value)
wrap(value, min, max)
```

### Parameters
Expand All @@ -192,6 +242,8 @@ All arguments must be of the same type.
| clamp | ✓ | ✓ |
| saturate | ✓ | ✓ |
| clamp01 | ✓ | ✓ |
| clip | ✓ | ✓ |
| wrap | ✓ | ✓ |

---

Expand Down Expand Up @@ -225,17 +277,13 @@ make test

## Potential Future Additions

While this extension currently provides `clamp`, `saturate`, and
While this extension currently provides `clamp`, `clip`, `wrap`, `saturate`, and
`clamp01`, it intentionally leaves room for other mathematically
range-based utilities that frequently appear in numerical computing,
analytics, and graphics domains.

Possible future additions include:

- **Wrap / Modulo Clamp**
Wraps values around a range instead of clamping them (e.g., angles,
cyclic time windows, periodic domains).

- **Ping-Pong**
Reflects values back and forth between bounds, useful for oscillating
ranges or bounded waveforms.
Expand All @@ -246,10 +294,6 @@ Possible future additions include:
- **Min-Max Normalization**
Scales values from an arbitrary range into a target range.

- **Clip**
A semantic alias for `clamp`, matching terminology used in NumPy and
PyTorch.

These functions share similar characteristics:

- Simple scalar logic
Expand Down
95 changes: 94 additions & 1 deletion src/clamp_extension.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ struct ClampOperator {

// Validate bounds: min_val must not be greater than max_val
if (min_val > max_val) {
throw InvalidInputException("CLAMP error: Minimum bound (%s) cannot be greater than maximum bound (%s).",
throw InvalidInputException("Error: Minimum bound (%s) cannot be greater than maximum bound (%s).",
std::to_string(min_val), std::to_string(max_val));
}
// Clamp value: If val < min_val, return min_val; if val > max_val, return max_val; else return val
Expand Down Expand Up @@ -71,6 +71,74 @@ static void SaturateFunction(DataChunk &args, ExpressionState &state, Vector &re
UnaryExecutor::Execute<T, T>(args.data[0], result, args.size(), SaturateOperator::Operation<T>);
}

//------------------------------------------------------------------------------
// WrapOperator: Wrap a value x into the range [min_val, max_val) using modular
// arithmetic. This is useful for cyclic values like angles.
//
// Definition:
// WRAP(x, min_val, max_val) = min_val + ((x - min_val) % (max_val - min_val))
//
// % is the floored modulus operator, which ensures the result is always in the
// range [0, max_val - min_val).
//------------------------------------------------------------------------------
// Helper for Integers
// Uses standard modulus operator and adjusts for negative results
template <class T>
static inline typename std::enable_if<std::is_integral<T>::value, T>::type ModuloLogic(T offset, T range) {
T result = offset % range;
if (result < 0)
result += range;
return result;
}

// Helper for Floating Point
// Uses std::fmod and adjusts for negative results
template <class T>
static inline typename std::enable_if<std::is_floating_point<T>::value, T>::type ModuloLogic(T offset, T range) {
T result = std::fmod(offset, range);
if (result < 0)
result += range;
return result;
}

// Main WrapOperator that uses the appropriate ModuloLogic based on the type
struct WrapOperator {
template <class T>
static inline T Operation(T val, T min_val, T max_val) {
// Use standard is_floating_point<T>::value for C++11 compatibility
if (std::is_floating_point<T>::value) {
if (std::isnan(static_cast<double>(val)) || std::isnan(static_cast<double>(min_val)) ||
std::isnan(static_cast<double>(max_val))) {
return std::numeric_limits<T>::quiet_NaN();
}
}

// Validate bounds: min_val must not be greater than or equal to max_val
if (min_val >= max_val) {
throw InvalidInputException(
"Error: Minimum bound (%s) cannot be greater than or equal to maximum bound (%s).",
std::to_string(min_val), std::to_string(max_val));
}

// The compiler picks the correct ModuloLogic overload at compile time
return min_val + ModuloLogic<T>(val - min_val, max_val - min_val);
}
};
//------------------------------------------------------------------------------
// WrapFunction: DuckDB executor wrapper for WrapOperator
//------------------------------------------------------------------------------
template <class T>
static void WrapFunction(DataChunk &args, ExpressionState &state, Vector &result) {
// Uses DuckDB's TernaryExecutor to apply WrapOperator::Operation to each row
// args.data[0]: value to wrap
// args.data[1]: minimum bound
// args.data[2]: maximum bound
// result: output vector
// args.size(): number of rows
TernaryExecutor::Execute<T, T, T, T>(args.data[0], args.data[1], args.data[2], result, args.size(),
WrapOperator::Operation<T>);
}

//------------------------------------------------------------------------------
// LoadInternal: Registers the clamp function(s) with DuckDB
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -99,6 +167,11 @@ static void LoadInternal(ExtensionLoader &loader) {
clamp.AddFunction(double_fun);
clamp.AddFunction(bigint_fun);

// Add alias for clamp
ScalarFunctionSet clamp_alias("clip");
clamp_alias.AddFunction(double_fun);
clamp_alias.AddFunction(bigint_fun);

// ------------------------------------------------------------------------------
// SATURATE
// ------------------------------------------------------------------------------
Expand All @@ -119,10 +192,30 @@ static void LoadInternal(ExtensionLoader &loader) {
saturate_alias.AddFunction(double_sat_fun);
saturate_alias.AddFunction(bigint_sat_fun);

// ------------------------------------------------------------------------------
// WRAP
// ------------------------------------------------------------------------------
ScalarFunctionSet wrap("wrap");

// Define wrap for DOUBLE type
auto double_wrap_fun = ScalarFunction({LogicalType::DOUBLE, LogicalType::DOUBLE, LogicalType::DOUBLE},
LogicalType::DOUBLE, WrapFunction<double>);
double_wrap_fun.null_handling = FunctionNullHandling::DEFAULT_NULL_HANDLING;

// Define wrap for BIGINT (int64_t) type
auto bigint_wrap_fun = ScalarFunction({LogicalType::BIGINT, LogicalType::BIGINT, LogicalType::BIGINT},
LogicalType::BIGINT, WrapFunction<int64_t>);
bigint_wrap_fun.null_handling = FunctionNullHandling::DEFAULT_NULL_HANDLING;

wrap.AddFunction(double_wrap_fun);
wrap.AddFunction(bigint_wrap_fun);

// Register the function set with DuckDB
loader.RegisterFunction(clamp);
loader.RegisterFunction(clamp_alias);
loader.RegisterFunction(saturate);
loader.RegisterFunction(saturate_alias);
loader.RegisterFunction(wrap);
}

//------------------------------------------------------------------------------
Expand Down
Loading
Loading