How can I calculate an integral? - c#

How to build an "integral" function from a Linq expression that returns a series of (time|value) pairs?
E.g. my Linq expression results in a list of (date|quantity) pairs:
var quantities = db.Stocks.Find(id).Trades.Select(
x => new
{
date = x.Price.Date.Subtract(unix).TotalMilliseconds,
value = x.Quantity
}).ToList();
What would be an efficient way to get a series of (date | cumulated quantity) where each record has a total of all quantities up to that point, plus the current quantity?
EDIT1 - to make it concrete: what above query returns could be
0: (01.01.2012 | 10)
1: (01.01.2013 | -5)
2: (01.01.2014 | 7)
What I'm looking for is to have the cumulated value at each discrete point of time:
0: (01.01.2012 | 10)
1: (01.01.2013 | 5)
2: (01.01.2014 | 12)
EDIT2 - A solution might be to assemble the result 'manually' and to chain each quantity to its predecessor's quantity:
var cumulatedQuantities = new List<Tuple<double, double>>();
cumulatedQuantities.Add(new Tuple<double, double>(quantities[0].date, quantities[0].value));
for (var i = 1; i <= quantities.Count - 1; i++ )
{
cumulatedQuantities.Add(new Tuple<double, double>(quantities[i].date, quantities[i].value + cumulatedQuantities[i-1].Item2));
}
Somehow, that feels pretty ugly .. but it works.

Group by date, then total the quantity in each group:
var quantities
= db.Stocks.Find(id).Trades
.GroupBy(x => x.Price.Date)
.Select(x => new
{
date = x.Key.Subtract(unix).TotalMilliseconds,
value = x.Sum(y => y.Quantity)
})
.ToList();
You could also store it in a dictionary:
var quantities
= db.Stocks.Find(id).Trades
.GroupBy(x => x.Price.Date)
.ToDictionary(x => x.Key, x => x.Sum(y => y.Quantity));
Given your clarification in your question, this will work:
var totalQty = 0;
var cumulatedQuantities =
db.Stocks.Find(id).Trades
.Select(t => new KeyValuePair<int, int>(t.Id, totalQty += t.Quantity)).ToList();

Looks like a job for Aggregate
db.Stocks.Find(id).Trades
.Aggregate(new List<Tuple<double,double>>(),
(result, next) => {
var total = (result.Any() ? result.Last().Item2 : 0) + next.value;
result.Add(Tuple.Create(next.date, total));
return result;
});
You may want to vary this a little, like using anonymous objects, a specific class, or a Dictionary to store your results, but this is the basic pattern.
If Trades isn't already ordered by date then you should do that too, by adding an .OrderBy(t => t.date) before the Aggregate

Related

How to apply filtering for specific column?

I have query like below:
var query = from operation in dbContext.Operations
select new OperationsDto
{
Id = o.Id,
ProcessDate = o.ProcessDate,
Amount = o.Credit
//Balance = ...
};
There is no Balance property in Operation entity. I have to calculate it.
Balance => (sum of operations' Amount which operation has happened before current operation ProcessDate) + current Amount). For example:
Id ProcessDate Amount Balance
1 2021.02.01 +100$ 50 + 100 = +150$ (
2 2021.02.03 -200$ 150$ + (-200) = -50$ (this get sum of amount where record's process date before 2021.02.03)
3 2019.01.01 +50$ 0 + 50$ = 50$ (because this is first operation. there is not record before 2019.01.01)
I want this in EF Core.
I know I can do this using foreach after retrieve data like below:
var operations = query.ToList();
foreach (var operation in operations)
{
operation.Balance = operations.Where(x => x.ProcessDate < operation.ProcessDate).Sum(o => o.Debit - o.Credit);
}
But I need to calculate in query
It may be possible to build a balance within the query, but frankly it would likely be quite inefficient as it would need to re-query against the Operations. Something like:
var query = dbContext.Operations
.Select(o => new OperationsDto
{
Id = o.Id,
ProcessDate = o.ProcessDate,
Credit = o.Credit,
Debit = o.Debit,
Balance = dbContext.Operations.Where(x => x.Id != o.Id && x.ProcessDate <= o.ProcessDate).Sum(x => x.Credit - x.Debit)
});
A faster solution would be to build your balance in memory:
var operations = query.OrderBy(o => o.ProcessDate).ToList();
decimal balance = 0;
foreach (var operation in operations)
{
balance += operation.Credit - operation.Debit;
operation.Balance = balance;
}
This only works if you are loading the entire range. If instead you wanted to query data after a particular date, you can fetch the initial balance and start from there:
var operations = query
.Where(o => o.ProcessDate >= startDate)
.OrderBy(o => o.ProcessDate).ToList();
decimal balance = dbContext.Operations
.Where(o => o.ProcessDate < startDate)
.Sum(o => o.Credit - o.Debit); // starting balance.
// I can't check if the above is allowed in EF, may need to do the below:
var sums = dbContext.Operations
.GroupBy(o => true)
.Where(o => o.ProcessDate < startDate)
.Select(o new
{
CreditSum = o.Sum(x => x.Credit),
DebitSum = o.Sum(x => x.Debit)
}).Single();
var balance = sums.CreditSum - sums.DebitSum; // starting balance.
foreach (var operation in operations)
{
balance += operation.Credit - operation.Debit;
operation.Balance = balance;
}

C# LINQ select until amount >= 0

This is example database table
Name | Quantanity
Book I | 1
Book II | 13
Book III | 5
etc...
And I want to select this rows until I will have 100 books usinq LINQ expression.
I was trying
.TakeWhile(x => (amount -= x.Quantanity) > 0);
But it gave me an error
"Expression tree cannot contain assignment operator"
int bookCount = 0;
var query = books
.OrderBy(b => b.Quantity) // to get count 100, otherwise exceed is likely
.AsEnumerable()
.Select(b => {
bookCount += b.Quantanity;
return new { Book = b, RunningCount = bookCount };
})
.TakeWhile(x => x.RunningCount <= 100)
.Select(x => x.Book);
Tim's solution is good, but note about it --- Only the part before the AsEnumerable() is being executed by the data server -- Basically, you are pulling the entire table into memory, and then processes it.
Let's see if we can improve that:
int bookCount = 0;
var query1 = (from b in books
where b.Quantity > 0 && b. Quantity <= 100
orderby b.Quantity
select b).Take(100).AsEnumerable();
var query = query1
.Select(b => {
bookCount += b.Quantity;
return new { Book = b, RunningCount = bookCount };
})
.TakeWhile(x => x.RunningCount <= 100)
.Select(x => x.Book);
This limits us to only 100 records in memory to look thru to get to a count of 100.

Grouping data between ranges using LINQ in C#

I have made a following code to create me a range between two numbers, and data is separated in 7 columns:
private List<double> GetRangeForElements(double minPrice, double maxPrice)
{
double determineRange = Math.Round(maxPrice / 7.00d, 3);
var ranges = new List<double>();
ranges.Insert(0, Math.Round(minPrice, 3));
ranges.Insert(1, determineRange);
for (int i = 2; i < 8; i++)
{
ranges.Insert(i, Math.Round(determineRange * i, 3));
}
return ranges;
}
Now I have list of ranges when I call the method:
var ranges = GetRangeForElements(1,1500);
On the other side now I have the data (a list) that contains following data (properties):
public double TransactionPrice {get;set;}
public int SaleAmount {get;set;}
Input data would be:
Transaction price Sale amount
114.5 4
331.5 6
169.59 8
695.99 14
1222.56 5
Generated range for between 1 and 1500 is:
1
214.28
428.57
642.85
857.14
1071.43
1285.71
1500.00
And the desired output would be:
Range Sales
(1 - 214.28) 12
(214.28 - 428.57) 6
(428.57 - 642.85) 0
(642.85 - 857.14) 14
(857.14 - 1071.43) 0
(1071.43 - 1285.71) 5
(1285.71 - 1500) 0
I've tried something like this:
var priceGroups = _groupedItems.GroupBy(x => ranges.FirstOrDefault(r => r > x.TransactionPrice))
.Select(g => new { Price = g.Key, Sales = g.Sum(x=>x.Sales) })
.ToList();
But this doesn't gives me what I want, the results I receive are completely messed up (I was able to verify the data and results manually)...
Can someone help me out?
P.S. guys, the ranges that have no sales should simply have value set to 0...
#blinkenknight here's a pic of what I'm saying, min price is = 2.45 , max price = 2.45
and the output of the 2nd method you posted is:
Since GetRangeForElements returns a List<double>, you cannot group by it. However, you can group by range index, and then use that index to get the range back:
var rangePairs = ranges.Select((r,i) => new {Range = r, Index = i}).ToList();
var priceGroups = _groupedItems
.GroupBy(x => rangePairs.FirstOrDefault(r => r.Range >= x.TransactionPrice)?.Index ?? -1)
.Select(g => new { Price = g.Key >= 0 ? rangePairs[g.Key].Range : g.Max(x => x.TransactionPrice), Sales = g.Sum(x=>x.Sales) })
.ToList();
Assuming that _groupedItems is a list, you could also start with ranges, and produce the results directly:
var priceGroups = ranges.Select(r => new {
Price = r
, Sales = _groupedItems.Where(x=>ranges.FirstOrDefault(y=>y >= x.TransactionPrice) == r).Sum(x => x.Sales)
});
Note: Good chances are, your GetRangeForElements has an error: it assumes that minPrice is relatively small in comparison to maxPrice / 7.00d. To see this problem, consider what would happen if you pass minPrice=630 and maxPrice=700: you will get 630, 100, 200, 300, ... instead of 630, 640, 650, .... To fix this problem, compute (maxPrice - minPrice) / 7.00d and use it as a step starting at minPrice:
private List<double> GetRangeForElements(double minPrice, double maxPrice) {
double step = (maxPrice - minPrice) / 7.0;
return Enumerable.Range(0, 8).Select(i => minPrice + i*step).ToList();
}

C# Linq How to enumerate over time periods between two dates to get data for a graph

I have a table of events that occur on certain dates, know i want to format the data to be able to draw a line graph of events over time. The data has a label which is the value on the x axis and data which is the value on the y axis.
The data should be between two dates, in this example its from and to and then it must be in time intervals of days
This was my first attempt.
var graphData = bookings.Where(x => x.DateCreated > from && x.DateCreated < to)
.GroupBy(x => new { x.DateCreated.Year, x.DateCreated.Month, x.DateCreated.Day })
.OrderBy(x => x.Key.Year).ThenBy(x => x.Key.Month).ThenBy(x => x.Key.Day)
.Select(x => new GraphData
{
Label = x.Key.Day + "/" + x.Key.Month + "/" + x.Key.Year,
Data = x.Count()
}).ToList();
This give the correct data for me to graph but only for the days which there where any events created.
I want to have the data for the days where nothing happened included in my graph so i can compare different kinds of events on the same graph.
I came up with this solution
var dateRanges = Enumerable.Range(0, 1 + to.Value
.Subtract(from.Value).Days)
.Select(offset => from.Value.AddDays(offset)).ToList();
var graphData = dateRanges.Select(day => new GraphData()
{
Label = day.ToString("D"),
Data = bookings.Count(x => x.DateCreated.Date == day.Date)
}).ToList();
It seems to be very inefficient.
Is there any way to do this with a linq query so that i don't have to do a count on each date?
The problem with your attempt is that it will do one query to the DB for each of your dates. Instead do one query to get the dates that have data then merge it with your range of dates in memory.
var dbData = bookings.Where(x => x.DateCreated > from && x.DateCreated < to)
.GroupBy(x => x.DateCreated.Date)
.Select(grp => new { grp.Key, grp.Count() })
.ToDictionary(x => x.Key, x => x.Count);
var graphData = Enumerable.Range(0, 1 + to.Value.Date.Subtract(from.Date.Value).Days)
.Select(offset => from.Value.Date.AddDays(offset))
.Select(day => new GraphData()
{
Label = day.ToString("D"),
Data = dbDate.TryGetValue(day, out var count) ? count : 0
}).ToList();
Also make sure to use the Date in your calculations because the difference between "1-1-2017 23:00:00" and "1-2-2017 00:00:00" is less than a day so your range would end up being just "1-1-2017".

linq groupby Months and add any missing months to the grouped data

I have created a linq statement which seems to be working ok. I may or maynot have written it correctly however its returning my expected results.
var grouped = RewardTransctions.GroupBy(t => new
{
t.PurchaseDate.Value.Month
}).Select(g => new TransactionDetail()
{
Month =
g.Where(w=>w.EntryType==1).Select(
(n =>
n.PurchaseDate.Value.Month))
.First(),
TransactionAmount = g.Count()
});
Now the results are returning 5 values grouped by months. Is it possible to add the 7 other missing months with a TransactionAmount = 0 to them?
The reason for my madness is I am trying to bind these values to a chart and having my x axis based on months. Currently its only showing the 5 months of records. If my data doesnt return any value for a month I some how want to add in the 0 value.
Any suggestions?
It's very simple if you use .ToLookup(...).
var lookup =
(from w in RewardTransctions
where w.EntryType == 1
select w).ToLookup(w => w.PurchaseDate.Value.Month);
var grouped =
from m in Enumerable.Range(1, 12)
select new TransactionDetail()
{
Month = m,
TransactionAmount = lookup[m].Count(),
};
How's that for a couple of simple LINQ queries?
When you're using LINQ to Objects, this query should do the trick:
var grouped =
from month in Enumerable.Range(1, 12)
select new TransactionDetail()
{
Month = month,
TransactionAmount = RewardTransactions
.Where(t => t.PurchaseDate.Value.Month == month).Count()
};
When RewardTransactions however is an IQueryable, you should first call AsEnumerable() on it.
Why not do it just like this:
var grouped =
RewardTransctions.GroupBy(t => t.PurchaseDate.Value.Month).Select(
g => new TransactionDetail { Month = g.Key, TransactionAmount = g.Count() }).ToList();
for (var i = 1; i <= 12; ++i)
{
if (grouped.Count(x => x.Month == i) == 0)
{
grouped.Add(new TransactionDetail { Month = i, TransactionAmount = 0 });
}
}
It's not entirely LINQ, but straight forward. I also simplified your LINQ query a bit ;-)
I guess If you do not use an anonymoustype(var), but create a custom type and do a .ToList() on your query that you can use .Add() on your list and bind the chart to the list.

Categories