{"id":30725094,"url":"https://github.com/escapace/coastal","last_synced_at":"2026-01-20T16:59:01.745Z","repository":{"id":307014624,"uuid":"1027500452","full_name":"escapace/coastal","owner":"escapace","description":"TypeScript utility library providing common functions","archived":false,"fork":false,"pushed_at":"2025-09-02T08:03:44.000Z","size":191,"stargazers_count":0,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"trunk","last_synced_at":"2025-09-02T10:08:09.890Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/escapace.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-07-28T05:35:14.000Z","updated_at":"2025-09-02T08:02:51.000Z","dependencies_parsed_at":"2025-09-02T10:01:29.078Z","dependency_job_id":"633cd81f-a769-482c-be23-6d695fc8b4d6","html_url":"https://github.com/escapace/coastal","commit_stats":null,"previous_names":["escapace/coastal"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/escapace/coastal","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escapace%2Fcoastal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escapace%2Fcoastal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escapace%2Fcoastal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escapace%2Fcoastal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/escapace","download_url":"https://codeload.github.com/escapace/coastal/tar.gz/refs/heads/trunk","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/escapace%2Fcoastal/sbom","scorecard":{"id":1236780,"data":{"date":"2025-09-02T08:03:02Z","repo":{"name":"github.com/escapace/coastal","commit":"d2e47e533ff3dafee1d178d5c5e06764b72a1887"},"scorecard":{"version":"v5.2.1","commit":"ab2f6e92482462fe66246d9e32f642855a691dc1"},"score":5.1,"checks":[{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#dangerous-workflow"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#pinned-dependencies"}},{"name":"Code-Review","score":0,"reason":"Found 0/13 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Info: jobLevel 'actions' permission set to 'read': .github/workflows/codeql-analysis.yaml:23","Info: jobLevel 'contents' permission set to 'read': .github/workflows/codeql-analysis.yaml:24","Info: jobLevel 'packages' permission set to 'read': .github/workflows/codeql-analysis.yaml:25","Warn: jobLevel 'security-events' permission set to 'write': .github/workflows/codeql-analysis.yaml:26","Info: jobLevel 'contents' permission set to 'read': .github/workflows/dependency-review.yaml:11","Info: jobLevel 'actions' permission set to 'read': .github/workflows/scorecard.yaml:19","Info: jobLevel 'contents' permission set to 'read': .github/workflows/scorecard.yaml:20","Warn: jobLevel 'security-events' permission set to 'write': .github/workflows/scorecard.yaml:22","Info: topLevel 'contents' permission set to 'read': .github/workflows/ci.yaml:18","Info: topLevel 'packages' permission set to 'read': .github/workflows/ci.yaml:19","Info: topLevel 'contents' permission set to 'read': .github/workflows/codeql-analysis.yaml:12","Info: topLevel 'packages' permission set to 'read': .github/workflows/codeql-analysis.yaml:13","Info: found token with 'none' permissions: .github/workflows/dependency-review.yaml:1","Warn: no topLevel permission defined: .github/workflows/release.yaml:1","Info: found token with 'none' permissions: .github/workflows/scorecard.yaml:1"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#token-permissions"}},{"name":"Maintained","score":0,"reason":"project was created within the last 90 days. Please review its contents carefully","details":["Warn: Repository was created within the last 90 days."],"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#maintained"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#packaging"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#binary-artifacts"}},{"name":"Dependency-Update-Tool","score":10,"reason":"update tool detected","details":["Info: detected update tool: Dependabot: .github/dependabot.yaml:1","Info: detected update tool: RenovateBot: .github/renovate.json5:1"],"documentation":{"short":"Determines if the project uses a dependency update tool.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#dependency-update-tool"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#cii-best-practices"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: Mozilla Public License 2.0: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#license"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 1 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#sast"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#fuzzing"}},{"name":"Security-Policy","score":10,"reason":"security policy file detected","details":["Info: security policy file detected: github.com/escapace/.github/SECURITY.md:1","Info: Found linked content: github.com/escapace/.github/SECURITY.md:1","Info: Found disclosure, vulnerability, and/or timelines in security policy: github.com/escapace/.github/SECURITY.md:1","Info: Found text in security policy: github.com/escapace/.github/SECURITY.md:1"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#security-policy"}},{"name":"CI-Tests","score":10,"reason":"1 out of 1 merged PRs checked by a CI test -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project runs tests before pull requests are merged.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#ci-tests"}},{"name":"Branch-Protection","score":3,"reason":"branch protection is not maximal on development and all release branches","details":["Info: 'allow deletion' disabled on branch 'trunk'","Info: 'force pushes' disabled on branch 'trunk'","Warn: 'branch protection settings apply to administrators' is disabled on branch 'trunk'","Warn: could not determine whether codeowners review is allowed","Warn: no status checks found to merge onto branch 'trunk'","Warn: PRs are not required to make changes on branch 'trunk'; or we don't have data to detect it.If you think it might be the latter, make sure to run Scorecard with a PAT or use Repo Rules (that are always public) instead of Branch Protection settings"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#branch-protection"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#signed-releases"}},{"name":"Contributors","score":0,"reason":"project has 0 contributing companies or organizations -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project has a set of contributors from multiple organizations (e.g., companies).","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#contributors"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-09-02T10:08:28.086Z","repository_id":307014624,"created_at":"2025-09-02T10:08:28.099Z","updated_at":"2025-09-02T10:08:28.099Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273439518,"owners_count":25105979,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-03T02:00:09.631Z","response_time":76,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-09-03T12:07:01.358Z","updated_at":"2026-01-20T16:59:01.703Z","avatar_url":"https://github.com/escapace.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# coastal\n\nTypeScript utility library providing common mathematical and array manipulation functions.\n\n## Installation\n\n```bash\npnpm add coastal\n```\n\n## Usage\n\n### Mathematical Functions\n\n#### adjustAngle\n\nAdjusts two angles to minimize interpolation distance.\n\n```typescript\nimport { adjustAngle } from 'coastal'\n\nadjustAngle(350, 10) // [350, 10] - no adjustment needed\nadjustAngle(10, 350) // [370, 350] - adjusts first angle\n```\n\n#### lerp\n\nLinear interpolation between two values.\n\n```typescript\nimport { lerp } from 'coastal'\n\nlerp(0, 100, 0.5) // 50\nlerp(0, 100, 0.25) // 25\nlerp(-10, 10, 0.5) // 0\n```\n\n#### lerpAngle\n\nLinear interpolation between two angles, taking the shortest path.\n\n```typescript\nimport { lerpAngle } from 'coastal'\n\nlerpAngle(350, 10, 0.5) // 0 - takes shortest path across 0°\nlerpAngle(90, 270, 0.5) // 180\n```\n\n#### lerpArray\n\nLinear interpolation between two arrays element-wise.\n\n```typescript\nimport { lerpArray } from 'coastal'\n\nlerpArray([0, 0], [2, 4], 0.5) // [1, 2]\nlerpArray([1, 2, 3], [4, 5], 0.5) // [2.5, 3.5] - uses minimum length\nlerpArray([], [1, 2], 0.5) // [] - empty array when either input is empty\n```\n\n**Edge cases:**\n\n- Empty arrays: Returns empty array when either input array is empty\n- Different lengths: Uses minimum length of the two input arrays\n\n#### normalize\n\nNormalizes an array of numbers so they sum to 1.\n\n```typescript\nimport { normalize } from 'coastal'\n\nnormalize([2, 2]) // [0.5, 0.5]\nnormalize([1, 2, 3]) // [0.16667, 0.33333, 0.5]\nnormalize([]) // [] - empty array\nnormalize([1, -1]) // [Infinity, -Infinity] - zero sum case\n```\n\n**Edge cases:**\n\n- Empty array: Returns empty array\n- Zero sum: Returns array with Infinity/-Infinity values (division by zero)\n- All zeros: Returns array with NaN values\n\n#### normalizeAngle\n\nNormalizes an angle to the range [0, 360).\n\n```typescript\nimport { normalizeAngle } from 'coastal'\n\nnormalizeAngle(450) // 90\nnormalizeAngle(-90) // 270\nnormalizeAngle(360) // 0\n```\n\n#### sum\n\nCalculates the sum of all numbers in an array.\n\n```typescript\nimport { sum } from 'coastal'\n\nsum([1, 2, 3, 4]) // 10\nsum([]) // 0\n```\n\n#### szudzik\n\nSzudzik pairing function - maps two non-negative integers to a unique non-negative integer.\n\n```typescript\nimport { szudzik } from 'coastal'\n\nszudzik(1, 1) // 3\nszudzik(5, 3) // 33\nszudzik(-1, 0) // -1 - deterministic but not bijective for negative inputs\nszudzik(1.5, 0) // 3.75 - deterministic but not bijective for non-integers\n```\n\n**Edge cases:**\n\n- Negative inputs: Produces deterministic results but pairing is not guaranteed to be unique or reversible\n- Non-integer inputs: Produces deterministic results but pairing is not guaranteed to be unique or reversible\n- Intended use: Non-negative integers for mathematically correct bijective pairing\n\n### Array Functions\n\n#### clamp\n\nConstrains a number to fall within specified bounds.\n\n```typescript\nimport { clamp } from 'coastal'\n\nclamp(15, 0, 10) // 10\nclamp(-5, 0, 10) // 0\nclamp(5, 0, 10) // 5\n```\n\n#### remove\n\nRemoves elements from an array in-place where the predicate returns true.\n\n```typescript\nimport { remove } from 'coastal'\n\nconst numbers = [1, 2, 3, 4, 5]\nremove(numbers, (x) =\u003e x % 2 === 0) // removes even numbers\nconsole.log(numbers) // [1, 3, 5]\n\n// Predicate receives value, index, and array\nconst items = ['a', 'b', 'c', 'd']\nremove(items, (value, index) =\u003e index % 2 === 0) // removes elements at even indices\nconsole.log(items) // ['b', 'd']\n```\n\n#### upsert\n\nUpdates an existing array element or inserts a new one based on a predicate match.\n\n```typescript\nimport { upsert } from 'coastal'\n\n// Insert when no match found\nconst users = [{ id: 1, name: 'Alice' }]\nupsert(users, { id: 2, name: 'Bob' }, (user) =\u003e user.id === 2)\nconsole.log(users) // [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]\n\n// Update when match found\nconst products = [{ id: 1, price: 100 }]\nupsert(products, { id: 1, price: 150 }, (product) =\u003e product.id === 1)\nconsole.log(products) // [{ id: 1, price: 150 }]\n\n// Works with primitives\nconst numbers = [1, 2, 3]\nupsert(numbers, 99, (n) =\u003e n \u003e 5) // no match, appends\nconsole.log(numbers) // [1, 2, 3, 99]\n\nconst tags = ['urgent', 'bug']\nupsert(tags, 'fixed', (tag) =\u003e tag === 'bug') // match found, replaces\nconsole.log(tags) // ['urgent', 'fixed']\n```\n\n**Behavior:**\n\n- Inserts at end of array when predicate finds no match\n- Replaces first matching element when predicate finds a match\n- Mutates the original array\n\n#### mapChunkBy\n\nMaps 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.\n\n```typescript\nimport { mapChunkBy } from 'coastal'\n\n// Fixed chunk size\nconst numbers = [1, 2, 3, 4, 5, 6]\nmapChunkBy(\n  numbers,\n  () =\u003e 2,\n  (chunk, index) =\u003e ({ index, sum: chunk.reduce((a, b) =\u003e a + b, 0) }),\n)\n// [{ index: 0, sum: 3 }, { index: 2, sum: 7 }, { index: 4, sum: 11 }]\n\n// Dynamic sizing based on content\nconst words = ['hi', 'hello', 'a', 'world', 'test']\nmapChunkBy(\n  words,\n  (word) =\u003e (word.length \u003e 3 ? 2 : 1),\n  (chunk, index) =\u003e ({ startIndex: index, text: chunk.join(' ') }),\n)\n// [{ startIndex: 0, text: 'hi' }, { startIndex: 1, text: 'hello a' }, { startIndex: 3, text: 'world test' }]\n\n// Text processing with sentence detection\nconst words = ['Hello', 'world!', 'How', 'are', 'you?', 'Fine.']\nmapChunkBy(\n  words,\n  (_, index) =\u003e {\n    // Take words until punctuation or max 3 words\n    let count = 1\n    for (let i = index; i \u003c Math.min(index + 3, words.length); i++) {\n      if (words[i].includes('!') || words[i].includes('?') || words[i].includes('.')) {\n        return i - index + 1\n      }\n      if (i \u003e index) count++\n    }\n    return count\n  },\n  (chunk) =\u003e chunk.join(' '),\n)\n// ['Hello world!', 'How are you?', 'Fine.']\n```\n\n**Edge cases:**\n\n- Empty array: Returns empty array\n- Chunk size exceeds remaining elements: Uses all remaining elements\n- Invalid chunk size: Throws Error for non-positive integers, decimals, NaN, or Infinity\n- Source array is not modified\n\n### Data Structures\n\n#### DirectAddressTable\n\nA direct address table providing O(1) lookup time for non-negative integer keys. The table is immutable after construction.\n\n```typescript\nimport { DirectAddressTable } from 'coastal'\n\n// Basic usage\nconst table = new DirectAddressTable([0, 2, 5], ['a', 'b', 'c'])\ntable.get(0) // 'a'\ntable.get(1) // undefined\ntable.get(2) // 'b'\ntable.get(5) // 'c'\n\n// Duplicate keys - last value wins\nconst table2 = new DirectAddressTable([1, 2, 1], ['first', 'second', 'third'])\ntable2.get(1) // 'third'\n\n// Invalid keys throw errors during construction\ntry {\n  new DirectAddressTable([-1, 0], ['a', 'b']) // throws Error\n} catch (e) {\n  console.log(e.message) // \"Invalid key: -1. Keys must be non-negative integers\"\n}\n\ntry {\n  new DirectAddressTable([1.5, 2], ['a', 'b']) // throws Error\n} catch (e) {\n  console.log(e.message) // \"Invalid key: 1.5. Keys must be non-negative integers\"\n}\n```\n\n**Performance characteristics:**\n\n- Time complexity: O(1) lookup\n- Space complexity: O(max_key) where max_key is the largest key value\n- Memory usage scales with the largest key, not the number of stored values\n\n**Edge cases:**\n\n- Empty key arrays: Throws RangeError\n- Mismatched array lengths: Keys without corresponding values are assigned undefined\n- Invalid keys: Throws Error for negative numbers or non-integers\n- Large key values: Uses memory proportional to the largest key value\n- Duplicate keys: Last value overwrites previous values\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fescapace%2Fcoastal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fescapace%2Fcoastal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fescapace%2Fcoastal/lists"}