coastal
v0.2.1
Published
TypeScript utility library providing common functions.
Readme
coastal
TypeScript utility library providing common mathematical and array manipulation functions.
Installation
pnpm add coastalUsage
Mathematical Functions
adjustAngle
Adjusts 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 anglelerp
Linear 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) // 0lerpAngle
Linear 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) // 180lerpArray
Linear 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
normalize
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
normalizeAngle
Normalizes an angle to the range [0, 360).
import { normalizeAngle } from 'coastal'
normalizeAngle(450) // 90
normalizeAngle(-90) // 270
normalizeAngle(360) // 0sum
Calculates the sum of all numbers in an array.
import { sum } from 'coastal'
sum([1, 2, 3, 4]) // 10
sum([]) // 0szudzik
Szudzik 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
Array Functions
clamp
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) // 5remove
Removes 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']upsert
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
mapChunkBy
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
Data Structures
DirectAddressTable
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
