Skip to main content
a more functional approach without the sting of multiple iterations
Source Link
Jeff Mercado
  • 5.4k
  • 28
  • 33
public class SalesRecord
{
    public DateTime Date { get; set; }
    public bool IsValid { get; set; }
    public int SalesType { get; set; }
    public decimal UnitPrice { get; set; }
}
public class SalesDataContainter
{
    private SortedList<DateTime, SalesRecord> salesData;
    private decimal GetInferredPrice(SalesRecord inferredSalesRecord)
    {
        Func<SalesRecord, bool> isValidRecord = record =>
            record.IsValid                                          //neigbor needs to be valid
            && record.SalesType == inferredSalesRecord.SalesType;   //has to have the same sales type
        var candidateRecords = salesData.Values
            .SkipWhile(record => !isValidRecord(record))
            .Where(isValidRecord);

        SalesRecord closestBefore = null;
        SalesRecord closestAfter = null;
        foreach (var salesRecord in candidateRecords)
        {
            //get the last found for before neighbor
            if (salesRecord.Date < inferredSalesRecord.Date)
            {
                closestBefore = salesRecord;
            }
            //get the first found for after neighbor
            else if (salesRecord.Date > inferredSalesRecord.Date)
            {
                closestAfter = salesRecord;
                break;
            }
        } 

        // there's a neighbor before
        if (closestBefore != null)
        {
            return (closestAfter != null)
                // we have neighbors on both sides
              ? (closestBefore.UnitPrice + closestAfter.UnitPrice) / 2M
              : closestBefore.UnitPrice;
        }
        else
        {
            return (closestAfter != null)
                // there's only a neighbor after
              ? closestAfter.UnitPrice
              : 0M;
        }
    }
}

An alternative would be to let LINQ do all the work:

public decimal GetInferredPrice(SalesRecord inferredSalesRecord)
{
    Func<SalesRecord, bool> isValidRecord = record =>
        record.IsValid                                          //neigbor needs to be valid
        && record.SalesType == inferredSalesRecord.SalesType;   //has to have the same sales type

    return salesData.Values
        .SkipWhile(record => !isValidRecord(record))
        .Where(isValidRecord)
        .GroupBy(
            record => record.Date.CompareTo(inferredSalesRecord.Date),
            record => record.UnitPrice as decimal?,
            (key, g) => (key < 0) // assumes no records will have equal dates
                ? g.LastOrDefault()
                : g.FirstOrDefault()
        )
        .Average() ?? 0M;
}
public class SalesRecord
{
    public DateTime Date { get; set; }
    public bool IsValid { get; set; }
    public int SalesType { get; set; }
    public decimal UnitPrice { get; set; }
}
public class SalesDataContainter
{
    private SortedList<DateTime, SalesRecord> salesData;
    private decimal GetInferredPrice(SalesRecord inferredSalesRecord)
    {
        Func<SalesRecord, bool> isValidRecord = record =>
            record.IsValid                                          //neigbor needs to be valid
            && record.SalesType == inferredSalesRecord.SalesType;   //has to have the same sales type
        var candidateRecords = salesData.Values
            .SkipWhile(record => !isValidRecord(record))
            .Where(isValidRecord);

        SalesRecord closestBefore = null;
        SalesRecord closestAfter = null;
        foreach (var salesRecord in candidateRecords)
        {
            //get the last found for before neighbor
            if (salesRecord.Date < inferredSalesRecord.Date)
            {
                closestBefore = salesRecord;
            }
            //get the first found for after neighbor
            else if (salesRecord.Date > inferredSalesRecord.Date)
            {
                closestAfter = salesRecord;
                break;
            }
        }
        // there's a neighbor before
        if (closestBefore != null)
        {
            return (closestAfter != null)
                // we have neighbors on both sides
              ? (closestBefore.UnitPrice + closestAfter.UnitPrice) / 2M
              : closestBefore.UnitPrice;
        }
        else
        {
            return (closestAfter != null)
                // there's only a neighbor after
              ? closestAfter.UnitPrice
              : 0M;
        }
    }
}
public class SalesRecord
{
    public DateTime Date { get; set; }
    public bool IsValid { get; set; }
    public int SalesType { get; set; }
    public decimal UnitPrice { get; set; }
}
public class SalesDataContainter
{
    private SortedList<DateTime, SalesRecord> salesData;
    private decimal GetInferredPrice(SalesRecord inferredSalesRecord)
    {
        Func<SalesRecord, bool> isValidRecord = record =>
            record.IsValid                                          //neigbor needs to be valid
            && record.SalesType == inferredSalesRecord.SalesType;   //has to have the same sales type
        var candidateRecords = salesData.Values
            .SkipWhile(record => !isValidRecord(record))
            .Where(isValidRecord);

        SalesRecord closestBefore = null;
        SalesRecord closestAfter = null;
        foreach (var salesRecord in candidateRecords)
        {
            //get the last found for before neighbor
            if (salesRecord.Date < inferredSalesRecord.Date)
            {
                closestBefore = salesRecord;
            }
            //get the first found for after neighbor
            else if (salesRecord.Date > inferredSalesRecord.Date)
            {
                closestAfter = salesRecord;
                break;
            }
        } 

        // there's a neighbor before
        if (closestBefore != null)
        {
            return (closestAfter != null)
                // we have neighbors on both sides
              ? (closestBefore.UnitPrice + closestAfter.UnitPrice) / 2M
              : closestBefore.UnitPrice;
        }
        else
        {
            return (closestAfter != null)
                // there's only a neighbor after
              ? closestAfter.UnitPrice
              : 0M;
        }
    }
}

An alternative would be to let LINQ do all the work:

public decimal GetInferredPrice(SalesRecord inferredSalesRecord)
{
    Func<SalesRecord, bool> isValidRecord = record =>
        record.IsValid                                          //neigbor needs to be valid
        && record.SalesType == inferredSalesRecord.SalesType;   //has to have the same sales type

    return salesData.Values
        .SkipWhile(record => !isValidRecord(record))
        .Where(isValidRecord)
        .GroupBy(
            record => record.Date.CompareTo(inferredSalesRecord.Date),
            record => record.UnitPrice as decimal?,
            (key, g) => (key < 0) // assumes no records will have equal dates
                ? g.LastOrDefault()
                : g.FirstOrDefault()
        )
        .Average() ?? 0M;
}
Source Link
Jeff Mercado
  • 5.4k
  • 28
  • 33

Just a few things I want to point out before tackling this:

  • Based on the comment, your first condition is flipped. You're skipping valid entries instead of invalid entries. It should be:

      if (!salesRecord.IsValid)
    
  • Your last condition in the loop could just break at that point. Since it is sorted, every valid item will be a match but you're only interested in the first.

  • You should consider making UnitPrice of type decimal. You're dealing with currency here so you should not be using double.

Using a little bit of LINQ here could make this more readable IMHO. It's easier to think of it as "looping through the list of candidate records" rather than "looping through all records and skipping some of them."

Personally, I would write the last set of conditions as I have shown using the conditional operator. You may or may not like it this way or this structure so your choice with that part.

public class SalesRecord
{
    public DateTime Date { get; set; }
    public bool IsValid { get; set; }
    public int SalesType { get; set; }
    public decimal UnitPrice { get; set; }
}
public class SalesDataContainter
{
    private SortedList<DateTime, SalesRecord> salesData;
    private decimal GetInferredPrice(SalesRecord inferredSalesRecord)
    {
        Func<SalesRecord, bool> isValidRecord = record =>
            record.IsValid                                          //neigbor needs to be valid
            && record.SalesType == inferredSalesRecord.SalesType;   //has to have the same sales type
        var candidateRecords = salesData.Values
            .SkipWhile(record => !isValidRecord(record))
            .Where(isValidRecord);

        SalesRecord closestBefore = null;
        SalesRecord closestAfter = null;
        foreach (var salesRecord in candidateRecords)
        {
            //get the last found for before neighbor
            if (salesRecord.Date < inferredSalesRecord.Date)
            {
                closestBefore = salesRecord;
            }
            //get the first found for after neighbor
            else if (salesRecord.Date > inferredSalesRecord.Date)
            {
                closestAfter = salesRecord;
                break;
            }
        }
        // there's a neighbor before
        if (closestBefore != null)
        {
            return (closestAfter != null)
                // we have neighbors on both sides
              ? (closestBefore.UnitPrice + closestAfter.UnitPrice) / 2M
              : closestBefore.UnitPrice;
        }
        else
        {
            return (closestAfter != null)
                // there's only a neighbor after
              ? closestAfter.UnitPrice
              : 0M;
        }
    }
}