Reputation: 7325
I have this data that I have to group by price , and check the range and continouity of data
date price
2014-01-01 10
2014-01-02 10
2014-01-03 10
2014-01-05 20
2014-01-07 30
2014-01-08 40
2014-01-09 50
2014-01-10 30
and the output should look like this
2014-01-01 2014-01-03 10
2014-01-05 2014-01-05 20
2014-01-07 2014-01-07 30
2014-01-08 2014-01-08 40
2014-01-09 2014-01-09 50
2014-01-10 2014-01-10 30
I tried so far
var result = list
.OrderBy(a => a.Date)
.GroupBy(a => a.Price)
.Select(x => new
{
DateMax = x.Max(a => a.Date),
DateMin = x.Min(a => a.Date),
Count = x.Count()
})
.ToList()
.Where(a => a.DateMax.Subtract(a.DateMin).Days == a.Count)
.ToList();
I am not really sure this takes care of continuous dates. All dates are unique!
Upvotes: 1
Views: 2155
Reputation: 203814
So first of we'll use a helper method to group consecutive items. It'll take a function that will be given the "previous" and "current" item, and it will then determine if that item should be in the current group, or should start a new one.
public static IEnumerable<IEnumerable<T>> GroupWhile<T>(
this IEnumerable<T> source, Func<T, T, bool> predicate)
{
using (var iterator = source.GetEnumerator())
{
if (!iterator.MoveNext())
yield break;
List<T> list = new List<T>() { iterator.Current };
T previous = iterator.Current;
while (iterator.MoveNext())
{
if (predicate(previous, iterator.Current))
{
list.Add(iterator.Current);
}
else
{
yield return list;
list = new List<T>() { iterator.Current };
}
previous = iterator.Current;
}
yield return list;
}
}
Now we're able to use that method to group the items and then select out the information that we need:
var query = data.OrderBy(item => item.Date)
.GroupWhile((previous, current) =>
previous.Date.AddDays(1) == current.Date
&& previous.Price == current.Price)
.Select(group => new
{
DateMin = group.First().Date,
DateMax = group.Last().Date,
Count = group.Count(),
Price = group.First().Price,
});
Upvotes: 9
Reputation: 35706
As an alternative to Servy's answer, which I find more elegant and obviously much more resuable,
You could do something more bespoke in one sweep (after ordering.)
public class ContiguousValuePeriod<TValue>
{
private readonly DateTime start;
private readonly DateTime end;
private readonly TValue value;
public ContiguousValuePeriod(
DateTime start,
DateTime end,
TValue value)
{
this.start = start;
this.end = end;
this.value = value;
}
public DateTime Start { get { return this.start; } }
public DateTime End { get { return this.start; } }
public TValue Value { get { return this.value; } }
}
public IEnumerable<ContiguousValuePeriod<TValue>>
GetContiguousValuePeriods<TValue, TItem>(
this IEnumerable<TItem> source,
Func<TItem, DateTime> dateSelector,
Func<TItem, TValue> valueSelector)
{
using (var iterator = source
.OrderBy(t => valueSelector(t))
.ThenBy(t => dateSelector(t))
.GetEnumerator())
{
if (!iterator.MoveNext())
{
yield break;
}
var periodValue = valueSelector(iterator.Current);
var periodStart = dateSelector(iterator.Current);
var periodLast = periodStart;
var hasTail = false;
while (iterator.MoveNext())
{
var thisValue = valueSelector(iterator.Current);
var thisDate = dateSelector(iterator.Current);
if (!thisValue.Equals(periodValue) ||
thisDate.Subtract(periodLast).TotalDays > 1.0)
{
// Period change
yield return new ContiguousValuePeriod(
periodStart,
periodLast,
periodValue);
periodStart = thisDate;
periodValue = thisValue;
hasTail = false;
}
else
{
hasTail = true;
}
periodLast = thisDate;
}
}
if (hasTail)
{
yield return new ContiguousValuePeriod(
periodStart,
periodLast,
periodValue);
}
}
which you use like,
var result = yourList.GetContiguousValuePeriods(
a => a.Date,
a => a.Price);
Upvotes: 0