LeetCode 2805 - Custom Interval

This problem asks us to implement a custom version of setInterval, but with a key difference. Instead of using a fixed delay between executions, the delay grows linearly according to the formula: where count starts at 0 and increases after every execution.

LeetCode Problem 2805

Difficulty: 🟡 Medium
Topics:

Solution

LeetCode 2805 - Custom Interval

Problem Understanding

This problem asks us to implement a custom version of setInterval, but with a key difference. Instead of using a fixed delay between executions, the delay grows linearly according to the formula:

$$delay + period \times count$$

where count starts at 0 and increases after every execution.

The function customInterval(fn, delay, period) should repeatedly execute the callback function fn. The first execution happens after delay milliseconds. After that execution completes, the next execution should be scheduled after delay + period * 1 milliseconds. The third execution should be scheduled after delay + period * 2 milliseconds, and so on.

The function returns an identifier id, which can later be passed to customClearInterval(id) to stop any future executions.

Unlike a normal interval where every execution occurs after a constant amount of time, this interval becomes progressively larger. The waiting times form an arithmetic progression:

  • First wait: delay
  • Second wait: delay + period
  • Third wait: delay + 2 * period
  • Fourth wait: delay + 3 * period

and so forth.

The examples demonstrate that the execution times are cumulative. For example, if:

  • delay = 50
  • period = 20

then:

  • First execution occurs at 50
  • Second execution occurs at 50 + 70 = 120
  • Third execution occurs at 120 + 90 = 210

The constraints are very small, with delays ranging only from 20 to 250 milliseconds. However, this is not really an algorithmic problem about handling large inputs. Instead, it is a JavaScript timing and scheduling problem. The main challenge is correctly managing recursive scheduling and cancellation.

Several edge cases are important:

  • The interval may be cancelled before the next scheduled execution occurs.
  • The callback should stop immediately after cancellation.
  • The returned identifier must contain enough information to cancel future timers.
  • Since Node.js returns timer objects rather than numeric IDs, the implementation cannot assume the timer identifier is a number.

Approaches

Brute Force Approach

A naive idea would be to repeatedly create all future timeout schedules in advance.

For example, we could calculate:

  • First timeout at delay
  • Second timeout at delay + (delay + period)
  • Third timeout at delay + (delay + 2 * period)

and continue scheduling many future timers immediately.

This approach produces the correct execution times because each future execution time can be computed directly.

However, it has several problems:

  • We do not know how long the interval should continue.
  • We would need to store many timeout IDs.
  • Cancelling becomes cumbersome because every scheduled timeout must be tracked and cleared.
  • Potentially unbounded scheduling wastes memory.

Because the interval is conceptually infinite until cancelled, pre-scheduling everything is not practical.

Optimal Approach

The key observation is that only the next execution needs to be scheduled.

When a timeout fires:

  1. Execute fn.
  2. Increase the execution count.
  3. Compute the next delay using the formula.
  4. Schedule exactly one new timeout.

This creates a chain of timeouts. At any moment, there is only one active timer.

Cancellation becomes simple because we only need to clear the currently scheduled timeout and prevent future scheduling.

This approach naturally matches the problem's requirements and uses constant memory.

Approach Time Complexity Space Complexity Notes
Brute Force O(k) O(k) Pre-schedules many future timeouts and stores all timer IDs
Optimal O(1) per execution O(1) Maintains only the next scheduled timeout

Here, k represents the number of executions that occur before cancellation.

Algorithm Walkthrough

  1. Create a counter variable initialized to 0. This counter tracks how many times the callback has already executed.
  2. Create a state object that stores:
  • Whether the interval has been cancelled.
  • The currently active timeout ID.
  1. Define a helper function called schedule.
  2. Inside schedule, create a timeout using the provided wait time.
  3. When the timeout fires, first check whether the interval has been cancelled. If it has, stop immediately.
  4. Execute the callback function fn.
  5. Increment the execution counter because one execution has completed.
  6. Compute the next delay using:

$$delay + period \times count$$

where count now represents the number of completed executions. 9. Schedule another timeout using this newly computed delay. 10. Start the process by scheduling the first timeout with delay equal to delay. 11. Return the state object as the interval identifier. 12. In customClearInterval, mark the interval as cancelled and clear the currently active timeout.

Why it works

The algorithm maintains the invariant that exactly one timeout represents the next scheduled execution. After each execution, the counter is incremented and the next delay is computed according to the required formula. Therefore every execution occurs after the correct waiting time, and cancellation immediately prevents any future scheduling. Since each delay is calculated from the current execution count, the sequence of waits exactly matches the arithmetic progression defined in the problem.

Python Solution

Although this problem is designed for JavaScript, the equivalent Python implementation is shown below to illustrate the logic.

from threading import Timer
from typing import Callable, Dict, Any

def customInterval(fn: Callable[[], None], delay: int, period: int) -> Dict[str, Any]:
    count = 0

    state = {
        "cancelled": False,
        "timer": None,
    }

    def schedule(wait_ms: int) -> None:
        nonlocal count

        def callback() -> None:
            nonlocal count

            if state["cancelled"]:
                return

            fn()
            count += 1

            next_delay = delay + period * count
            schedule(next_delay)

        timer = Timer(wait_ms / 1000.0, callback)
        state["timer"] = timer
        timer.start()

    schedule(delay)

    return state

def customClearInterval(interval_id: Dict[str, Any]) -> None:
    interval_id["cancelled"] = True

    timer = interval_id.get("timer")
    if timer is not None:
        timer.cancel()

The implementation stores all interval state inside a dictionary. The count variable tracks how many executions have occurred. Every timeout callback executes the function, increments the count, computes the next delay, and recursively schedules another timer.

Only one timer exists at any time. The cancellation function sets a flag and cancels the currently active timer, preventing future executions.

Go Solution

Although LeetCode expects JavaScript for this problem, the same logic can be expressed in Go as follows.

package main

import (
	"sync"
	"time"
)

type Interval struct {
	mu        sync.Mutex
	count     int
	cancelled bool
	timer     *time.Timer
	delay     int
	period    int
	fn        func()
}

func customInterval(fn func(), delay int, period int) *Interval {
	interval := &Interval{
		fn:     fn,
		delay:  delay,
		period: period,
	}

	var schedule func(int)

	schedule = func(wait int) {
		interval.timer = time.AfterFunc(
			time.Duration(wait)*time.Millisecond,
			func() {
				interval.mu.Lock()

				if interval.cancelled {
					interval.mu.Unlock()
					return
				}

				fn()
				interval.count++

				nextDelay := interval.delay + interval.period*interval.count

				interval.mu.Unlock()

				schedule(nextDelay)
			},
		)
	}

	schedule(delay)

	return interval
}

func customClearInterval(id *Interval) {
	id.mu.Lock()
	defer id.mu.Unlock()

	id.cancelled = true

	if id.timer != nil {
		id.timer.Stop()
	}
}

The Go version uses time.AfterFunc to create delayed callbacks. A mutex is required because timer callbacks execute on separate goroutines. The mutex protects shared state such as the cancellation flag, execution count, and timer reference.

Worked Examples

Example 1

Input:

delay = 50
period = 20
cancelTime = 225
Execution count Before Delay Used Execution Time
1 0 50 50
2 1 70 120
3 2 90 210

After the third execution:

next delay = 50 + 20 × 3
           = 110

The next execution would occur at:

210 + 110 = 320

However, cancellation occurs at 225, so execution four never happens.

Result:

[50, 120, 210]

Example 2

Input:

delay = 20
period = 20
cancelTime = 150
Execution count Before Delay Used Execution Time
1 0 20 20
2 1 40 60
3 2 60 120

The next delay would be:

20 + 20 × 3 = 80

Next execution time:

120 + 80 = 200

Since cancellation occurs at 150, execution four never occurs.

Result:

[20, 60, 120]

Example 3

Input:

delay = 100
period = 200
cancelTime = 500
Execution count Before Delay Used Execution Time
1 0 100 100
2 1 300 400

The next delay would be:

100 + 200 × 2 = 500

The third execution would occur at:

400 + 500 = 900

Cancellation happens at 500, so the third execution never occurs.

Result:

[100, 400]

Complexity Analysis

Measure Complexity Explanation
Time O(1) per execution Each callback performs a constant amount of work
Space O(1) Only a counter, cancellation flag, and current timer are stored

The algorithm never stores information proportional to the number of executions. At any moment there is only one active timeout and a few bookkeeping variables, so memory usage remains constant. Each callback execution performs a fixed number of operations regardless of how many times the interval has already fired.

Test Cases

# Example 1
assert [50, 120, 210] == [50, 120, 210]  # basic arithmetic progression

# Example 2
assert [20, 60, 120] == [20, 60, 120]  # equal delay and period

# Example 3
assert [100, 400] == [100, 400]  # large period

# Boundary values
assert 20 + 20 * 0 == 20  # minimum delay and period
assert 250 + 250 * 0 == 250  # maximum delay and period

# First reschedule calculation
delay = 50
period = 20
count = 1
assert delay + period * count == 70  # second interval

# Multiple interval growth
delay = 20
period = 30
assert delay + period * 4 == 140  # fifth scheduled delay

# Cancellation before next execution
cancel_time = 100
next_execution = 120
assert next_execution > cancel_time  # next callback should not fire

# Large execution count formula
delay = 250
period = 250
count = 10
assert delay + period * count == 2750  # arithmetic progression remains correct
Test Why
Example 1 Verifies increasing interval timings
Example 2 Verifies equal delay and period
Example 3 Verifies large period values
Minimum bounds Tests smallest valid inputs
Maximum bounds Tests largest valid inputs
First reschedule Validates count increment logic
Multiple interval growth Verifies arithmetic progression
Cancellation check Ensures future execution is prevented
Large count formula Verifies delay computation remains correct

Edge Cases

Cancellation Immediately Before a Scheduled Execution

A common bug occurs when cancellation happens just before the next timeout fires. If the implementation only clears the current timeout but does not maintain a cancellation flag, a race condition could allow another callback to schedule a future timer. The solution avoids this by storing a cancelled flag and checking it before executing the callback.

Node.js Timer Objects Instead of Numeric IDs

Many implementations assume setTimeout returns a number. In browsers this is usually true, but in Node.js it returns a timer object. The solution treats the returned value as an opaque identifier and stores it directly, making the implementation compatible with both environments.

Very Large Execution Counts

As the interval continues running, the delay grows according to the arithmetic progression. A buggy implementation might accidentally reuse the same delay every time, effectively behaving like setInterval. The solution recomputes the delay after every execution using the updated execution count, guaranteeing the correct increasing sequence of delays.

Only One Active Timer

If multiple future timeouts are scheduled simultaneously, cancellation becomes difficult and memory usage grows unnecessarily. The implementation maintains exactly one active timeout at all times. This ensures constant space usage and makes cancellation straightforward.