TypeScript utility library providing common mathematical and array manipulation functions.
pnpm add coastalAdjusts two angles to minimize interpolation distance.
import { adjustAngle } from 'coastal'
adjustAngle(350, 10) // [350, 10] - no adjustment needed
adjustAngle(10, 350) // [370, 350] - adjusts first angleLinear interpolation between two values.
import { lerp } from 'coastal'
lerp(0, 100, 0.5) // 50
lerp(0, 100, 0.25) // 25
lerp(-10, 10, 0.5) // 0Linear interpolation between two angles, taking the shortest path.
import { lerpAngle } from 'coastal'
lerpAngle(350, 10, 0.5) // 0 - takes shortest path across 0°
lerpAngle(90, 270, 0.5) // 180Linear interpolation between two arrays element-wise.
import { lerpArray } from 'coastal'
lerpArray([0, 0], [2, 4], 0.5) // [1, 2]
lerpArray([1, 2, 3], [4, 5], 0.5) // [2.5, 3.5] - uses minimum length
lerpArray([], [1, 2], 0.5) // [] - empty array when either input is emptyEdge cases:
- Empty arrays: Returns empty array when either input array is empty
- Different lengths: Uses minimum length of the two input arrays
Normalizes an array of numbers so they sum to 1.
import { normalize } from 'coastal'
normalize([2, 2]) // [0.5, 0.5]
normalize([1, 2, 3]) // [0.16667, 0.33333, 0.5]
normalize([]) // [] - empty array
normalize([1, -1]) // [Infinity, -Infinity] - zero sum caseEdge cases:
- Empty array: Returns empty array
- Zero sum: Returns array with Infinity/-Infinity values (division by zero)
- All zeros: Returns array with NaN values
Normalizes an angle to the range [0, 360).
import { normalizeAngle } from 'coastal'
normalizeAngle(450) // 90
normalizeAngle(-90) // 270
normalizeAngle(360) // 0Calculates the sum of all numbers in an array.
import { sum } from 'coastal'
sum([1, 2, 3, 4]) // 10
sum([]) // 0Szudzik pairing function - maps two non-negative integers to a unique non-negative integer.
import { szudzik } from 'coastal'
szudzik(1, 1) // 3
szudzik(5, 3) // 33
szudzik(-1, 0) // -1 - deterministic but not bijective for negative inputs
szudzik(1.5, 0) // 3.75 - deterministic but not bijective for non-integersEdge cases:
- Negative inputs: Produces deterministic results but pairing is not guaranteed to be unique or reversible
- Non-integer inputs: Produces deterministic results but pairing is not guaranteed to be unique or reversible
- Intended use: Non-negative integers for mathematically correct bijective pairing
Constrains a number to fall within specified bounds.
import { clamp } from 'coastal'
clamp(15, 0, 10) // 10
clamp(-5, 0, 10) // 0
clamp(5, 0, 10) // 5Removes elements from an array in-place where the predicate returns true.
import { remove } from 'coastal'
const numbers = [1, 2, 3, 4, 5]
remove(numbers, (x) => x % 2 === 0) // removes even numbers
console.log(numbers) // [1, 3, 5]
// Predicate receives value, index, and array
const items = ['a', 'b', 'c', 'd']
remove(items, (value, index) => index % 2 === 0) // removes elements at even indices
console.log(items) // ['b', 'd']Updates an existing array element or inserts a new one based on a predicate match.
import { upsert } from 'coastal'
// Insert when no match found
const users = [{ id: 1, name: 'Alice' }]
upsert(users, { id: 2, name: 'Bob' }, (user) => user.id === 2)
console.log(users) // [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
// Update when match found
const products = [{ id: 1, price: 100 }]
upsert(products, { id: 1, price: 150 }, (product) => product.id === 1)
console.log(products) // [{ id: 1, price: 150 }]
// Works with primitives
const numbers = [1, 2, 3]
upsert(numbers, 99, (n) => n > 5) // no match, appends
console.log(numbers) // [1, 2, 3, 99]
const tags = ['urgent', 'bug']
upsert(tags, 'fixed', (tag) => tag === 'bug') // match found, replaces
console.log(tags) // ['urgent', 'fixed']Behavior:
- Inserts at end of array when predicate finds no match
- Replaces first matching element when predicate finds a match
- Mutates the original array
Maps over an array using dynamically-sized chunks, where each chunk size is determined by examining the current element and its position. Similar to Array.map() but operates on variable-length chunks instead of individual elements.
import { mapChunkBy } from 'coastal'
// Fixed chunk size
const numbers = [1, 2, 3, 4, 5, 6]
mapChunkBy(
numbers,
() => 2,
(chunk, index) => ({ index, sum: chunk.reduce((a, b) => a + b, 0) }),
)
// [{ index: 0, sum: 3 }, { index: 2, sum: 7 }, { index: 4, sum: 11 }]
// Dynamic sizing based on content
const words = ['hi', 'hello', 'a', 'world', 'test']
mapChunkBy(
words,
(word) => (word.length > 3 ? 2 : 1),
(chunk, index) => ({ startIndex: index, text: chunk.join(' ') }),
)
// [{ startIndex: 0, text: 'hi' }, { startIndex: 1, text: 'hello a' }, { startIndex: 3, text: 'world test' }]
// Text processing with sentence detection
const words = ['Hello', 'world!', 'How', 'are', 'you?', 'Fine.']
mapChunkBy(
words,
(_, index) => {
// Take words until punctuation or max 3 words
let count = 1
for (let i = index; i < Math.min(index + 3, words.length); i++) {
if (words[i].includes('!') || words[i].includes('?') || words[i].includes('.')) {
return i - index + 1
}
if (i > index) count++
}
return count
},
(chunk) => chunk.join(' '),
)
// ['Hello world!', 'How are you?', 'Fine.']Edge cases:
- Empty array: Returns empty array
- Chunk size exceeds remaining elements: Uses all remaining elements
- Invalid chunk size: Throws Error for non-positive integers, decimals, NaN, or Infinity
- Source array is not modified
A direct address table providing O(1) lookup time for non-negative integer keys. The table is immutable after construction.
import { DirectAddressTable } from 'coastal'
// Basic usage
const table = new DirectAddressTable([0, 2, 5], ['a', 'b', 'c'])
table.get(0) // 'a'
table.get(1) // undefined
table.get(2) // 'b'
table.get(5) // 'c'
// Duplicate keys - last value wins
const table2 = new DirectAddressTable([1, 2, 1], ['first', 'second', 'third'])
table2.get(1) // 'third'
// Invalid keys throw errors during construction
try {
new DirectAddressTable([-1, 0], ['a', 'b']) // throws Error
} catch (e) {
console.log(e.message) // "Invalid key: -1. Keys must be non-negative integers"
}
try {
new DirectAddressTable([1.5, 2], ['a', 'b']) // throws Error
} catch (e) {
console.log(e.message) // "Invalid key: 1.5. Keys must be non-negative integers"
}Performance characteristics:
- Time complexity: O(1) lookup
- Space complexity: O(max_key) where max_key is the largest key value
- Memory usage scales with the largest key, not the number of stored values
Edge cases:
- Empty key arrays: Throws RangeError
- Mismatched array lengths: Keys without corresponding values are assigned undefined
- Invalid keys: Throws Error for negative numbers or non-integers
- Large key values: Uses memory proportional to the largest key value
- Duplicate keys: Last value overwrites previous values