简体   繁体   中英

how to use LINQ TakeWhile and Join to a list

I want to map oldest possible credit invoice's date to a list from sale table. I have a list in following manner.

var tb = // Some other method to get balances.
cust    balance
1           1000
2           2000
3           3000
... so on ...

These balance are accumulation of few invoices or sometimes may be one invoice.

public class Sales
{
    public int id { get; set; }
    public DateTime saleDate { get; set; }
    public int? cust { get; set; }
    public decimal invoiceValue { get; set; }
    // Other properties...
}

Sample data for cust 1

saleInvNo  saleDate     cust invoiceValue 
1          2018/12/01   1   500
12         2018/12/20   1   750

If I fetch now, report should be as follows.

cust    balance     balanceFromDate
1          1000     2018/12/01 // balance from this onwards.
2          2000     ???
3          3000     ???

Is there easy way to achieve this through LINQ.

I tried with TakeWhile but attempt was not successful.

foreach (var t in tb)
{
    var sum = 0m;
    var q = _context.SaleHeaders.Where(w=>w.cust == t.cust)
        .OrderByDescending(o=>o.saleDate)
    .Select(s => new { s.id, s.saleDate, s.invoiceValue }).AsEnumerable()
    .TakeWhile(x => { var temp = sum; sum += x.invoiceValue; return temp > t.balance; });
}

Note: Sales table is having ~75K records.

More Info...

Customers may pay partial amount than Sale Invoice. All Payments, Invoices are posted to another table thats why balances comes from a complex query on another table.

Sales table has purely raw sales data.

Now, cust 1 balance is 1000 than the value is of last sale invoice or even last to last sale invoice's full or partial. I need to map "balance from onwards".

So you have two sequences: a sequence of Balances and a sequence of Sales .

Every Balance has at least an int property CustomerId , and a BalanceValue

Every Sale has at least a nullable int property CustomerId and a DateTime property SaleDate

class Balance
{
     public int CustomerId {get; set;}
     public decimal BalanceValue {get; set;}
}
class Sale
{
     public int? CustomerId {get; set;}
     public DateTime SaleDate {get; set;}
} 

Apparently it is possible to have a Sale without a Customer.

Now given a sequence of Balances and Sales , you want for every Balance one object, containing the CustomerId , the BalanceValue and the SaleDate that this Customer has in the sequence of Sales .

Your requirement doesn't specify what you want with Balances in your sequence of Balances that have a Customer without a Sale in the sequence of Sales. Let's assume you want those items in your result with a null LastSaleDate .

IEnumerable<Balance> balances = ...
IEnumerable<Sales> sales = ...

We are not interested in sales without a customer. So let's filter them out first:

var salesWithCustomers = sales.Where(sale => sale.CustomerId != null);

Now make groups of Balances with their sales, by groupjoiningon CustomerId:

var balancesWithSales = balances.GroupJoin(salesWithCustomers, // GroupJoin balances and sales
    balance => balance.CustomerId,       // from every Balance take the CustomerId
    sale => sale.CustomerId,             // from every Sale take the CustomerId. I know it is not null,
    (balance, salesOfBalance) => new     // from every balance, with all its sales
    {                                    // make one new object  
         CustomerId = balance.CustomerId,
         Balance = balance.BalanceValue,

         LastSaleDate = salesOfBalance                // take all salesOfBalance
            .Select(sale => sale.SaleDate)            // select the saleDate
            .OrderByDescending(sale => sale.SaleDate) // order: newest dates first
            .FirstOrDefault(),                        // keep only the first element
    })

The calculation of the LastSaleDate orders all elements, and keeps only the first.

Although this solution workd, it is not efficient to order all other elements if you only need the largest one. If you are working with IEnumerables, instead of IQueryables (as would be in a database), you can optimize this, by creating a function.

I implement this as an extension function. See extension methods demystified

static DateTime? NewestDateOrDefault(this IEnumerable<DateTime> dates)
{
    IEnumerator<DateTime> enumerator = dates.GetEnumerator();
    if (!enumerator.MoveNext())
    {
        // sequence of dates is empty; there is no newest date
        return null;
    }
    else
    {   // sequence contains elements
        DateTime newestDate = enumerator.Current;
        while (enumerator.MoveNext())
        {   // there are more elements
            if (enumerator.Current > newestDate)
               newestDate = enumerator.Current);
        }
        return newestDate;
    }
}

Usage:

LastSaleDate = salesOfBalance
    .Select(sale => sale.SaleDate)
    .NewesDateOrDefault();

Now you know that instead of sorting the complete sequence you'll enumerate the sequence only once.

Note: you can't use Enumerable.Aggregate for this, because it doesn't work with empty sequences.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM