# Calculate certain ratios of "work", "low work", and "no work" over large date ranges

I have the following algorithm to calculate certain ratios of "work", "low work" and "no work" over a set date range (startDate, endDate). The algorithm works fine, but when I increase the date range it obviously runs slower because it has to loop over every day of that range for every minute. Is there a way to improve the performance with a different kind of loop or for the where clause to find the current date within the listWorkTime.

private List<DateSharedWork> CalculateDateSharedWork(DateTime startDate, DateTime endDate, ICollection<WorkTime> listWorkTime)
{

List<DateSharedWork> listDateSharedWork = new List<DateSharedWork>();

// +1 to include last day at full
int range = endDate.Subtract(startDate).Days + 1;

// start at startDate
Parallel.For(0, range, i =>
{

//set minute interval
double everyNMinutes = 1.0;

// reset counter
int work_counter = 0;
int lowWork_counter = 0;
int noWork_counter = 0;

int l = (int)(minutesADay / everyNMinutes);

for (int j = 0; j < l; j++)
{

DateTime check15 = currDate.AddMinutes(j * everyNMinutes);

// check if listWorkTime includes current date
var foundTime = listWorkTime.Where(x => check15 >= x.FromDate && check15 <= x.ToDate).ToList();

if (foundTime.Count(x => x.TimeRangeId == 1) > 0)
{
// found interval that is within work hours
work_counter++;
noWork_counter++;
}
else
{
if (foundTime.Count(x => x.TimeRangeId == 2) > 0)
{
// found intervall that is within low work hours
lowWork_counter++;
noWork_counter++;

}
}
};

double work = everyNMinutes / minutesADay * work_counter;
double lowWork = everyNMinutes / minutesADay * lowWork_counter;
double noWork = 1.0 - (everyNMinutes / minutesADay * noWork_counter);

});

listDateSharedWork.Sort((x, y) => DateTime.Compare(x.Date, y.Date));

return listDateSharedWork;

}


Edit

class definitions of DateSharedWork and WorkTime

public class DateSharedWork
{

public DateSharedWork(DateTime date, double? work = 0.0, double? lowWork = 0.0, double? noWork = 1.0)
{
this.Date = date;
this.Work = work.Value;
this.LowWork = lowWork.Value;
this.NoWork = noWork.Value;
}

public DateTime Date { get; private set; }
public double Work { get; private set; }
public double LowWork { get; private set; }
public double NoWork { get; private set; }
}

public class WorkTime
{
public int Id { get; set; }
public DateTime? FromDate{ get; set; }
public DateTime? ToDate{ get; set; }
public int? TimeRangeId{ get; set; }
}


Edit2

starting from the startDate value as the first day and increasing on a minute scale for a the whole day, I get the ratios of normal work and low work for that day by the flag TimeRangeId, listWorkTime contains information when a certain period was either work or low work, these periods can overlap and also beeing a duplicate, but normal work (TimeRangeId == 1) dominates

• Could you please share with us the definitions of WorkTime and DateSharedWork? Sep 7, 2021 at 7:12
• Could you please confirm the following? You receive a start, an end datetime and a collection of timeframes. You want to calculate the intersection on a minute scale. You also want to distinguish normal work and low work based on a flag. Is my understanding correct? Sep 7, 2021 at 7:54
• that is correct, additional information on Edit2 Sep 7, 2021 at 8:35
• Can please provide some sample data to be able to test / play with it locally? Sep 7, 2021 at 8:40
• Performance upgrading is often a careful balance between reasonable performance and reasonable code readability/maintainability. To that end: at what kind of date range (and with how many WorkTime entries) is the performance becoming unreasonable by your standards? How long does it then take? How long are you expecting it to take? If not a hard line, how much are you willing to sacrifice readability/maintainability for squeezed performance? Sep 7, 2021 at 8:58

Just a few quick shots...

• Instead of having DateTime check15 which leads to the assumption there may be some more DateTime structs laying around like check1...check15 you could just juse currDate which by the way should be renamed to currentDate because abbreviations will make the code harder to read. By using currentDate you could replace (j * everyNMinutes in currDate.AddMinutes()by just 1.0 or better everyNMinutes.

• foundTime as a name for a List isn't the best choice as a List implies more than one item. How about foundTimes ? But much more important is that for each minute the code iterates over listWorkTime at worst twice. If you only use Count() to check if there is any item found, then just use Any(). Count() will iterate over all items while Any() stops at the first found item.

• You should use a profiler to check if removing ToList after Where() will get more performance.

• the ideomatic way in c# to name variables is using camelCase casing instead of snake_casecasing.

• instead of an if inside an else block you should use a else if instead.

• What bothers me more is what happens if there are items in foundTimes which would satisfy both conditions?

Implementing the mentioned changes without taking into account the last point looks like so

private List<DateSharedWork> CalculateDateSharedWork(DateTime startDate, DateTime endDate, ICollection<WorkTime> listWorkTime)
{

List<DateSharedWork> listDateSharedWork = new List<DateSharedWork>();

// +1 to include last day at full
int range = endDate.Subtract(startDate).Days + 1;

// start at startDate
Parallel.For(0, range, i =>
{

//set minute interval
double everyNMinutes = 1.0;

// reset counter
int workCounter = 0;
int lowWorkCounter = 0;
int noWorkCounter = 0;

int l = (int)(minutesADay / everyNMinutes);

for (int j = 0; j < l; j++)
{

// check if listWorkTime includes current date
var foundTimes = listWorkTime.Where(x => currentDate >= x.FromDate && currentDate <= x.ToDate).ToList();

if (foundTimes.Any(x => x.TimeRangeId == 1))
{
// found interval that is within work hours
workCounter++;
noWorkCounter++;
}
else if (foundTimes.Any(x => x.TimeRangeId == 2))
{
// found intervall that is within low work hours
lowWorkCounter++;
noWorkCounter++;
}

};

double work = everyNMinutes / minutesADay * workCounter;
double lowWork = everyNMinutes / minutesADay * lowWorkCounter;
double noWork = 1.0 - (everyNMinutes / minutesADay * noWorkCounter);