0

I have a recurring pattern that I see in some of the reports that I have to generate. These reports are date range based reports and need to be aggregated by different levels.

For the sake of simplicity, let's assume that this report produces a title and a quota at the leaf node level (lowest level). At the line item level (which is a collection of various leaf nodes), I would like to aggregate the Quota and provide a separate title. These line items would further roll up to another level which would again aggregate the quota and have a unique title.

So the report would be something like this:

ROOT LEVEL | Title = "Main Report" | Quota = 100
   Month Level    | Title = "Jan" | Quota = 100
     Week Level   | Title = "Week 1" | Quota = 25
     Week Level   | Title = "Week 2" | Quota = 75

Is there way for me to build this using a composite pattern? I have tried numerous approaches. Most of them fall short because I cannot effectively aggregate/sum up the quota to the higher level.

I can build an interface like so:

public interface IInventoryReportItem
{ 
    string Title { get; set; }
    int Quota { get; set; }
}

Then I can build a Line Item like so:

public class LineItem : IInventoryReportItem

I can also build a collection like so:

 public class LineItems : IList<IInventoryReportItem>, IInventoryReportItem
{
    private readonly List<IInventoryReportItem> _subLineItems;

    public LineItems()
    {
        _subLineItems = new List<IInventoryReportItem>();
    }

And my report would be like so:

public class InventoryReport
{
    public DateRange DateRange { get; set; }
    public LineItems LineItems { get; set; }

}

I can build the report easily in a hierarchical fashion now, but I still need to call the aggregation functions from the outside as opposed to it auto-calculating this for me:

var report = new InventoryReport();

        var week1Days = new LineItems
        {
            new LineItem {Quota = 20, Title = "Day 1"},
            new LineItem {Quota = 10, Title = "Day 2"}
        };

        var week2Days = new LineItems
        {
            new LineItem {Quota = 10, Title = "Day 1"},
            new LineItem {Quota = 10, Title = "Day 2"}
        };

        var week1 = new LineItems {week1Days};
        week1.Quota = week1.Sum(x => x.Quota);
        week1.Title = "Week1";


        var week2 = new LineItems {week2Days};
        week2.Quota = week2.Sum(x => x.Quota);
        week2.Title = "Week2";

        var month1 = new LineItems(new List<IInventoryReportItem> {week1, week2});
        month1.Title = "January";
        month1.Quota = month1.Sum(x => x.Quota);

        report.LineItems = new LineItems(new List<IInventoryReportItem> {month1});

Is there a way I can still have the flexibility of adding either a single line item or a range of items, and it also auto-calculate/aggregate the data for me using the composite?

Any help would be great!

Thank You, Anup

Anup Marwadi
  • 2,517
  • 4
  • 25
  • 42
  • 1
    Implement a getter for `LineItems`' `Quota` property that checks its own length, and if greater than 0, sums its children's `Quota`s. I'm not sure whether that fits in the "composite pattern", but I don't let patterns get in the way of code that works. – Heretic Monkey Mar 14 '17 at 20:27

2 Answers2

0

For me, it seems you're looking for RX (Reactive eXtensions) so you don't have to sum manually each time at each level. Instead, just setup necessary subscriptions and get re-calculations automatically. For example: Good example of Reactive Extensions Use

Community
  • 1
  • 1
Yury Schkatula
  • 5,291
  • 2
  • 18
  • 42
  • Hello, that would be fine in a connected scenario. This is behind an api that I would like to send down the wire as JSON. – Anup Marwadi Mar 14 '17 at 22:36
0

I solved this problem. For those who are interested, here's how I solved it:

I built an interface as shown:

public interface IInventoryReportItem
{
    string Title { get; set; }
    int Quota { get; }
    int TotalTicketsSold { get; }
    int TotalUnitsSold { get; }
    decimal TotalSalesAmount { get; }
}

I implemented this interface in a class called as a LineItem as follows:

public class LineItem : IInventoryReportItem
{

    public LineItem(string title, int quota, int totalTicketsSold, int totalUnitsSold, int totalCheckedIn,
        decimal totalSalesAmount)
    {
        Title = title;
        Quota = quota;
        TotalUnitsSold = totalUnitsSold;
        TotalTicketsSold = totalTicketsSold;
        TotalCheckedIn = totalCheckedIn;
        TotalSalesAmount = totalSalesAmount;
    }

    public string Title { get; set; }
    public int Quota { get; }
    public int TotalTicketsSold { get; }
    public int TotalUnitsSold { get; }
    public int TotalCheckedIn { get; }
    public decimal TotalSalesAmount { get; }

}

I also created a custom collection class called LineItems as shown. Note that the collection is of the type IInventoryReportItem itself:

public class LineItems : IInventoryReportItem
{
    public string Title { get; set; }
    public int Quota => Contents?.Sum(x => x.Quota) ?? 0;
    public int TotalTicketsSold => Contents?.Sum(x => x.TotalTicketsSold) ?? 0;
    public int TotalUnitsSold => Contents?.Sum(x => x.TotalUnitsSold) ?? 0;
    public decimal TotalSalesAmount => Contents?.Sum(x => x.TotalSalesAmount) ?? 0;

    public readonly List<IInventoryReportItem> Contents;

    public LineItems(List<IInventoryReportItem> lineItems)
    {
        Contents = lineItems ?? new List<IInventoryReportItem>();
    }

}

All of the aggregation is done at this collection class level.

The report class is as follows:

public class InventoryReport
{
    public DateRange DateRange { get; set; }
    public IInventoryReportItem LineItems { get; set; }

}

I was then able to build the report like so:

 Report = new InventoryReport();
        var week1 = new LineItems(new List<IInventoryReportItem>
        {
            new LineItem("Day1", 10, 10, 10, 4, 100),
            new LineItem("Day2", 10, 5, 5, 1, 50)
        })
        {Title = "Week1"};

        var week2 = new LineItems(new List<IInventoryReportItem>
        {
            new LineItem("Day1", 20, 20, 20, 20, 200),
            new LineItem("Day2", 20, 5, 5, 5, 50)
        }) {Title = "Week2"};

        var month1 = new LineItems(new List<IInventoryReportItem> {week1, week2}) {Title = "January"};
        Report.LineItems = new LineItems(new List<IInventoryReportItem> {month1}) {Title = "Daily Report"};

The final output (JSON) that I receive from my API is like so:

{
"lineItems": {
"contents": [
  {
    "contents": [
      {
        "contents": [
          {
            "title": "Day1",
            "quota": 10,
            "totalTicketsSold": 10,
            "totalUnitsSold": 10,
            "totalCheckedIn": 4,
            "totalSalesAmount": 100
          },
          {
            "title": "Day2",
            "quota": 10,
            "totalTicketsSold": 5,
            "totalUnitsSold": 5,
            "totalCheckedIn": 1,
            "totalSalesAmount": 50
          }
        ],
        "title": "Week1",
        "quota": 20,
        "totalTicketsSold": 15,
        "totalUnitsSold": 15,
        "totalSalesAmount": 150
      },
      {
        "contents": [
          {
            "title": "Day1",
            "quota": 20,
            "totalTicketsSold": 20,
            "totalUnitsSold": 20,
            "totalCheckedIn": 20,
            "totalSalesAmount": 200
          },
          {
            "title": "Day2",
            "quota": 20,
            "totalTicketsSold": 5,
            "totalUnitsSold": 5,
            "totalCheckedIn": 5,
            "totalSalesAmount": 50
          }
        ],
        "title": "Week2",
        "quota": 40,
        "totalTicketsSold": 25,
        "totalUnitsSold": 25,
        "totalSalesAmount": 250
      }
    ],
    "title": "January",
    "quota": 60,
    "totalTicketsSold": 40,
    "totalUnitsSold": 40,
    "totalSalesAmount": 400
  }
],
"title": "Daily Report",
"quota": 60,
"totalTicketsSold": 40,
"totalUnitsSold": 40,
"totalSalesAmount": 400
  }
}

Using this approach, I was able to eliminate the overhead of performing an aggregation and was still able to use a collection or individual items using the same signature.

Hopefully someone finds this helpful!

Anup Marwadi
  • 2,517
  • 4
  • 25
  • 42