{"id":22109735,"url":"https://github.com/hexsprite/intervaltree","last_synced_at":"2026-05-08T02:22:47.770Z","repository":{"id":40305802,"uuid":"76895008","full_name":"hexsprite/intervaltree","owner":"hexsprite","description":null,"archived":false,"fork":false,"pushed_at":"2025-09-19T16:50:48.000Z","size":1416,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-09-19T18:36:51.996Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hexsprite.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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}},"created_at":"2016-12-19T20:30:54.000Z","updated_at":"2025-09-19T16:50:52.000Z","dependencies_parsed_at":"2023-12-28T18:30:41.658Z","dependency_job_id":"57d67a9c-68de-4119-a478-d1e6c427b2b5","html_url":"https://github.com/hexsprite/intervaltree","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/hexsprite/intervaltree","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hexsprite%2Fintervaltree","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hexsprite%2Fintervaltree/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hexsprite%2Fintervaltree/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hexsprite%2Fintervaltree/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hexsprite","download_url":"https://codeload.github.com/hexsprite/intervaltree/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hexsprite%2Fintervaltree/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279014484,"owners_count":26085535,"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-10-13T02:00:06.723Z","response_time":61,"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":"2024-12-01T09:38:21.592Z","updated_at":"2026-05-08T02:22:47.754Z","avatar_url":"https://github.com/hexsprite.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# intervaltree JS\n\nA mutable, self-balancing interval tree for JavaScript/TypeScript.\n\nWritten in TypeScript with no external dependencies. Uses a red-black tree under the hood for O(log n) operations.\n\n## Install\n\n```bash\nnpm install intervaltree\n```\n\n## Quick Start\n\n```js\nimport { Interval, IntervalTree } from 'intervaltree'\n\nconst tree = new IntervalTree()\ntree.addInterval(1, 5, 'data for 1-5')\ntree.addInterval(3, 7)\ntree.addInterval(8, 10)\n\n// Query by point\nconst intervals = tree.searchPoint(4)  // Returns intervals containing point 4\n// Result: [Interval(1, 5, 'data for 1-5'), Interval(3, 7)]\n\n// Query by range (overlaps)\nconst overlapping = tree.searchOverlap(6, 9)  // Returns intervals overlapping [6, 9)\n// Result: [Interval(3, 7), Interval(8, 10)]\n```\n\n## Examples\n\n### Creating a Tree\n\n```js\nimport { Interval, IntervalTree } from 'intervaltree'\n\n// Empty tree\nconst tree = new IntervalTree()\n\n// From array of Interval objects\nconst intervals = [\n  new Interval(1, 3, 'first'),\n  new Interval(5, 8, 'second'),\n  new Interval(7, 10, 'third')\n]\nconst tree2 = new IntervalTree(intervals)\n\n// From tuples\nconst tree3 = IntervalTree.fromTuples([\n  [1, 3],\n  [5, 8],\n  [7, 10, 'with data']\n])\n```\n\n### Adding Intervals\n\n```js\n// Add with explicit Interval object\ntree.add(new Interval(1, 5, 'my data'))\n\n// Add with convenience method\ntree.addInterval(10, 15, 'more data')\n\n// Add multiple intervals at once\ntree.addAll([\n  new Interval(20, 25),\n  new Interval(30, 35, 'bulk add')\n])\n\n// Note: Duplicate intervals (same start, end, and data) are ignored\ntree.addInterval(1, 5)\ntree.addInterval(1, 5)  // This is a no-op\nconsole.log(tree.size)  // 1\n```\n\n### Querying the Tree\n\n```js\n// Find all intervals containing a point\nconst pointResult = tree.searchPoint(6)\n// Returns: Array of Interval objects containing point 6\n\n// Find all intervals overlapping a range\nconst overlapResult = tree.searchOverlap(5, 10)\n// Returns: All intervals that overlap with [5, 10)\n\n// Find intervals completely enveloped by a range\nconst enveloped = tree.searchEnveloped(0, 100)\n// Returns: All intervals where start \u003e= 0 and end \u003c= 100\n\n// Find intervals by minimum length starting at/after a point\nconst byLength = tree.searchByLengthStartingAt(3, 5)\n// Returns: All intervals of length \u003e= 3 starting at position 5 or later\n\n// Find first interval of minimum length — O(log n) with early termination\nconst first = tree.findOneByLengthStartingAt(3, 5)\n// Returns: First interval of length \u003e= 3 starting at/after position 5\n// If found interval starts before 5, it's adjusted to start at 5\n\n// With optional filter function\nconst filtered = tree.findOneByLengthStartingAt(3, 5, iv =\u003e iv.data?.priority === 'high')\n```\n\n### Tree Navigation\n\n```js\n// Get the earliest interval (by start)\nconst earliest = tree.first()   // O(log n)\n\n// Get the latest interval (by start)\nconst latest = tree.last()      // O(log n)\n\n// O(1) size check (no traversal needed)\nconsole.log(tree.size)  // Number of intervals\n```\n\n### Accessing Interval Properties\n\n```js\nconst interval = new Interval(4, 7, { id: 123, name: 'test' })\n\nconsole.log(interval.start)   // 4\nconsole.log(interval.end)     // 7\nconsole.log(interval.data)    // { id: 123, name: 'test' }\nconsole.log(interval.length)  // 3\n\n// Check if interval contains a point\ninterval.containsPoint(5)  // true\ninterval.containsPoint(7)  // false (end is exclusive)\n\n// Check if interval overlaps with a range\ninterval.overlapsWith(3, 6)  // true\n```\n\n### Removing Intervals\n\n```js\n// Remove a specific interval\nconst toRemove = new Interval(1, 5, 'my data')\ntree.remove(toRemove)\n\n// Remove multiple intervals\ntree.removeAll([interval1, interval2])\n\n// Remove all intervals overlapping a range\ntree.removeEnveloped(5, 10)\n// Removes only intervals completely contained within [5, 10)\n```\n\n### Chopping (Advanced)\n\nThe `chop` method removes a section from all intervals in the tree, splitting intervals that partially overlap the chopped region.\n\n```js\nconst tree = new IntervalTree()\ntree.addInterval(0, 10)\ntree.addInterval(5, 15)\ntree.addInterval(12, 20)\n\n// Remove the region [7, 13) from all intervals\ntree.chop(7, 13)\n\nconsole.log(tree.toTuples())\n// Result: [[0, 7], [13, 15], [13, 20]]\n// The interval [0, 10] becomes [0, 7]\n// The interval [5, 15] becomes [13, 15]\n// The interval [12, 20] becomes [13, 20]\n```\n\n### Batch Chopping\n\nThe `chopAll` method removes multiple ranges in a single operation, much faster than calling `chop()` individually when removing many ranges (e.g., marking calendar events as busy time).\n\n```js\nconst tree = new IntervalTree()\ntree.addInterval(0, 100)\n\n// Remove multiple ranges at once\ntree.chopAll([[10, 20], [30, 40], [50, 60]])\n\nconsole.log(tree.toTuples())\n// Result: [[0, 10], [20, 30], [40, 50], [60, 100]]\n\n// Overlapping chop ranges are merged automatically\ntree.chopAll([[10, 30], [20, 40]])\n// Equivalent to chopping [10, 40]\n```\n\nFor small numbers of ranges (≤3), `chopAll` delegates to individual `chop()` calls. For larger batches, it sorts and merges the ranges, then does a single linear sweep — O(n log n) instead of O(n × m) for n ranges on m intervals.\n\n### Merging Overlapping Intervals\n\n```js\nconst tree = IntervalTree.fromTuples([\n  [1, 5],\n  [4, 8],   // Overlaps with [1, 5]\n  [10, 12],\n  [11, 15]  // Overlaps with [10, 12]\n])\n\ntree.mergeOverlaps()\n\nconsole.log(tree.toTuples())\n// Result: [[1, 8], [10, 15]]\n```\n\n### Utility Methods\n\n```js\n// Get all intervals as array\nconst allIntervals = tree.toArray()\n\n// Get sorted array of intervals\nconst sorted = tree.toSorted()  // Sorted by start, then end\n\n// Get as array of tuples\nconst tuples = tree.toTuples()  // [[start, end], [start, end, data], ...]\n\n// Get tree size\nconsole.log(tree.size)  // Number of intervals in tree\n\n// Clone the tree\nconst cloned = tree.clone()\n\n// Get string representation\nconsole.log(tree.toString())\n// Output: \"IntervalTree([ Interval(1, 5, length=4), Interval(10, 15, length=5) ])\"\n\n// Verify tree structure (for debugging, only in development mode)\ntree.verify()\n```\n\n### Equality, Serialization, and Hashing\n\nComparing two trees, detecting changes, and serializing to JSON all use\nthe same canonical form (sorted intervals + data). Construction order\ndoesn't matter:\n\n```js\nconst a = IntervalTree.fromTuples([[0, 10], [20, 30]])\n\nconst b = new IntervalTree()\nb.addInterval(20, 30)\nb.addInterval(0, 10)\n\na.equals(b)     // true — same intervals, regardless of insertion order\na.hash() === b.hash()  // true — hash is semantic\n```\n\n`JSON.stringify` emits the canonical form too, so hashes and diffs are\nstable across serialize/deserialize cycles:\n\n```js\nJSON.stringify(a)\n// → '[[0,10,null],[20,30,null]]'\n```\n\nUse `equals()` when you expect inequality and want an early exit; use\n`hash()` when you want a cacheable fingerprint (e.g. memoization keys,\ncross-request change detection).\n\n### Working with Dates\n\nSince intervals work with numbers, you can use timestamps for date-based intervals:\n\n```js\nconst schedule = new IntervalTree()\n\n// Add time slots (using timestamps)\nconst start = new Date('2024-01-01T09:00:00').getTime()\nconst end = new Date('2024-01-01T10:00:00').getTime()\nschedule.addInterval(start, end, 'Morning meeting')\n\n// Query for a specific time\nconst when = new Date('2024-01-01T09:30:00').getTime()\nconst conflicts = schedule.searchPoint(when)\n\n// Find available slots\nconst dayStart = new Date('2024-01-01T08:00:00').getTime()\nconst minDuration = 60 * 60 * 1000  // 1 hour in milliseconds\nconst available = schedule.findOneByLengthStartingAt(minDuration, dayStart)\n```\n\n## API Reference\n\n### IntervalTree\n\n**Construction:**\n- `constructor(intervals?: Interval\u003cT\u003e[])` - Create a new tree, optionally with initial intervals\n- `static fromTuples\u003cT\u003e(tuples: Array\u003c[number, number] | [number, number, T]\u003e)` - Create from tuple array\n\n**Adding/Removing:**\n- `add(interval: Interval\u003cT\u003e)` - Add an interval to the tree\n- `addInterval(start: number, end: number, data?: T)` - Convenience method to add interval\n- `addAll(intervals: Interval\u003cT\u003e[])` - Add multiple intervals\n- `remove(interval: Interval\u003cT\u003e)` - Remove a specific interval\n- `removeAll(intervals: Interval\u003cT\u003e[])` - Remove multiple intervals\n- `removeEnveloped(start: number, end: number)` - Remove intervals contained within range\n\n**Searching:**\n- `searchPoint(point: number)` - Find all intervals containing a point\n- `searchOverlap(start: number, end: number)` - Find all intervals overlapping a range\n- `searchEnveloped(start: number, end: number)` - Find intervals completely within a range\n- `searchByLengthStartingAt(length: number, start: number)` - Find intervals by minimum length\n- `findOneByLengthStartingAt(minLength: number, startingAt: number, filterFn?: (iv: Interval\u003cT\u003e) =\u003e boolean)` - O(log n) first matching interval with optional filter\n\n**Navigation:**\n- `first(): Interval\u003cT\u003e | null` - Get the earliest interval by start (O(log n))\n- `last(): Interval\u003cT\u003e | null` - Get the latest interval by start (O(log n))\n\n**Boolean Checks:**\n- `contains(point: number): boolean` - Check if any interval contains a point\n- `overlaps(start: number, end: number): boolean` - Check if any interval overlaps with range\n- `isEmpty: boolean` - Check if the tree is empty (getter)\n\n**Set Operations:**\n- `union(other: IntervalTree\u003cT\u003e): IntervalTree\u003cT\u003e` - Combine all intervals from both trees\n- `intersection(other: IntervalTree\u003cT\u003e): IntervalTree\u003cT\u003e` - Find overlapping regions between trees\n- `difference(other: IntervalTree\u003cT\u003e): IntervalTree\u003cT\u003e` - Remove overlapping intervals from other tree\n\n**Iteration \u0026 Transformation:**\n- `[Symbol.iterator]()` - Enables for...of loops and spread operator\n- `forEach(callback: (interval: Interval\u003cT\u003e, index: number) =\u003e void)` - Execute callback for each interval\n- `map\u003cU\u003e(callback: (interval: Interval\u003cT\u003e) =\u003e Interval\u003cU\u003e): IntervalTree\u003cU\u003e` - Transform intervals\n\n**Manipulation:**\n- `chop(start: number, end: number)` - Remove a region from all intervals\n- `mergeOverlaps()` - Merge all overlapping intervals\n\n**Utility:**\n- `clone(): IntervalTree\u003cT\u003e` - Create a deep copy of the tree\n- `toArray(): Interval\u003cT\u003e[]` - Get all intervals as an array\n- `toSorted(): Interval\u003cT\u003e[]` - Get all intervals sorted by start then end\n- `toTuples(): IntervalTuple\u003cT\u003e[]` - Get intervals as tuple array\n- `size: number` - Get the number of intervals in the tree (getter)\n\n**Equality \u0026 Serialization:**\n- `equals(other: IntervalTree\u003cT\u003e): boolean` - Semantic tree equality by sorted intervals + data (short-circuits on size mismatch). Use this instead of comparing hashes when inequality is likely — it exits on the first mismatch.\n- `hash(): string` - SHA-256 of the canonical interval list. Same intervals ⇒ same hash, regardless of construction order or op sequence. Useful for change detection and memoization cache keys.\n- `toJSON(): Array\u003c[number, number, T | undefined]\u003e` - Sorted intervals as `[start, end, data]` tuples. `JSON.stringify(tree)` uses this, so serialized trees are stable and topology-independent.\n\n### Interval\n\n- `constructor(start: number, end: number, data?: unknown)` - Create an interval\n- `start` - Get the start point (inclusive)\n- `end` - Get the end point (exclusive)\n- `data` - Get the associated data\n- `length` - Get the interval length (end - start)\n- `containsPoint(point: number)` - Check if interval contains a point\n- `overlapsWith(start: number, end: number)` - Check if interval overlaps with range\n- `equals(other: Interval)` - Check equality with another interval\n\n### `compareIntervals(a: Interval, b: Interval)`\n\nStandalone comparator function for sorting intervals (by start, then by end).\n\n## TypeScript Generics\n\nThe library now supports full TypeScript generics for type-safe interval data:\n\n```typescript\ninterface Task {\n  id: number\n  title: string\n  priority: 'high' | 'medium' | 'low'\n}\n\nconst schedule = new IntervalTree\u003cTask\u003e()\nschedule.addInterval(9, 17, {\n  id: 1,\n  title: 'Work on project',\n  priority: 'high'\n})\n\n// Full type safety - TypeScript knows the data type!\nconst tasks = schedule.searchPoint(12)\nconsole.log(tasks[0].data?.title)  // Type-safe access\n```\n\nWorks with `fromTuples` too:\n\n```typescript\nconst tree = IntervalTree.fromTuples\u003cstring\u003e([\n  [1, 5, 'Meeting'],\n  [10, 15, 'Lunch'],\n])\n```\n\n## Iterator Support\n\nUse `for...of` loops and other iterable features:\n\n```typescript\nconst tree = IntervalTree.fromTuples([\n  [1, 5],\n  [10, 15],\n  [20, 25],\n])\n\n// Iterate with for...of\nfor (const interval of tree) {\n  console.log(`${interval.start}-${interval.end}`)\n}\n\n// Use spread operator\nconst intervals = [...tree]\n\n// Use Array.from\nconst array = Array.from(tree)\n```\n\n## Boolean Checks\n\nConvenient methods for membership checks:\n\n```typescript\nconst schedule = IntervalTree.fromTuples([\n  [9, 17],   // 9am - 5pm\n  [18, 20],  // 6pm - 8pm\n])\n\n// Check if a point is in any interval\nschedule.contains(12)  // true (noon is during 9-17)\nschedule.contains(17)  // false (end is exclusive)\nschedule.contains(19)  // true\n\n// Check if range overlaps with any interval\nschedule.overlaps(8, 10)   // true\nschedule.overlaps(17, 18)  // false\n\n// Check if tree is empty\nschedule.isEmpty  // false\n```\n\n## Functional Operations\n\n### forEach\n\nExecute a callback for each interval:\n\n```typescript\ntree.forEach((interval, index) =\u003e {\n  console.log(`${index}: ${interval.toString()}`)\n})\n```\n\n### map\n\nTransform intervals and create a new tree:\n\n```typescript\nconst tree = IntervalTree.fromTuples\u003cstring\u003e([\n  [1, 5, 'task1'],\n  [10, 15, 'task2'],\n])\n\n// Shift all intervals by 10 units\nconst shifted = tree.map(interval =\u003e\n  new Interval(\n    interval.start + 10,\n    interval.end + 10,\n    interval.data\n  )\n)\n\n// Result: [[11, 15, 'task1'], [20, 25, 'task2']]\n```\n\nYou can even change the data type:\n\n```typescript\nconst withStrings = new IntervalTree\u003cstring\u003e(...)\nconst withNumbers = withStrings.map(interval =\u003e\n  new Interval\u003cnumber\u003e(\n    interval.start,\n    interval.end,\n    interval.data?.length ?? 0\n  )\n)\n```\n\n## Set Operations\n\nCombine and compare interval trees with set-like operations:\n\n### union\n\nCombine all intervals from two trees:\n\n```typescript\nconst schedule1 = IntervalTree.fromTuples([\n  [9, 12],\n  [14, 17],\n])\n\nconst schedule2 = IntervalTree.fromTuples([\n  [10, 11],\n  [18, 20],\n])\n\nconst combined = schedule1.union(schedule2)\n// Contains all intervals from both trees\n```\n\n### intersection\n\nFind overlapping regions between two trees:\n\n```typescript\nconst available = IntervalTree.fromTuples([\n  [9, 17],   // 9am-5pm\n  [18, 22],  // 6pm-10pm\n])\n\nconst requested = IntervalTree.fromTuples([\n  [10, 12],  // 10am-12pm\n  [20, 23],  // 8pm-11pm\n])\n\nconst conflicts = available.intersection(requested)\n// Result: [[10, 12], [20, 22]]\n// Only the overlapping time ranges\n```\n\n### difference\n\nRemove intervals from one tree that overlap with another:\n\n```typescript\nconst fullDay = IntervalTree.fromTuples([[0, 24]])  // Entire day\n\nconst meetings = IntervalTree.fromTuples([\n  [9, 10],   // 9am meeting\n  [14, 15],  // 2pm meeting\n])\n\nconst freeTime = fullDay.difference(meetings)\n// Result: [[0, 9], [10, 14], [15, 24]]\n// Available time slots\n```\n\n## Real-World Examples\n\n### Scheduling System\n\n```typescript\ninterface Meeting {\n  title: string\n  attendees: string[]\n  room: string\n}\n\nconst calendar = new IntervalTree\u003cMeeting\u003e()\n\ncalendar.addInterval(9, 10, {\n  title: 'Daily Standup',\n  attendees: ['Alice', 'Bob', 'Charlie'],\n  room: 'Conference A'\n})\n\ncalendar.addInterval(14, 15.5, {\n  title: 'Code Review',\n  attendees: ['Alice', 'David'],\n  room: 'Conference B'\n})\n\n// Check if 11am is free\nif (!calendar.contains(11)) {\n  console.log('11am is available!')\n}\n\n// Find all afternoon meetings\nconst afternoon = calendar.searchOverlap(12, 18)\nafternoon.forEach(interval =\u003e {\n  console.log(interval.data?.title)\n})\n\n// Iterate through all meetings\nfor (const meeting of calendar) {\n  console.log(`${meeting.data?.title} in ${meeting.data?.room}`)\n}\n```\n\n### Time Range Management\n\n```typescript\nconst workWeek = IntervalTree.fromTuples([\n  [1, 6],  // Monday-Friday\n])\n\nconst holidays = IntervalTree.fromTuples([\n  [3, 4],  // Wednesday holiday\n])\n\nconst workDays = workWeek.difference(holidays)\n// Result: [[1, 3], [4, 6]]\n// Monday-Tuesday and Thursday-Friday\n```\n\n### Resource Allocation\n\n```typescript\nconst server1 = IntervalTree.fromTuples\u003cstring\u003e([\n  [0, 100, 'Job A'],\n  [150, 300, 'Job B'],\n])\n\nconst server2 = IntervalTree.fromTuples\u003cstring\u003e([\n  [50, 200, 'Job C'],\n  [250, 350, 'Job D'],\n])\n\n// Find resource conflicts\nconst conflicts = server1.intersection(server2)\nif (!conflicts.isEmpty) {\n  console.log('Resource conflict detected!')\n}\n\n// Get all jobs across both servers\nconst allJobs = server1.union(server2)\nfor (const job of allJobs) {\n  console.log(job.data)\n}\n```\n\n## Interval Semantics\n\nThis library uses **half-open intervals** `[start, end)` where:\n- `start` is **inclusive** (inside the interval)\n- `end` is **exclusive** (outside the interval)\n\n**Examples:**\n- `Interval(1, 5)` contains points 1, 2, 3, 4 (but **not** 5)\n- `interval.containsPoint(1)` → `true`\n- `interval.containsPoint(5)` → `false`\n- `interval.length` → `4` (simply `end - start`, no +1 needed)\n\n**Why half-open intervals?**\n1. **Length calculation**: Just `end - start` (no off-by-one errors)\n2. **Adjacent intervals**: `[1, 5)` and `[5, 10)` don't overlap\n3. **Empty intervals**: `[5, 5)` is naturally empty\n4. **Consistency**: Matches JavaScript conventions (`Array.slice`, `substring`, etc.)\n\nThis follows the recommendation from Edsger W. Dijkstra's 1982 note on interval notation and is used by most programming languages and CS literature.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhexsprite%2Fintervaltree","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhexsprite%2Fintervaltree","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhexsprite%2Fintervaltree/lists"}