← run

js-02-merge-intervals

1.000
10/10 tests· algorithms
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
```javascript
export function mergeIntervals(intervals) {
    if (intervals.length === 0) {
        return [];
    }

    // Create a shallow copy of the input array to avoid mutating it,
    // then sort by the start time of each interval.
    const sortedIntervals = [...intervals].sort((a, b) => a[0] - b[0]);

    const merged = [];
    
    // Initialize with the first interval. We create a new array here to ensure
    // we don't mutate the original interval objects from the input.
    merged.push([sortedIntervals[0][0], sortedIntervals[0][1]]);

    for (let i = 1; i < sortedIntervals.length; i++) {
        const current = sortedIntervals[i];
        const last = merged[merged.length - 1];

        // Check if the current interval overlaps or touches the last merged interval.
        // Touching is defined as current.start <= last.end.
        if (current[0] <= last[1]) {
            // Merge by extending the end of the last interval if necessary.
            last[1] = Math.max(last[1], current[1]);
        } else {
            // No overlap, add the current interval as a new merged interval.
            merged.push([current[0], current[1]]);
        }
    }

    return merged;
}
```

Test output (stdout)

TAP version 13
# Subtest: empty input returns empty array
ok 1 - empty input returns empty array
  ---
  duration_ms: 0.611114
  type: 'test'
  ...
# Subtest: single interval is returned unchanged
ok 2 - single interval is returned unchanged
  ---
  duration_ms: 0.103061
  type: 'test'
  ...
# Subtest: unsorted disjoint input is sorted
ok 3 - unsorted disjoint input is sorted
  ---
  duration_ms: 0.068151
  type: 'test'
  ...
# Subtest: overlapping intervals merge
ok 4 - overlapping intervals merge
  ---
  duration_ms: 0.526714
  type: 'test'
  ...
# Subtest: touching endpoints merge
ok 5 - touching endpoints merge
  ---
  duration_ms: 0.07188
  type: 'test'
  ...
# Subtest: containment keeps the wider interval
ok 6 - containment keeps the wider interval
  ---
  duration_ms: 0.049831
  type: 'test'
  ...
# Subtest: disjoint intervals are left separate
ok 7 - disjoint intervals are left separate
  ---
  duration_ms: 0.04356
  type: 'test'
  ...
# Subtest: duplicates collapse to one
ok 8 - duplicates collapse to one
  ---
  duration_ms: 0.045541
  type: 'test'
  ...
# Subtest: mixed unsorted overlapping and disjoint
ok 9 - mixed unsorted overlapping and disjoint
  ---
  duration_ms: 0.06004
  type: 'test'
  ...
# Subtest: the input array is not mutated
ok 10 - the input array is not mutated
  ---
  duration_ms: 0.097601
  type: 'test'
  ...
1..10
# tests 10
# suites 0
# pass 10
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 31.125573
Reasoning (chain-of-thought)
The user wants an ES module `solution.js` that exports a function `mergeIntervals(intervals)`.

Requirements:
1. Input: Array of `[start, end]` pairs.
2. Output: Array of merged intervals, sorted by start.
3. Logic:
   - Overlapping intervals merge.
   - Touching intervals merge (e.g., `[1, 3]` and `[3, 5]` -> `[1, 5]`).
   - Containment handled (max end).
   - Input order arbitrary.
   - Duplicates allowed.
   - Do not mutate input.
   - Empty input -> `[]`.

Algorithm:
1. Handle empty input: return `[]`.
2. Create a copy of the input array to avoid mutation.
3. Sort the copied array based on the start time of intervals. If start times are equal, sorting by end time is optional but good for consistency.
4. Initialize a result array with the first interval from the sorted list.
5. Iterate through the rest of the sorted intervals:
   - Let `current` be the interval being processed.
   - Let `last` be the last interval added to the result array.
   - Check if `current` overlaps or touches `last`.
     - Overlap condition: `current.start <= last.end`.
   - If they overlap:
     - Merge them by updating `last.end` to `Math.max(last.end, current.end)`.
   - If they do not overlap:
     - Push `current` to the result array.
6. Return the result array.

Let's double check the "touching" rule.
`[1, 3]` and `[3, 5]`.
Sorted: `[1, 3], [3, 5]`.
Last: `[1, 3]`. Current: `[3, 5]`.
`current.start` (3) <= `last.end` (3). True.
Merge: `last.end` = max(3, 5) = 5.
Result: `[1, 5]`. Correct.

Example: `[1, 4], [2, 5]`
Sorted: `[1, 4], [2, 5]`
Last: `[1, 4]`. Current: `[2, 5]`.
2 <= 4. Merge. End = max(4, 5) = 5.
Result: `[1, 5]`. Correct.

Example: `[3, 5], [1, 2]`
Sorted: `[1, 2], [3, 5]`
Last: `[1, 2]`. Current: `[3, 5]`.
3 <= 2. False.
Push `[3, 5]`.
Result: `[[1, 2], [3, 5]]`. Correct.

Example: `[1, 10], [2, 3], [4, 8]`
Sorted: `[1, 10], [2, 3], [4, 8]`
Last: `[1, 10]`. Current: `[2, 3]`.
2 <= 10. Merge. End = max(10, 3) = 10.
Last: `[1, 10]`. Current: `[4, 8]`.
4 <= 10. Merge. End = max(10, 8) = 10.
Result: `[[1, 10]]`. Correct.

Implementation details:
- Use `slice()` or spread `[...intervals]` to copy.
- Sort comparator: `(a, b) => a[0] - b[0]`.
- Loop.

Code structure:
```javascript
export function mergeIntervals(intervals) {
    if (!intervals || intervals.length === 0) {
        return [];
    }

    // Create a shallow copy to avoid mutating the input
    const sortedIntervals = [...intervals].sort((a, b) => a[0] - b[0]);

    const merged = [sortedIntervals[0]];

    for (let i = 1; i < sortedIntervals.length; i++) {
        const current = sortedIntervals[i];
        const last = merged[merged.length - 1];

        // Check for overlap or touching
        if (current[0] <= last[1]) {
            // Merge intervals by updating the end of the last interval
            last[1] = Math.max(last[1], current[1]);
        } else {
            // No overlap, add current interval to the result
            merged.push(current);
        }
    }

    return merged;
}
```

Wait, strictly speaking, modifying `last[1]` modifies the array object inside `merged`. Since `merged` contains references to the interval arrays from `sortedIntervals` (which are references to original intervals), does this count as mutating input?
The requirement says "Do not mutate the input array".
Usually, this means don't change the order of elements in the input array or remove/add elements to it.
However, if I modify the content of the sub-arrays (the intervals themselves), I am mutating the objects passed in.
To be safe and purely functional regarding the input data structure:
1. Sort a copy of the array.
2. When merging, create new arrays for the merged intervals.

Let's refine to ensure no mutation of input objects either.

Refined Algorithm:
1. Copy input: `const sorted = [...intervals].sort(...)`.
2. `merged` array.
3. Push `[sorted[0][0], sorted[0][1]]` (new array).
4. Loop:
   - `current` = `sorted[i]`
   - `last` = `merged[merged.length - 1]`
   - If overlap: `last[1] = Math.max(last[1], current[1])`. (Modifying `last` is fine because `last` is a new array created in step 3 or previous iterations, not from input).
   - Else: `merged.push([current[0], current[1]])`.

Wait, `sorted` contains references to original intervals.
If I do `merged.push(sorted[0])`, `merged` holds reference to original.
If I then do `last[1] = ...`, I mutate original.
So I must ensure `merged` holds new arrays.

Correct approach:
```javascript
export function mergeIntervals(intervals) {
    if (!intervals || intervals.length === 0) {
        return [];
    }

    // Sort a copy of the input array based on start times
    const sorted = [...intervals].sort((a, b) => a[0] - b[0]);

    const merged = [];
    
    // Initialize with the first interval (create a new array to avoid mutation)
    merged.push([sorted[0][0], sorted[0][1]]);

    for (let i = 1; i < sorted.length; i++) {
        const current = sorted[i];
        const last = merged[merged.length - 1];

        // If current interval overlaps or touches the last merged interval
        if (current[0] <= last[1]) {
            // Update the end of the last interval
            last[1] = Math.max(last[1], current[1]);
        } else {
            // No overlap, add current interval as a new entry (new array)
            merged.push([current[0], current[1]]);
        }
    }

    return merged;
}
```
This ensures `intervals` array order is not changed (due to spread) and interval objects inside are not mutated (due to creating new arrays `[...]` when pushing to `merged`).

One edge case: `intervals` might be null/undefined? The prompt implies it's an array, but checking length handles empty. The type signature implies array.

Let's verify the constraints again.
"intervals is an array of [start, end] pairs".
"start <= end".
"sorted ascending by start".

The logic holds.

One small optimization/cleanup:
The sort comparator `a[0] - b[0]` is standard.
The check `current[0] <= last[1]` handles touching (`3 <= 3`) and overlapping (`2 <= 4`).

Final check on requirements:
- ES module: Yes (`export function`).
- Filename: `solution.js`.
- Fun
…[16612 chars elided]…
esult `[1, 1]`. Correct.

Edge case: `[1, 2], [2, 3], [3, 4]`.
Sorted: same.
Merged: `[1, 2]`.
Current `[2, 3]`. 2 <= 2. Merge -> `[1, 3]`.
Current `[3, 4]`. 3 <= 3. Merge -> `[1, 4]`.
Result `[1, 4]`. Correct.

Edge case: `[1, 5], [2, 3]`.
Sorted: `[1, 5], [2, 3]`.
Merged: `[1, 5]`.
Current `[2, 3]`. 2 <= 5. Merge -> `[1, 5]`.
Result `[1, 5]`. Correct.

Edge case: `[2, 3], [1, 5]`.
Sorted: `[1, 5], [2, 3]`.
Same as above. Correct.

I'm confident.

```javascript
export function mergeIntervals(intervals) {
    if (intervals.length === 0) {
        return [];
    }

    // Sort a copy of the intervals by start time
    const sorted = [...intervals].sort((a, b) => a[0] - b[0]);

    const merged = [];
    // Initialize with the first interval (create new array to avoid mutation)
    merged.push([sorted[0][0], sorted[0][1]]);

    for (let i = 1; i < sorted.length; i++) {
        const current = sorted[i];
        const last = merged[merged.length - 1];

        // Check if current interval overlaps or touches the last merged interval
        if (current[0] <= last[1]) {
            // Merge intervals by updating the end of the last interval
            last[1] = Math.max(last[1], current[1]);
        } else {
            // No overlap, add current interval as a new entry
            merged.push([current[0], current[1]]);
        }
    }

    return merged;
}
```
I'll use this version. It's clean.

One check: "Do not mutate the input array".
If I use `intervals.sort()`, I mutate.
I used `[...intervals].sort()`.
If I modify `sorted[i]` (which is a reference to input element), I mutate input element.
I read from `sorted[i]` but never write to it.
I write to `last` which is an element of `merged`.
`merged` elements are created via `[...]` or `push([...])`.
So `merged` elements are new arrays.
So input elements are never written to.
Input array order is never changed.
Perfect.

One detail: `intervals` might contain non-array items? "intervals is an array of [start, end] pairs". So no.

I will formulate the response now.