The Reason Behind It
Recently, I had a scenario where we had one large date range from which we needed to export email. We quickly noticed that the files became very large, and we also suspected that not everything was being exported correctly. That last part was difficult to verify because of the file size. So I created a function to split the date range into smaller segments.
Of course, this function is not only useful for email exports. When you build reports, analyse log data, or retrieve data from APIs and databases, you often run into the same problem: a large date range needs to be split into smaller pieces.
Many systems also have limits on the amount of data you can retrieve in a single query. And as I briefly mentioned earlier, smaller time windows are often better for performance, error handling, and parallelisation.
In this blog, we will look at the Get-DateSlices function, which allows you to easily split a date range into monthly or yearly segments.
The Problem
Suppose you want to retrieve data from 15 March 2024 through 20 August 2025.
Many scripts solve this with complex loops that continuously calculate the next month. That often works, but it quickly becomes harder when you need to account for:
- Different month lengths
- Leap years
- Start dates in the middle of a month
- End dates in the middle of a period
The Get-DateSlices function takes care of this logic for you.
The Function
Function Get-DateSlices {
param (
[DateOnly]$StartDate,
[DateOnly]$EndDate,
[ValidateSet("Month", "Year")]
[string]$Interval = "Month"
)
[System.Collections.Generic.List[PSCustomObject]]$slices = @()
$currentStart = $StartDate
$CurrentEnd = $null
while ($currentStart -lt $EndDate) {
$currentEnd = switch($Interval) {
"Month" {
switch ([boolean]$CurrentEnd) {
$true { $currentStart.AddMonths(1).AddDays(-1) }
$false {
$lastDayOfMonth = [DateTime]::DaysInMonth($currentStart.Year, $currentStart.Month)
$currentStart.AddDays($lastDayOfMonth - $currentStart.Day)
}
}
}
"Year" {
switch ([boolean]$CurrentEnd) {
$true { $currentStart.AddYears(1).AddDays(-1) }
$false { [DateOnly]::new($currentStart.Year, 12, 31) }
}
}
}
if ($currentEnd -gt $EndDate) {
$currentEnd = $EndDate
}
$slices.Add([PSCustomObject]@{
Start = $currentStart
End = $currentEnd
})
$currentStart = $currentEnd.AddDays(1)
}
return $slices
}
The function accepts three parameters:
Get-DateSlices -StartDate <DateOnly> -EndDate <DateOnly> -Interval Month
Or:
Get-DateSlices -StartDate <DateOnly> -EndDate <DateOnly> -Interval Year
The output consists of a list of objects, each containing a start and end date.
Start End
----- ---
2024-03-15 2024-03-31
2024-04-01 2024-04-30
2024-05-01 2024-05-31
...
And if we highlight one start or end value, we can see that it is a DateOnly object. This means we have access to more properties than just the date value itself. That can be useful when we have to work with machines where different date formats are being used.
Year : 2024
Month : 4
Day : 1
DayOfWeek : Monday
DayOfYear : 92
DayNumber : 738976
Why DateOnly?
Since .NET 6, we have had access to the DateOnly class.
Normally, DateTime is often used, but in this case I only needed dates. If you include the time component as well, you can run into unintended issues.
For example:
2025-01-01 00:00:00
versus
2025-01-01 13:45:12
Technically, these are different values, while for many reports only the calendar date is relevant.
By using DateOnly:
- You avoid time components
- You prevent timezone-related issues
- Your code becomes easier to read
- You make it clear that only the date matters
How Does the Logic Work?
The function starts at the specified start date.
$currentStart = $StartDate
A while loop then determines the end of the current slice each time.
while ($currentStart -lt $EndDate)
Depending on the selected interval, the function calculates where the current period ends.
Month Mode
During the first iteration, the function first checks what the last day of the current month is.
$lastDayOfMonth = [DateTime]::DaysInMonth(
$currentStart.Year,
$currentStart.Month
)
For a start date of:
15-03-2024
the first slice becomes:
15-03-2024 through 31-03-2024
After that, full months are generated:
01-04-2024 through 30-04-2024
01-05-2024 through 31-05-2024
...
Year Mode
The same principle is applied to years.
The first slice runs from the specified start date to the end of the calendar year:
15-03-2024 through 31-12-2024
After that, full years are created:
01-01-2025 through 31-12-2025
Correctly Trimming the Final Period
A common mistake with this type of function is that the final slice extends beyond the desired end date.
That is why the function contains this check:
if ($currentEnd -gt $EndDate) {
$currentEnd = $EndDate
}
This guarantees that the final period never falls outside the specified range.
Example:
Get-DateSlices -StartDate ([DateOnly]'2024-03-15') -EndDate ([DateOnly]'2024-08-20')
Result:
2024-03-15 - 2024-03-31
2024-04-01 - 2024-04-30
2024-05-01 - 2024-05-31
2024-06-01 - 2024-06-30
2024-07-01 - 2024-07-31
2024-08-01 - 2024-08-20
The final period stops exactly on 20 August.
Practical Use Cases
API Queries
Many APIs limit the number of records per request.
By querying per month or per year:
foreach ($slice in $slices) {
Invoke-RestMethod ...
}
you can avoid time-outs and limits.
SQL Reporting
For large datasets, it can be more efficient to run queries per period:
WHERE LogDate BETWEEN @StartDate AND @EndDate
This prevents heavy scans across multiple years of data.
Log Analysis
Think of:
- Windows Event Logs
- FSLogix logging
- Apache/Nginx logs
- Security auditing
By splitting time windows, processing remains manageable and scalable.
Parallel Processing
The generated slices can easily be processed in parallel.
$slices | ForEach-Object -Parallel {
# Process time segment
}
For large datasets, this can result in a significant performance improvement.
Designed with Reusability and Flexibility in Mind
When developing this function, a number of important design principles were central:
- Using DateOnly to avoid unnecessary complexity around times and timezones.
- Flexible interval selection, allowing date ranges to be easily split by month or by year.
- Accurate boundary handling, ensuring that both the first and last period always align correctly with the specified range.
- Clear and PowerShell-friendly output through the use of PSCustomObject.
- An easily extensible design, making it simple to add support for weekly, quarterly, or even custom intervals.
Conclusion
Get-DateSlices is a compact but powerful helper function for anyone who regularly works with reports, APIs, or historical datasets. By automatically splitting large date ranges into logically bounded periods, your code becomes simpler, easier to maintain, and often faster as well.
It is exactly the kind of function you write once, add to your personal PowerShell module, and then keep reusing for years.

Leave a Reply