How can I merge contiguous periods? - c#

Is there a simple way to merge contiguous periods (StartDate to EndDate) having the same Value?
Input:
ID StartDate EndDate Value
1 2014-01-01 2014-01-31 71
2 2014-02-01 2014-02-28 71
3 2014-03-01 2014-03-31 71
4 2014-04-01 2014-04-30 50,12
5 2014-05-01 2014-05-31 50,12
6 2014-06-01 2014-06-30 71
7 2014-08-01 2014-08-31 71 (a month is skipped here)
8 2014-09-01 2014-09-30 71
So those lines will be merged as follows:
1, 2 and 3 to 01-01-2014 03-31-2014 71
4 and 5 to 2014-04-01 05-31-2014 71
6 will remain the same
7 and 8 to 2014-08-01 2014-09-30 71
Output should be:
StartDate EndDate Value
2014-01-01 2014-03-31 71
2014-04-01 2014-05-31 50,12
2014-06-01 2014-06-30 71
2014-08-01 2014-09-30 71
I have tried this:
public List<PeriodInterval> MergePeriods(List<PeriodInterval> samples)
{
var merged = samples.OrderBy(s => s.StartDate)
.ThenBy(s => s.StartDate)
//select each item with its index
.Select((s, i) => new
{
sample = s,
index = i
})
// group by date miuns index to group consecutive items
.GroupBy(si => new
{
date = si.StartDate.AddDays(1),
content = si.Valeur
})
.Select(g => new PeriodInterval
{
StartDate = g.Min(s => s.StartDate),
EndDate = g.Max(s => s.EndDate),
Valeur = g.First().Valeur
});
return merged.ToList();
}

Create extension method which batches sequential itemd by some condition, which checks two sequential items in source sequence:
public static IEnumerable<IEnumerable<T>> SequentialGroup<T>(
this IEnumerable<T> source, Func<T, T, bool> predicate)
{
using(var iterator = source.GetEnumerator())
{
if (!iterator.MoveNext())
yield break;
List<T> batch = new List<T> { iterator.Current };
while (iterator.MoveNext())
{
if (!predicate(batch[batch.Count - 1], iterator.Current))
{
yield return batch;
batch = new List<T>();
}
batch.Add(iterator.Current);
}
if (batch.Any())
yield return batch;
}
}
With this method you can create batches of items which have sequential date and same value:
items.SequentialGroup((a, b) =>
a.Value == b.Value && (b.StartDate - a.EndDate).Days <= 1)
Creating aggregated items from these groups is easy. Assume your items look like:
public class Item
{
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public string Value { get; set; }
public string Line { get; set; }
}
Query:
var query = items.SequentialGroup((a, b) =>
a.Value == b.Value && (b.StartDate - a.EndDate).Days <= 1)
.Select((g,i) => new Item {
Value = g.First().Value,
StartDate = g.Min(f => f.StartDate),
EndDate = g.Max(f => f.EndDate),
Line = String.Format("mergedLine_{0}", i + 1)
});
For your sample input output will be:
[
{
StartDate: "2014-01-01T00:00:00",
EndDate: "2014-03-31T00:00:00",
Value: "71",
Line: "mergedLine_1"
},
{
StartDate: "2014-04-01T00:00:00",
EndDate: "2014-05-31T00:00:00",
Value: "50,12",
Line: "mergedLine_2"
},
{
StartDate: "2014-06-01T00:00:00",
EndDate: "2014-06-30T00:00:00",
Value: "71",
Line: "mergedLine_3"
},
{
StartDate: "2014-08-01T00:00:00",
EndDate: "2014-09-30T00:00:00",
Value: "71",
Line: "mergedLine_4"
}
]

Related

Dynamic Pivoting in Linq c# MVC

I have below data in my linq list.
StatusID Count MonthYear
======== ===== =========
1 0 Jan 2014
2 1 Feb 2013
1 2 Jan 2013
3 1 Dec 2014
2 0 Nov 2014
5 6 Jun 2015
Now my requirement is i need above list in below format. Where MonthYear column data is not fix. It can be any month and year data.
StatusID Jan 2013 Feb 2013 Jan 2014 Nov 2014 Dec 2014 Jun 2015
======== ======== ======== ======== ======== ======== ========
1 2 0 0 0 0 0
2 0 1 0 0 0 0
3 0 0 0 0 1 0
5 0 0 0 0 0 6
I read lots of solution on stackoverflow and even tried my own way but i was not get success. Below is code which i tried last.
var pvtData = new PivotData(new[] { "StatusID", "MonthYear" }, new SumAggregatorFactory("count"));
pvtData.ProcessData(finalList, (o, f) =>
{
var custData = (MontlyChartModified)o;
switch (f)
{
case "StatusID": return custData.StatusID;
case "MonthYear":
return custData.MonthYear;
case "count": return custData.count;
}
return null;
});
Please help me if you have any idea how to do dynamic pivoting.
Below is my code which i implemented. but when i debug and put watch on result var it shows me error "result The name 'result' does not exist in the current context"
public class MontlyChartModified
{
public int? StatusID { get; set; }
public int count { get; set; }
public string MonthYear { get; set; }
}
List<MontlyChartModified> finalList = new List<MontlyChartModified>();
finalList = (from x in Okdata
select new MontlyChartModified
{
StatusID = x.StatusID,
count = x.count,
MonthYear = x.Month.ToString() + " " + x.Year.ToString()
}).ToList();
var columns = new[] { "StatusID" }.Union(finalList.Select(a => a.MonthYear).OrderBy(a => a.Split(' ')[1]).ThenBy(a => a)).ToList();
var result = finalList.GroupBy(g => g.StatusID).OrderBy(g => g.Key).Select(g => columns.Select(c =>
{
if (c == "StatusID") return g.Key;
var val = g.FirstOrDefault(r => r.MonthYear == c);
return val != null ? val.count : 0;
}).ToList()).ToList();
my final result.
I managed to do it with grouping:
First, I create list of columns, sorted by year and month alphabetically:
var columns = new[] { "StatusID" }.Union(lst.Select(a => a.MonthYear).OrderBy(a => a.Split(' ')[1]).ThenBy(a => a)).ToList();
Second, results are groupped by StatusID and for each group I create List of values for each MonthYear column:
var result = lst.GroupBy(g => g.StatusID).OrderBy(g => g.Key).Select(g => columns.Select(c =>
{
if (c == "StatusID") return g.Key;
var val = g.FirstOrDefault(r => r.MonthYear == c);
return val != null ? val.Count : 0;
}).ToList()).ToList();
Input list is defined as follows:
var lst = new List<MontlyChartModified>()
{
new MontlyChartModified(){StatusID = 1, Count = 0, MonthYear = "Jan 2014"},
new MontlyChartModified(){StatusID = 2, Count = 1, MonthYear = "Feb 2013"},
new MontlyChartModified(){StatusID = 1, Count = 2, MonthYear = "Jan 2013"},
new MontlyChartModified(){StatusID = 3, Count = 1, MonthYear = "Dec 2014"},
new MontlyChartModified(){StatusID = 2, Count = 0, MonthYear = "Nov 2014"},
new MontlyChartModified(){StatusID = 5, Count = 6, MonthYear = "Jun 2015"},
};
class MontlyChartModified
{
public int StatusID { get; set; }
public int Count { get; set; }
public string MonthYear { get; set; }
}
result is List<List<int>>

How to convert List of objects with date to array indexed with day of month?

What is the easiest way to convert List<DayValue> to array, where index is day of month:
class DayValue
{
public DateTime Day { get; set; }
public object Value { get; set; }
}
I need to do this inline, because this will be used in linq. I know I can just iterate throug items and set array values, but this needs to be done line that
list.FunctionThatConvertsItIntoArray()
Example:
Input list:
{ 2012-11-01, "Value1" }
{ 2012-11-03, "Value2" }
{ 2012-11-05, "Value3" }
Output array:
1 => "Value1"
2 => null
3 => "Value2"
4 => null
5 => "Value3"
6 => null
7 => null
8 => null
9 => null
10 => null
11 => null
12 => null
13 => null
14 => null
15 => null
16 => null
17 => null
18 => null
19 => null
20 => null
21 => null
22 => null
23 => null
24 => null
25 => null
26 => null
27 => null
28 => null
29 => null
30 => null
var days = Enumerable.Range(1, 31)
.Select(i => list.Find(x => x.Day.Day == i))
.Select(d => d != null ? d.Value : null)
.ToArray();
That solves my problem:
Enumerable.Range(0, 31).Select(
index => (list.FirstOrDefault(item => item.Day.Day == index) ?? new DayValue()).Value).ToArray();
Complete solution:
public class DayValue
{
public DateTime Day { get; set; }
public object Value { get; set; }
}
var list = new List<DayValue>();
list.Add(new DayValue { Day = new DateTime(2012, 11, 1), Value = "Value1" });
list.Add(new DayValue { Day = new DateTime(2012, 11, 3), Value = "Value2" });
list.Add(new DayValue { Day = new DateTime(2012, 11, 5), Value = "Value3" });
var result = Enumerable.Range(0, 31).Select(
index => (list.FirstOrDefault(item => item.Day.Day == index) ?? new DayValue()).Value).ToArray();

Group by same value and contiguous date

var myDic = new SortedDictionary<DateTime,int> ()
{ { new DateTime(0), 0 },
{ new DateTime(1), 1 },
{ new DateTime(2), 1 },
{ new DateTime(3), 0 },
{ new DateTime(4), 0 },
{ new DateTime(5), 2 }
};
How can group these items (with a LINQ request) like this :
group 1 :
startDate: 0, endDate:0, value:0
group 2 :
startDate: 1, endDate:2, value:1
group 3 :
startDate: 3, endDate:4, value:0
group 4 :
startDate: 5, endDate:5, value:2
group are defined by contiguous date and same values.
Is it possible with a groupby ?
Just use a keyGenerating function. This example presumes your dates are already ordered in the source with no gaps.
int currentValue = 0;
int groupCounter = 0;
Func<KeyValuePair<DateTime, int>, int> keyGenerator = kvp =>
{
if (kvp.Value != currentValue)
{
groupCounter += 1;
currentValue = kvp.Value;
}
return groupCounter;
}
List<IGrouping<int, KeyValuePair<DateTime, int>> groups =
myDictionary.GroupBy(keyGenerator).ToList();
It looks like you are trying to group sequential dates over changes in the value. I don't think you should use linq for the grouping. Instead you should use linq to order the dates and iterate over that sorted list to create your groups.
Addition 1
While you may be able to build your collections with by using .Aggregate(). I still think that is the wrong approach.
Does your data have to enter this function as a SortedDictionary?
I'm just guessing, but these are probably records ordered chronologically.
If so, do this:
public class Record
{
public DateTime Date { get; set; }
public int Value { get; set; }
}
public class Grouper
{
public IEnumerable<IEnumerable<Record>> GroupRecords(IEnumerable<Record> sortedRecords)
{
var groupedRecords = new List<List<Record>>();
var recordGroup = new List<Record>();
groupedRecords.Add(recordGroup);
foreach (var record in sortedRecords)
{
if (recordGroup.Count > 0 && recordGroup.First().Value != record.Value)
{
recordGroup = new List<Record>();
groupedRecords.Add(recordGroup);
}
recordGroup.Add(record);
}
return groupedRecords;
}
}

Linq produce a list of missing records for a given date range

Say I have a list of the following class:
public class Holding
{
public string HoldingId{ get; set; }
public DateTime date { get; set; }
}
There needs to be a holding for each day in a given date range. I need to be able to produce a list of holdings that are missing for the range.
So say I have the following data that I need to check over the range 1 June 2010 - 5 June 2010:
HoldingId Date
1 01-06-2010
1 02-06-2010
1 04-06-2010
2 02-06-2010
2 03-06-2010
2 05-06-2010
3 03-06-2010
For this set of data the missing holdings would be:
HoldingId Date
1 03-06-2010
1 05-06-2010
2 01-06-2010
2 04-06-2010
3 01-06-2010
3 02-06-2010
3 04-06-2010
3 05-06-2010
I have produced the list range of dates using the answer to the following question:
Find missing dates for a given range.
I can't quite get my head around how to go forward from here...I assume I'll need to group by HoldingId to produce an array of dates and then do range.Except(holdings.dates) or something to that effect.
Does anyone have a nice solution to this problem using Linq?
you're quite right in howit should be done; here is what I got;
List<Holding> holdings = new List<Holding>();
holdings.Add(new Holding(){ date=Convert.ToDateTime("01-06-2010"), HoldingId = "1" });
holdings.Add(new Holding(){ date=Convert.ToDateTime("02-06-2010"), HoldingId = "1" });
holdings.Add(new Holding(){ date=Convert.ToDateTime("04-06-2010"), HoldingId = "1" });
holdings.Add(new Holding(){ date=Convert.ToDateTime("02-06-2010"), HoldingId = "2" });
holdings.Add(new Holding(){ date=Convert.ToDateTime("03-06-2010"), HoldingId = "2" });
holdings.Add(new Holding(){ date=Convert.ToDateTime("05-06-2010"), HoldingId = "2" });
holdings.Add(new Holding(){ date=Convert.ToDateTime("03-06-2010"), HoldingId = "3" });
List<DateTime> dateRange = new List<DateTime>();
dateRange.Add(Convert.ToDateTime("01-06-2010"));
dateRange.Add(Convert.ToDateTime("02-06-2010"));
dateRange.Add(Convert.ToDateTime("03-06-2010"));
dateRange.Add(Convert.ToDateTime("04-06-2010"));
dateRange.Add(Convert.ToDateTime("05-06-2010"));
Dictionary<string, List<DateTime>> missingHoldings = new Dictionary<string, List<DateTime>>();
foreach(var holdGrp in holdings.GroupBy (h => h.HoldingId))
{
var missingDates = dateRange.Except(holdGrp.Select(h => h.date)).ToList();
missingHoldings.Add(holdGrp.Key, missingDates);
}
An alternative approach:
public static List<Holding> MissingHoldings(List<Holding> existingHoldings, DateTime startDate, DateTime endDate)
{
var missingHoldings = new List<Holding>();
var holdingIds = existingHoldings.Select(h => h.HoldingId).Distinct().ToList();
var dates = new List<DateTime>();
for (var current = startDate.Date; current <= endDate.Date; current = current.AddDays(1))
{
dates.Add(current);
}
foreach (var holdingId in holdingIds)
{
missingHoldings
.AddRange(
dates.Where(date => !existingHoldings.Any(h => h.HoldingId == holdingId && h.date == date))
.Select(date => new Holding {HoldingId = holdingId, date = date}));
}
return missingHoldings;
}
A pure Linq Query inspired by saj's answer :
var missingHoldingsList =
from h in holdings.GroupBy( h => h.HoldingId )
from d in dateRange.Except( h.Select(x => x.date) )
orderby h.Key, d
select new Holding { date = d , HoldingId = h.Key };
and a loop-less version of saj's answer:
var missingHoldingsDict = (
from h in holdings.GroupBy(h => h.HoldingId)
select new
{
key = h.Key,
holdings =
from d in dateRange.Except(h.Select(x => x.date))
select new Holding { date = d, HoldingId = h.Key }
}
).ToDictionary(
h => h.key,
h => h.holdings.ToList()
);

How do I make this NHibernate QueryOver query return rows for empty groups

The following NHibernate QueryOver query is counting the number of applications for each month, within a given date range.
However, I don't get any results for months that don't have any applications in them but I want to actually have Count = 0 returned for those months.
So how would I change the query to return a row as well for months that don't have any applications in them?
DateTimeOffset endDate = DateTimeOffset.Now;
DateTimeOffset startDate = endDate.AddMonths(-12);
var result = Session.QueryOver<Application>()
.WhereRestrictionOn(c => c.SubmissionDate).IsBetween(startDate).And(endDate)
.SelectList(list => list
.Select(Projections.SqlGroupProjection(
"YEAR(SubmissionDate) As [Year]",
"YEAR(SubmissionDate)",
new[] { "YEAR" },
new IType[] { NHibernateUtil.Int32 }))
.Select(Projections.SqlGroupProjection(
"MONTH(SubmissionDate) As [Month]",
"MONTH(SubmissionDate)",
new[] { "MONTH" },
new IType[] { NHibernateUtil.Int32 }))
.SelectCount(x => x.Id))
.OrderBy(Projections.SqlFunction(
"YEAR",
NHibernateUtil.Int32,
Projections.Property<Application>(item => item.SubmissionDate))).Asc
.ThenBy(Projections.SqlFunction(
"MONTH",
NHibernateUtil.Int32,
Projections.Property<Application>(item => item.SubmissionDate))).Asc
.List<object[]>()
.Select(n => new
{
Year = n[0],
Month = n[1],
Count = (int)n[2]
}));
Update: taking your idea with DateTime.AddMonths() it gets even shorter
DateTime lastMonth = startdate;
var unionresults = result.SelectMany(r =>
{
var actualDate = new DateTime(r.Year, r.Month, 1);
var results = Enumerable.Repeat(1, Months)
.Select(i => lastMonth.AddMonths(i))
.TakeWhile(date => date < actualDate)
.Select(date => new { Year = date.Year, Month = date.Month, Count = 0 })
.Concat(new[] { r });
lastMonth = actualDate;
return results;
});
Original:
i think you have to add that data after the query. here an example using linq to fill in missing months
var result = <query>;
int lastMonth = 1;
var unionresults = result.SelectMany(r =>
{
var results = new[] { r }.AsEnumerable();
if (lastMonth > r.Month)
{
results = Enumerable.Range(lastMonth, 12 - lastMonth).Select(month => new { Year = r.Year, Month = month, Count = 0 })
.Concat(Enumerable.Range(1, r.Month).Select(month => new { Year = r.Year, Month = month, Count = 0 }))
.Concat(results);
}
else if (lastMonth < r.Month)
{
results = Enumerable.Range(lastMonth, r.Month - lastMonth)
.Select(month => new { Year = r.Year, Month = month, Count = 0 })
.Concat(results);
}
lastMonth = r.Month + 1;
if (lastMonth > 12)
{
lastMonth = 1;
}
return results;
});
It cannot be done with a few simple changes. The SQL query that is generated by your QueryOver() cannot count what does not exist in the first place.
You could probably do it with a UNION or a JOIN using a virtual/temporary table (depending on the DBMS) but that would make the query overly complicated.
I suggest adding a loop after your query that iterates through the list, copies the elements to a new list and adds any non-existing months to that new list. Something like this:
class YearMonthCount
{
public int Year { get; set; }
public int Month { get; set; }
public int Count { get; set; }
}
// Start and End dates
DateTime startDate = new DateTime(2011, 9, 1);
DateTime endDate = new DateTime(2012, 6, 1);
// this would be a sample of the QueryOver() result
List<YearMonthCount> result = new List<YearMonthCount>();
result.Add(new YearMonthCount { Year = 2011, Month = 10, Count = 2 });
result.Add(new YearMonthCount { Year = 2011, Month = 11, Count = 3 });
result.Add(new YearMonthCount { Year = 2012, Month = 1, Count = 4 });
result.Add(new YearMonthCount { Year = 2012, Month = 2, Count = 1 });
result.Add(new YearMonthCount { Year = 2012, Month = 4, Count = 1 });
result.Add(new YearMonthCount { Year = 2012, Month = 5, Count = 1 });
int i = 0;
List<YearMonthCount> result2 = new List<YearMonthCount>();
// iterate through result list, add any missing entry
while (startDate <= endDate)
{
bool addNewEntry = true;
// check to avoid OutOfBoundsException
if (i < result.Count)
{
DateTime listDate = new DateTime(result[i].Year, result[i].Month, 1);
if (startDate == listDate)
{
// entry is in the QueryOver result -> add this
result2.Add(result[i]);
i++;
addNewEntry = false;
}
}
if (addNewEntry)
{
// entry is not in the QueryOver result -> add a new entry
result2.Add(new YearMonthCount {
Year = startDate.Year, Month = startDate.Month, Count = 0 });
}
startDate = startDate.AddMonths(1);
}
This could probably be done more elegantly but it gets the job done.
Thanks to all the answers, this is how I ended up doing it:
DateTime endDate = DateTime.Now;
DateTime startDate = endDate.AddMonths(-Months);
var result = Session.QueryOver<Application>()
.WhereRestrictionOn(c => c.SubmissionDate).IsBetween(startDate).And(endDate)
.SelectList(list => list
.Select(Projections.SqlGroupProjection(
"YEAR(SubmissionDate) As [Year]",
"YEAR(SubmissionDate)",
new[] { "YEAR" },
new IType[] { NHibernateUtil.Int32 }))
.Select(Projections.SqlGroupProjection(
"MONTH(SubmissionDate) As [Month]",
"MONTH(SubmissionDate)",
new[] { "MONTH" },
new IType[] { NHibernateUtil.Int32 }))
.SelectCount(x => x.Id))
.List<object[]>()
.Select(n => new
{
Year = (int)n[0],
Month = (int)n[1],
Count = (int)n[2]
}).ToList();
var finalResult = result
.Union(
Enumerable.Range(0, Months - 1).Select(n => new
{
Year = startDate.AddMonths(n).Year,
Month = startDate.AddMonths(n).Month,
Count = 0
})
.Where(n => !result.Any(r => r.Year == n.Year && r.Month == n.Month)))
.OrderBy(n => n.Year).ThenBy(n => n.Month);

Categories