js-02-merge-intervals
1.000
Challenge · difficulty 2/5
# Merge overlapping intervals
Implement an ES module **`solution.js`** exporting a single function:
```js
export function mergeIntervals(intervals) { /* ... */ }
```
`intervals` is an array of `[start, end]` pairs of numbers, each a **closed** interval with
`start <= end`. Merge all overlapping intervals and return a new array of merged intervals
**sorted ascending by start**.
Rules:
- Intervals that **touch** are merged: `[1, 3]` and `[3, 5]` become `[1, 5]`.
- A merged interval's end is the **maximum** end of the intervals that went into it (handle full
containment, e.g. `[1, 10]` swallows `[2, 3]`).
- The input may be in any order and may contain duplicates. Do **not** mutate the input array.
- An empty input returns `[]`.
Examples:
```js
mergeIntervals([[1, 4], [2, 5]]) // => [[1, 5]]
mergeIntervals([[3, 5], [1, 2]]) // => [[1, 2], [3, 5]]
mergeIntervals([[1, 3], [3, 5]]) // => [[1, 5]]
mergeIntervals([[1, 10], [2, 3], [4, 8]]) // => [[1, 10]]
mergeIntervals([]) // => []
```
tests/solution.test.js
import { test } from "node:test";
import { strict as assert } from "node:assert";
import { mergeIntervals } from "./solution.js";
test("empty input returns empty array", () => {
assert.deepEqual(mergeIntervals([]), []);
});
test("single interval is returned unchanged", () => {
assert.deepEqual(mergeIntervals([[1, 3]]), [[1, 3]]);
});
test("unsorted disjoint input is sorted", () => {
assert.deepEqual(mergeIntervals([[3, 5], [1, 2]]), [[1, 2], [3, 5]]);
});
test("overlapping intervals merge", () => {
assert.deepEqual(mergeIntervals([[1, 4], [2, 5]]), [[1, 5]]);
});
test("touching endpoints merge", () => {
assert.deepEqual(mergeIntervals([[1, 3], [3, 5]]), [[1, 5]]);
});
test("containment keeps the wider interval", () => {
assert.deepEqual(mergeIntervals([[1, 10], [2, 3], [4, 8]]), [[1, 10]]);
});
test("disjoint intervals are left separate", () => {
assert.deepEqual(mergeIntervals([[1, 2], [4, 5]]), [[1, 2], [4, 5]]);
});
test("duplicates collapse to one", () => {
assert.deepEqual(mergeIntervals([[1, 2], [1, 2]]), [[1, 2]]);
});
test("mixed unsorted overlapping and disjoint", () => {
assert.deepEqual(
mergeIntervals([[8, 10], [1, 3], [2, 6], [15, 18]]),
[[1, 6], [8, 10], [15, 18]],
);
});
test("the input array is not mutated", () => {
const input = [[3, 5], [1, 2]];
mergeIntervals(input);
assert.deepEqual(input, [[3, 5], [1, 2]]);
});
System prompt
You are an expert programmer. Solve the task exactly as specified. Output your solution as fenced code blocks using the required file name(s) and the exact function/type signatures requested. Prefer correctness; do not include prose outside code unless asked.
Proposed solution
```js
export function mergeIntervals(intervals) {
if (!intervals || intervals.length === 0) {
return [];
}
// Create a sorted copy to avoid mutating the input
const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
const result = [];
let currentStart = sorted[0][0];
let currentEnd = sorted[0][1];
for (let i = 1; i < sorted.length; i++) {
const [start, end] = sorted[i];
// Intervals overlap or touch if start <= currentEnd
if (start <= currentEnd) {
currentEnd = Math.max(currentEnd, end);
} else {
result.push([currentStart, currentEnd]);
currentStart = start;
currentEnd = end;
}
}
// Push the final merged interval
result.push([currentStart, currentEnd]);
return result;
}
```Test output (stdout)
TAP version 13 # Subtest: empty input returns empty array ok 1 - empty input returns empty array --- duration_ms: 0.668692 type: 'test' ... # Subtest: single interval is returned unchanged ok 2 - single interval is returned unchanged --- duration_ms: 0.10702 type: 'test' ... # Subtest: unsorted disjoint input is sorted ok 3 - unsorted disjoint input is sorted --- duration_ms: 0.06208 type: 'test' ... # Subtest: overlapping intervals merge ok 4 - overlapping intervals merge --- duration_ms: 0.525401 type: 'test' ... # Subtest: touching endpoints merge ok 5 - touching endpoints merge --- duration_ms: 0.07711 type: 'test' ... # Subtest: containment keeps the wider interval ok 6 - containment keeps the wider interval --- duration_ms: 0.05935 type: 'test' ... # Subtest: disjoint intervals are left separate ok 7 - disjoint intervals are left separate --- duration_ms: 0.04904 type: 'test' ... # Subtest: duplicates collapse to one ok 8 - duplicates collapse to one --- duration_ms: 0.03785 type: 'test' ... # Subtest: mixed unsorted overlapping and disjoint ok 9 - mixed unsorted overlapping and disjoint --- duration_ms: 0.056831 type: 'test' ... # Subtest: the input array is not mutated ok 10 - the input array is not mutated --- duration_ms: 0.09557 type: 'test' ... 1..10 # tests 10 # suites 0 # pass 10 # fail 0 # cancelled 0 # skipped 0 # todo 0 # duration_ms 30.986892
Reasoning (chain-of-thought)
The task is to implement a function that merges overlapping intervals. Let me break down the requirements:
1. Input: array of [start, end] pairs of numbers
2. Output: new array of merged intervals sorted by start
3. Intervals that touch (end of one equals start of another) should be merged
4. Merged interval's end is the maximum end of all overlapping intervals
5. Don't mutate the input array
6. Empty input returns []
The algorithm:
1. If input is empty, return []
2. Sort intervals by start time
3. Iterate through sorted intervals and merge overlapping ones
4. Two intervals overlap if the current interval's start <= previous interval's end (considering they touch)
5. When merging, update the end to be the maximum of both ends
Let me write the solution:
```js
export function mergeIntervals(intervals) {
if (!intervals || intervals.length === 0) {
return [];
}
// Create a copy to avoid mutating input
const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
const result = [];
let currentStart = sorted[0][0];
let currentEnd = sorted[0][1];
for (let i = 1; i < sorted.length; i++) {
const [start, end] = sorted[i];
// Check if current interval overlaps with or touches the previous one
if (start <= currentEnd) {
// Merge by extending the end if needed
currentEnd = Math.max(currentEnd, end);
} else {
// No overlap, push the current merged interval and start a new one
result.push([currentStart, currentEnd]);
currentStart = start;
currentEnd = end;
}
}
// Push the last interval
result.push([currentStart, currentEnd]);
return result;
}
```
I'm verifying the logic with a few test cases. The algorithm handles different scenarios of interval merging correctly. When intervals touch or overlap, they get combined into a single interval. The sorting ensures we process intervals in order, and the merging logic captures the maximum end point.
The implementation covers edge cases like empty arrays, single intervals, and multiple overlapping intervals. It creates a new array without modifying the original input.