C# - group list by TimeSpan starting from specific start point - c#

I want to group my list by time step (hour, day, week etc.) and count sum for each group but starting from specific time.
Now I've got input list:
TIME VALUE
11:30 2
11:50 2
12:00 6
12:30 10
12:50 2
and hour step
var timeStep=new TimeSpan(1,0,0);
and I'm grouping my list with something like this
var myList = list.GroupBy(x =>
{
return x.Time.Ticks / timeStep.Ticks;
})
.Select(g => new { Time = new DateTime(g.Key * timeStep.Ticks), Value = g.Sum(x => x.Value) }).ToList();
It works fine (also for any other step, e.g. daily, weekly) and gives result:
TIME SUM
11:00 4
12:00 18
But now I have to group my list with hour step but starting from e.g. 30 minute of hour, so what can I do to have something like this:
TIME SUM
11:30 10
12:30 12

It is preferable to use a custom DateTme comparer:
internal class DateTimeComparer : IEqualityComparer<DateTime>
{
public bool Equals(DateTime x, DateTime y)
{
return GetHashCode(x) == GetHashCode(y);
// In general, this shouldn't be written (because GetHashCode(x) can equal GetHashCode(y) even if x != y (with the given comparer)).
// But here, we have: x == y <=> GetHashCode(x) == GetHashCode(y)
}
public int GetHashCode(DateTime obj)
{
return (int)((obj - new TimeSpan(0, 30, 0)).Ticks / new TimeSpan(1, 0, 0).Ticks);
}
}
with:
var myList = list.GroupBy(x => x.Time, new DateTimeComparer())
.Select(g => new { Time = g.Key, Value = g.Sum(x => x.Value) }).ToList();

Related

Group dateTime by hour range

I got a list like this:
class Article
{
...
Public DateTime PubTime{get;set}
...
}
List<Article> articles
Now I want to group this list with hour range :[0-5,6-11,12-17,18-23]
I know there is a cumbersome way to do this:
var firstRange = articles.Count(a => a.PubTime.Hour >= 0 && a.PubTime.Hour <= 5);
But I want to use a elegant way. How can I do that?Use Linq Or anything others?
Group by Hour / 6:
var grouped = articles.GroupBy(a => a.PubTime.Hour / 6);
IDictionary<int, int> CountsByHourGrouping = grouped.ToDictionary(g => g.Key, g => g.Count());
The key in the dictionary is the period (0 representing 0-5, 1 representing 6-11, 2 representing 12-17, and 3 representing 18-23). The value is the count of articles in that period.
Note that your dictionary will only contain values where those times existed in the source data, so it won't always contain 4 items.
You could write a CheckRange Function, which takes your values and returns a bool. To make your code more reusable and elegant.
Function Example:
bool CheckRange (this int number, int min, int max)
=> return (number >= min && number <= max);
You could now use this function to check if the PubTime.Hour is in the correct timelimit.
Implementation Example:
var firstRange = articles.Count(a => a.CheckRange(0, 5));

c# Create list of selected time intervals

I'm trying to create a downtown that will display times from 00:00 to 24:59 at a selected number of intervals (and this could change from 5 minutes to 10 minutes etc
so for example a list of
00:10
00:20
00:30
or could be
00:15
00:30
I'm using the following which works, but only for a selected number of iterations (33):
List<string> query = Enumerable
.Range(0, 33)
.Select(i => DateTime.Today
.AddHours(0)
.AddMinutes(i * (double)IntervalParameter)
.ToString())
.ToList();
*IntervalParameter = 10 for the example above.
I'm looking to adapt this so it runs the full 24 hours time frame. Just looking for the most efficient way to do this.
Why not compute the number of items?
int IntervalParameter = 5;
// .Range(0, 1440 / IntervalParameter) - see Zohar Peled's comment -
// is brief, but less readable
List<string> query = Enumerable
.Range(0, (int) (new TimeSpan(24, 0, 0).TotalMinutes / IntervalParameter))
.Select(i => DateTime.Today
.AddMinutes(i * (double)IntervalParameter) // AddHours is redundant
.ToString("HH:mm")) // Let's provide HH:mm format
.ToList();
Something like this?
public static IEnumerable<TimeSpan> Intervals(TimeSpan inclusiveStart, TimeSpan exclusiveEnd, TimeSpan increment)
{
for (var time = inclusiveStart; time < exclusiveEnd; time += increment)
yield return time;
}
Example usage:
foreach (var time in Intervals(TimeSpan.Zero, TimeSpan.FromDays(1), TimeSpan.FromMinutes(15)))
{
Console.WriteLine(time);
}

Linq filter by time range

I'm trying to build a Linq query that filter between 2 times of day.
First I need to filter between dates (ie.: 12/26/2013 to 01/26/2014) and after this search, the results may be filtered between times (ie.: records between 19:00 and 7:00).
I've tried with this query but it does not works:
orders = CurrentOrders.Where(o => o.ImportedOn >= dateFrom && o.ImportedOn <= dateTo);
orders = orders.Where(o => o.ImportedOn.TimeOfDay >= tFrom && o.ImportedOn.TimeOfDay <= tTo);
tFrom and tTo both are TimeSpan.
Any help?
Regards
Edit: Hi I'm editing my own question just to clarify the problem and attach a solution that I've found (I'm not sure if it's the most efficient) :
When I say "does not work" I mean "no records are returned that matched the criteria".
The previous query works only for time between the same day (ie: from 15:00 to 20:00). The problem is when the time span to the next day, for example form 19:00pm to 7:00am.
The solution that I propose is add a simple if to check if tFrom is less than tTo and otherwise add 1 to date in the search:
if (tFrom < tTo)
{
orders = orders.Where(o => o.ImportedOn.TimeOfDay >= tFrom && o.ImportedOn.TimeOfDay <= tTo);
}
else
{
orders = orders.Where(o => o.ImportedOn.TimeOfDay <= tFrom && o.ImportedOn.AddDays(1).TimeOfDay <= tTo);
}
I don't think your queries will give you results when the time range is 9 PM to 4 AM for example. Let's say that ImportedOn is 7 PM and you are checking that 7 PM is less than 9 PM, which is OK, and also checking that 7 PM is less than 4 AM, which is false. I don't see any difference if you add a day because you are only considering the time. Adding a date doesn't change the time.
My proposal is to create two time intervals when the time from is greater than time to (9 PM to 4 AM for example).
I created an extension method for DateTime so we can check if the date belongs to a time range.
public static bool IsInTimeRange(this DateTime obj, DateTime timeRangeFrom, DateTime timeRangeTo)
{
TimeSpan time = obj.TimeOfDay, t1From = timeRangeFrom.TimeOfDay, t1To = timeRangeTo.TimeOfDay;
// if the time from is smaller than the time to, just filter by range
if (t1From <= t1To)
{
return time >= t1From && time <= t1To;
}
// time from is greater than time to so two time intervals have to be created: one {timeFrom-12AM) and another one {12AM to timeTo}
TimeSpan t2From = TimeSpan.MinValue, t2To = t1To;
t1To = TimeSpan.MaxValue;
return (time >= t1From && time <= t1To) || (time >= t2From && time <= t2To);
}
Edited: Note that it is not necessary to compare time with t2From and t1To because the comparison is always gonna be true but it makes the code easier to read because it explicitly checks that the date belongs to one of the two intervals.
I also wrote these unit tests:
[TestMethod]
public void TimeRangeFilter_timeFrom_is_smaller_than_timeTo()
{
// arrange
List<DateTime> dates = new List<DateTime>()
{
DateTime.Today.AddHours(2), // 2 AM
DateTime.Today.AddHours(9), // 9 AM
DateTime.Today.AddHours(12), // 12 PM
DateTime.Today.AddHours(15), // 3 PM
DateTime.Today.AddHours(18), // 6 PM
DateTime.Today.AddHours(23).AddMinutes(50), // 11:50 PM
DateTime.Today, // 0 AM
};
// interval: 10 AM to 4 PM
DateTime timeFrom = DateTime.Today.AddHours(10), timeTo = DateTime.Today.AddHours(16);
// act
var datesInPeriod = dates.Where(p => p.IsInTimeRange(timeFrom, timeTo));
// assert
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 2));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 9));
Assert.IsTrue(datesInPeriod.Any(p => p.Hour == 12));
Assert.IsTrue(datesInPeriod.Any(p => p.Hour == 15));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 18));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 23));
}
[TestMethod]
public void TimeRangeFilter_timeFrom_is_greater_than_timeTo()
{
// arrange
List<DateTime> dates = new List<DateTime>()
{
DateTime.Today.AddHours(2), // 2 AM
DateTime.Today.AddHours(9), // 9 AM
DateTime.Today.AddHours(12), // 12 PM
DateTime.Today.AddHours(15), // 3 PM
DateTime.Today.AddHours(18), // 6 PM
DateTime.Today.AddHours(23).AddMinutes(50), // 11:50 PM
DateTime.Today, // 0 AM
};
// interval: 10 PM to 4 AM
DateTime timeFrom = DateTime.Today.AddHours(22), timeTo = DateTime.Today.AddHours(4);
// act
var datesInPeriod = dates.Where(p => p.IsInTimeRange(timeFrom, timeTo));
// assert
Assert.IsTrue(datesInPeriod.Any(p => p.Hour == 2));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 9));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 12));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 15));
Assert.IsFalse(datesInPeriod.Any(p => p.Hour == 18));
Assert.IsTrue(datesInPeriod.Any(p => p.Hour == 23));
}
ImportedOn.TimeOfDay
returns just a Time. For example:
DateTime t = DateTime.Now;
System.Diagnostics.Debug.WriteLine(t.ToString());
var t2 = t.TimeOfDay;
System.Diagnostics.Debug.WriteLine(t2.ToString());
returns:
02.06.2014 11:48:33
11:48:33.6671525
So you can just compare the Dates, there is no need of TimeOfDay.
Linq to SQL: how to query a time range in a DateTime field
If you are tring to select on a table the records that have a DateTime field (in this example called DateFiled) between two DateTime value, if you specify:
DateFiled >= dateValue && DateFiled <= dateValue
you select all the record between this value, but if you need to select only the records that between these two dates correspond to a specific time range, you’ll need to add a column that computes the hour.
If you do not want to add it to the database, you will need to add it to your model and set it with the same value as the DateField field. For example:
public async Task<List<MyModel>> GetFilteredResultByDateAndTime
(DateTime startDate, DateTime endDate)
{
var result = from mytable in _context.MyTable
where
mytable.DateField >= startDate.Date
&& mytable.DateField <= endDate.Date
select new MyModel
{
DateField = mytable.DateField.Date,
DateFieldTime = mytable.DateField,
// Other fields of the model
};
// Now you can filter the result by the time
var filteredResult = from r in result
where
r.DateFieldTime.TimeOfDay >= startDate.TimeOfDay
&& r.DateFieldTime.TimeOfDay <= endDate.TimeOfDay
select r;
return await filteredResult.ToListAsync();
}

LINQ Grouping by Sum Value

Say I have a class like so:
public class Work
{
public string Name;
public double Time;
public Work(string name, double time)
{
Name = name;
Time = time;
}
}
And I have a List<Work> with about 20 values that are all filled in:
List<Work> workToDo = new List<Work>();
// Populate workToDo
Is there any possible way that I can group workToDo into segments where each segments sum of Time is a particular value? Say workToDo has values like so:
Name | Time
A | 3.50
B | 2.75
C | 4.25
D | 2.50
E | 5.25
F | 3.75
If I want the sum of times to be 7, each segment or List<Work> should have a bunch of values where the sum of all the Times is 7 or close to it. Is this even remotely possible or is it just a stupid question/idea? I am using this code to separate workToDo into segments of 4:
var query = workToDo.Select(x => x.Time)
.Select((x, i) => new { Index = i, Value = x})
.GroupBy(y => y.Index / 4)
.ToList();
But I am not sure how to do it based on the Times.
Here's a query that segments your data in groups where the times are near to 7, but not over:
Func<List<Work>,int,int,double> sumOfRange = (list, start, end) => list
.Skip(start)
.TakeWhile ((x, index) => index <= end)
.ToList()
.Sum (l => l.Time);
double segmentSize = 7;
var result = Enumerable.Range(0, workToDo.Count ())
.Select (index => workToDo
.Skip(index)
.TakeWhile ((x,i) => sumOfRange(workToDo, index, i)
<= segmentSize));
The output for your example data set is:
A 3.5
B 2.75
total: 6.25
B 2.75
C 4.25
total: 7
C 4.25
D 2.5
total: 6.75
D 2.5
total: 2.5
E 5.25
total: 5.25
F 3.75
total: 3.75
If you want to allow a segments to total over seven, then you could increase the segmentSize variable by 25% or so (i.e. make it 8.75).
This solution recurses through all combinations and returns the ones whose sums are close enough to the target sum.
Here is the pretty front-end method that lets you specify the list of work, the target sum, and how close the sums must be:
public List<List<Work>> GetCombinations(List<Work> workList,
double targetSum,
double threshhold)
{
return GetCombinations(0,
new List<Work>(),
workList,
targetSum - threshhold,
targetSum + threshhold);
}
Here is the recursive method that does all of the work:
private List<List<Work>> GetCombinations(double currentSum,
List<Work> currentWorks,
List<Work> remainingWorks,
double minSum,
double maxSum)
{
// Filter out the works that would go over the maxSum.
var newRemainingWorks = remainingWorks.Where(x => currentSum + x.Time <= maxSum)
.ToList();
// Create the possible combinations by adding each newRemainingWork to the
// list of current works.
var sums = newRemainingWorks
.Select(x => new
{
Works = currentWorks.Concat(new [] { x }).ToList(),
Sum = currentSum + x.Time
})
.ToList();
// The initial combinations are the possible combinations that are
// within the sum range.
var combinations = sums.Where(x => x.Sum >= minSum).Select(x => x.Works);
// The additional combinations get determined in the recursive call.
var newCombinations = from index in Enumerable.Range(0, sums.Count)
from combo in GetCombinations
(
sums[index].Sum,
sums[index].Works,
newRemainingWorks.Skip(index + 1).ToList(),
minSum,
maxSum
)
select combo;
return combinations.Concat(newCombinations).ToList();
}
This line will get combinations that sum to 7 +/- 1:
GetCombinations(workToDo, 7, 1);
What you are describing is a packing problem (where the tasks are being packed into 7-hour containers). Whilst it would be possible to use LINQ syntax in a solution to this problem, there is no solution inherent in LINQ that I am aware of.

Grouping by every n minutes

I'm playing around with LINQ and I was wondering how easy could it be to group by minutes, but instead of having each minute, I would like to group by every 5 minutes.
For example, currently I have:
var q = (from cr in JK_ChallengeResponses
where cr.Challenge_id == 114
group cr.Challenge_id
by new { cr.Updated_date.Date, cr.Updated_date.Hour, cr.Updated_date.Minute }
into g
select new {
Day = new DateTime(g.Key.Date.Year, g.Key.Date.Month, g.Key.Date.Day, g.Key.Hour, g.Key.Minute, 0),
Total = g.Count()
}).OrderBy(x => x.Day);
What do I have to do to group my result for each 5 minutes?
To group by n of something, you can use the following formula to create "buckets":
((int)(total / bucket_size)) * bucket_size
This will take the total, divide it, cast to integer to drop off any decimals, and then multiply again, which ends up with multiples of bucket_size. So for instance (/ is integer division, so casting isn't necessary):
group cr.Challenge_id
by new { cr.Updated_Date.Year, cr.Updated_Date.Month,
cr.Updated_Date.Day, cr.Updated_Date.Hour,
Minute = (cr.Updated_Date.Minute / 5) * 5 }
//Data comes for every 3 Hours
where (Convert.ToDateTime(capcityprogressrow["Data Captured Date"].ToString()).Date != DateTime.Now.Date || (Convert.ToDateTime(capcityprogressrow["Data Captured Date"].ToString()).Date == DateTime.Now.Date && (Convert.ToInt16(capcityprogressrow["Data Captured Time"])) % 3 == 0))
group capcityprogressrow by new { WCGID = Convert.ToInt32(Conversions.GetIntEntityValue("WCGID", capcityprogressrow)), WCGName = Conversions.GetEntityValue("WIRECENTERGROUPNAME", capcityprogressrow).ToString(), DueDate = Convert.ToDateTime(capcityprogressrow["Data Captured Date"]), DueTime = capcityprogressrow["Data Captured Time"].ToString() } into WCGDateGroup
// For orderby with acsending order
.OrderBy(x => x.WcgName).ThenBy(x => x.DataCapturedDateTime).ThenBy(x => x.DataCapturedTime).ToList();

Categories