Difficulty: Easy | Acceptance: 84.90% | Paid: No Topics: N/A
Given a function fn, an array of arguments args, an interval in milliseconds t, and an initial delay delay, return a cancel function cancelfn.
The function fn should be called every t milliseconds, starting after delay milliseconds.
The returned function cancelfn should cancel the interval.
- Examples
- Constraints
- Approach 1: Using setTimeout and setInterval
- Approach 2: Using Recursive setTimeout
Examples
Example 1:
Input: fn = (x) => console.log(x), args = [42], t = 100, delay = 1000
Output: [{"time": 1000, "returned": 42}]
Explanation:
const cancelfn = cancellable((x) => console.log(x), [42], 100, 1000);
setTimeout(() => cancelfn(), 1200);
The cancellation was scheduled to run after 1200ms, but fn was already called at 1000ms.
Example 2:
Input: fn = (x) => console.log(x), args = [42], t = 100, delay = 1000
Output: []
Explanation:
const cancelfn = cancellable((x) => console.log(x), [42], 100, 1000);
setTimeout(() => cancelfn(), 200);
The cancellation was scheduled to run after 200ms, so fn was never called.
Example 3:
Input: fn = (x) => console.log(x), args = [42], t = 100, delay = 1000
Output: [{"time": 1000, "returned": 42}, {"time": 1100, "returned": 42}]
Explanation:
const cancelfn = cancellable((x) => console.log(x), [42], 100, 1000);
setTimeout(() => cancelfn(), 1500);
The cancellation was scheduled to run after 1500ms, so fn was called at 1000ms and 1100ms.
Constraints
- fn is a function
- args is a valid JSON array
- 1 <= args.length <= 10
- 30 <= t <= 100
- 10 <= cancelTimeMs <= 500
Approach 1: Using setTimeout and setInterval
Intuition
Use setTimeout to handle the initial delay and setInterval to handle the recurring execution. The cancel function must clear both timers to handle cancellation at any stage.
Steps
- Use
setTimeoutto wait for thedelayperiod. - Inside the timeout callback, execute
fnimmediately and then start asetIntervalto runfneverytmilliseconds. - Store the IDs of both the timeout and the interval.
- The returned
cancelfnclears both the timeout (if it hasn’t fired) and the interval (if it has started).
import threading
def cancellable(fn, args, t, delay):
timer = None
interval = None
def start_interval():
nonlocal interval
fn(*args)
interval = threading.Timer(t / 1000.0, start_interval)
interval.start()
timer = threading.Timer(delay / 1000.0, start_interval)
timer.start()
def cancel():
if timer:
timer.cancel()
if interval:
interval.cancel()
return cancel
Complexity
- Time: O(1) for setup, O(k) for execution where k is the number of times fn is called.
- Space: O(1)
- Notes: Requires managing two timer IDs.
setIntervalcan drift iffntakes longer thantto execute.
Approach 2: Using Recursive setTimeout
Intuition
Use a single setTimeout loop. After the initial delay, execute fn and then schedule the next execution using setTimeout again. This avoids managing two separate timer IDs and handles the initial delay naturally within the loop logic.
Steps
- Define a recursive function that executes
fnand then schedules itself withsetTimeout. - Use a variable to store the current timer ID.
- Initialize the loop with the initial
delay. - The returned
cancelfnsimply clears the current timer ID, stopping the recursion.
import threading
def cancellable(fn, args, t, delay):
timer = None
def run():
nonlocal timer
fn(*args)
timer = threading.Timer(t / 1000.0, run)
timer.start()
timer = threading.Timer(delay / 1000.0, run)
timer.start()
def cancel():
if timer:
timer.cancel()
return cancel
Complexity
- Time: O(1) for setup, O(k) for execution.
- Space: O(1)
- Notes: Only one timer ID needs to be managed. This approach ensures that
fnis not called again untiltmilliseconds after the previous call completes, preventing overlap iffnis slow.