Skip to content

TypeScript utility library providing common functions

License

Notifications You must be signed in to change notification settings

escapace/coastal

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

coastal

TypeScript utility library providing common mathematical and array manipulation functions.

Installation

pnpm add coastal

Usage

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 angle

lerp

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) // 0

lerpAngle

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) // 180

lerpArray

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 empty

Edge 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 case

Edge 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) // 0

sum

Calculates the sum of all numbers in an array.

import { sum } from 'coastal'

sum([1, 2, 3, 4]) // 10
sum([]) // 0

szudzik

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-integers

Edge 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) // 5

remove

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

About

TypeScript utility library providing common functions

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •