npm package: @statedelta-libs/operators
Description: Functional operators library optimized for JSON DSL - Ramda-inspired, TypeScript-first
Author: Anderson D. Rosa
Latest version: 0.2.3

# @statedelta-libs/operators

> Functional operators library optimized for JSON DSL. Ramda-inspired, TypeScript-first.

[![Bundle Size](https://img.shields.io/badge/bundle-~7kb-green)](./dist)
[![TypeScript](https://img.shields.io/badge/TypeScript-first-blue)](./src)

## Features

- **Complete**: 200+ operators (Core, List, Object, Logic, Math, String, Type, Advanced)
- **Lightweight**: ~7kb bundle (vs ~50kb Ramda)
- **TypeScript-first**: Strong type inference
- **Async native**: `pipeAsync`, `composeAsync`, `mapAsync`
- **JSON DSL ready**: Designed for `{ $fn: "map", args: [...] }`
- **Placeholder support**: Skip arguments with `__`
- **Lens support**: Immutable nested updates
- **Transducers**: Efficient transformations

## Installation

```bash
pnpm add @statedelta-libs/operators
```

## Quick Start

```typescript
import { pipe, map, filter, sum, prop } from '@statedelta-libs/operators';

// Pipe - left-to-right composition
const totalActiveItems = pipe(
  items,
  filter(prop('active')),
  map(prop('price')),
  sum
);

// With operators
import { curry, __, propEq, pick, merge } from '@statedelta-libs/operators';

const getVipDiscount = pipe(
  filter(propEq('isVip', true)),
  map(pick(['id', 'discount'])),
);
```

---

## API Reference

### Core

| Function | Description |
|----------|-------------|
| `identity(x)` | Returns argument unchanged |
| `always(x)` | Returns function that always returns x |
| `T()` / `F()` | Always true / always false |
| `tap(fn, x)` | Execute side-effect, return original |
| `curry(fn)` | Curry with placeholder support |
| `curryN(n, fn)` | Curry with explicit arity |
| `partial(fn, args)` | Partial application from left |
| `partialRight(fn, args)` | Partial application from right |
| `pipe(val, ...fns)` | Left-to-right composition |
| `pipeBuilder(...fns)` | Create reusable pipe |
| `compose(...fns)` | Right-to-left composition |
| `pipeWith(transformer, fns)` | Pipe with custom transformer |
| `pipeAsync(...fns)` | Async pipe |
| `composeAsync(...fns)` | Async compose |
| `pipeK(...fns)` | Reader/context-aware pipe |
| `composeK(...fns)` | Reader/context-aware compose |
| `letIn(bindings, fn)` | Let bindings |
| `call(fn, ...args)` | Call function with spread args |
| `apply(fn, args)` | Call function with args array |
| `__` | Placeholder for skipping args |

```typescript
// Pipe
pipe(5, x => x + 1, x => x * 2); // 12

// Curry with placeholder
const subtract = curry((a, b) => a - b);
const subtractFrom10 = subtract(__, 10);
subtractFrom10(15); // 5

// Let bindings
letIn(
  { doubled: x => x * 2, incremented: x => x + 1 },
  ({ doubled, incremented }) => doubled + incremented
)(5); // 10 + 6 = 16

// call / apply
call(add, 1, 2);          // 3
apply(add, [1, 2]);        // 3
call(map(toUpper), ['a']); // ['A']
```

---

### List

| Function | Description |
|----------|-------------|
| `map(fn, list)` | Transform each element |
| `filter(pred, list)` | Keep elements matching predicate |
| `reject(pred, list)` | Remove elements matching predicate |
| `reduce(fn, init, list)` | Reduce to single value |
| `reduceRight(fn, init, list)` | Reduce from right |
| `head(list)` | First element |
| `tail(list)` | All except first |
| `last(list)` | Last element |
| `init(list)` | All except last |
| `nth(n, list)` | Element at index |
| `take(n, list)` | First n elements |
| `takeLast(n, list)` | Last n elements |
| `takeWhile(pred, list)` | Take while predicate true |
| `drop(n, list)` | Remove first n |
| `dropLast(n, list)` | Remove last n |
| `dropWhile(pred, list)` | Drop while predicate true |
| `slice(start, end, list)` | Slice of list |
| `find(pred, list)` | First match |
| `findIndex(pred, list)` | Index of first match |
| `findLast(pred, list)` | Last match |
| `indexOf(val, list)` | Index of value |
| `includes(val, list)` | Contains value? |
| `flatten(list)` | Flatten one level |
| `unnest(list)` | Alias for flatten |
| `chain(fn, list)` | Map then flatten |
| `uniq(list)` | Remove duplicates |
| `uniqBy(fn, list)` | Remove duplicates by key |
| `sort(comparator, list)` | Sort with comparator |
| `sortBy(fn, list)` | Sort by derived value |
| `reverse(list)` | Reverse list |
| `groupBy(fn, list)` | Group by key |
| `concat(a, b)` | Concatenate lists |
| `append(val, list)` | Add to end |
| `prepend(val, list)` | Add to start |
| `update(idx, val, list)` | Set value at index (immutable) |
| `insert(idx, val, list)` | Insert at position (immutable) |
| `remove(start, count, list)` | Remove elements by position (immutable) |
| `adjust(idx, fn, list)` | Transform element at index (immutable) |
| `zip(a, b)` | Pair elements |
| `zipWith(fn, a, b)` | Pair with function |
| `zipObj(keys, values)` | Create object from pairs |
| `some(pred, list)` | Any match? |
| `every(pred, list)` | All match? |
| `none(pred, list)` | None match? |
| `length(list)` | Count elements |
| `sum(list)` | Sum numbers |
| `product(list)` | Product of numbers |
| `mean(list)` | Average |
| `median(list)` | Median |
| `min(list)` | Minimum |
| `max(list)` | Maximum |
| `count(pred, list)` | Count elements matching predicate |
| `minBy(fn, list)` | Element with minimum derived value |
| `maxBy(fn, list)` | Element with maximum derived value |
| `countBy(fn, list)` | Count by key |
| `range(start, end)` | Generate range |
| `partition(pred, list)` | Split by predicate |
| `splitEvery(n, list)` | Split into chunks |
| `indexBy(fn, list)` | Index by key |

```typescript
// Transform
pipe(
  [1, 2, 3, 4, 5],
  filter(x => x > 2),
  map(x => x * 2),
  sum
); // 24

// Group and count
groupBy(prop('category'), products);
countBy(prop('status'), orders);

// Search
find(propEq('id', 123), users);

// Modify (immutable)
update(1, 'x', ['a', 'b', 'c']);    // ['a', 'x', 'c']
insert(1, 'x', ['a', 'b', 'c']);    // ['a', 'x', 'b', 'c']
remove(1, 1, ['a', 'b', 'c']);      // ['a', 'c']
adjust(0, toUpper, ['a', 'b']);     // ['A', 'b']

// Aggregation
count(x => x > 0, [-1, 0, 1, 2]);  // 2
minBy(prop('age'), users);          // user with lowest age
maxBy(prop('age'), users);          // user with highest age
```

---

### Object

| Function | Description |
|----------|-------------|
| `prop(key, obj)` | Get property |
| `path(path, obj)` | Get nested property |
| `propOr(def, key, obj)` | Get property with default |
| `pathOr(def, path, obj)` | Get nested with default |
| `props(keys, obj)` | Get multiple properties |
| `pluck(key, list)` | Extract property from each |
| `has(key, obj)` | Has own property? |
| `hasPath(path, obj)` | Has nested path? |
| `objOf(key, val)` | Create single-key object |
| `assoc(key, val, obj)` | Set property (immutable) |
| `assocPath(path, val, obj)` | Set nested (immutable) |
| `dissoc(key, obj)` | Remove property |
| `dissocPath(path, obj)` | Remove nested property |
| `pick(keys, obj)` | Select properties |
| `omit(keys, obj)` | Exclude properties |
| `merge(a, b)` | Shallow merge |
| `mergeDeep(a, b)` | Deep merge |
| `mergeWith(fn, a, b)` | Merge with conflict resolver |
| `evolve(transforms, obj)` | Transform properties |
| `applySpec(spec, obj)` | Build object from spec |
| `extend(spec, obj)` | Add computed properties |
| `keys(obj)` | Get keys |
| `values(obj)` | Get values |
| `entries(obj)` | Get entries |
| `fromEntries(entries)` | Create from entries |
| `invert(obj)` | Swap keys/values |

```typescript
// Access
prop('name', user); // 'John'
path(['address', 'city'], user); // 'NYC'
pathOr('N/A', ['address', 'zip'], user);

// Check
has('name', user);                    // true
hasPath(['address', 'city'], user);   // true

// Create
objOf('name', 'John');                // { name: 'John' }

// Transform (immutable)
assocPath(['settings', 'theme'], 'dark', user);
evolve({ age: inc, name: toUpper }, user);

// Select
pick(['id', 'name'], user);
omit(['password'], user);

// Merge
merge(defaults, userSettings);
mergeDeep(baseConfig, overrides);
mergeWith((a, b) => a + b, { x: 1 }, { x: 2 }); // { x: 3 }

// Invert
invert({ a: '1', b: '2', c: '1' }); // { '1': ['a', 'c'], '2': ['b'] }
```

---

### Logic

| Function | Description |
|----------|-------------|
| `equals(a, b)` | Deep equality |
| `identical(a, b)` | Reference equality |
| `gt(a, b)` | Greater than |
| `gte(a, b)` | Greater than or equal |
| `lt(a, b)` | Less than |
| `lte(a, b)` | Less than or equal |
| `not(x)` | Boolean not |
| `and(a, b)` | Boolean and |
| `or(a, b)` | Boolean or |
| `both(f, g)` | Both predicates true |
| `either(f, g)` | Either predicate true |
| `complement(pred)` | Negate predicate |
| `allPass(preds)` | All predicates pass |
| `anyPass(preds)` | Any predicate passes |
| `ifElse(pred, onTrue, onFalse)` | Conditional |
| `when(pred, fn)` | Apply if true |
| `unless(pred, fn)` | Apply if false |
| `cond(pairs)` | Switch-like conditional |
| `tryCatch(fn, handler)` | Try/catch wrapper |
| `propEq(key, val, obj)` | Property equals? |
| `propSatisfies(pred, key, obj)` | Property satisfies? |
| `pathEq(path, val, obj)` | Path equals? |
| `pathSatisfies(pred, path, obj)` | Path satisfies? |
| `where(spec, obj)` | Match spec |
| `whereEq(spec, obj)` | Match values |
| `is(Ctor, val)` | Instance check |
| `isNil(x)` | Is null/undefined? |
| `isEmpty(x)` | Is empty? |
| `isNotNil(x)` | Is not null/undefined? |
| `isNotEmpty(x)` | Is not empty? |
| `defaultTo(def, val)` | Default for nil |
| `coalesce(vals)` | First non-nil from array |

```typescript
// Predicates
filter(propEq('status', 'active'), items);
filter(where({ age: gte(__, 18), verified: equals(true) }), users);

// Conditionals
ifElse(
  propEq('role', 'admin'),
  always(fullAccess),
  always(limitedAccess)
)(user);

cond([
  [propEq('type', 'A'), handleA],
  [propEq('type', 'B'), handleB],
  [T, handleDefault]
])(item);
```

---

### Math

| Function | Description |
|----------|-------------|
| `add(a, b)` | Addition |
| `subtract(a, b)` | Subtraction |
| `multiply(a, b)` | Multiplication |
| `divide(a, b)` | Division |
| `modulo(a, b)` | Remainder (JS `%`) |
| `mathMod(a, b)` | Mathematical modulo (always >= 0) |
| `negate(x)` | Negate number |
| `inc(x)` | Increment |
| `dec(x)` | Decrement |
| `clamp(min, max, val)` | Clamp to range |
| `abs(x)` | Absolute value |
| `round(x)` | Round |
| `floor(x)` | Floor |
| `ceil(x)` | Ceiling |
| `pow(base, exp)` | Power |

```typescript
// Math operations
map(multiply(2), [1, 2, 3]); // [2, 4, 6]
clamp(0, 100, value);
pipe(prices, map(multiply(1.1)), map(round));

// modulo vs mathMod
modulo(-1, 4);   // -1 (JS remainder)
mathMod(-1, 4);  //  3 (mathematical modulo, always non-negative)
```

---

### String

| Function | Description |
|----------|-------------|
| `toUpper(str)` | Uppercase |
| `toLower(str)` | Lowercase |
| `trim(str)` | Trim whitespace |
| `split(sep, str)` | Split string |
| `join(sep, list)` | Join list |
| `replace(pattern, replacement, str)` | Replace |
| `startsWith(prefix, str)` | Starts with? |
| `endsWith(suffix, str)` | Ends with? |
| `test(regex, str)` | Test regex |
| `match(regex, str)` | Match regex |
| `substring(start, end, str)` | Substring |
| `padStart(len, char, str)` | Pad start |
| `padEnd(len, char, str)` | Pad end |
| `concatStr(a, b)` | Concatenate strings |
| `capitalize(str)` | First letter uppercase |
| `uncapitalize(str)` | First letter lowercase |
| `camelCase(str)` | Convert to camelCase |
| `snakeCase(str)` | Convert to snake_case |
| `kebabCase(str)` | Convert to kebab-case |
| `pascalCase(str)` | Convert to PascalCase |
| `words(str)` | Extract words (split by whitespace) |
| `lines(str)` | Split by newlines |
| `unlines(list)` | Join by newlines |
| `truncate(len, str)` | Truncate with ellipsis |
| `isBlank(str)` | Empty or whitespace only? |
| `template(tpl, data)` | String interpolation `{key}` |
| `charCodeAt(n, str)` | Char code at position |
| `fromCharCode(n)` | Char from code |

```typescript
// Case conversion
capitalize('hello');          // 'Hello'
camelCase('hello_world');     // 'helloWorld'
snakeCase('helloWorld');      // 'hello_world'
kebabCase('HelloWorld');      // 'hello-world'
pascalCase('hello_world');    // 'HelloWorld'

// Text processing
words('  hello   world  ');   // ['hello', 'world']
lines('a\nb\nc');             // ['a', 'b', 'c']
unlines(['a', 'b', 'c']);     // 'a\nb\nc'
truncate(10, 'Hello World!'); // 'Hello W...'

// Validation
isBlank('   ');               // true
isBlank('hello');             // false

// Interpolation
template('Hello {name}!', { name: 'World' }); // 'Hello World!'

// Char codes
charCodeAt(0, 'A');           // 65
fromCharCode(65);             // 'A'

// Pipe example
pipe('  Hello World  ', trim, toLower, split(' '), map(capitalize), join(' '));
// 'Hello World'
```

---

### Type

| Function | Description |
|----------|-------------|
| `toString(x)` | Convert to string |
| `toNumber(x)` | Convert to number |
| `toBoolean(x)` | Convert to boolean |
| `toArray(x)` | Convert to array |
| `toObject(x)` | Convert to object |
| `type(x)` | Get type name |

---

### Advanced

#### Lens

Immutable nested updates.

| Function | Description |
|----------|-------------|
| `lens(getter, setter)` | Create lens |
| `lensProp(key)` | Lens for property |
| `lensPath(path)` | Lens for nested path |
| `lensIndex(i)` | Lens for array index |
| `view(lens, obj)` | Get via lens |
| `set(lens, val, obj)` | Set via lens |
| `over(lens, fn, obj)` | Transform via lens |

```typescript
const nameLens = lensProp('name');
const addressCityLens = lensPath(['address', 'city']);

view(nameLens, user); // 'John'
set(nameLens, 'Jane', user); // { name: 'Jane', ... }
over(nameLens, toUpper, user); // { name: 'JOHN', ... }
```

#### Async

| Function | Description |
|----------|-------------|
| `mapAsync(fn, list)` | Parallel async map |
| `mapSerial(fn, list)` | Sequential async map |
| `filterAsync(pred, list)` | Async filter |
| `reduceAsync(fn, init, list)` | Async reduce |

```typescript
await mapAsync(fetchUser, userIds);
await mapSerial(processInOrder, items);
```

#### Transducers

Efficient transformations without intermediate arrays.

| Function | Description |
|----------|-------------|
| `mapT(fn)` | Map transducer |
| `filterT(pred)` | Filter transducer |
| `takeT(n)` | Take transducer |
| `dropT(n)` | Drop transducer |
| `composeT(...xforms)` | Compose transducers |
| `transduce(xform, reducer, init, list)` | Apply transducer |
| `into(to, xform, from)` | Transduce into collection |

```typescript
const xform = composeT(
  filterT(x => x > 2),
  mapT(x => x * 2),
  takeT(3)
);

transduce(xform, (acc, x) => acc + x, 0, [1, 2, 3, 4, 5, 6, 7]);
```

---

## Scope

All pure functions are available as a single `scope` object, useful when you need to pass operators as a `Record<string, Function>` (e.g., for JSON DSL evaluation):

```typescript
import { scope } from '@statedelta-libs/operators';

// scope contains all 200+ functions, no placeholders or non-function values
// Type-safe: satisfies Record<string, (...args: any[]) => any>

// Use with DSL engines that expect a function-only scope
evaluate(expression, scope);

// Or pick what you need
const { map, filter, sum } = scope;
```

> **Note:** `__` (placeholder) and `isPlaceholder` are **not** included in `scope` since they are not functions. Import them directly when needed.

---

## TypeScript

Full type inference throughout:

```typescript
import { pipe, map, filter, prop, sum } from '@statedelta-libs/operators';

// Types flow through pipe
const result = pipe(
  [{ price: 10, active: true }, { price: 20, active: false }],
  filter(prop('active')),  // { price: number, active: boolean }[]
  map(prop('price')),      // number[]
  sum                      // number
);

// Curried functions preserve types
const getPrice = prop('price'); // <T>(obj: T) => T['price']
```

---

## Bundle Size

| Build | Size |
|-------|------|
| ESM | ~7kb |
| CJS | ~7kb |
| Types | ~22kb |

## Comparison with Ramda

| Aspect | Ramda | @statedelta-libs/operators |
|--------|-------|------------------------|
| Functions | 280+ | 200+ |
| Bundle | ~50kb | ~7kb |
| TypeScript | Weak types | Strong inference |
| Async | Not native | Native support |
| Lens | Yes | Yes |
| Transducers | Yes | Yes |
| String utils | Basic | Extended (case, template, etc.) |
| JSON DSL | No | Native (`scope`, `call`, `apply`) |

---

## License

MIT