views:

144

answers:

6

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 }

+1  A: 
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 ...;
  }
});
John Fisher
+3  A: 

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.

Mark Byers
This works to spec, and is very simple, and would perform well.
Patrick Karcher
+1 for works to spec. :p
Tanzelax
THANKS Mark! Spot on.
Addie
Actually even if it works to specs, there is no requirement to create new list of objects as you can traverse ordered list and return only new, missing objects using yield directive (details below)
too
+3  A: 

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();
JaredPar
He has Jan10 and Jan11 in the same list.
Tanzelax
@Tanzelax, that looks like a typo, I'll add a comment to confirm
JaredPar
Nah, he wants 14 elements in the final list, from jan 2010 to feb 2011
Tanzelax
Interestingly, just about everyone that posted a response missed that in his example. :p
Tanzelax
@Tanzelax. Correct, I specifically need it to take into account multiple years.
Addie
A: 

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()));
Anthony Pegram
oh, similar with my solution :)
Nagg
A: 

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);
Nagg
A: 

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
too