Calculate Date Ranges in Chunks by Year

Viewed 133

Writing a small application to calculate interest but the rate changes yearly. Needed to break the range into smaller date ranges when ever it crosses a year boundary. I wrote a little for loop to do it but it's rather clunky. Wondering if there are any built in functions to do this in C# (possible linq). Would basically be looking to return a list of date ranges with the corresponding base year (shortened code for readability).

    static void Main(string[] args)
    {

        var dateStart = DateTime.Parse("2/10/2018");
        var dateEnd = DateTime.Parse("3/10/2021");

        var years = Years(dateStart, dateEnd);

        var baseYear = dateStart.Year;
        Console.WriteLine(baseYear);

        var loopDateStart = dateStart;
        var loopDateEnd = DateTime.Now;

        for (int i = 0; i < years + 1; i++)
        {
            if (i < years) {
                loopDateEnd = DateTime.Parse("1/1/" + (baseYear + 1));
                
                Console.WriteLine(loopDateEnd + " ... " + loopDateStart);
                Console.WriteLine((loopDateEnd - loopDateStart).Days);

                loopDateStart = loopDateEnd;
                baseYear++; 
            }
            else {
                
                loopDateEnd = dateEnd;
                
                Console.WriteLine(loopDateEnd + " ... " + loopDateStart);
                Console.WriteLine((loopDateEnd - loopDateStart).Days);
            }
        }
    }

    public static int Years(DateTime start, DateTime end)
    {
        return (end.Year - start.Year - 1) +
            (((end.Month > start.Month) ||
            ((end.Month == start.Month) && (end.Day >= start.Day))) ? 1 : 0);
    }
4 Answers

Try:

var start = DateTime.Parse("4/5/2017");
var end = DateTime.Parse("3/1/2019");

DateTime chunkEnd;

for (var chunkStart = start; chunkStart < end; chunkStart = chunkEnd.AddDays(1))
{
    var lastDay = new DateTime(chunkStart.Year, 12, 31);
    chunkEnd = end > lastDay ? lastDay : end;
    var days = (chunkEnd - chunkStart).Days;
    Console.WriteLine($"{chunkStart:d} - {chunkEnd:d}; {days} days");
}

Produces:

4/5/2017 - 12/31/2017; 270 days
1/1/2018 - 12/31/2018; 364 days
1/1/2019 - 3/1/2019; 59 days

I came up with the following:

static IEnumerable<(DateTime,DateTime)> ChunkByYear(DateTime start, DateTime end)
{
    // Splits <start,end> into chunks each belonging to a different year
    while(start <= end)        
    {
        var tempEnd = new DateTime(start.Year, 12, 31);

        if(tempEnd >= end ) {
            yield return (start, end);
            yield break;
        }

        yield return (start, tempEnd);
        start = tempEnd.AddDays(1);
    }
}

Here are some results:

4/05/2017 to 3/01/2019:
 4/05/2017->31/12/2017
 1/01/2018->31/12/2018
 1/01/2019->3/01/2019

4/05/2017 to 4/05/2017:
 4/05/2017->4/05/2017

31/12/2017 to 31/12/2019:
 31/12/2017->31/12/2017
 1/01/2018->31/12/2018
 1/01/2019->31/12/2019

31/12/2019 to 31/12/2019:
 31/12/2019->31/12/2019

31/12/2018 to 1/01/2019:
31/12/2018->31/12/2018
1/01/2019->1/01/2019

Group by years:

using System.Collections.Generic;
using System.Linq;
...
static void Main(string[] args)
{
    DateTime dateStart = DateTime.Parse("2/10/2018");
    DateTime dateEnd = DateTime.Parse("3/10/2021");

    // Group all possible dates by year
    foreach(var group in  GetDates(dateStart, dateEnd).GroupBy(date => date.Year))
    {
        Console.WriteLine(group.Key); // The key of the group is year
        Console.WriteLine($"{group.Min()} ... {group.Max()}"); // Range: From minimum to maximum, order doesn't matter.
        Console.WriteLine($"{group.First()} ... {group.Last()}"); //or Range version 2: From first to last, order matters.
        Console.WriteLine(group.Count());   // Count days
    }
}

/// <summary>
/// Get all days blindly, might need to pay attention to days on the boundaries
/// </summary>
private static IEnumerable<DateTime> GetDates(DateTime start, DateTime end)
{
    // TODO: Check start <= end;
    DateTime current = start;
    while(current <= end)
    {
        yield return current;
        current = current.AddDays(1);
    }
}

Sure, we can use LINQ:

var x = Enumerable.Range(dateStart.Year, (dateEnd.Year-dateStart.Year)+1)
    .Select(y => new{
        F = new[]{dateStart, new DateTime(y,1,1)}.Max(),
        T = new[]{dateEnd, new DateTime(y,12,31)}.Min()
    });

It generates an enumerable list of objects that have an F and a T property (from and to) that are your ranges.

It works by using Enumerable.Range to make a list of years: 2018,2019,2020,2021 by starting at 2108 and proceeding for 4 years (2018 to 2018 is one year entry, 2018 to 2021 is 4 year entries)

Then we just turn them into dates using new DateTime(year,amonth,aday) - when were making start dates, amonth and aday are 1 and 1, when making end dates they're 12 and 31

Then we just ask for every year y, "which date is greater, the startdate, or the 1-Jan-y" and "which date is lesser, the enddate or the 31-Dec-y " - for the initial and final date entry it's the startdate and the enddate that are greater and lesser. For other years it's the jan/dec dates. This gives the ranges you want

    foreach(var xx in x){
        Console.WriteLine(xx.F +" to "+xx.T);
    }
2/10/2018 12:00:00 AM to 12/31/2018 12:00:00 AM
1/1/2019 12:00:00 AM to 12/31/2019 12:00:00 AM
1/1/2020 12:00:00 AM to 12/31/2020 12:00:00 AM
1/1/2021 12:00:00 AM to 3/10/2021 12:00:00 AM

If you want to do other work like the number of days between, you can do xx.T-xx.F in the loop, to make a timespan etc

Related