I have a list of objects where each object has a string and a int. Each string is a date in the yyyy-M-d format.
The list can contain dates 30, 90 or 365 days from the first date
So a list of items (of 30 days) would be:
2017-7-25 10
2017-7-24 3
2017-7-23 7
2017-7-22 4
2017-7-21 2
2017-7-20 4
..
2017-6-27 5
2017-6-26 8
I want to group these dates by 5 days such that:
2017-7-21 30
2017-7-16 (Sum of values from 7-16 till 7-20)
and so on.
I cant figure out the lambda for this.
var grouped = from x in list
group x by DateTime.Parse(x.date)/5
select new { date = x.????????, count = x.Sum()}
If you have declared a class like the following:
internal class DayNumber
{
public string Day { get; set; }
public int Number { get; set; }
}
and you have defined a list like the following one:
var list = new List<DayNumber>
{
new DayNumber {Day = "2017-7-25", Number = 10},
new DayNumber {Day = "2017-7-24", Number = 3},
new DayNumber {Day = "2017-7-23", Number = 7},
new DayNumber {Day = "2017-7-22", Number = 4},
new DayNumber {Day = "2017-7-21", Number = 2},
new DayNumber {Day = "2017-7-20", Number = 4},
new DayNumber {Day = "2017-7-19", Number = 5},
new DayNumber {Day = "2017-7-18", Number = 8},
new DayNumber {Day = "2017-7-17", Number = 2},
new DayNumber {Day = "2017-7-16", Number = 3}
};
then you could try something like this:
var grouped = list.Select(item => new
{
Parsed = DateTime.ParseExact(item.Day, "yyyy-M-dd", CultureInfo.InvariantCulture),
Number = item.Number
})
.OrderBy(item => item.Parsed)
.Select((item, index) => new
{
Index = index,
Item = item
})
.GroupBy(item => item.Index / 5)
.Select(gr => new
{
Date = gr.First().Item.Parsed,
Count = gr.Sum(x => x.Item.Number)
})
.ToList();
This will works regardless of date uniqueness and gaps between dates.
Assuming we have class representing your object
public class MyClass
{
public string DateString { get; set; }
public int SomeInt { get; set; }
}
then we have array of this objects
MyClass[] array = new[]
{
new MyClass { DateString = "2017-7-25", SomeInt = 10 },
new MyClass { DateString = "2017-7-24", SomeInt = 3 },
new MyClass { DateString = "2017-7-23", SomeInt = 7 },
new MyClass { DateString = "2017-7-22", SomeInt = 4 },
new MyClass { DateString = "2017-7-21", SomeInt = 2 },
new MyClass { DateString = "2017-7-20", SomeInt = 4 },
new MyClass { DateString = "2017-7-25", SomeInt = 5 },
new MyClass { DateString = "2017-6-26", SomeInt = 8 }
};
In this case code will be
// get object array with parsed dates
var arrayWithDates = array.Select(el =>
new
{
Date = DateTime.ParseExact(el.DateString, "yyyy-M-d", CultureInfo.InvariantCulture),
SomeInt = el.SomeInt
});
// get minimum date
DateTime minDate = arrayWithDates.Min(el => el.Date);
// get maximum date
DateTime maxDate = arrayWithDates.Max(el => el.Date);
// get total days
int days = (maxDate - minDate).Days;
// getting all dates from minDate to maxDate
IEnumerable<DateTime> dateRange = Enumerable.Range(0, days + 1)
.Select(el => minDate.AddDays(el));
// split all dates into groups of 5 dates
IEnumerable<DateTime[]> groupedDateRanges = dateRange
.Select((el, index) => new { el.Date, index })
.GroupBy(el => el.index / 5)
.Select(g => g.Select(el => el.Date).ToArray());
var results = groupedDateRanges
// getting list of object within each range
.Select(groupedDateRange => arrayWithDates.Where(el => groupedDateRange.Contains(el.Date)))
// selecting minimum date of range, maximum date of range and sum by int value
.Select(item =>
new
{
MinDate = item.Min(el => el.Date),
MaxDate = item.Max(el => el.Date),
Sum = item.Sum(el => el.SomeInt)
});
KeyValuePair<string, int> kvp = new KeyValuePair<string, int>();
IList < KeyValuePair < string, int> > list= new
List<KeyValuePair<string, int>>();
list.Add(new KeyValuePair<string, int>("2017 - 7 - 25", 10));
list.Add(new KeyValuePair<string, int>("2017 - 7 - 24", 3));
list.Add(new KeyValuePair<string, int>("2017 - 7 - 23", 7));
list.Add(new KeyValuePair<string, int>("2017 - 7 - 22", 4));
list.Add(new KeyValuePair<string, int>("2017 - 7 - 21", 2));
list.Add(new KeyValuePair<string, int>("2017 - 7 - 25", 5));
list.Add(new KeyValuePair<string, int>("2017 - 6 - 26", 8));
list.Add(new KeyValuePair<string, int>("2017 - 5 - 26", 18));
TimeSpan interval = TimeSpan.FromDays(5);
var output=list.GroupBy(x => DateTime.Parse(x.Key).Ticks / interval.Ticks)
.Select((n) => new { GroupId = new DateTime(n.Key * interval.Ticks), Sum= n.Sum(x => x.Value) })
Related
I have 2 lists in C#:
public class AvailableSlot
{
public DateTime DateTime;
public string Name
}
List<AvailableSlot> list1 = GetList();
List<AvailableSlot> list2 = GetAnotherList();
I want to call intersect on these lists to find out where there are items in both lists for the same date. I know i can use .Intersect to get this info but I have a slightly more complicated requirement. I want to return a intersected list but i want to this list to contain a list of objects with all of the name in them. so something like this:
List<AvailableSlot2> intersectedList . ..
where AvailableSlot2 is this below:
public class AvailableSlot2
{
public DateTime DateTime;
public string[] Names;
}
Is there any ability to do this transformation after trying to intersect between two lists?
I would just union the two lists, group by DateTime and then pull out the names from the group:
var list1 = new List<AvailableSlot>
{
new AvailableSlot { DateTime = new DateTime(2013, 2, 1), Name = "Alpha" },
new AvailableSlot { DateTime = new DateTime(2013, 2, 2), Name = "Bravo" },
new AvailableSlot { DateTime = new DateTime(2013, 2, 3), Name = "Charlie" },
new AvailableSlot { DateTime = new DateTime(2013, 2, 1), Name = "Delta" },
new AvailableSlot { DateTime = new DateTime(2013, 2, 2), Name = "Echo" },
new AvailableSlot { DateTime = new DateTime(2013, 2, 3), Name = "Foxtrot" },
new AvailableSlot { DateTime = new DateTime(2013, 2, 4), Name = "Golf" },
new AvailableSlot { DateTime = new DateTime(2013, 2, 5), Name = "Hotel" }
};
var list2 = new List<AvailableSlot>
{
new AvailableSlot { DateTime = new DateTime(2013, 2, 1), Name = "Apple" },
new AvailableSlot { DateTime = new DateTime(2013, 2, 2), Name = "Bannana" },
new AvailableSlot { DateTime = new DateTime(2013, 2, 1), Name = "Dog" },
new AvailableSlot { DateTime = new DateTime(2013, 2, 2), Name = "Egg" },
new AvailableSlot { DateTime = new DateTime(2013, 2, 5), Name = "Hi" }
};
var list3 = list1.Where (l => list2.Where (li => l.DateTime == li.DateTime).Any ())
.Union(list2.Where (l => list1.Where (li => l.DateTime == li.DateTime).Any ()));
var groupedItems = from slot in list3
group slot by slot.DateTime into grp
select new AvailableSlot2 {
DateTime = grp.Key,
Names = grp.Select (g => g.Name).ToArray()
};
foreach(var g in groupedItems)
{
Console.WriteLine(g.DateTime);
foreach(var name in g.Names)
Console.WriteLine(name);
Console.WriteLine("---------------------");
}
Output:
2/1/2013 12:00:00 AM
Alpha
Delta
Apple
Dog
---------------------
2/2/2013 12:00:00 AM
Bravo
Echo
Bannana
Egg
---------------------
2/5/2013 12:00:00 AM
Hotel
Hi
---------------------
You can use a LINQ to Objects Join() to line up items with the same DateTime property and then collect all the names into an array
var joinedItems = from slot1 in list1
join slot2 in list2
on slot1.DateTime equals slot2.DateTime into g
where g.Any()
select new AvailableSlot2
{
DateTime = slot1.DateTime,
Names = Enumerable.Range(slot1.Name,1).Union(g.Select(s => s.Name)).ToArray()
}
You can make use of ToLookup:
DateTime dt1 = new DateTime(2013, 2, 1);
DateTime dt2 = new DateTime(2013, 3, 1);
DateTime dt3 = new DateTime(2013, 4, 1);
var list1 = new List<AvailableSlot>
{
new AvailableSlot{DateTime = dt1, Name = "n1",},
new AvailableSlot{DateTime = dt2, Name = "n2",},
new AvailableSlot{DateTime = dt1, Name = "n3",},
};
var list2 = new List<AvailableSlot>
{
new AvailableSlot{DateTime = dt1, Name = "n1",},
new AvailableSlot{DateTime = dt2, Name = "n2",},
new AvailableSlot{DateTime = dt3, Name = "n3",},
};
var intersected = list1.Select (l => l.DateTime).
Intersect(list2.Select (l => l.DateTime));
var lookup = list1.Union(list2).ToLookup (
slot => slot.DateTime, slot => slot);
lookup.Where (l => intersected.Contains(l.Key)).Select (
slot => new
{
DateTime=slot.Key,
Names=slot.Select (s => s.Name)
});
Which in this case gives the result:
DateTime Names
01/02/2013 00:00 n1
n3
n1
01/03/2013 00:00 n2
n2
You could of course use Names=slot.Select(s => s.Name).Distinct() to get a distinct list of names.
I have an object and two lists as follows:
public class MyObject
{
public int Key;
public DateTime Day;
public decimal Value;
}
List<MyObject> listA = new List<MyObject>()
{
new MyObject() { Key = 1, Day = new DateTime(2012, 12, 17), Value = 8 },
new MyObject() { Key = 1, Day = new DateTime(2012, 12, 18), Value = 8 },
new MyObject() { Key = 2, Day = new DateTime(2012, 12, 17), Value = 8 },
new MyObject() { Key = 3, Day = new DateTime(2012, 12, 17), Value = 4 },
new MyObject() { Key = 4, Day = new DateTime(2012, 12, 17), Value = 4 }
};
List<MyObject> listB = new List<MyObject>()
{
new MyObject() { Key = 1, Day = new DateTime(2012, 12, 17), Value = 2 },
new MyObject() { Key = 1, Day = new DateTime(2012, 12, 18), Value = 8 },
new MyObject() { Key = 3, Day = new DateTime(2012, 12, 17), Value = 8 },
new MyObject() { Key = 4, Day = new DateTime(2012, 12, 17), Value = 4 },
new MyObject() { Key = 5, Day = new DateTime(2012, 12, 17), Value = 10 }
};
The results I am looking for are:
List<MyObject> listChanges = new List<MyObject>()
{
new MyObject() { Key = 1, Day = new DateTime(2012, 12, 17), Value = -6 },
new MyObject() { Key = 2, Day = new DateTime(2012, 12, 17), Value = -8 },
new MyObject() { Key = 3, Day = new DateTime(2012, 12, 17), Value = 4 },
new MyObject() { Key = 5, Day = new DateTime(2012, 12, 17), Value = 10 }
};
Essentially, I am trying to create a list which contains the changes that would be required to make listA into listB. Thus, while Except, and Intersect from LINQ could be used I don't think they will have the best performance to do such a task as you would still need another comparison to get the difference in the values.
One thought I had is: If I loop through listA, I can remove the item from listA and from listB (if it is found and at this time I can determine the +/- differences). Once I have finished with listA, listB would only contain additions.
How can I get the change results?
This should do it. It'll throw an exception if any of your Key/Day combinations are not unique within either of your inputs.
public static IEnumerable<MyObject> GetChanges(
IEnumerable<MyObject> from, IEnumerable<MyObject> to)
{
var dict = to.ToDictionary(mo => new { mo.Key, mo.Day });
// Check that keys are distinct in from, too:
var throwaway = from.ToDictionary(mo => new { mo.Key, mo.Day });
// Adjustments of items found in "from"
foreach (MyObject mo in from)
{
var key = new { mo.Key, mo.Day };
MyObject newVal;
if (dict.TryGetValue(key, out newVal))
{
// Return item indicating adjustment
yield return new MyObject {
Key = mo.Key, Day = mo.Day, Value = newVal.Value - mo.Value };
dict.Remove(key);
}
else
{
// Return item indicating removal
yield return new MyObject {
Key = mo.Key, Day = mo.Day, Value = -mo.Value };
}
}
// Creation of new items found in "to"
foreach (MyObject mo in dict.Values)
{
// Return item indicating addition
// (Clone as all our other yields are new objects)
yield return new MyObject {
Key = mo.Key, Day = mo.Day, Value = mo.Value };
}
}
You could speed this up by removing the uniqueness check on from or doing it on-the-fly (try adding each item's key-parts to a HashSet), but I don't think you can avoid looping over part of to twice - once to build the dictionary, once to return the remainder.
First I would implement an IEqualityComparer<T> that checks equality based on both the Key and Day properties. Then you could use linq as follows:
var notInA = listB.Except(listA, myEqualityComparer);
var notInB = listA.Except(listB, myEqualityComparer)
.Select(o => {
return new MyObject {
Key = item.Key,
Day = item.Day,
Value = item.Value * -1
};
});
var listA2 = listA.Intersect(listB, myEqualityComparer)
.OrderBy(o => o.Key)
.ThenBy(o => o.Day);
var listB2 = listB.Intersect(listA, myEqualityComparer)
.OrderBy(o => o.Key)
.ThenBy(o => o.Day);
var diff = listA2.Zip(listB2, (first,second) => {
return new MyObject {
Key = first.Key,
Day = first.Day,
Value = second.Value - first.Value
});
diff = diff.Concat(notInA).Concat(notInB);
how about this :
List<MyObject> listA = new List<MyObject>(){
new MyObject() { Key = 1, Day = new DateTime(2012, 12, 17), Value = 8 },
new MyObject() { Key = 2, Day = new DateTime(2012, 12, 17), Value = 8 },
new MyObject() { Key = 1, Day = new DateTime(2012, 12, 18), Value = 8 },
new MyObject() { Key = 4, Day = new DateTime(2012, 12, 17), Value = 4 },
new MyObject() { Key = 3, Day = new DateTime(2012, 12, 17), Value = 4 }
};
List<MyObject> listB = new List<MyObject>(){
new MyObject() { Key = 1, Day = new DateTime(2012, 12, 17), Value = 2 },
new MyObject() { Key = 3, Day = new DateTime(2012, 12, 17), Value = 8 },
new MyObject() { Key = 4, Day = new DateTime(2012, 12, 17), Value = 4 },
new MyObject() { Key = 1, Day = new DateTime(2012, 12, 18), Value = 8 },
new MyObject() { Key = 5, Day = new DateTime(2012, 12, 17), Value = 10 }
};
List<MyObject> listChanges = Comparer(listA, listB);
MyObject[] hasil = listChanges.ToArray();
for (int a = 0; a < hasil.Length;a++ ) {
Console.WriteLine(hasil[a].Key+" "+hasil[a].Day+" "+hasil[a].Value);
}
and the function :
private MyObject[] sort(List<MyObject> input) {
//sort input with it's key
MyObject[] gg = input.ToArray();
for (int a = 0; a < input.Count; a++) {
for (int b = a + 1; b < input.Count; b++) {
if (gg[a].Key > gg[b].Key) {
MyObject temp = gg[a];
gg[a] = gg[b];
gg[b] = temp;
}
}
}
//sort input, if key is same => sort the date
for (int a = 0; a < input.Count; a++) {
int indStart = a;
int indEnd = a;
for (int b = a + 1; b < input.Count; b++) {
if (gg[a].Key == gg[b].Key) {
indEnd++;
} else {
b = input.Count;
}
}
a = indEnd;
for (int c = indStart; c <= indEnd; c++) {
for (int d = c + 1; d <= indEnd; d++) {
if (gg[c].Day > gg[d].Day) {
MyObject temp = gg[c];
gg[c] = gg[d];
gg[d] = temp;
}
}
}
}
return gg;
}
private List<MyObject> Comparer(List<MyObject> listA, List<MyObject> listB) {
List<MyObject> output = new List<MyObject>();
//if you sure that the list was sorted, u just remove the sort function
MyObject[] ff = sort(listA);
MyObject[] gg = sort(listB);
Boolean[] masuk = new Boolean[gg.Length];
//foreach element in listA, search the changes in input
for (int a = 0; a < listA.Count;a++ ) {
//find element in input which contains the changes of element in listA
Boolean ins = false;
for (int b = 0; b < listB.Count;b++ ) {
if (masuk[b])
continue;
if (ff[a].Key >= gg[b].Key) {
if (ff[a].Key == gg[b].Key && ff[a].Day == gg[b].Day){
masuk[b] = true;
if (gg[b].Value != ff[a].Value) {
output.Add(new MyObject() { Key = gg[b].Key, Day = gg[b].Day, Value = gg[b].Value - ff[a].Value });
b = listB.Count;
}
ins = true;
}
} else {
b = listB.Count;
}
}
if (!ins) {
output.Add(new MyObject() { Key = ff[a].Key, Day = ff[a].Day, Value = -ff[a].Value });
}
}
for (int a = 0; a < gg.Length;a++ ) {
if(!masuk[a]){
output.Add(new MyObject() { Key = gg[a].Key, Day = gg[a].Day, Value = gg[a].Value });
}
}
return output;
}
and the output :
1 12/17/2012 12:00:00 AM -6
2 12/17/2012 12:00:00 AM -8
3 12/17/2012 12:00:00 AM 4
5 12/17/2012 12:00:00 AM 10
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);
Have a list like this:
01/01/2009, 120
04/01/2009, 121
30/12/2009, 520
01/01/2010, 100
04/01/2010, 101
31/12/2010, 540
I need to find the last value for each year, e.g. the result would be 520, 540?
var lastValues = records.OrderByDescending(r => r.Date)
.GroupBy(r => r.Date.Year)
.Select(g => g.First().Value);
class Program
{
static void Main()
{
var list = new[]
{
new { Date = new DateTime(2009, 1, 1), Value = 120 },
new { Date = new DateTime(2009, 4, 1), Value = 121 },
new { Date = new DateTime(2009, 12, 30), Value = 520 },
new { Date = new DateTime(2010, 1, 1), Value = 100 },
new { Date = new DateTime(2009, 4, 1), Value = 101 },
new { Date = new DateTime(2010, 12, 31), Value = 540 },
};
var result = list
.GroupBy(x => x.Date.Year)
.Select(g => new { Date = g.Key, MaxValue = g.Max(x => x.Value) });
foreach (var item in result)
{
Console.WriteLine(item);
}
}
}
I have a list of objects. Each object has an integer quantity and a DateTime variable which contains a month and year value. I'd like to traverse the list and pad the list by adding missing months (with quantity 0) so that all consecutive months are represented in the list. What would be the best way to accomplish this?
Example:
Original List
{ Jan10, 3 }, { Feb10, 4 }, { Apr10, 2 }, { May10, 2 }, { Aug10, 3 }, { Sep10, -3 }, { Oct10, 6 }, { Nov10, 3 }, { Dec10, 7 }, { Feb11, 3 }
New List
{ Jan10, 3 }, { Feb10, 4 }, {Mar10, 0}, { Apr10, 2 }, { May10, 2 }, { Jun10, 0 }, { Jul10, 0 } { Aug10, 3 }, { Sep10, -3 }, { Oct10, 6 }, { Nov10, 3 }, { Dec10, 7 }, { Jan11, 0 }, { Feb11, 3 }
One possible algorithm is to keep track of the previous and current months. If the difference between previous and current is 1 month, append current to the result. If the difference is more than one month, add the missing months first, then afterwards copy the current month.
Foo prev = months.First();
List<Foo> result = new List<Foo> { prev };
foreach (Foo foo in months.Skip(1))
{
DateTime month = prev.Month;
while (true)
{
month = month.AddMonths(1);
if (month >= foo.Month)
{
break;
}
result.Add(new Foo { Month = month, Count = 0 });
}
result.Add(foo);
prev = foo;
}
Results:
01-01-2010 00:00:00: 3
01-02-2010 00:00:00: 4
01-03-2010 00:00:00: 0
01-04-2010 00:00:00: 2
01-05-2010 00:00:00: 2
01-06-2010 00:00:00: 0
01-07-2010 00:00:00: 0
01-08-2010 00:00:00: 3
01-09-2010 00:00:00: -3
01-10-2010 00:00:00: 6
01-11-2010 00:00:00: 3
01-12-2010 00:00:00: 7
01-01-2011 00:00:00: 0
01-02-2011 00:00:00: 3
Other code needed to make this compile:
class Foo
{
public DateTime Month { get; set; }
public int Count { get; set; }
}
List<Foo> months = new List<Foo>
{
new Foo{ Month = new DateTime(2010, 1, 1), Count = 3 },
new Foo{ Month = new DateTime(2010, 2, 1), Count = 4 },
new Foo{ Month = new DateTime(2010, 4, 1), Count = 2 },
new Foo{ Month = new DateTime(2010, 5, 1), Count = 2 },
new Foo{ Month = new DateTime(2010, 8, 1), Count = 3 },
new Foo{ Month = new DateTime(2010, 9, 1), Count = -3 },
new Foo{ Month = new DateTime(2010, 10, 1), Count = 6 },
new Foo{ Month = new DateTime(2010, 11, 1), Count = 3 },
new Foo{ Month = new DateTime(2010, 12, 1), Count = 7 },
new Foo{ Month = new DateTime(2011, 2, 1), Count = 3 }
};
Note: For simplicity I haven't handled the case where the original list is empty but you should do this in production code.
Lets assume the structure is held as a List<Tuple<DateTime,int>>.
var oldList = GetTheStartList();
var map = oldList.ToDictionary(x => x.Item1.Month);
// Create an entry with 0 for every month 1-12 in this year
// and reduce it to just the months which don't already
// exist
var missing =
Enumerable.Range(1,12)
.Where(x => !map.ContainsKey(x))
.Select(x => Tuple.Create(new DateTime(2010, x,0),0))
// Combine the missing list with the original list, sort by
// month
var all =
oldList
.Concat(missing)
.OrderBy(x => x.Item1.Month)
.ToList();
var months = new [] { "Jan", "Feb", "Mar", ... };
var yourList = ...;
var result = months.Select(x => {
var yourEntry = yourList.SingleOrDefault(y => y.Month = x);
if (yourEntry != null) {
return yourEntry;
} else {
return new ...;
}
});
If I am understand correctly with "DateTime" month:
for (int i = 0; i < 12; i++)
if (!original.Any(n => n.DateTimePropery.Month == i))
original.Add(new MyClass {DateTimePropery = new DateTime(2010, i, 1), IntQuantity = 0});
var sorted = original.OrderBy(n => n.DateTimePropery.Month);
One way is to implement an IEqualityComparer<> of your object, then you can create a list of "filler" objects to add to your existing list using the "Except" extension method. Sort of like below
public class MyClass
{
public DateTime MonthYear { get; set; }
public int Quantity { get; set; }
}
public class MyClassEqualityComparer : IEqualityComparer<MyClass>
{
#region IEqualityComparer<MyClass> Members
public bool Equals(MyClass x, MyClass y)
{
return x.MonthYear == y.MonthYear;
}
public int GetHashCode(MyClass obj)
{
return obj.MonthYear.GetHashCode();
}
#endregion
}
And then you can do something like this
// let this be your real list of objects
List<MyClass> myClasses = new List<MyClass>()
{
new MyClass () { MonthYear = new DateTime (2010,1,1), Quantity = 3},
new MyClass() { MonthYear = new DateTime (2010,12,1), Quantity = 2}
};
List<MyClass> fillerClasses = new List<MyClass>();
for (int i = 1; i < 12; i++)
{
MyClass filler = new MyClass() { Quantity = 0, MonthYear = new DateTime(2010, i, 1) };
fillerClasses.Add(filler);
}
myClasses.AddRange(fillerClasses.Except(myClasses, new MyClassEqualityComparer()));
Considering years, speed and extensibility it can be done as enumerable extension (possibly even using generic property selector).
If dates are already truncated to month and list is ordered before FillMissing is executed, please consider this method:
public static class Extensions
{
public static IEnumerable<Tuple<DateTime, int>> FillMissing(this IEnumerable<Tuple<DateTime, int>> list)
{
if(list.Count() == 0)
yield break;
DateTime lastDate = list.First().Item1;
foreach(var tuple in list)
{
lastDate = lastDate.AddMonths(1);
while(lastDate < tuple.Item1)
{
yield return new Tuple<DateTime, int>(lastDate, 0);
lastDate = lastDate.AddMonths(1);
}
yield return tuple;
lastDate = tuple.Item1;
}
}
}
and in the example form:
private List<Tuple<DateTime, int>> items = new List<Tuple<DateTime, int>>()
{
new Tuple<DateTime, int>(new DateTime(2010, 1, 1), 3),
new Tuple<DateTime, int>(new DateTime(2010, 2, 1), 4),
new Tuple<DateTime, int>(new DateTime(2010, 4, 1), 2),
new Tuple<DateTime, int>(new DateTime(2010, 5, 1), 2),
new Tuple<DateTime, int>(new DateTime(2010, 8, 1), 3),
new Tuple<DateTime, int>(new DateTime(2010, 9, 1), -3),
new Tuple<DateTime, int>(new DateTime(2010, 10, 1), 6),
new Tuple<DateTime, int>(new DateTime(2010, 11, 1), 3),
new Tuple<DateTime, int>(new DateTime(2010, 12, 1), 7),
new Tuple<DateTime, int>(new DateTime(2011, 2, 1), 3)
};
public Form1()
{
InitializeComponent();
var list = items.FillMissing();
foreach(var element in list)
{
textBox1.Text += Environment.NewLine + element.Item1.ToString() + " - " + element.Item2.ToString();
}
}
which will result in textbox containing:
2010-01-01 00:00:00 - 3
2010-02-01 00:00:00 - 4
2010-03-01 00:00:00 - 0
2010-04-01 00:00:00 - 2
2010-05-01 00:00:00 - 2
2010-06-01 00:00:00 - 0
2010-07-01 00:00:00 - 0
2010-08-01 00:00:00 - 3
2010-09-01 00:00:00 - -3
2010-10-01 00:00:00 - 6
2010-11-01 00:00:00 - 3
2010-12-01 00:00:00 - 7
2011-01-01 00:00:00 - 0
2011-02-01 00:00:00 - 3