https://github.com/escapace/coastal
TypeScript utility library providing common functions
https://github.com/escapace/coastal
Last synced: 6 months ago
JSON representation
TypeScript utility library providing common functions
- Host: GitHub
- URL: https://github.com/escapace/coastal
- Owner: escapace
- License: mpl-2.0
- Created: 2025-07-28T05:35:14.000Z (12 months ago)
- Default Branch: trunk
- Last Pushed: 2025-09-02T08:03:44.000Z (10 months ago)
- Last Synced: 2025-09-02T10:08:09.890Z (10 months ago)
- Language: TypeScript
- Homepage:
- Size: 187 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# coastal
TypeScript utility library providing common mathematical and array manipulation functions.
## Installation
```bash
pnpm add coastal
```
## Usage
### Mathematical Functions
#### adjustAngle
Adjusts two angles to minimize interpolation distance.
```typescript
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.
```typescript
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.
```typescript
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.
```typescript
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.
```typescript
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).
```typescript
import { normalizeAngle } from 'coastal'
normalizeAngle(450) // 90
normalizeAngle(-90) // 270
normalizeAngle(360) // 0
```
#### sum
Calculates the sum of all numbers in an array.
```typescript
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.
```typescript
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.
```typescript
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.
```typescript
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.
```typescript
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.
```typescript
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.
```typescript
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