Facebook Pixel

2777. Date Range Generator 🔒

Problem Description

This problem asks you to create a generator function that produces a sequence of dates within a given range.

You're given three parameters:

  • start: A string representing the starting date in format "YYYY-MM-DD"
  • end: A string representing the ending date in format "YYYY-MM-DD"
  • step: A positive integer indicating how many days to skip between each generated date

The generator should yield dates starting from start and continuing up to and including end, with each successive date being step days after the previous one.

For example, if you have:

  • Start date: "2023-04-01"
  • End date: "2023-04-04"
  • Step: 1

The generator would yield: "2023-04-01", "2023-04-02", "2023-04-03", "2023-04-04"

If the step were 2 instead, it would yield: "2023-04-01", "2023-04-03"

The key requirements are:

  1. Use a generator function (function* syntax in TypeScript/JavaScript)
  2. All yielded dates must be strings in "YYYY-MM-DD" format
  3. Include both the start and end dates in the range (if the end date is reachable with the given step)
  4. The generator stops when the next date would exceed the end date
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The core idea is to treat dates as objects that we can increment by a certain number of days, then convert them back to the required string format for output.

Since we need to produce values one at a time (lazily) rather than all at once, a generator function is the natural choice. Generators allow us to pause execution after each yield and resume when the next value is requested.

The approach follows these logical steps:

  1. Convert strings to Date objects: JavaScript's Date objects have built-in methods for date arithmetic, making it easy to add days. We convert both start and end strings to Date objects for easier manipulation.

  2. Use a loop with a moving pointer: We maintain a currentDate variable that starts at the beginning and moves forward by step days each iteration. This is similar to iterating through an array with i += step instead of i++.

  3. Yield formatted dates: At each valid position (where currentDate <= endDate), we convert the current Date object back to the "YYYY-MM-DD" format and yield it. The toISOString() method gives us a format like "2023-04-01T00:00:00.000Z", and we can slice the first 10 characters to get just the date portion.

  4. Increment and check bounds: After yielding, we add step days to the current date using setDate(currentDate.getDate() + step). The loop continues as long as we haven't exceeded the end date.

This approach leverages JavaScript's date handling capabilities while using a generator to provide memory-efficient, on-demand date generation.

Solution Approach

The implementation uses a generator function to create an iterator that yields dates on demand. Let's walk through the solution step by step:

1. Function Signature

function* dateRangeGenerator(start: string, end: string, step: number): Generator<string>

The function* syntax defines a generator function that returns a Generator<string> object.

2. Date Initialization

const startDate = new Date(start);
const endDate = new Date(end);
let currentDate = startDate;
  • Convert the input strings to Date objects for easier manipulation
  • Initialize currentDate as the starting point for iteration

3. Main Loop Logic

while (currentDate <= endDate) {
    yield currentDate.toISOString().slice(0, 10);
    currentDate.setDate(currentDate.getDate() + step);
}

The loop continues as long as currentDate hasn't exceeded endDate. In each iteration:

  • Yield the formatted date: currentDate.toISOString() produces a string like "2023-04-01T00:00:00.000Z". We use .slice(0, 10) to extract just the date portion "2023-04-01".

  • Increment the date: currentDate.getDate() returns the day of the month (1-31). Adding step to it and passing to setDate() moves the date forward. JavaScript's Date object automatically handles month and year rollovers (e.g., adding 5 days to March 29 correctly gives April 3).

Key Implementation Details:

  • The comparison currentDate <= endDate works because JavaScript Date objects can be compared directly using relational operators
  • The generator automatically stops when the loop ends (no more values to yield)
  • Each call to .next() on the generator resumes execution from where it last yielded
  • The generator maintains its state between calls, remembering the current position in the date range

Time Complexity: O(n) where n is the number of dates yielded ((end - start) / step + 1)
Space Complexity: O(1) as the generator only maintains a single current date in memory at any time

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example with:

  • start = "2023-04-10"
  • end = "2023-04-15"
  • step = 2

Step 1: Initialize the dates

startDate = new Date("2023-04-10")  // April 10, 2023
endDate = new Date("2023-04-15")    // April 15, 2023
currentDate = startDate              // April 10, 2023

Step 2: First iteration

  • Check: currentDate (Apr 10) <= endDate (Apr 15)? ✓ Yes
  • Yield: "2023-04-10" (from toISOString().slice(0, 10))
  • Update: currentDate.setDate(10 + 2) → currentDate becomes April 12, 2023

Step 3: Second iteration

  • Check: currentDate (Apr 12) <= endDate (Apr 15)? ✓ Yes
  • Yield: "2023-04-12"
  • Update: currentDate.setDate(12 + 2) → currentDate becomes April 14, 2023

Step 4: Third iteration

  • Check: currentDate (Apr 14) <= endDate (Apr 15)? ✓ Yes
  • Yield: "2023-04-14"
  • Update: currentDate.setDate(14 + 2) → currentDate becomes April 16, 2023

Step 5: Fourth iteration

  • Check: currentDate (Apr 16) <= endDate (Apr 15)? ✗ No
  • Loop ends, generator completes

Final output sequence: "2023-04-10", "2023-04-12", "2023-04-14"

Notice how April 15 (the end date) is not included because our step of 2 days from April 14 would take us to April 16, which exceeds the end date. The generator correctly stops before yielding any dates beyond the specified range.

Solution Implementation

1def date_range_generator(start, end, step):
2    """
3    Generator function that yields dates in a range with a specified step
4  
5    Args:
6        start (str): The start date in YYYY-MM-DD format
7        end (str): The end date in YYYY-MM-DD format
8        step (int): The number of days to increment for each iteration
9  
10    Yields:
11        str: Dates in YYYY-MM-DD format within the specified range
12    """
13    from datetime import datetime, timedelta
14  
15    # Parse the start and end date strings into datetime objects
16    start_date = datetime.strptime(start, '%Y-%m-%d')
17    end_date = datetime.strptime(end, '%Y-%m-%d')
18  
19    # Initialize current date to start from the beginning of the range
20    current_date = start_date
21  
22    # Iterate through dates until we exceed the end date
23    while current_date <= end_date:
24        # Convert current date to YYYY-MM-DD string format
25        yield current_date.strftime('%Y-%m-%d')
26      
27        # Increment current date by the specified step (in days)
28        current_date += timedelta(days=step)
29
30
31# Example usage:
32# g = date_range_generator('2023-04-01', '2023-04-04', 1)
33# next(g)  # '2023-04-01'
34# next(g)  # '2023-04-02'
35# next(g)  # '2023-04-03'
36# next(g)  # '2023-04-04'
37# next(g)  # StopIteration exception
38
1import java.time.LocalDate;
2import java.time.format.DateTimeFormatter;
3import java.util.Iterator;
4import java.util.NoSuchElementException;
5
6/**
7 * Generator class that yields dates in a range with a specified step
8 */
9public class DateRangeGenerator implements Iterator<String>, Iterable<String> {
10    private LocalDate currentDate;
11    private final LocalDate endDate;
12    private final int step;
13    private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE;
14  
15    /**
16     * Constructor for DateRangeGenerator
17     * @param start - The start date in YYYY-MM-DD format
18     * @param end - The end date in YYYY-MM-DD format  
19     * @param step - The number of days to increment for each iteration
20     */
21    public DateRangeGenerator(String start, String end, int step) {
22        // Parse the start and end date strings into LocalDate objects
23        this.currentDate = LocalDate.parse(start, formatter);
24        this.endDate = LocalDate.parse(end, formatter);
25        this.step = step;
26    }
27  
28    /**
29     * Check if there are more dates in the range
30     * @return true if there are more dates, false otherwise
31     */
32    @Override
33    public boolean hasNext() {
34        // Check if current date is within the range (inclusive)
35        return !currentDate.isAfter(endDate);
36    }
37  
38    /**
39     * Get the next date in the range
40     * @return The next date in YYYY-MM-DD format
41     * @throws NoSuchElementException if no more dates are available
42     */
43    @Override
44    public String next() {
45        // Throw exception if we've exceeded the end date
46        if (!hasNext()) {
47            throw new NoSuchElementException("No more dates in range");
48        }
49      
50        // Store current date value to return
51        String dateString = currentDate.format(formatter);
52      
53        // Increment current date by the specified step (in days)
54        currentDate = currentDate.plusDays(step);
55      
56        // Return the date string in YYYY-MM-DD format
57        return dateString;
58    }
59  
60    /**
61     * Returns an iterator over the date range
62     * @return Iterator for this date range generator
63     */
64    @Override
65    public Iterator<String> iterator() {
66        return this;
67    }
68  
69    /**
70     * Static factory method to create a new DateRangeGenerator
71     * @param start - The start date in YYYY-MM-DD format
72     * @param end - The end date in YYYY-MM-DD format
73     * @param step - The number of days to increment for each iteration
74     * @return A new DateRangeGenerator instance
75     */
76    public static DateRangeGenerator dateRangeGenerator(String start, String end, int step) {
77        return new DateRangeGenerator(start, end, step);
78    }
79  
80    /**
81     * Example usage:
82     * DateRangeGenerator g = DateRangeGenerator.dateRangeGenerator("2023-04-01", "2023-04-04", 1);
83     * g.next(); // "2023-04-01"
84     * g.next(); // "2023-04-02"
85     * g.next(); // "2023-04-03"
86     * g.next(); // "2023-04-04"
87     * g.hasNext(); // false
88     */
89}
90
1#include <string>
2#include <sstream>
3#include <iomanip>
4#include <ctime>
5#include <vector>
6#include <memory>
7
8class DateRangeGenerator {
9private:
10    std::tm start_date;
11    std::tm end_date;
12    std::tm current_date;
13    int step_days;
14    bool is_done;
15  
16    /**
17     * Parse a date string in YYYY-MM-DD format into a tm structure
18     * @param date_str - The date string to parse
19     * @return tm structure representing the date
20     */
21    std::tm parseDate(const std::string& date_str) {
22        std::tm tm = {};
23        std::istringstream ss(date_str);
24        ss >> std::get_time(&tm, "%Y-%m-%d");
25        // Set time to noon to avoid DST issues
26        tm.tm_hour = 12;
27        return tm;
28    }
29  
30    /**
31     * Format a tm structure into YYYY-MM-DD string format
32     * @param date - The tm structure to format
33     * @return String in YYYY-MM-DD format
34     */
35    std::string formatDate(const std::tm& date) {
36        std::ostringstream oss;
37        oss << std::put_time(&date, "%Y-%m-%d");
38        return oss.str();
39    }
40  
41    /**
42     * Compare two tm structures
43     * @param date1 - First date
44     * @param date2 - Second date
45     * @return true if date1 <= date2, false otherwise
46     */
47    bool compareDates(const std::tm& date1, const std::tm& date2) {
48        std::time_t time1 = std::mktime(const_cast<std::tm*>(&date1));
49        std::time_t time2 = std::mktime(const_cast<std::tm*>(&date2));
50        return time1 <= time2;
51    }
52  
53    /**
54     * Add days to a tm structure
55     * @param date - The date to modify
56     * @param days - Number of days to add
57     */
58    void addDays(std::tm& date, int days) {
59        std::time_t time = std::mktime(&date);
60        time += days * 24 * 60 * 60; // Convert days to seconds
61        date = *std::localtime(&time);
62    }
63
64public:
65    /**
66     * Constructor for DateRangeGenerator
67     * @param start - The start date in YYYY-MM-DD format
68     * @param end - The end date in YYYY-MM-DD format  
69     * @param step - The number of days to increment for each iteration
70     */
71    DateRangeGenerator(const std::string& start, const std::string& end, int step) 
72        : step_days(step), is_done(false) {
73        // Parse the start and end date strings into tm structures
74        start_date = parseDate(start);
75        end_date = parseDate(end);
76      
77        // Initialize current date to start from the beginning of the range
78        current_date = start_date;
79      
80        // Check if range is already exceeded
81        if (!compareDates(current_date, end_date)) {
82            is_done = true;
83        }
84    }
85  
86    /**
87     * Check if generator has more values
88     * @return true if no more values, false otherwise
89     */
90    bool done() const {
91        return is_done;
92    }
93  
94    /**
95     * Get the next date in the range
96     * @return Next date in YYYY-MM-DD format, or empty string if done
97     */
98    std::string next() {
99        // Check if we've exceeded the range
100        if (is_done) {
101            return "";
102        }
103      
104        // Store current date value to return
105        std::string result = formatDate(current_date);
106      
107        // Increment current date by the specified step (in days)
108        addDays(current_date, step_days);
109      
110        // Check if we've now exceeded the end date
111        if (!compareDates(current_date, end_date)) {
112            is_done = true;
113        }
114      
115        return result;
116    }
117};
118
119/**
120 * Example usage:
121 * DateRangeGenerator g("2023-04-01", "2023-04-04", 1);
122 * g.next(); // "2023-04-01"
123 * g.next(); // "2023-04-02"
124 * g.next(); // "2023-04-03"
125 * g.next(); // "2023-04-04"
126 * g.done(); // true
127 */
128
1/**
2 * Generator function that yields dates in a range with a specified step
3 * @param start - The start date in YYYY-MM-DD format
4 * @param end - The end date in YYYY-MM-DD format
5 * @param step - The number of days to increment for each iteration
6 * @yields {string} Dates in YYYY-MM-DD format within the specified range
7 */
8function* dateRangeGenerator(start: string, end: string, step: number): Generator<string> {
9    // Parse the start and end date strings into Date objects
10    const startDate: Date = new Date(start);
11    const endDate: Date = new Date(end);
12  
13    // Initialize current date to start from the beginning of the range
14    const currentDate: Date = new Date(startDate);
15  
16    // Iterate through dates until we exceed the end date
17    while (currentDate <= endDate) {
18        // Convert current date to ISO string format and extract YYYY-MM-DD portion
19        yield currentDate.toISOString().slice(0, 10);
20      
21        // Increment current date by the specified step (in days)
22        currentDate.setDate(currentDate.getDate() + step);
23    }
24}
25
26/**
27 * Example usage:
28 * const g = dateRangeGenerator('2023-04-01', '2023-04-04', 1);
29 * g.next().value; // '2023-04-01'
30 * g.next().value; // '2023-04-02'
31 * g.next().value; // '2023-04-03'
32 * g.next().value; // '2023-04-04'
33 * g.next().done; // true
34 */
35

Time and Space Complexity

Time Complexity: O(n) where n is the number of dates in the range, calculated as ⌈(endDate - startDate) / step⌉ + 1.

Each iteration of the generator performs constant time operations:

  • yield statement with toISOString().slice(): O(1)
  • Date comparison currentDate <= endDate: O(1)
  • Date increment with setDate() and getDate(): O(1)

Since the generator yields exactly n values (one for each date in the range with the given step), the overall time complexity is O(n).

Space Complexity: O(1)

The generator uses constant extra space regardless of the date range size:

  • Three Date objects (startDate, endDate, currentDate): O(1)
  • No additional data structures that grow with input size
  • The generator yields values one at a time rather than storing all dates in memory

The space complexity remains constant because generators are lazy and don't pre-compute or store all values. Each value is computed on-demand when next() is called.

Common Pitfalls

1. Date Object Mutation in JavaScript/TypeScript

In the JavaScript/TypeScript implementation, a critical pitfall is that Date objects are mutable, and the line currentDate.setDate(currentDate.getDate() + step) modifies the original object. If someone tries to store or reference the yielded dates, they might encounter unexpected behavior:

// Problematic approach - storing references
function* dateRangeGenerator(start, end, step) {
    const startDate = new Date(start);
    const endDate = new Date(end);
    let currentDate = startDate;  // This creates a reference, not a copy!
  
    while (currentDate <= endDate) {
        yield currentDate;  // Yielding the object directly
        currentDate.setDate(currentDate.getDate() + step);
    }
}

// This would cause all collected dates to show the same final value
const dates = [...dateRangeGenerator('2023-04-01', '2023-04-04', 1)];
// All elements in dates array would reference the same mutated object

Solution: Always create a new Date object or yield the string representation immediately:

function* dateRangeGenerator(start, end, step) {
    const startDate = new Date(start);
    const endDate = new Date(end);
    let currentDate = new Date(startDate);  // Create a copy
  
    while (currentDate <= endDate) {
        yield currentDate.toISOString().slice(0, 10);  // Yield string immediately
        currentDate.setDate(currentDate.getDate() + step);
    }
}

2. Timezone and DST Issues

Both implementations can face issues with timezone handling and Daylight Saving Time (DST) transitions:

# Problem: Using local timezone can cause unexpected date skips during DST
from datetime import datetime, timedelta

start_date = datetime.strptime('2023-03-11', '%Y-%m-%d')  # DST transition date in US
# Adding days might not work as expected around DST boundaries

Solution: Use date-only operations or UTC to avoid timezone complications:

from datetime import date, timedelta

def date_range_generator(start, end, step):
    # Use date objects instead of datetime to avoid time components
    start_date = date.fromisoformat(start)
    end_date = date.fromisoformat(end)
    current_date = start_date
  
    while current_date <= end_date:
        yield current_date.isoformat()  # Returns YYYY-MM-DD format
        current_date += timedelta(days=step)

3. Large Step Values Causing Month/Year Overflow

When using large step values, the JavaScript setDate() method handles overflow automatically, but this behavior might not be immediately obvious:

// If currentDate is January 30 and step is 5
currentDate.setDate(30 + 5);  // Becomes February 4 (or 5 depending on leap year)

While this is generally correct behavior, developers might not expect automatic month/year rollovers.

Solution: Document this behavior clearly or use a more explicit date arithmetic library:

// Using explicit date arithmetic for clarity
function* dateRangeGenerator(start, end, step) {
    let currentDate = new Date(start);
    const endDate = new Date(end);
  
    while (currentDate <= endDate) {
        yield currentDate.toISOString().slice(0, 10);
        // More explicit: create new date with added milliseconds
        currentDate = new Date(currentDate.getTime() + step * 24 * 60 * 60 * 1000);
    }
}

4. Invalid Date String Format

Both implementations assume the input strings are valid and in the correct format. Invalid inputs will cause runtime errors:

# This will raise ValueError
date_range_generator('2023/04/01', '2023-04-04', 1)  # Wrong date format
date_range_generator('2023-13-01', '2023-04-04', 1)  # Invalid month

Solution: Add input validation:

def date_range_generator(start, end, step):
    from datetime import datetime, timedelta
  
    try:
        start_date = datetime.strptime(start, '%Y-%m-%d')
        end_date = datetime.strptime(end, '%Y-%m-%d')
    except ValueError:
        raise ValueError(f"Invalid date format. Expected YYYY-MM-DD")
  
    if start_date > end_date:
        raise ValueError("Start date must be before or equal to end date")
  
    if step <= 0:
        raise ValueError("Step must be a positive integer")
  
    current_date = start_date
    while current_date <= end_date:
        yield current_date.strftime('%Y-%m-%d')
        current_date += timedelta(days=step)
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More