Skip to content

Num — Number Utilities

Working with numbers in standard JavaScript or TypeScript often leads to code cluttered with anonymous arrow functions, manual isNaN checks, and verbose range-bounding conditionals.

Simple pipelines — such as transforming API response counts, filtering temperature readings, or calculating averages from form inputs — frequently require us to write repetitive expressions like n => n * 100, Math.max(0, Math.min(100, value)), or checking if a parsed number is actually a number.

Num replaces these imperative checks and inline math formulas with declarative, curried, pipeline-ready operations. By modeling arithmetic and validation as first-class functions, we can compose clear, self-describing transformations without scattering temporary arrow functions throughout our codebase.

The problem with scattered arithmetic and NaN

Section titled “The problem with scattered arithmetic and NaN”

Consider a backend service that processes incoming query parameters representing user-configurable slider values:

function parseSliderValue(rawInput: string | undefined): number {
  if (rawInput === undefined) {
    return 50; // Default fallback
  }

  const parsed = parseFloat(rawInput);
  if (isNaN(parsed)) {
    return 50;
  }

  // Clamp the value between 0 and 100
  return Math.max(0, Math.min(100, parsed));
}

This function combines parsing, validation, clamping, and fallback logic within a single imperative block. If we want to map this over an array of inputs, we must write a wrapping helper function or embed inline ternary checks.

Additionally, standard JavaScript arithmetic contains structural pitfalls: dividing by zero does not fail at the type level or throw an exception — it returns Infinity, which silently propagates through our application, causing unpredictable mathematical errors downstream.

The Num module provides curried arithmetic, parsing, clamping, and bounds-checking utilities designed for pipelines.

To convert a string into a number without encountering the NaN trap, we use Num.parse. It returns a Maybe<number> context which explicitly models potential parsing failure:

import { Num } from "@nlozgachev/pipelined/data";

Num.parse("42");    // Some(42)
Num.parse("3.14");  // Some(3.14)
Num.parse("abc");   // None
Num.parse("");      // None

Arithmetic functions in Num are curried and place the primary data argument last. This makes them ideal for composition within pipe and Arr.map:

import { pipe } from "@nlozgachev/pipelined/composition";
import { Arr } from "@nlozgachev/pipelined/data";

// Scale an array of scores
const baseScores = [10, 20, 30];
const scaled = pipe(
  baseScores,
  Arr.map(Num.multiply(1.5))
); // [15, 30, 45]

// Subtract a fee
const finalAmounts = pipe(
  [100, 200, 300],
  Arr.map(Num.subtract(10)) // equivalent to x => x - 10
); // [90, 190, 290]

To prevent runtime errors and silent Infinity propagation, division and remainder operations return a Maybe context, returning None if the divisor is zero:

// Safe division
pipe(100, Num.divide(5)); // Some(20)
pipe(100, Num.divide(0)); // None

// Safe remainder
pipe(10, Num.remainder(3)); // Some(1)
pipe(10, Num.remainder(0)); // None

We can clamp a number to a specific range or test its membership using Num.clamp, Num.between, and Num.inRange:

// Constrain a value to the range [0, 100] (both inclusive)
pipe(150, Num.clamp(0, 100)); // 100
pipe(-5, Num.clamp(0, 100));  // 0
pipe(42, Num.clamp(0, 100));  // 42

// Test if a value falls within a range (both inclusive)
const isWithinRange = pipe(25, Num.between(10, 50)); // true

// Test if a value falls within a half-open range [start, end) (start inclusive, end exclusive)
const isInHalfOpenRange = pipe(10, Num.inRange(1, 10)); // false (10 is excluded)
const isWithinHalfOpenRange = pipe(5, Num.inRange(1, 10)); // true

The Num.is drawer provides boolean predicates for inspecting number characteristics within pipelines:

Num.is.zero(0);        // true
Num.is.integer(42);    // true
Num.is.float(3.14);    // true
Num.is.finite(100);    // true
Num.is.nan(NaN);       // true
Num.is.even(4);        // true
Num.is.odd(3);         // true
Num.is.positive(5);    // true
Num.is.negative(-5);   // true

These predicates compose directly with array helpers like Arr.filter:

pipe(
  [-2, -1, 0, 1, 2, 3, 3.14],
  Arr.filter(Num.is.positive),
  Arr.filter(Num.is.integer),
); // [1, 2, 3]

To build sequences of numbers without manual for loops or pre-allocating arrays, we use Num.range. It generates an array of numbers from start to end (both inclusive) with a customizable step:

Num.range(0, 5);      // [0, 1, 2, 3, 4, 5]
Num.range(0, 10, 2);  // [0, 2, 4, 6, 8, 10]
Num.range(0, 9, 2);   // [0, 2, 4, 6, 8] (stops before exceeding the limit)
Num.range(5, 0);      // [] (when start > end, the range is empty)

Calculating statistics on arrays of numbers using standard JavaScript arrays can be unsafe. Calling Math.min or Math.max on an empty array returns Infinity or -Infinity, and dividing a sum by the length of an empty array returns NaN.

Num provides safe aggregate functions that model empty collections explicitly:

// Summing values (returns 0 for empty arrays)
Num.sum([10, 20, 30]); // 60
Num.sum([]);           // 0

// Calculating average (returns None for empty arrays, avoiding division by zero)
Num.mean([10, 20, 30]); // Some(20)
Num.mean([]);           // None

// Finding bounds safely
Num.min([5, 12, 3]); // Some(3)
Num.min([]);         // None

Num.max([5, 12, 3]); // Some(12)
Num.max([]);         // None

We can combine all of these utilities to build clean, self-contained data flows. Here, we parse raw user inputs, filter out invalid numbers, clamp the valid ones, and calculate their average:

import { Maybe } from "@nlozgachev/pipelined/core";

const rawInputs = ["120", "invalid", "85", "40", "0"];

const averageValidScore = pipe(
  rawInputs,
  Arr.filterMap(Num.parse),          // [120, 85, 40, 0] (skips "invalid")
  Arr.map(Num.clamp(0, 100)),       // [100, 85, 40, 0] (clamps 120 to 100)
  Num.mean,                         // Some(56.25)
  Maybe.getOrElse(() => 0)          // 56.25 (defaults to 0 if no valid inputs)
);
  • Sanitizing and clamping untrusted numeric inputs: When extracting pagination offsets, page limits, or price filter boundaries from query strings, numbers can fall outside permitted domain ranges. Num.parse and Num.clamp sanitize and constrain values point-free in pipeline workflows.
  • Safe statistical computations without Infinity or NaN: Calling standard functions like Math.min() on an empty collection returns Infinity, while dividing by an empty list length yields NaN. Num.min, Num.max, Num.mean, and Num.median return Maybe<number>, guaranteeing that empty datasets are handled explicitly.
  • Division-by-zero protection in progress calculations: Calculating task percentages (e.g. completed / total) when total === 0 produces NaN or Infinity in standard JavaScript, causing broken UI progress bars. Num.divide guards against division by zero safely.
  • Coordinate and bounding box constraints in charts and maps: Clamping cursor offsets, zoom levels, or viewport coordinates within min/max thresholds (Num.clamp, Num.inRange) without writing manual ternary chains.
  • Point-free mathematical pipelines: In shopping cart calculations, currency conversions, and rate adjustments, combining arithmetic operations (Num.add, Num.multiply, Num.round) inside pipe keeps mathematical transformations readable without temporary variables.