Addie
Addie

Reputation: 1691

Traverse list to add elements using .NET

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 }

Upvotes: 4

Views: 1030

Answers (6)

Mark Byers
Mark Byers

Reputation: 838696

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.

Upvotes: 3

Michał Turecki
Michał Turecki

Reputation: 3167

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

Upvotes: 0

Anthony Pegram
Anthony Pegram

Reputation: 126932

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()));

Upvotes: 0

EgorBo
EgorBo

Reputation: 6142

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);

Upvotes: 0

JaredPar
JaredPar

Reputation: 755141

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();

Upvotes: 3

John Fisher
John Fisher

Reputation: 22717

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 ...;
  }
});

Upvotes: 1

Related Questions