Home > Software engineering >  Calculate growth and return as rows to a separate List
Calculate growth and return as rows to a separate List

Time:03-29

I have an iList that contains yearly values of several units of observation (e.g. BBV Value, Price, Tax, Other). Usually the value contains a decimal but some years in a range any given unit of observation be contain null or 0.

For many of those data units I need to graph their annual growth as a percentage, which I'd like to return in YearlyGrowthTable. Also returning the average annual growth, or a way to calculate that back in the main code after the data is returned, would be a bonus.

While I plan to duplicate this class for each data unit, I feel like there'd be a way to code this so I could pass each data unit through a single class if I were just a little smarter.

Below is my clumsy attempt to do so for BVV Value. I've included building the source YearlyValuesTable table rather than present an actual table to help you help me, but in reality the yearly values would be passed in from a DB read. Sorry if that makes it long

using System;
using System.Collections.Generic;
using System.Linq;

public class YearlyValuesTable
{
    public string Code { get; set; }
    public int Year { get; set; }
    public string Type { get; set; }
    public decimal? Value { get; set; }
    public decimal? Growth { get; set; }
    public bool Active { get; set; }
    
    public override string ToString() => $"Code: {Code}   |   Year: {Year}   |   Type: {Type}   |   Value: {Value}   |   Active: {Active} ";
}

//new YearlyGrowthTable { Code = yv.code, Year = year, Type = "BBVVal", Value = growth },
public class YearlyGrowth
{
    public string Code { get; set; }
    public int? Year { get; set; }
    public string Type { get; set; }
    public decimal? Value { get; set; }
    
    public override string ToString() => $"Code: {Code}   |   Year: {Year}   |   Type: {Type}   |   Value: {Value} ";
}   

public class Program
{
    public static void CalculateYearlyBBVGrowth()
    {
        List<YearlyValuesTable> yv = new()
        {
            new YearlyValuesTable { Code = "ABC", Year = 2010, Type = "Other", Value = 71.20m, Active = true },
            new YearlyValuesTable { Code = "ABC", Year = 2010, Type = "BBVVal", Value = 3445m, Active = true },
            new YearlyValuesTable { Code = "ABC", Year = 2011, Type = "BBVVal", Value = 17667m, Active = true },
            new YearlyValuesTable { Code = "ABC", Year = 2012, Type = "BBVVal", Value = -2884m, Active = true },
            new YearlyValuesTable { Code = "ABC", Year = 2013, Type = "BBVVal", Value = 6577m, Active = true },
            new YearlyValuesTable { Code = "ABC", Year = 2014, Type = "BBVVal", Value = 3963m, Active = true },
            new YearlyValuesTable { Code = "ABC", Year = 2015, Type = "BBVVal", Value = 7183m, Active = true },
            new YearlyValuesTable { Code = "ABC", Year = 2016, Type = "BBVVal", Value = -4561m, Active = true },
            new YearlyValuesTable { Code = "ABC", Year = 2017, Type = "BBVVal", Value = -807m, Active = true },
            new YearlyValuesTable { Code = "ABC", Year = 2018, Type = "BBVVal", Value = 1109m, Active = true },
            new YearlyValuesTable { Code = "ABC", Year = 2019, Type = "BBVVal", Value = null, Active = true },
            new YearlyValuesTable { Code = "ABC", Year = 2020, Type = "BBVVal", Value = 38860m, Active = true },
            new YearlyValuesTable { Code = "ABC", Year = 2021, Type = "BBVVal", Value = 41312m, Active = true },
            new YearlyValuesTable { Code = "ABC", Year = 2022, Type = "BBVVal", Value = null, Active = true }
        };
            string code = yv.Where(x => x.Year > 1).Select(x => x.Code).FirstOrDefault();
            int? oldestYear = yv.Where(x => x.Type == "BBVVal" && x.Active).OrderBy(x => x.Year).Select(x => x.Year).FirstOrDefault();      // may not need this
            decimal? oldestBBV = yv.Where(x => x.Type == "BBVVal" && x.Active).OrderBy(x => x.Year).Select(x => x.Value).FirstOrDefault();
            //decimal BBVValue = oldestBBV.HasValue ? oldestBBV.Value : 0.0m;   //doesn't seem to be necessary

            //Work out growth between BBV value year on year 
            //Can this line be in the control code and pass any individual data unit of observation  (BBV, Tax, Price) to this class rather than repeat the whole class for each data unit?    
            //CalculateYearlyGrowth instead of CalculateYearlyBBVGrowth, CalculateYearlyTaxGrowth, etc?
            IList<decimal?> yearlyBBVValues = yv.Where(x => x.Type == "BBVVal" && x.Active).OrderBy(x => x.Year).Select(x => x.Value).ToList();
            IList<decimal> BBVGrowth = new List<decimal>(); 
            int? year = oldestYear;
            decimal previousBBVValue = 0.00m;
            var growth = 0.00m;
            foreach (var BBV in yearlyBBVValues)
            {
                if (previousBBVValue == 0.00m)
                {
                    growth = 0.00m;
                }
                else
                {
                    growth = BBV.HasValue ? (BBV.Value - previousBBVValue)/Math.Abs(previousBBVValue) * 100 : 0.00m;
                    growth = Math.Truncate(100 * growth) / 100;
                }   
                if (year > oldestYear)
                {
                    BBVGrowth.Add(growth);
                    // I suspect I'll need a new IList for yearly growth so I can display as a graph later as Year x growth for each data Type
                    IEnumerable<YearlyGrowth> AnnualGrowth = new YearlyGrowth()
                    {
                        Code = code, 
                        Year = year, 
                        Type = "BBVVal", 
                        Value = growth
                    };

                    List<YearlyGrowth> YearlyGrowthTable = AnnualGrowth.ToList();
                    //new YearlyGrowthTable = { Code = yv.code, Year = year, Type = "BBVVal", Value = growth },
                }
                previousBBVValue = BBV.HasValue ? BBV.Value : 0.00m;
                Console.WriteLine(year.ToString()   " | "   BBV.ToString()   " | "   growth.ToString() );       // View for testing only
                year  ;                 
            }
            
            decimal averageBBVGrowth = BBVGrowth.Average() / 100;
            //Console.WriteLine(">>> Average Growth: "   averageBBVGrowth.ToString("P"));       // View for testing only
        
        foreach (var entry in BBVGrowth)
        {
            Console.WriteLine("BBV growth: "   entry);      // View for testing only
        }

        foreach (var entry in YearlyGrowthTable)
        {
            Console.WriteLine("- "   entry);        // View for testing only
        }
        
        // Return the percentage growth percentage as  /-#.## 
        return YearlyGrowthTable();
    }
}

CodePudding user response:

Does this work for your needs?

List<YearlyValuesTable> yv = new()
{
    new YearlyValuesTable { Code = "ABC", Year = 2010, Type = "Other", Value = 71.20m, Active = true },
    new YearlyValuesTable { Code = "ABC", Year = 2010, Type = "BBVVal", Value = 3445m, Active = true },
    new YearlyValuesTable { Code = "ABC", Year = 2011, Type = "BBVVal", Value = 17667m, Active = true },
    new YearlyValuesTable { Code = "ABC", Year = 2012, Type = "BBVVal", Value = -2884m, Active = true },
    new YearlyValuesTable { Code = "ABC", Year = 2013, Type = "BBVVal", Value = 6577m, Active = true },
    new YearlyValuesTable { Code = "ABC", Year = 2014, Type = "BBVVal", Value = 3963m, Active = true },
    new YearlyValuesTable { Code = "ABC", Year = 2015, Type = "BBVVal", Value = 7183m, Active = true },
    new YearlyValuesTable { Code = "ABC", Year = 2016, Type = "BBVVal", Value = -4561m, Active = true },
    new YearlyValuesTable { Code = "ABC", Year = 2017, Type = "BBVVal", Value = -807m, Active = true },
    new YearlyValuesTable { Code = "ABC", Year = 2018, Type = "BBVVal", Value = 1109m, Active = true },
    new YearlyValuesTable { Code = "ABC", Year = 2019, Type = "BBVVal", Value = null, Active = true },
    new YearlyValuesTable { Code = "ABC", Year = 2020, Type = "BBVVal", Value = 38860m, Active = true },
    new YearlyValuesTable { Code = "ABC", Year = 2021, Type = "BBVVal", Value = 41312m, Active = true },
    new YearlyValuesTable { Code = "ABC", Year = 2022, Type = "BBVVal", Value = null, Active = true }
};

IEnumerable<YearlyGrowth> ComputeGrowth(string code, string type)
{
    var valids =
        yv
            .Where(x => x.Code == code)
            .Where(x => x.Type == type)
            .Where(x => x.Active)
            .Where(x => x.Value.HasValue && x.Value.Value != 0m)
            .OrderBy(x => x.Year)
            .ToArray();
    var lookup = valids.ToLookup(x => x.Year, x => x.Value.Value);
    int first = valids.First().Year;
    int last = valids.Last().Year;
    int count = last - first   1;
    foreach (var year in Enumerable.Range(first, count).Skip(1))
    {
        yield return new YearlyGrowth()
        {
            Code = code,
            Type = type,
            Year = year,
            Value =
                (lookup[year - 1].Any() && lookup[year].Any())
                ? lookup[year].Average() / lookup[year - 1].Average()
                // .Average() here only to remove multiple values if any
                // Otherwise a .First() would work.
                : null
        };
    }
}

foreach (var yg in ComputeGrowth("ABC", "BBVVal"))
{
    Console.WriteLine($"Year {yg.Year} had {(yg.Value.HasValue ? yg.Value.Value.ToString("0.0%") : "null")} growth");
}

The output I get is this:

Year 2011 had 512.8% growth
Year 2012 had -16.3% growth
Year 2013 had -228.1% growth
Year 2014 had 60.3% growth
Year 2015 had 181.3% growth
Year 2016 had -63.5% growth
Year 2017 had 17.7% growth
Year 2018 had -137.4% growth
Year 2019 had null growth
Year 2020 had null growth
Year 2021 had 106.3% growth

Just a couple of words of advice when doing your calculations - don't store values as percentages and don't truncate or round during calculations. Only ever format as a percentage and truncate when displaying your end results.

  • Related