LINQ query to Sum value over date ranges - c#

I'm trying to create a linq query that would produce a collection of date ranges with sums of the Capacity value taking into the account that ranges can overlap and I'd like a sum and a distinct date range for that overlapping periods. Thanks.
public ActionResult Index()
{
List<Capacities> _list = new List<Capacities>{
new Capacities {StartDate = DateTime.Parse("01/01/2013"), StopDate = DateTime.Parse("01/01/2013 06:00"), Capacity = 100},
new Capacities {StartDate = DateTime.Parse("01/01/2013 04:00"), StopDate = DateTime.Parse("01/02/2013 00:00"), Capacity = 120},
new Capacities {StartDate = DateTime.Parse("01/04/2013"), StopDate = DateTime.Parse("01/04/2013 15:00"), Capacity = 100},
new Capacities {StartDate = DateTime.Parse("01/04/2013 15:00"), StopDate = DateTime.Parse("01/04/2013 18:00"), Capacity = 150}
};
//results expected
//01/01/2013 00:00 - 01/01/2013 04:00 100
//01/01/2013 04:00 - 01/01/2013 06:00 220
//01/01/2013 06:00 - 01/02/2013 00:00 120
//01/04/2013 00:00 - 01/04/2013 15:00 100
//01/04/2013 15:00 - 01/04/2013 18:00 150
return View();
}
public class Capacities
{
public DateTime StartDate { get; set; }
public DateTime StopDate { get; set; }
public int Capacity {get;set;}
}

I did some programming, but I extended your code quite a bit. But I was able to use LINQ in the very end :-)
my code:
SortedSet<DateTime> splitdates = new SortedSet<DateTime>();
foreach (var item in _list)
{
splitdates.Add(item.Period.Start);
splitdates.Add(item.Period.End);
}
var list = splitdates.ToList();
var ranges = new List<DateRange>();
for (int i = 0; i < list.Count - 1; i++)
ranges.Add(new DateRange() { Start = list[i], End = list[i + 1] });
var result = from range in ranges
from c in _list
where c.Period.Intersect(range) != null
group c by range into r
select new Capacities(r.Key.Start, r.Key.End, r.Sum(a => a.Capacity));
Complete code is here: http://pastebin.com/wazbb1r3
Note that the output differs because of locale. Also, some bits are not necessary like DateRange.Contains().
On the two loops above, I have no idea how to transform them to LINQ in a readable manner.

Related

Optimising the process of a Huge List<T> in C#

I'm working on a scheduling algorithm that generates/assigns time-slots to a List of Recipients based on the following restrictions:
Max Recipients per Minute
Max Recipients per Hour
Suppose that the delivery Start Time is 2018-10-17 9:00 AM and we have 19 recipients with Max of 5 per min and and 10 per hour, so the output should be:
5 Recipients will be scheduled on 2018-10-17 9:00 AM
5 Recipients will be scheduled on 2018-10-17 9:01 AM
5 Recipients will be scheduled on 2018-10-17 10:00 AM
4 Recipients will be scheduled on 2018-10-17 10:01 AM
The algorithm is very accurate, but the way it works is as following:
First it generates a list of time-slots or time-windows that are accurately fits the no. of recipients based on the restrictions i mentioned before.
then, I'm moving whatever available in the List of Time-Slots for each set/group or recipients.
in the list of Time-Slots I added a counter that increments for every recipient added to it, so in this way I can track the no. of each recipients added to each time-slot to respect the Max per Min./Hr restrictions.
The previous process it simplified in this code snippet - I'm using While Loop to iterate, in my case when having 500K recipients this is taking 28 minutes to get it done!
I tried to use Parallel.ForEach but I couldn't figure out how to implement it in this case.
DateTime DeliveryStart = DateTime.Now;
//This list has DateTime: Time-windows values starting from DeliveryStart to the Max value of the time needed to schedule the Recipients
var listOfTimeSlots = new List<Tuple<DateTime, bool, int>>();
//List of Recipients with Two types of data: DateTime to tell when its scheduled and int value refers to the Recipient's ID
var ListOfRecipients = new List<Tuple<DateTime, int>>();
List<Tuple<int, DateTime>> RecipientsWithTimeSlots= new List<Tuple<int, DateTime>>();
int noOfRecipients = ListOfRecipients.Count;
int Prevhour = 0, _AddedPerHour = 0, Prevday = 0;
// Scheduling restrictions
int _MaxPerHour = 5400, _MaxPerMinute = 90;
int i = 0;
int indexStart = 0;
// ...
// ...
// Code to fill listOfTimeSlots ListOfRecipients with Data
while (noOfRecipients > 0)
{
var TimeStamp = listOfTimeSlots[i];
int hour = TimeStamp.Item1.Hour;
int day = TimeStamp.Item1.Day;
if (Prevhour == 0)
{
Prevhour = hour;
Prevday = day;
}
if (Prevhour != hour)
{
Prevhour = hour;
_AddedPerHour = 0;
}
if (_AddedPerHour >= _MaxPerHour)
{
var tmpItem = listOfTimeSlots.Where(l => l.Item1.Hour == hour && l.Item1.Day == day).LastOrDefault();
int indexOfNextItem = listOfTimeSlots.LastIndexOf(tmpItem) + 1;
i = indexOfNextItem;
_AddedPerHour = 0;
continue;
}
else
{
int endIndex;
endIndex = _MaxPerMinute > noOfRecipients ? noOfRecipients : _MaxPerMinute;
if (endIndex > Math.Abs(_AddedPerHour - _MaxPerHour))
endIndex = Math.Abs(_AddedPerHour - _MaxPerHour);
var RecipientsToIteratePerMinute = ListOfRecipients.GetRange(indexStart, endIndex);
foreach (var item in RecipientsToIteratePerMinute)
{
RecipientsWithTimeSlots.Add(new Tuple<int, DateTime>(item.Item2, TimeStamp.Item1));
listOfTimeSlots[i] = new Tuple<DateTime, bool, int>(TimeStamp.Item1, true, listOfTimeSlots[i].Item3 + 1);
_AddedPerHour++;
}
indexStart += endIndex;
noOfRecipients -= endIndex;
i++;
}
}
I simplified the code in here, for not making it so complex to understand, all i want it to speed-up the while loop or replacing it with a Parallel.ForEach.
THE WHILE LOOP IS NEVER SIMPLIFIED, THIS IS HOW IT EXACTLY WORKS \
Any help or suggestion is appreciated.
Here is a different approach. It creates the groups of ids first, then assigns them the date based on the requirements.
First, a class to represent the groups (avoid them tuples):
public class RecipientGroup
{
public RecipientGroup(DateTime scheduledDateTime, IEnumerable<int> recipients)
{
ScheduledDateTime= scheduledDateTime;
Recipients = recipients;
}
public DateTime ScheduledDateTime { get; private set; }
public IEnumerable<int> Recipients { get; private set; }
public override string ToString()
{
return string.Format($"Date: {ScheduledDateTime.ToShortDateString()} {ScheduledDateTime.ToLongTimeString()}, count: {Recipients.Count()}");
}
}
Then a class to iterate through the groups. You will see why this is needed later:
public class GroupIterator
{
public GroupIterator(DateTime scheduledDateTime)
{
ScheduledDateTime = scheduledDateTime;
}
public DateTime ScheduledDateTime { get; set; }
public int Count { get; set; }
}
Now, the code:
DateTime DeliveryStart = new DateTime(2018, 10, 17);
//List of Recipients (fake populate function)
IEnumerable<int> allRecipients = PopulateRecipients();
// Scheduling restrictions
int maxPerMinute = 90;
int maxPerHour = 270;
//Creates groups broken down by the max per minute.
var groupsPerMinute = allRecipients
.Select((s, i) => new { Value = s, Index = i })
.GroupBy(x => x.Index / maxPerMinute)
.Select(group => group.Select(x => x.Value).ToArray());
//This will be the resulting groups
var deliveryDateGroups = new List<RecipientGroup>();
//Perform an aggregate run on the groups using the iterator
groupsPerMinute.Aggregate(new GroupIterator(DeliveryStart), (iterator, ids) =>
{
var nextBreak = iterator.Count + ids.Count();
if (nextBreak >= maxPerHour)
{
//Will go over limit, split
var difference = nextBreak-maxPerHour;
var groupSize = ids.Count() - difference;
//This group completes the batch
var group = new RecipientGroup(iterator.ScheduledDateTime, ids.Take(groupSize));
deliveryDateGroups.Add(group);
var newDate = iterator.ScheduledDateTime.AddHours(1).AddMinutes(-iterator.ScheduledDateTime.Minute);
//Add new group with remaining recipients.
var stragglers = new RecipientGroup(newDate, ids.Skip(groupSize));
deliveryDateGroups.Add(stragglers);
return new GroupIterator(newDate, difference);
}
else
{
var group = new RecipientGroup(iterator.ScheduledDateTime, ids);
deliveryDateGroups.Add(group);
iterator.ScheduledDateTime = iterator.ScheduledDateTime.AddMinutes(1);
iterator.Count += ids.Count();
return iterator;
}
});
//Output minute group count
Console.WriteLine($"Group count: {deliveryDateGroups.Count}");
//Groups by hour
var byHour = deliveryDateGroups.GroupBy(g => new DateTime(g.ScheduledDateTime.Year, g.ScheduledDateTime.Month, g.ScheduledDateTime.Day, g.ScheduledDateTime.Hour, 0, 0));
Console.WriteLine($"Hour Group count: {byHour.Count()}");
foreach (var group in byHour)
{
Console.WriteLine($"Date: {group.Key.ToShortDateString()} {group.Key.ToShortTimeString()}; Count: {group.Count()}; Recipients: {group.Sum(g => g.Recipients.Count())}");
}
Output:
Group count: 5556
Hour Group count: 1852
Date: 10/17/2018 12:00 AM; Count: 3; Recipients: 270
Date: 10/17/2018 1:00 AM; Count: 3; Recipients: 270
Date: 10/17/2018 2:00 AM; Count: 3; Recipients: 270
Date: 10/17/2018 3:00 AM; Count: 3; Recipients: 270
Date: 10/17/2018 4:00 AM; Count: 3; Recipients: 270
Date: 10/17/2018 5:00 AM; Count: 3; Recipients: 270
... and so on for all 1852 groups.
This takes about 3 seconds to complete.
I am sure there are edge cases. I wrote this in a hurry so just think about those.

Finding lowest price for overlapping date ranges - C# algorithm

There are prices set for certain time periods... I'm having trouble coming up with an algorithm to determine the lowest price for a specific time period.
I'm doing this with a list of objects, where the object has properties DateTime StartDate, DateTime EndDate, decimal Price.
For example, two price sets and their active date ranges:
A. 09/26/16 - 12/31/17 at $20.00
B. 12/01/16 - 12/31/16 at $18.00
You can see that B is inside the A time period and is lower.
I need that converted to this:
A. 09/26/16 - 11/30/16 at $20.00
B. 12/01/16 - 12/31/16 at $18.00
C. 01/01/17 - 12/31/17 at $20.00
It has to work for any number of date ranges and combinations. Has anyone come across anything I can manipulate to get the result I need? Or any suggestions?
Edit: My data structure:
public class PromoResult
{
public int ItemId { get; set; }
public decimal PromoPrice { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public int PromoType { get; set; } // can ignore this...
}
This is a great case for using Linq. Assuming your price range object is called PriceRecord...
You will need to create a list of all dates and then filter down to price records that are between two consecutive dates. An implementation might look something like this:
public static IEnumerable<PriceRecord> ReduceOverlaps(IEnumerable<PriceRecord> source)
{
// Get a list of all edges of date ranges
// edit, added OrderBy (!)
var edges = source.SelectMany(record => new[] { record.StartDate, record.EndDate }).OrderBy(d => d).ToArray();
// iterate over pairs of edges (i and i-1)
for (int i = 1; i < edges.Length; i++)
{
// select min price for range i-1, i
var price = source.Where(r => r.StartDate <= edges[i - 1] && r.EndDate >= edges[i]).Select(r => r.Price).Min();
// return a new record from i-1, i with price
yield return new PriceRecord() { StartDate = edges[i - 1], EndDate = edges[i], Price = price };
}
}
I haven't tested this and you may need to tinker with the comparison operators, but it may be a good starting point.
I have now tested the code, the example here works with the data in the question.
Feel free to propose edits to improve this example.
I will use 2 functions DateRange and GroupSequenceWhile
List<PromoResult> promoResult = new List<PromoResult>()
{
new PromoResult() { PromoPrice=20, StartDate = new DateTime(2016, 9, 26),EndDate=new DateTime(2017, 12, 31)},
new PromoResult() { PromoPrice=18, StartDate = new DateTime(2016, 12, 1),EndDate=new DateTime(2016, 12, 31)}
};
var result = promoResult.SelectMany(x => DateRange(x.StartDate, x.EndDate, TimeSpan.FromDays(1))
.Select(y => new { promo = x, date = y }))
.GroupBy(x => x.date).Select(x => x.OrderBy(y => y.promo.PromoPrice).First())
.OrderBy(x=>x.date)
.ToList();
var final = result.GroupSequenceWhile((x, y) => x.promo.PromoPrice == y.promo.PromoPrice)
.Select(g => new { start = g.First().date, end = g.Last().date, price = g.First().promo.PromoPrice })
.ToList();
foreach (var r in final)
{
Console.WriteLine(r.price + "$ " + r.start.ToString("MM/dd/yy", CultureInfo.InvariantCulture) + " " + r.end.ToString("MM/dd/yy", CultureInfo.InvariantCulture));
}
OUTPUT:
20$ 09/26/16 11/30/16
18$ 12/01/16 12/31/16
20$ 01/01/17 12/31/17
Algorithm:
1- create a <day,price> tuple for each item in promoResult list
2- group this tuples by day and select min price
3- order this tuples by date
4- select the starting and ending day when there is a change in price in consecutive days
IEnumerable<DateTime> DateRange(DateTime start, DateTime end, TimeSpan period)
{
for (var dt = start; dt <= end; dt = dt.Add(period))
{
yield return dt;
}
}
public static IEnumerable<IEnumerable<T>> GroupSequenceWhile<T>(this IEnumerable<T> seq, Func<T, T, bool> condition)
{
List<T> list = new List<T>();
using (var en = seq.GetEnumerator())
{
if (en.MoveNext())
{
var prev = en.Current;
list.Add(en.Current);
while (en.MoveNext())
{
if (condition(prev, en.Current))
{
list.Add(en.Current);
}
else
{
yield return list;
list = new List<T>();
list.Add(en.Current);
}
prev = en.Current;
}
if (list.Any())
yield return list;
}
}
}
Doesn't directly answer your question, but here is some SQL that I used to solve a similar problem I had (simplified down a bit, as I was also dealing with multiple locations and different price types):
SELECT RI.ItemNmbr, RI.UnitPrice, RI.CasePrice
, RP.ProgramID
, Row_Number() OVER (PARTITION BY RI.ItemNmbr,
ORDER BY CASE WHEN RI.UnitPrice > 0
THEN RI.UnitPrice
ELSE 1000000 END ASC
, CASE WHEN RI.CasePrice > 0
THEN RI.CasePrice
ELSE 1000000 END ASC
, RP.EndDate DESC
, RP.BeginDate ASC
, RP.ProgramID ASC) AS RowNumBtl
, Row_Number() OVER (PARTITION BY RI.UnitPrice,
ORDER BY CASE WHEN RI.CasePrice > 0
THEN RI.CasePrice
ELSE 1000000 END ASC
, CASE WHEN RI.UnitPrice > 0
THEN RI.UnitPrice
ELSE 1000000 END ASC
, RP.EndDate DESC
, RP.BeginDate ASC
, RP.ProgramID ASC) AS RowNumCase
FROM RetailPriceProgramItem AS RI
INNER JOIN RetailPriceMaster AS RP
ON RP.ProgramType = RI.ProgramType AND RP.ProgramID = RI.ProgramID
WHERE RP.ProgramType='S'
AND RP.BeginDate <= #date AND RP.EndDate >= #date
AND RI.Active=1
I select from that where RowNumBtl=1 for the UnitPrice and RowNumCase=1 for the CasePrice. If you then create a table of dates (which you can do using a CTE), you can cross apply on each date. This is a bit inefficient, since you only need to test at border conditions between date ranges, so... good luck with that.
I would start with the ranges in date order based on starting date, add the first entry as a range in its entirety so:
09/26/16 - 12/31/17 at $20.00
TBD:
12/01/16 - 12/31/16 at $18.00
Next grab the next range you have, if it overlaps with the previous one, split the overlap (there are few kinds of overlaps, make sure to handle them all) taking the minimum value for the overlapped region:
09/26/16 - 11/30/16 at $20.00
12/01/16 - 12/31/16 at $18.00
TBD:
01/01/17 - 12/31/17 at $20.00
Note that you don't have the last one yet as you would take any splits that occur after and put them back into your sorted list of "yet to be compared" items.
Try this
lets say we have:
public class DatePrice
{
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public decimal Price { get; set; }
}
and
IList<DatePrice> list = new List<DatePrice>(); // populate your data from the source..
var lowestPriceItem = list.OrderBy(item => item.Price).First();
should give you the lowest price item.

ASP.NET MVC Filter datetime by weeks

I've got a Web API and a Get method, returning a query:
var query = from results in context.Table
where results.Date>= startDate && results.Date <= endDate
select new
{
Week = { this is where I need a method to group by weeks },
Average = results.Where(x => x.Number).Average()
}
return query.ToList();
I want to calculate the average for each 7 days (that being the first week).
Example:
Average 1 ... day 7 (Week 1)
Average 2 ... day 14 (Week 2)
How can I do that? Being given an interval of datetimes, to filter it by weeks (not week of year)
Try this (not tested with tables)
var avgResult = context.QuestionaireResults
.Where(r => (r.DepartureDate >= startDate && r.DepartureDate <= endDate)).ToList()
.GroupBy( g => (Decimal.Round(g.DepartureDate.Day / 7)+1))
.Select( g => new
{
Week = g.Key,
Avg = g.Average(n => n.Number)
});
You will need to group by the number of days, since a reference date, divided by 7, so
.GroupBy(x => Math.Floor(((x.DepartureDate - new DateTime(1980,1,1)).TotalDays + 2) / 7))
Subtracting "Jan 1, 1980" from your departure date, gives you a TimeSpan object with the difference between the two dates. The TotalDays property of that timespan gives you timespan in days. Adding 2 corrects for the fact that "Jan 1, 1980" was a Tuesday. Dividing by 7 gives you the number of weeks since then. Math.Floor rounds it down, so that you get a consistent integer for the week, given any day of the week or portion of days within the week.
You could simplify a little by picking a reference date that is a Sunday (assuming that is your "first day of the week"), so you dont have to add 2 to correct. Like so:
.GroupBy(x => Math.Floor(((x.DepartureDate - new DateTime(1979,12,30)).TotalDays) / 7))
If you are sure that your data all falls within a single calendar year, you could maybe use the Calendar.GetWeekOfYear method to figure out the week, but I am not sure it would be any simpler.
Why not write a stored procedure, I think there may be some limitations on your flexibility using Linq because of the idea that normally the GroupBy groups by value (the value of the referenced "thing") so you can group by State, or Age, but I guess you can Group week... (new thought)
Add a property called EndOfWeek and for example, the end of this week is (Sunday let's say) then EndOfWeek = 9.2.16 whereas last week was 8.28.16... etc. then you can easily group but you still have to arrange the data.
I know I didn't answer the question but I hope that I sparked some brain activity in an area that allows you to solve the problem.
--------- UPDATED ----------------
simple solution, loop through your records, foreach record determine the EndOfWeek for that record. After this you will now have a groupable value. Easily group by EndOfWeek. Simple!!!!!!!!!!!! Now, #MikeMcCaughan please tell me how this doesn't work? Is it illogical to extend an object? What are you talking about?
------------ HERE IS THE CODE ----------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SandboxConsole
{
class Program
{
static void Main(string[] args)
{
var t = new Transactions();
List<Transactions> transactions = t.GetTransactions();
// Now let's add a Weeks end date so we can determine the average per week
foreach(var transaction in transactions)
{
var transactionDayOfWeek = transaction.TransactionDate;
int daysUntilEndOfWeek_Sat = ((int)DayOfWeek.Saturday - (int)transactionDayOfWeek.DayOfWeek + 7) % 7;
transaction.Newly_Added_Property_To_Group_By_Week_To_Get_Averages = transactionDayOfWeek.AddDays(daysUntilEndOfWeek_Sat).ToShortDateString();
//Console.WriteLine("{0} {")
}
foreach(var weekEnd in transactions.GroupBy(tt => tt.Newly_Added_Property_To_Group_By_Week_To_Get_Averages))
{
decimal weekTotal = 0;
foreach(var trans in weekEnd)
{
weekTotal += trans.Amount;
}
var weekAverage = weekTotal / 7;
Console.WriteLine("Week End: {0} - Avg {1}", weekEnd.Key.ToString(), weekAverage.ToString("C"));
}
Console.ReadKey();
}
}
class Transactions
{
public int Id { get; set; }
public string SomeOtherProp { get; set; }
public DateTime TransactionDate { get; set; }
public decimal Amount { get; set; }
public string Newly_Added_Property_To_Group_By_Week_To_Get_Averages { get; set; }
public List<Transactions> GetTransactions()
{
var results = new List<Transactions>();
for(var i = 0; i<100; i++)
{
results.Add(new Transactions
{
Id = i,
SomeOtherProp = "Customer " + i.ToString(),
TransactionDate = GetRandomDate(i),
Amount = GetRandomAmount()
});
}
return results;
}
public DateTime GetRandomDate(int i)
{
Random gen = new Random();
DateTime startTime = new DateTime(2016, 1, 1);
int range = (DateTime.Today - startTime).Days + i;
return startTime.AddDays(gen.Next(range));
}
public int GetRandomAmount()
{
Random rnd = new Random();
int amount = rnd.Next(1000, 10000);
return amount;
}
}
}
------------ OUTPUT ---------------
Sample Output

Calculating number of nights in overlapping dates

I'm having an issue figuring out a logical way to solve this problem.
I have a list of date ranges, say for example:
01/01/15 - 11/01/15
02/01/15 - 04/01/15
09/01/15 - 13/01/15
18/01/15 - 20/01/15
What I need to do is figure out the total number of nights covered over all of these date ranges.
So for the example the total should be 14 nights:
01/01/15 - 11/01/15 // 10 nights
02/01/15 - 04/01/15 // Ignored as nights are covered in 1-11
09/01/15 - 13/01/15 // 2 nights as 11th and 12th nights haven't been covered
18/01/15 - 20/01/15 // 2 nights
I can easily figure out the total number of nights using min-max dates but that ignores the missing dates (14-17 in the example) and is what I can't figure out.
Is there any way to find the total number of days missing to help figure this out?
Here's a way using a HashSet:
public static int CountDays(IEnumerable<TimeRange> periods)
{
var usedDays = new HashSet<DateTime>();
foreach (var period in periods)
for (var day = period.Start; day < period.End; day += TimeSpan.FromDays(1))
usedDays.Add(day);
return usedDays.Count;
}
This assumes that your date ranges are half-open intervals (i.e. the start date is considered part of the range but the end date is not).
Here's a complete program to demonstrate. The answer is 14:
using System;
using System.Collections.Generic;
namespace ConsoleApplication2
{
public sealed class TimeRange
{
public DateTime Start { get; private set; }
public DateTime End { get; private set; }
public TimeRange(string start, string end)
{
Start = DateTime.Parse(start);
End = DateTime.Parse(end);
}
}
internal class Program
{
public static void Main()
{
var periods = new []
{
new TimeRange("01/01/15", "11/01/15"),
new TimeRange("02/01/15", "04/01/15"),
new TimeRange("09/01/15", "13/01/15"),
new TimeRange("18/01/15", "20/01/15")
};
Console.WriteLine(CountDays(periods));
}
public static int CountDays(IEnumerable<TimeRange> periods)
{
var usedDays = new HashSet<DateTime>();
foreach (var period in periods)
for (var day = period.Start; day < period.End; day += TimeSpan.FromDays(1))
usedDays.Add(day);
return usedDays.Count;
}
}
}
NOTE: This is NOT very efficient for large date ranges! If you have large date ranges to consider, an approach that combines overlapping ranges into single ranges would be better.
[EDIT] Fixed the code to use half-open intervals rather than closed intervals.
You could calculate it like this:
var laterDateTime = Convert.ToDateTime("11/01/15 ");
var earlierDateTime = Convert.ToDateTime("01/01/15");
TimeSpan dates = laterDateTime - earlierDateTime;
int nights = dates.Days - 1;
you could convert whatever you have into DateTime.
Then you can subtract both DateTimes with the - operator.
Your result will be a type of struct TimeSpan.
TimeSpan hat a Days property. Substract 1 from that and you recieve the nights.
between 2 Days is 1 Night
between 3 Days are 2 Nights
between 4 Days are 3 Nights
I am sure you can do the rest.
Assuming there are two nights between e.g. Jan 1 and Jan 3, this should work. If you already have DateTime values instead of string, you can get rid of the parsing bit. Basically, I'm using DateTime.Subtract() to calculate the number of days (i.e. nights) between two dates.
namespace DateTest1
{
using System;
using System.Collections.Generic;
using System.Globalization;
class Program
{
static void Main(string[] args)
{
var intervals = new List<Tuple<string, string>>
{
new Tuple<string, string>("01/01/15", "11/01/15"),
new Tuple<string, string>("02/01/15", "04/01/15"),
new Tuple<string, string>("09/01/15", "13/01/15"),
new Tuple<string, string>("18/01/15", "20/01/15")
};
var totalNights = 0;
foreach (var interval in intervals)
{
var dateFrom = DateTime.ParseExact(interval.Item1, "dd/MM/yy", CultureInfo.InvariantCulture);
var dateTo = DateTime.ParseExact(interval.Item2, "dd/MM/yy", CultureInfo.InvariantCulture);
var nights = dateTo.Subtract(dateFrom).Days;
Console.WriteLine("{0} - {1}: {2} nights", interval.Item1, interval.Item2, nights);
totalNights += nights;
}
Console.WriteLine("Total nights: {0}", totalNights);
}
}
}
01/01/15 - 11/01/15: 10 nights
02/01/15 - 04/01/15: 2 nights
09/01/15 - 13/01/15: 4 nights
18/01/15 - 20/01/15: 2 nights
Total nights: 18
Press any key to continue . . .
Something like this should work:
internal class Range
{
internal DateTime From, To;
public Range(string aFrom, string aTo)
{
From = DateTime.ParseExact(aFrom, "dd/mm/yy", CultureInfo.InvariantCulture);
To = DateTime.ParseExact(aTo, "dd/mm/yy", CultureInfo.InvariantCulture);
}
}
public static int ComputeNights(IEnumerable<Range> ranges)
{
var vSet = new HashSet<DateTime>();
foreach (var range in ranges)
for (var i = range.From; i < range.To; i = i.AddDays(1)) vSet.Add(i)
return vSet.Count;
}
The code to run your example:
var vRanges = new List<Range>
{
new Range("01/01/15", "11/01/15"),
new Range("02/01/15", "04/01/15"),
new Range("09/01/15", "13/01/15"),
new Range("18/01/15", "20/01/15"),
};
var v = ComputeNights(vRanges);
v evaluates to 14
I think this solution will be faster then looping through ranges with inner loop for inserting days in list. This solution doesn't requires additional space. It is O(1) and it loops through ranges only once so it's complexity is O(n). But it assumes that your ranges are ordered by startdate. If not you can always order them easily:
var p = new[]
{
new Tuple<DateTime, DateTime>(DateTime.ParseExact("01/01/15", "dd/MM/yy", CultureInfo.InvariantCulture), DateTime.ParseExact("11/01/15", "dd/MM/yy", CultureInfo.InvariantCulture)),
new Tuple<DateTime, DateTime>(DateTime.ParseExact("02/01/15", "dd/MM/yy", CultureInfo.InvariantCulture), DateTime.ParseExact("04/01/15", "dd/MM/yy", CultureInfo.InvariantCulture)),
new Tuple<DateTime, DateTime>(DateTime.ParseExact("09/01/15", "dd/MM/yy", CultureInfo.InvariantCulture), DateTime.ParseExact("13/01/15", "dd/MM/yy", CultureInfo.InvariantCulture)),
new Tuple<DateTime, DateTime>(DateTime.ParseExact("18/01/15", "dd/MM/yy", CultureInfo.InvariantCulture), DateTime.ParseExact("20/01/15", "dd/MM/yy", CultureInfo.InvariantCulture))
};
int days = (p[0].Item2 - p[0].Item1).Days;
var endDate = p[0].Item2;
for(int i = 1; i < p.Length; i++)
{
if(p[i].Item2 > endDate)
{
days += (p[i].Item2 - (p[i].Item1 > endDate ? p[i].Item1 : endDate)).Days;
endDate = p[i].Item2;
}
}
Just in case you didn't have enough answers here is one more using Linq and Aggregate. Returns 14 nights.
List<Tuple<DateTime, DateTime>> dates = new List<Tuple<DateTime, DateTime>>
{
Tuple.Create(new DateTime(2015, 1,1), new DateTime(2015, 1,11)),
Tuple.Create(new DateTime(2015, 1,2), new DateTime(2015, 1,4)),
Tuple.Create(new DateTime(2015, 1,9), new DateTime(2015, 1,13)),
Tuple.Create(new DateTime(2015, 1,18), new DateTime(2015, 1,20))
};
var availableDates =
dates.Aggregate<Tuple<DateTime, DateTime>,
IEnumerable<DateTime>,
IEnumerable<DateTime>>
(new List<DateTime>(),
(allDates, nextRange) => allDates.Concat(Enumerable.Range(0, (nextRange.Item2 - nextRange.Item1).Days)
.Select(e => nextRange.Item1.AddDays(e))),
allDates => allDates);
var numDays =
availableDates.Aggregate<DateTime,
Tuple<DateTime, int>,
int>
(Tuple.Create(DateTime.MinValue, 0),
(acc, nextDate) =>
{
int daysSoFar = acc.Item2;
if ((nextDate - acc.Item1).Days == 1)
{
daysSoFar++;
}
return Tuple.Create(nextDate, daysSoFar);
},
acc => acc.Item2);

LINQ aggregate 30 minute interval to hour

I'm not a super expert on LINQ, I've a data below provided by third party:
Data
Start: 6:00
End: 6:30
value: 1
Start: 7:00
End: 7:30
value: 1
Start: 8:00
End: 8:30
value: 1
Start: 9:00
End: 9:30
value: 1
Start: 10:00
End: 10:30
value: 1
Start: 11:00
End: 11:30
value: 1
Start: 12:00
End: 12:30
value: 1
Start: 13:00
End: 13:30
value: 1
Start: 14:00
End: 14:30
value: 1
...
Start: 05:00
End: 05:30
value: 1
This data keeps going for a week then 30 days and 365days.
I need to transform each 30minute block in to an hour.
e.g
Start: 6:00
End: 7:00
Value: 2
Start:7:00
End: 8:00
Value:2
......
Assuming that Start, End and Value comes as one row, could someone help how above can be achieved?
This query is able to group by the given AggregationType and it is able to filter out incomplete groups using the second parameter checkType.
private enum AggerationType { Year = 1, Month = 2, Day = 3, Hour = 4 }
private IList<Data> RunQuery(AggerationType groupType, AggerationType checkType)
{
// The actual query which does to trick
var result =
from d in testList
group d by new {
d.Start.Year,
Month = (int)groupType >= (int)AggerationType.Month ? d.Start.Month : 1,
Day = (int)groupType >= (int)AggerationType.Day ? d.Start.Day : 1,
Hour = (int)groupType >= (int)AggerationType.Hour ? d.Start.Hour : 1
} into g
// The where clause checks how much data needs to be in the group
where CheckAggregation(g.Count(), checkType)
select new Data() { Start = g.Min(m => m.Start), End = g.Max(m => m.End), Value = g.Sum(m => m.Value) };
return result.ToList();
}
private bool CheckAggregation(int groupCount, AggerationType checkType)
{
int requiredCount = 1;
switch(checkType)
{
// For year all data must be multiplied by 12 months
case AggerationType.Year:
requiredCount = requiredCount * 12;
goto case AggerationType.Month;
// For months all data must be multiplied by days in month
case AggerationType.Month:
// I use 30 but this depends on the given month and year
requiredCount = requiredCount * 30;
goto case AggerationType.Day;
// For days all data need to be multiplied by 24 hour
case AggerationType.Day:
requiredCount = requiredCount * 24;
goto case AggerationType.Hour;
// For hours all data need to be multiplied by 2 (because slots of 30 minutes)
case AggerationType.Hour:
requiredCount = requiredCount * 2;
break;
}
return groupCount == requiredCount;
}
Here some Test data if you want:
class Data
{
public DateTime Start { get; set; }
public DateTime End { get; set; }
public int Value { get; set; }
}
// Just setup some test data simulary to your example
IList<Data> testList = new List<Data>();
DateTime date = DateTime.Parse("6:00");
// This loop fills just some data over several years, months and days
for (int year = date.Year; year > 2010; year--)
{
for(int month = date.Month; month > 0; month--)
{
for (int day = date.Day; day > 0; day--)
{
for(int hour = date.Hour; hour > 0; hour--)
{
DateTime testDate = date.AddHours(-hour).AddDays(-day).AddMonths(-month).AddYears(-(date.Year - year));
testList.Add(new Data() { Start = testDate, End = testDate.AddMinutes(30), Value = 1 });
testList.Add(new Data() { Start = testDate.AddMinutes(30), End = testDate.AddHours(1), Value = 1 });
}
}
}
}
Below is the code. It seems a little bit ugly because of switch statement. It would be better to refactor it but it should show the idea.
var items = input.Split('\n');
Func<string, string> f = s =>
{
var strings = s.Split(new[] {':'}, 2);
var key = strings[0];
var value = strings[1];
switch (key.ToLower())
{
case "start":
return s;
case "value":
return String.Format("{0}: {1}", key, Int32.Parse(value) + 1);
case "end":
return String.Format("{0}: {1:h:mm}", key,
DateTime.Parse(value) +
TimeSpan.FromMinutes(30));
default:
return "";
}
};
var resultItems = items.Select(f);
Console.Out.WriteLine("result = {0}",
String.Join(Environment.NewLine, resultItems));
It's actually quite hard to completely approach this with with pure LINQ. To make life easier, you'll need to write atleast one helper method that allows you to transform an enumeration. Take a look at the example below. Here I make use of an IEnumerable of TimeInterval and have a custom Split method (implemented with C# iterators) that Joins two elements together in one Tuple:
class TimeInterval
{
DateTime Start;
DateTime End;
int Value;
}
IEnumerable<TimeInterval> ToHourlyIntervals(
IEnunumerable<TimeInterval> halfHourlyIntervals)
{
return
from pair in Split(halfHourlyIntervals)
select new TimeInterval
{
Start = pair.Item1.Start,
End = pair.Item2.End,
Value = pair.Item1.Value + pair.Item2.Value
};
}
static IEnumerable<Tuple<T, T>> Split<T>(
IEnumerable<T> source)
{
using (var enumerator = source.GetEnumerator())
{
while (enumerator.MoveNext())
{
T first = enumerator.Current;
if (enumerator.MoveNext())
{
T second = enumerator.Current;
yield return Tuple.Create(first, second);
}
}
}
}
The same can be applied to the first part of the problem (extracting half hourly TimeIntervals from the list of strings):
IEnumerable<TimeInterval> ToHalfHourlyIntervals(
IEnumerable<string> inputLines)
{
return
from triple in TripleSplit(inputLines)
select new TimeInterval
{
Start = DateTime.Parse(triple.Item1.Replace("Start: ", "")),
End = DateTime.Parse(triple.Item2.Replace("End: ", "")),
Value = Int32.Parse(triple.Item3)
};
}
Here I make use of a custom TripleSplit method that returns a Tuple<T, T, T> (which will be easy to write). With this in place, the complete solution would look like this:
// Read data lazilzy from disk (or any other source)
var lines = File.ReadLines(path);
var halfHourlyIntervals = ToHalfHourlyIntervals(lines);
var hourlyIntervals = ToHourlyIntervals(halfHourlyIntervals);
foreach (var interval in hourlyIntervals)
{
// process
}
What's nice about this solution is that it is completely deferred. It processes one line at a time, which allows you to process indefinately big sources without the danger of any out of memory exception, which seems important considering your given requirement:
This data keeps going for a week then 30 days and 365days.

Categories

Resources