Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sum difference in timespans, sorted by travel days per year

So I have all these travel dates, from-date and to-date. I want to add up all the travel days, and sort them by year. However if one travel for period which spans two years, my code will reach the wrong sum :-(

Given

From date   To date      Number of days
01.01.2001  01.02.2001   32
01.01.2002  01.02.2002   32
01.05.2002  01.08.2002   93
20.12.2002  01.03.2003   72
01.02.2009  01.02.2010   366
01.01.2013  02.02.2015   763

                   Sum   1358

My code produces this. However, it makes a mistake:

Year    Total days
2001    32
2002    137
2003    60
2009    334
2010    32
2013    365
2014    398        <---- here is a case where my code is wrong

Sum     1358

Code

var dates = new Dictionary<int, int>();
var stays = GetStays();
var returnString = "Year, Total days<br><br>";
foreach (var stay in stays)
{
    var totalTravelDays = stay.ToDate.Value.AddDays(1) - stay.FromDate;

    var currentYear = stay.FromDate.Value.Year;
    var nextYear = stay.FromDate.Value.AddYears(1).Year;
    var nextYearDate = new DateTime(stay.FromDate.Value.Year, 1, 1).AddYears(1);

    var daysInThisYear = new TimeSpan?();
    var daysInNextYear = new TimeSpan?();

    if (stay.FromDate.Value.Year != stay.ToDate.Value.Year)
    {
        daysInThisYear = nextYearDate - stay.FromDate;
        daysInNextYear = totalTravelDays - daysInThisYear;
    }
    else
    {
        daysInThisYear = totalTravelDays;
        daysInNextYear = new TimeSpan(0);
    }

    if (dates.ContainsKey(currentYear))
        dates[currentYear] += daysInThisYear.Value.Days;
    else
        dates[currentYear] = daysInThisYear.Value.Days;

    if (dates.ContainsKey(nextYear))
        dates[nextYear] += daysInNextYear.Value.Days;
    else
        dates[nextYear] = daysInNextYear.Value.Days;
}

Help appreciated :)

like image 911
kabforks Avatar asked Jan 12 '23 15:01

kabforks


2 Answers

Assuming that var stays = List<Stay>();, you may try this:

var days = stays.SelectMany(s =>
        Enumerable
            .Range(0, (s.ToDate - s.FromDate).Days + 1)
            .Select(d => s.FromDate.AddDays(d)))
    .GroupBy(d => d.Year)
    .Select(s => new { Year = s.Key, TotalDays = s.Count() })
    .ToList();

days.ForEach(d =>
{
    Console.WriteLine("{0} {1}", d.Year, d.TotalDays);
});

The output of the above is:

2001 32
2002 137
2003 60
2009 334
2010 32
2013 365
2014 365
2015 33
like image 59
Alex Filipovici Avatar answered Jan 27 '23 07:01

Alex Filipovici


If you write a helper method to split date ranges into several ranges partitioned by year:

IEnumerable<Tuple<DateTime,DateTime>> 
    SplitDateRangeByYear(DateTime fromDate, DateTime toDate)
{
    var start = fromDate;
    for(var y = fromDate.Year; y < toDate.Year; ++y)
    {
        var nextYear = y + 1;
        var nextYearStartDate = new DateTime(nextYear, 1, 1);
        yield return Tuple.Create(start, nextYearStartDate);
        start = nextYearStartDate;
    }
    yield return Tuple.Create(start, toDate);
}

Then you can write some handy Linq to do your bidding:

    var yearlyTotals = stays
        .SelectMany(s => SplitDateRangeByYear(s.FromDate, s.ToDate))
        .GroupBy(x => x.Item1.Year)
        .Select(g => new{
                Year = g.Key, 
                NumDays= g.Sum(x => (x.Item2 - x.Item1).TotalDays)});

This is a more general solution that you requested because it will properly deal with sub-day TimeSpan components (i.e. your ranges include times of day).

like image 38
spender Avatar answered Jan 27 '23 07:01

spender