C# Method to Compare DateTime Fields Specified During Execution? - c#

My project has many objects with date fields, and I often need to select everything where one such field is within a date range.
For example:
public class Contract
{
public DateTime SignDate { get; set; }
public DateTime ReleaseDate { get; set; }
}
public class PersonalCheck
{
public DateTime SignDate { get; set; }
public DateTime ProcessDate { get; set; }
public DateTime VoidDate { get; set; }
}
If I only cared about SignDate, it would be easy. I would declare an Interface...
public interface IObjectWithSignDate
{
DateTime SignDate { get; set; }
}
...change my other objects to inherit from it, then create a method like this:
public static IQueryable<T> SignedWithin<T>(this IQueryable<T> items, DateTime start, DateTime end) where T : IObjectWithSignDate
{
return items.Where(q => q.SignDate >= start && q.SignDate <= end);
}
How can I avoid rewriting this function for ReleaseDate, ProcessDate, VoidDate, etc.? Can I make this method take in an IQueryable of any object and a variable telling it which date field to run this selector against?
Note this would have to be able to a) execute in LinqToEntities to run against a database and b) not add a lot of overhead (as I'm fearful reflection might do)

Simple but specific
You can add an extension method like this:
public static class DateTimeExtensions
{
public static bool IsBetween(this DateTime thisDateTime, DateTime start, DateTime end)
{
return thisDateTime >= start && thisDateTime <= end;
}
}
which you can unit test in isolation.
Then you can use this on whichever DateTime field you want to check. For example:
var start = new DateTime(2017, 1, 1);
var end = new DateTime(2017, 12, 31, 23, 59, 59);
IList<Contract> contracts = new List<Contract>(); // or anything enumerable
var contractsSignedBetween = contracts.Where(x => x.SignDate.IsBetween(start, end));
var contractsReleasedBetween = contracts.Where(x => x.ReleaseDate.IsBetween(start, end));
(Notice how I set the start datetime to have 00:00:00 time, and the end datetime to have 23:59:59 time [feel free to include milliseconds as well], so that times within the last day are included.)
Making that reusable
If you find yourself needing to do that a lot, you could do an extension for that
public static class EnumerableContractsExtensions
{
public static IEnumerable<Contract> SignedBetween(this IEnumerable<Contract> contracts, DateTime start, DateTime end)
{
return contracts.Where(x => x.SignDate.IsBetween(start, end));
}
}
and use it like this
var contractsSignedBetween = contracts.SignedBetween(start, end);
which could also be unit tested in isolation.
More flexible but specific
Use an expression to say which date you want...
public static class EnumerableContractsExtensions
{
public static IEnumerable<Contract> Between(this IEnumerable<Contract> contracts, Func<Contract, DateTime> selector, DateTime start, DateTime end)
{
return contracts.Where(x => selector(x).IsBetween(start, end));
}
}
and then do:
var contractsSignedBetween = contracts.Between(x => x.SignDate, start, end);
var contractsReleasedBetween = contracts.Between(x => x.ReleaseDate, start, end);
Flexible and generic
Go the whole hog and do it generically (although you can't make it an extension method since it's generic):
public static class EnumerableExtensions
{
public static IEnumerable<T> Between<T>(IEnumerable<T> items, Func<T, DateTime> selector, DateTime start, DateTime end)
{
return items.Where(x => selector(x).IsBetween(start, end));
}
}
again, this is testable in its own right, and can be used like this:
IList<Contract> contracts = new List<Contract>();
IList<PersonalCheck> personalChecks = new List<PersonalCheck>();
var contractsSignedBetween = EnumerableExtensions.Between(contracts, x => x.SignDate, start, end);
var checksSignedBetween = EnumerableExtensions.Between(personalChecks, x => x.SignDate, start, end);
Making it IQueryable
To make this work as IQueryable the approach needs to shift to an expression tree, since LINQ to Entities does not know how to translate a method into SQL.
public static IQueryable<TSource> Between<TSource, TKey>(
this IQueryable<TSource> source,
Expression<Func<TSource, TKey>> keySelector,
TKey low,
TKey high)
where TKey : IComparable<TKey>
{
Expression key = keySelector.Body;
Expression lowerBound = Expression.LessThanOrEqual(Expression.Constant(low), key);
Expression upperBound = Expression.LessThanOrEqual(key, Expression.Constant(high));
Expression and = Expression.AndAlso(lowerBound, upperBound);
Expression<Func<TSource, bool>> lambda =
Expression.Lambda<Func<TSource, bool>>(and, keySelector.Parameters);
return source.Where(lambda);
}
which would still be used like this:
var contractsSignedBetween = contracts.Between(x => x.SignDate, start, end);
And this works for things other than DateTimes as well. Hope this helps.

Related

Join an IEnumerable in C# when the key selector shall return a set of keys

I've a container of data called Meeting
public struct Meeting
{
public string Id { get; set; }
public DateTime Start { get; set; }
public DateTime End { get; set; }
}
This is very simple. It has an Id, and two date times marking the time when this meeting starts and the date time when this meeting end.
I need to return a Lookup that for each day maps all the meeting scheduled for that day.
I've implemented this IEnumerable extension:
public static IEnumerable<ILookup<D, T>> GroupBy<T, D> (this IEnumerable<T> outer,
Func<T, IEnumerable<D>> func)
{
if (outer == null)
throw new ArgumentNullException(nameof(outer));
if (func == null)
throw new ArgumentNullException(nameof(func));
Dictionary<D, IEnumerable<T>> lookup = new Dictionary<D, IEnumerable<T>>();
foreach(T item in outer)
{
IEnumerable<D> keySelectory = func(item);
if (keySelectory != null)
{
foreach(D newKey in keySelectory)
{
if (!lookup.ContainsKey(newKey))
{
lookup.Add(newKey, new List<T>() { item });
}
else
{
List<T> temp = lookup[newKey].ToList();
temp.Add(item);
lookup[newKey] = temp;
}
}
}
}
return WHAT?
}
I'm lost at the end because the Lookup constructor is kept private so I cannot instantiate it.
Because I would like to call something like that:
meetings.GroupBy(meeting => GetDatesFromStartAndEndTimes(meeting));
where the method GetDatesFromStartAndEndTimes is trivial.
Can anyone tell me how to proceed? Thanks.
If you're just wondering how to create a Lookup, you use the LINQ ToLookup extension method:
var meetingsByDate = meetings
.SelectMany(meeting => GetDatesFromStartAndEndTimes(meeting),
(meeting, date) => new{meeting, date})
.ToLookup(pair => pair.date, pair.meeting);

Generating a parameter for Linq to handle ranges in dates

I want to achieve the following expression or as near as possible.
IRangePredicate range = new Range();
DbContext context = new dbModel();
context.Table1.Where(x => range.IsInRange(x.CreatedAt) && x.type == 1).ToList();
and range will produce a partial expression for the linq query, that can either be resolved as:
CreatedAt >= from && CreatedAt <= to
Or
CreatedAt >= from
Or
CreatedAt <= To
to be used in the linq query.
Eventually, I would like to extend this method to include the possibilities of less or more without equals as well.
and use it as a sort of "arguement dependency injection".
However, my attempts fail to even compile, as either
Expression<Func<DateTime, bool>> can't be used as a partial parameter, and I need to define the following query for these special filters. Which I don't want to do. I want it to read as "normal" Linq.
Or I need to simply insert them as Func only. Which might work, but as soon as I try to do that on a Context Linq Query, the thing explodes, because Entity Framework, does not play well if it is not formatted as an Expression
Can anyone guide me in the right direction?
Example of what I tried: (Please note this does not compile, because that is my entire issue :D )
EDIT From here:
-I have commented out the line of code that doesn't compile, so you have a compilable example. It just doesn't work if you try to do it on a DbContext set.
public interface IRangeFunctional
{
bool GetRange(DateTime param);
}
public interface IRange
{
Expression<Func<DateTime, bool>> GetRange(DateTime param);
}
public class RangeFunctional : IRangeFunctional
{
private DateTime _from;
private DateTime _to;
public RangeFunctional(DateTime from, DateTime to)
{
_from = from;
_to = to;
}
public bool GetRange(DateTime param)
{
return param >= _from && param <= _to;
}
}
public class Range : IRange
{
private DateTime _from;
private DateTime _to;
public Range(DateTime from, DateTime to)
{
_from = from;
_to = to;
}
public Expression<Func<DateTime, bool>> GetRange(DateTime param)
{
return (x => param >= _from && param <= _to);
}
}
public class Invoice
{
public DateTime CreatedAt { get; set; }
public int typeId { get; set; }
}
[TestClass]
public class TestRange
{
List<Invoice> list = new List<Invoice>()
{
new Invoice()
{
CreatedAt = new DateTime(2018,1,1,0,0,0), typeId = 1
},
new Invoice()
{
CreatedAt = new DateTime(2018,1,2,0,0,0), typeId = 1
},
new Invoice()
{
CreatedAt = new DateTime(2018,1,1,0,0,0), typeId = 2
},
new Invoice()
{
CreatedAt = new DateTime(2018,1,2,0,0,0), typeId = 2
}
};
[TestMethod]
public void RangeTest()
{
Range r = new Range(new DateTime(2018, 1, 1, 0, 0, 0), new DateTime(2018, 1, 2, 0, 0, 0));
RangeFunctional rf = new RangeFunctional(new DateTime(2018, 1, 1, 0, 0, 0), new DateTime(2018, 1, 2, 0, 0, 0));
List<Invoice> partialListFunc = list.Where(x => x.typeId == 2 && rf.GetRange(x.CreatedAt)).ToList();
//List<Invoice> partialList = list.Where(x => x.typeId == 2 && r.GetRange(x.CreatedAt)).ToList();
Assert.AreEqual(2, partialListFunc.Count);
}
}
Okay, So I added the base method that game me the idea, as a demo example, where I am just using ordinary "bool" to accomplish a pure search by link in generic collections.
However, I want to reuse this logic, or as close as possibe, to allow me to accomplish this towards a DbContext.
I have a base crud controller for any type of table towards the Db, however, I would like to enhance this bit, but letting the programmer to implement a strategy pattern over a partial classes generated from either code first, or db first models in C#.
However, in order to translate the Linq to SQL, I need to convert my "just bool" return type into expressions. I got that far. But How the heck do I make "subsets" of predicates, that can be unified over a single collection?
I see some code examples that require you to chain the queries. And that might end up being the solution. This just seems so... ugly.
I just can't get my brain to think up the syntax to do this. And it is frustrating me :D Forgive me if this cannot be done, because I am simply stupid. It just seems sort of intuitive to me that this should be possible.
Here is a sample implementation. It uses an extension method for modifying Expressions to build a new Expression:
public static class ExpressionExt {
/// <summary>
/// Replaces a sub-Expression with another Expression inside an Expression
/// </summary>
/// <param name="orig">The original Expression.</param>
/// <param name="from">The from Expression.</param>
/// <param name="to">The to Expression.</param>
/// <returns>Expression with all occurrences of from replaced with to</returns>
public static Expression Replace(this Expression orig, Expression from, Expression to) => new ReplaceVisitor(from, to).Visit(orig);
}
/// <summary>
/// Standard ExpressionVisitor to replace an Expression with another in an Expression.
/// </summary>
public class ReplaceVisitor : ExpressionVisitor {
readonly Expression from;
readonly Expression to;
public ReplaceVisitor(Expression from, Expression to) {
this.from = from;
this.to = to;
}
public override Expression Visit(Expression node) => node == from ? to : base.Visit(node);
}
Now you can build a filter interface and some implementations to represent differing types of filters and an IQueryable extension that uses it to filter:
public interface IFilter<TMember> {
Expression<Func<TData, bool>> FilterFn<TData>(Expression<Func<TData, TMember>> memberFn);
}
public class FilterDateTimeRange : IFilter<DateTime?> {
public DateTime? from;
public DateTime? to;
public FilterDateTimeRange(DateTime? fromDT, DateTime? toDT) {
from = fromDT;
to = toDT;
}
public Expression<Func<T, bool>> FilterFn<T>(Expression<Func<T, DateTime?>> memberFn) {
Expression<Func<DateTime?, bool>> rangeBodyTemplate;
if (from.HasValue) {
if (to.HasValue)
rangeBodyTemplate = dt => from.Value <= dt && dt <= to.Value;
else
rangeBodyTemplate = dt => from.Value <= dt;
}
else if (to.HasValue) {
rangeBodyTemplate = dt => dt <= to.Value;
}
else
rangeBodyTemplate = dt => true;
return Expression.Lambda<Func<T, bool>>(rangeBodyTemplate.Body.Replace(rangeBodyTemplate.Parameters[0], memberFn.Body), memberFn.Parameters);
}
}
public class FilterDateRange : IFilter<DateTime?> {
public DateTime? from;
public DateTime? to;
public FilterDateRange(DateTime? fromDT, DateTime? toDT) {
from = fromDT?.Date;
to = toDT?.Date;
}
public Expression<Func<T, bool>> FilterFn<T>(Expression<Func<T, DateTime?>> memberFn) {
Expression<Func<DateTime?, bool>> rangeBodyTemplate;
if (from.HasValue) {
if (to.HasValue)
rangeBodyTemplate = dt => from <= (dt == null ? dt : dt.Value.Date) && (dt == null ? dt : dt.Value.Date) <= to;
else
rangeBodyTemplate = dt => from.Value <= (dt == null ? dt : dt.Value.Date);
}
else if (to.HasValue) {
rangeBodyTemplate = dt => (dt == null ? dt : dt.Value.Date) <= to.Value;
}
else
rangeBodyTemplate = dt => true;
return Expression.Lambda<Func<T, bool>>(rangeBodyTemplate.Body.Replace(rangeBodyTemplate.Parameters[0], memberFn.Body), memberFn.Parameters);
}
}
public class FilterStartsWith : IFilter<String> {
public string start;
public FilterStartsWith(string start) => this.start = start;
public Expression<Func<T, bool>> FilterFn<T>(Expression<Func<T, string>> memberFn) {
Expression<Func<string, bool>> rangeBodyTemplate;
if (!String.IsNullOrEmpty(start))
rangeBodyTemplate = s => s.StartsWith(start);
else
rangeBodyTemplate = s => true;
return Expression.Lambda<Func<T, bool>>(rangeBodyTemplate.Body.Replace(rangeBodyTemplate.Parameters[0], memberFn.Body), memberFn.Parameters);
}
}
public static class FilterExt {
public static IQueryable<TData> WhereFilteredBy<TData, TMember>(this IQueryable<TData> src, IFilter<TMember> r, Expression<Func<TData, TMember>> memberFn) => src.Where(r.FilterFn(memberFn));
}
Given all this, you use it like so:
var r1 = new FilterDateTimeRange(DateTime.Now.AddDays(-1).Date, DateTime.Now.AddDays(-1).Date);
var yesterdayFilter = new FilterDateRange(DateTime.Now.AddDays(-1), DateTime.Now.AddDays(-1));
var r1a = Accounts.Where(r1.RangeFilter<Accounts>(a => a.Modified_date));
var ya = Accounts.WhereFilteredBy(yesterdayFilter, a => a.Modified_date);
Since the C# type inference engine isn't as sophisticated as e.g. F# and won't infer through return expressions, you must specify the type when using the standard Where but an IQueryable extension replacement Where can infer the type from the first parameter (e.g. Accounts).
Since the IFilter is generic, you can use other types of filters such as FilterStartsWith to filter on other types of fields:
List<Table1> Table1InRangeWithName(IFilter<DateTime?> range, IFilter<string> name) => context.Table1.WhereFilteredBy(range, t1 => t1.Modified_date).WhereFilteredBy(name, t1 => t1.Name).ToList();
And then call it with a pre-created FilterDataRange and FilterStartsWith:
var nameFilter = new FilterStartsWith("TEST");
var ans = Table1InRangeWithName(yesterdayFilter, nameFilter);

Streamline Date Parsing into method

I'm trying to think of a cleaner way to do this. I want to move this into a helper method but I do not like using out params. I realize I have to use out params for the TryParse, I don't have a choice but I'd like to get this into some kind of reusable method:
startDate and endDate are in "yyyy/mm/dd" format and are string to begin with and I'm parsing below.
DateTime startDt;
DateTime endDt;
startDt = (DateTime.TryParse(startDate, out startDt) ? startDt : DateTime.Now);
endDt = ((!DateTime.TryParse(endDate, out endDt) || string.IsNullOrEmpty(endDate))) ? (startDt.AddMinutes(Configuration.Instance.RecentOrdersWindowDurationMinutes)) : endDt;
Well, if you want two results from a single method you have to either use out parameters or some wrapping type. If you're lazy and only doing this once, you could just use Tuple<DateTime, DateTime>.
public Tuple<DateTime, DateTime> GetRange(string startDate, string endDate)
{
DateTime startDt;
DateTime endDt;
if (!DateTime.TryParse(startDate, out startDt))
startDate = DateTime.Now;
if (string.IsNullOrEmpty(endDate) || !DateTime.TryParse(endDate, out endDt))
endDt = startDt.AddMinutes(Configuration.Instance.RecentOrdersWindowDurationMinutes);
return new Tuple<DateTime, DateTime>(startDt, endDt);
}
If you're looking for the more generalized case, dealing with just one at a time, you could write a TryParse that accepts a default value.
public DateTime TryParseOrDefault(string str, DateTime def)
{
DateTime ret;
if (DateTime.TryParse(str, out ret))
return ret;
else
return def;
{
Edit:
In re to a comment, your concrete type would just look something like this:
public class DateRange
{
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
Then you could use that instead of the Tuple<DateTime, DateTime>.
You can declare your own TryParse helper method, but instead of returning bool and setting the actual DateTime value using out parameter, you can return DateTime?, which will let you set default value quite nicely using ?? operator:
public static class DateTimeUtils
{
public static DateTime? TryParse(string value)
{
DateTime result;
if (!DateTime.TryParse(value, out result))
return null;
return result;
}
}
var myString = "01/01/2015";
var myDateTime = DateTimeUtils.TryParse(myString) ?? DateTime.Now;
This way you don't have to declare myDateTime before calling TryParse, like you'd have to do with standard DateTime.TryParse method.
In your case it would be something like:
DateTime startDt = DateTimeUtils.TryParse(startDate) ?? DateTime.Now;
DateTime endDt = DateTimeUtils.TryParse(endDate) ?? startDt.AddMinutes(Configuration.Instance.RecentOrdersWindowDurationMinutes);

How to know if a DateTime is between a DateRange in C#

I need to know if a Date is between a DateRange. I have three dates:
// The date range
DateTime startDate;
DateTime endDate;
DateTime dateToCheck;
The easy solution is doing a comparison, but is there a smarter way to do this?
Nope, doing a simple comparison looks good to me:
return dateToCheck >= startDate && dateToCheck < endDate;
Things to think about though:
DateTime is a somewhat odd type in terms of time zones. It could be UTC, it could be "local", it could be ambiguous. Make sure you're comparing apples with apples, as it were.
Consider whether your start and end points should be inclusive or exclusive. I've made the code above treat it as an inclusive lower bound and an exclusive upper bound.
Usually I create Fowler's Range implementation for such things.
public interface IRange<T>
{
T Start { get; }
T End { get; }
bool Includes(T value);
bool Includes(IRange<T> range);
}
public class DateRange : IRange<DateTime>
{
public DateRange(DateTime start, DateTime end)
{
Start = start;
End = end;
}
public DateTime Start { get; private set; }
public DateTime End { get; private set; }
public bool Includes(DateTime value)
{
return (Start <= value) && (value <= End);
}
public bool Includes(IRange<DateTime> range)
{
return (Start <= range.Start) && (range.End <= End);
}
}
Usage is pretty simple:
DateRange range = new DateRange(startDate, endDate);
range.Includes(date)
You could use extension methods to make it a little more readable:
public static class DateTimeExtensions
{
public static bool InRange(this DateTime dateToCheck, DateTime startDate, DateTime endDate)
{
return dateToCheck >= startDate && dateToCheck < endDate;
}
}
Now you can write:
dateToCheck.InRange(startDate, endDate)
You can use:
return (dateTocheck >= startDate && dateToCheck <= endDate);
I’ve found the following library to be the most helpful when doing any kind of date math. I’m still amazed nothing like this is part of the .Net framework.
http://www.codeproject.com/Articles/168662/Time-Period-Library-for-NET
Following on from Sergey's answer, I think this more generic version is more in line with Fowler's Range idea, and resolves some of the issues with that answer such as being able to have the Includes methods within a generic class by constraining T as IComparable<T>. It's also immutable like what you would expect with types that extend the functionality of other value types like DateTime.
public struct Range<T> where T : IComparable<T>
{
public Range(T start, T end)
{
Start = start;
End = end;
}
public T Start { get; }
public T End { get; }
public bool Includes(T value) => Start.CompareTo(value) <= 0 && End.CompareTo(value) >= 0;
public bool Includes(Range<T> range) => Start.CompareTo(range.Start) <= 0 && End.CompareTo(range.End) >= 0;
}
In case anyone wants it as a Validator
using System;
using System.ComponentModel.DataAnnotations;
namespace GROOT.Data.Validation;
internal class DateRangeAttribute : ValidationAttribute
{
public string EndDate;
public string StartDate;
public override bool IsValid(object value)
{
return (DateTime)value >= DateTime.Parse(StartDate) && (DateTime)value <= DateTime.Parse(EndDate);
}
}
Usage
[DateRange(
StartDate = "01/01/2020",
EndDate = "01/01/9999",
ErrorMessage = "Property is outside of range")
]

Sum of TimeSpans in C#

I have a collection of objects that include a TimeSpan variable:
MyObject
{
TimeSpan TheDuration { get; set; }
}
I want to use LINQ to sum those times.
Of course, (from r in MyCollection select r.TheDuration).Sum(); doesn't work!
I'm thinking of changing the datatype of TheDuration to an int and then summing it and converting the sum to a TimeSpan. That will be messy because each TheDuration in my collection is used in as a timespan somewhere else.
Any suggestion on this summation?
Unfortunately, there isn't a an overload of Sum that accepts an IEnumerable<TimeSpan>. Additionally, there's no current way of specifying operator-based generic constraints for type-parameters, so even though TimeSpan is "natively" summable, that fact can't be picked up easily by generic code.
One option would be to, as you say, sum up an integral-type equivalent to the timespan instead, and then turn that sum into a TimeSpan again. The ideal property for this is TimeSpan.Ticks, which round-trips accurately. But it's not necessary to change the property-type on your class at all; you can just project:
var totalSpan = new TimeSpan(myCollection.Sum(r => r.TheDuration.Ticks));
Alternatively, if you want to stick to the TimeSpan's + operator to do the summing, you can use the Aggregate operator:
var totalSpan = myCollection.Aggregate
(TimeSpan.Zero,
(sumSoFar, nextMyObject) => sumSoFar + nextMyObject.TheDuration);
This works well (code based on Ani's answer)
public static class StatisticExtensions
{
public static TimeSpan Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, TimeSpan> selector)
{
return source.Select(selector).Aggregate(TimeSpan.Zero, (t1, t2) => t1 + t2);
}
}
Usage :
If Periods is a list of objects with a Duration property
TimeSpan total = Periods.Sum(s => s.Duration)
I believe this is the cleanest LINQ extension:
public static class LinqExtensions
{
public static TimeSpan Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, TimeSpan> func)
{
return new TimeSpan(source.Sum(item => func(item).Ticks));
}
}
Usage is the same:
TimeSpan total = Periods.Sum(s => s.Duration)
Here's what I tried and it worked:
System.Collections.Generic.List<MyObject> collection = new List<MyObject>();
MyObject mb = new MyObject();
mb.TheDuration = new TimeSpan(100000);
collection.Add(mb);
mb.TheDuration = new TimeSpan(100000);
collection.Add(mb);
mb.TheDuration = new TimeSpan(100000);
collection.Add(mb);
var sum = (from r in collection select r.TheDuration.Ticks).Sum();
Console.WriteLine( sum.ToString());
//here we have new timespan that is sum of all time spans
TimeSpan sumedup = new TimeSpan(sum);
public class MyObject
{
public TimeSpan TheDuration { get; set; }
}
I put this in a class to add an extension method to a collection of timespans:
public static class Extensions:
{
public static TimeSpan TotalTime(this IEnumerable<TimeSpan> TheCollection)
{
int i = 0;
int TotalSeconds = 0;
var ArrayDuration = TheCollection.ToArray();
for (i = 0; i < ArrayDuration.Length; i++)
{
TotalSeconds = (int)(ArrayDuration[i].TotalSeconds) + TotalSeconds;
}
return TimeSpan.FromSeconds(TotalSeconds);
}
}
So now, I can write TotalDuration = (my LINQ query that returns a collection of timespan).TotalTime();
Voila!
This works for both a collection, and a property within a collection;
void Main()
{
var periods = new[] {
new TimeSpan(0, 10, 0),
new TimeSpan(0, 10, 0),
new TimeSpan(0, 10, 0),
};
TimeSpan total = periods.Sum();
TimeSpan total2 = periods.Sum(p => p);
Debug.WriteLine(total);
Debug.WriteLine(total2);
// output: 00:30:00
// output: 00:30:00
}
public static class LinqExtensions
{
public static TimeSpan Sum(this IEnumerable<TimeSpan> timeSpanCollection)
{
return timeSpanCollection.Sum(s => s);
}
public static TimeSpan Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, TimeSpan> func)
{
return new TimeSpan(source.Sum(item => func(item).Ticks));
}
}
I created extension method for IEnumerable<TimeSpan>
public static class TimeSpanExtensions
{
public static TimeSpan Sum(this IEnumerable<TimeSpan> source)
{
return source.Aggregate(TimeSpan.Zero, (subtotal, time) => subtotal.Add(time));
}
}
Thanks to that I can use:
IEnumerable<TimeSpan> times = new List<TimeSpan>();
TimeSpan totalTime = times.Sum();
You can use .Aggregate rather than .Sum, and pass it a timespan-summing function that you write, like this:
TimeSpan AddTimeSpans(TimeSpan a, TimeSpan b)
{
return a + b;
}
Once you understand that timespans can't be summed and know to use Ticks, it seems to me that this extension to just convert a long into a timespan looks more linq-ee. I believe it lets the reader have a more readable view of the operation:
var times = new[] { new TimeSpan(0, 10, 0), new TimeSpan(0, 20, 0), new TimeSpan(0, 30, 0) };
times.Sum(p => p.Ticks)
.ToTimeSpan(); // output: 01:00:00
Here is the one extension:
public static class LongExtensions
{
public static TimeSpan ToTimeSpan(this long ticks)
=> new TimeSpan(ticks);
}

Categories

Resources