Linq sum by variable number of monthS or weekS - c#

SqlServer table contains records of time and hours burned, materials cost by vendors
[IdPK] [DateTime] [MaterialsCost] [Hours] [Vendor_FK] [Project_FK] [Lat] [Long]
The user decides how far back he wants to see totals and for which column value, i.e. he wants totals going back - X number of monthS, or X number of weekS on any columns_FK filter value.
For e.g. he wants totals for cost, hours on either, Vendor = Nike or ProjectX, going back (1) month, (3) weeks, or 2 months from a certain date. So, I'm trying to get the totals for based on a parameterized Linq query.
Question:
Using any of columns as a filter/selector in linq, how to get the Monthly/weekly totals for cost,hours - going back (X) number of months or weeks?
Also, should I write separate queries for months vs weeks, and separate queries for each column (columns value selected by the user, is passed to me as a genric val)?
// I tried this... but just stuck
DateTime now = DateTime.Now;
Int goBack = passedinGoingBack; // amount of units to go back
var backUnits = obj.GetType().GetProperty(name); // tried getting month or week??
var getRows = table.AsEnumerable()
var columnName = passedInColumnName;
var filter = passedInValue;
.Where(r => r.Field<DateTime>(columnName).Year == now.Year
&& r.Field<DateTime>(columnName).Month == now.Month);

This question is a little bit opinion-based. You can do it in any way it is convenient for you. That's how I would do this.
First: you can use Sum LINQ function for getting total according to the given summing rule.
var monthlyRows = table.AsEnumerable()
.Where(r => r.Field<DateTime>("ColumnName").Year == now.Year
&& r.Field<DateTime>("ColumnName").Month == now.Month);
var monthlyTotalForCost = monthlyRows.Sum(r => r.Field<decimal>("CostColumn"));
var monthlyTotalForHours = monthlyRows.Sum(r => r.Field<decimal>("HoursColumn"));
var weeklyRows = table.AsEnumerable()
.Where(r => r.Field<DateTime>("ColumnName").Year == now.Year
&& r.Field<DateTime>("ColumnName").Week == now.Week);
var weeklyTotalForCost = monthlyRows.Sum(r => r.Field<decimal>("CostColumn"));
var weeklyTotalForHours = monthlyRows.Sum(r => r.Field<decimal>("HoursColumn"));
Second: I would do separate queries for weekly and monthly totals as that' a sum of different entity values and there can be a different logic.
However, I would do a helping function for me like this:
public static class TableLinqHelper
{
public static SumOfTableColumn<T>(this IEnumerable<DataRow> rows, string columnName)
{
return rows.Sum(r => r.Field<T>(columnName));
}
public static DateTime GetDate(this DataRow row)
{
return row.Field<DateTime>("DateColumn");
}
public static GetTotalForCost(this IEnumerable<DataRow> row)
{
return SumOfTableColumn<decimal>(row, "CostColumn");
}
public static GetTotalForHours(this IEnumerable<DataRow> row)
{
return SumOfTableColumn<double>(row, "HoursColumn");
}
}
var monthlyRows = table.AsEnumerable()
.Where(r => r.GetTime().Year == now.Year
&& r.GetTime().Month == now.Month);
var weeklyRows = table.AsEnumerable()
.Where(r => r.GetTime().Year == now.Year
&& r.GetTime().Week == now.Week);
var monthlyTotalForCost = monthlyRows.GetTotalForCost();
var monthlyTotalForHours = monthlyRows.GetTotalForHours();
var weeklyTotalForCost = weeklyRows.GetTotalForCost();
var weeklyTotalForHours = weeklyRows.GetTotalForHours();
Update:
Filtering: You can filter your results using Where LINQ.
Dictionary<string, object> Filters = new Dictionary<string, object>();
Filters.Add("VendorColumn", "Nike");
Filters.Add("Hours", 7.0);
foreach (var filter in Filters)
{
monthlyRows = monthlyRows.Where(r => // ...);
}
Several monthes and weeks: You can change the condition of DateTime where.
int rowsToBeShown = 4;
var monthlyRows = table.AsEnumerable()
.Where(r => r.Field<DateTime>("ColumnName") > DateTime.Now.AddMonths(-rowsToBeShown));
It will show results for the last 4 monthes.
LINQ provides very convenient and flexible tools for data manipulation. It's all up to your fantasy.

Related

Linq query returns a zero

I'm trying to search values according to a selected week from a datetime picker,from a datatable using following Linq queries.But It returns me only a 0.
Picking the date:
DateTime selecteddate = dateTimePicker1.Value.Date;
int currentDayOfWeek = (int)selecteddate.DayOfWeek;
DateTime sunday = selecteddate.AddDays(-currentDayOfWeek);
DateTime monday = sunday.AddDays(1);
if (currentDayOfWeek == 0)
{
monday = monday.AddDays(-7);
}
List<DateTime> dates = Enumerable.Range(0, 7)
.Select(days => monday.AddDays(days))
.ToList();
My LINQ query as follows.
var sumIncomes = dataTable
.AsEnumerable()
.Where(r => r["Type"]?.ToString() == "Income" &&
dates.Contains((DateTime)r["Date"]))
.Sum(rs => (decimal)rs["Amount"]);
var sumExpenses = dataTable
.AsEnumerable()
.Where(r => r["Type"]?.ToString() == "Expense" &&
dates.Contains((DateTime)r["Date"]))
.Sum(rs => (decimal)rs["Amount"]);
My data table looks like this.
I'm not sure what causing this behaviour.
I created table like yours and here is what I found, your LINQ expression missing some things.
List<DateTime> dates = Enumerable.Range(0, 7)
.Select(days => monday.AddDays(days))
.ToList();
dates.Add(new DateTime(2021, 5, 10)); //add for simplicity to match one record from your table didn't have time to create win form project
var sumIncomes = datatable.AsEnumerable()
.Where(r => r.Field<string>("Type").Trim() == "Income" &&
dates.Contains(r.Field<DateTime>("Date")))
.Sum(r => r.Field<int>("Amount"));
As user #user18387401 noted you should use Field extension method provided with column type in this case string. When you insert data in your table if column type is nvarchar(10) you will have data in column like this "Income " not like this "Income" so you always need to trim string value from database.
r.Field<string>("Type").Trim()
Second issue you have in Sum method (decimal)rs["Amount"] on this line program will throw runtime exception "Invalid cast" because you have int column for Amount and here you try to cast in decimal, you should provide correct type in Field method.
r.Field<int>("Amount")
Hope this helps and works for you.

LINQ: how to get 1 month records

The following query is returning a number of daily orders. I want to create another query that will return int number of orders in one month. The problem is some months have 29days, some 30 or 31. Any help is appreciated!
public int GetNewDailyOrders()
{
return _DbContext.Carts.Where(x => x.Created >= DateTime.UtcNow.AddDays(-1)).Count();
}
public int GetMonthlyOrders(int month, int year)
{
return _DbContext.Carts.Count(x => x.Created.Year == year && x.Created.Month == month);
}
may be, you should consider support different timezones, summertime or not, something like this as well
My chief complaint with Nobody's answer is that it manipulates table data, which generally kills the db's ability to use indexes
Consider instead working out the date range and querying it instead:
var n = DateTime.Now;
var f = new DateTime(n.Year, n.Month, 1);
var t = new DateTime(n.Year, n.Month + 1, 1);
_dbContext.Carts.Count(c => c.CreatedDate >= f && c.CreatedDate < t);

How to sort a list after AddRange?

new to C#, SQL and Linq. I have two lists, one "dataTransactions" (fuel from gas stations) and a similar one "dataTransfers" (fuel from slip tanks).
They each access a different table from SQL and get combined later.
List<FuelLightTruckDataSource> data = new List<FuelLightTruckDataSource>();
using (SystemContext ctx = new SystemContext())
{
List<FuelLightTruckDataSource> dataTransactions
= ctx.FuelTransaction
.Where(tx => DbFunctions.TruncateTime(tx.DateTime) >= from.Date && DbFunctions.TruncateTime(tx.DateTime) <= to.Date
//&& tx.AssetFilled.AssignedToEmployee.Manager
&& tx.AssetFilled.AssignedToEmployee != null
//&
&& tx.AssetFilled.AssetType.Code == "L"
&& (tx.FuelProductType.FuelProductClass.Code == "GAS" || tx.FuelProductType.FuelProductClass.Code == "DSL"))
.GroupBy(tx => new { tx.AssetFilled, tx.DateTime, tx.FuelProductType.FuelProductClass, tx.FuelCard.FuelVendor, tx.City, tx.Volume, tx.Odometer}) //Added tx.volume to have individual transactions
.Select(g => new FuelLightTruckDataSource()
{
Asset = g.FirstOrDefault().AssetFilled,
Employee = g.FirstOrDefault().AssetFilled.AssignedToEmployee,
ProductClass = g.FirstOrDefault().FuelProductType.FuelProductClass,
Vendor = g.FirstOrDefault().FuelCard.FuelVendor,
FillSource = FuelFillSource.Transaction,
Source = "Fuel Station",
City = g.FirstOrDefault().City.ToUpper(),
Volume = g.FirstOrDefault().Volume,
Distance = g.FirstOrDefault().Odometer,
Date = g.FirstOrDefault().DateTime
})
.ToList();
In the end, I use
data.AddRange(dataTransactions);
data.AddRange(dataTransfers);
to put the two lists together and generate a fuel consumption report.
Both lists are individually sorted by Date, but after "AddRange" the "dataTransfers" just gets added to the end, losing my sort by Date. How do I sort the combined result again by date after using the "AddRange" command?
Try this:
data = data.OrderBy(d => d.Date).ToList();
Or if you want to order descending:
data = data.OrderByDescending(d => d.Date).ToList();
You can call List<T>.Sort(delegate).
https://msdn.microsoft.com/en-us/library/w56d4y5z(v=vs.110).aspx
Example:
data.Sort(delegate(FuelLightTruckDataSource x, FuelLightTruckDataSource y)
{
// your sort logic here.
});
Advantage: this sort doesn't create a new IList<T> instance as it does in OrderBy. it's a small thing, but to some people this matters, especially for performance and memory sensitive situations.

Unknown column GroupBy1.K1 in field list

First off, this worked with MSSQL Server but we've switched over to MySQL and now it isn't working.
I have a simple query which returns total values based off a certain year.
It looks like this:
public Dictionary<string, decimal> GetYearValues(int[] SupplierID) {
_ctx.Database.CommandTimeout = 5 * 60;
var Total = (from x in _ctx.Invoices
where x.Turnover == true &&
SupplierID.Contains(x.FK_SupplierID)
group x by new { x.InvoiceDate.Year } into summ
select new {
Year = summ.Key.Year,
TurnOver = summ.Sum(s => s.NetAmount_Home ?? 0)
}).ToDictionary(x => x.Year.ToString(), x => x.TurnOver);
return Total;
}
I've noticed that nothing is wrong with the query, it's only the ToDictionary which fails and gives the error message. Can anyone tell me why?

Speeding up linq group sum queries

I have a query that processes about 500 records pulled from various tables, grouped and then summaried (if that's a word) into a working report. Everything works fine but it takes about 30 seconds to run this one report and i'm getting complaints from my users.
The procedure in question is this one:
public static List<LabourEfficiencies> GetLabourEfficienciesByTimeSheet(DateTime dateFrom, DateTime dateTo)
{
CS3Entities ctx = new CS3Entities();
//get all relevant timesheetline items
var tsItems = from ti in ctx.TimeSheetItems
where ti.TimeSheetHeader.Date >= dateFrom && ti.TimeSheetHeader.Date <= dateTo && ti.TimeSheetHeader.TimeSheetCategory != "NON-PROD"
select new TimesheetLine
{
TimesheetNo = ti.TimeSheetNo,
HoursProduced = ti.HoursProduced,
HoursProducedNet = ti.HoursProducedNet,
ItemID = ti.ItemID,
ProcessID = ti.ProcessID,
ProcessDuration = ti.ProcessDuration,
DowntimeHours = 0M
};
//get all relevant downtimeline items
var tsDownT = from dt in ctx.DowntimeItems
where dt.TimeSheetHeader.Date >= dateFrom && dt.TimeSheetHeader.Date <= dateTo && dt.TimeSheetHeader.TimeSheetCategory != "NON-PROD"
select new TimesheetLine
{
TimesheetNo = dt.TimeSheetNo,
HoursProduced = 0M,
HoursProducedNet = 0M,
ItemID = "",
ProcessID = "",
ProcessDuration = 0M,
DowntimeHours = dt.DowntimeHours
};
//combine them into single table
var tsCombi = tsItems.Concat(tsDownT);
var flatQuery = (from c in tsCombi
join th in ctx.TimeSheetHeaders on c.TimesheetNo equals th.TimeSheetNo
select new
{
th.TimeSheetNo,
th.EmployeeNo,
th.TimeSheetCategory,
th.Date,
c.HoursProduced,
c.ProcessDuration,
th.HoursWorked,
c.HoursProducedNet,
c.DowntimeHours,
c.ItemID
});
//add employee details & group by timesheet no (1 line per timesheet no)
//NB. FnTlHrs checks whether there are any indirect hrs & deducts them if there are
var query = flatQuery.GroupBy(f => f.TimeSheetNo).Select(g => new LabourEfficiencies
{
Eno = g.FirstOrDefault().EmployeeNo,
Dept =g.FirstOrDefault().TimeSheetCategory,
Date = g.FirstOrDefault().Date,
FnGrHrs =g.Where(w =>w.TimeSheetCategory == "FN" &&!w.ItemID.StartsWith("090")).Sum(h => h.HoursProduced),
FnTlHrs =g.Where(w =>w.ItemID.StartsWith("090")).Sum(h => h.ProcessDuration) >0? (g.FirstOrDefault(w =>w.TimeSheetCategory =="FN").HoursWorked) -(g.Where(w =>w.ItemID.StartsWith("090")).Sum(h =>h.ProcessDuration)): g.FirstOrDefault(w =>w.TimeSheetCategory =="FN").HoursWorked,
RmGrHrs =g.Where(w =>w.TimeSheetCategory == "RM").Sum(h => h.HoursProduced),RmGrHrsNet =g.Where(w =>w.TimeSheetCategory == "RM").Sum(h => h.HoursProducedNet),
RmTlHrs =g.FirstOrDefault(w =>w.TimeSheetCategory == "RM").HoursWorked,
MpGrHrs =g.Where(w =>w.TimeSheetCategory =="MATPREP").Sum(h => h.HoursProduced),
MpTlHrs =g.FirstOrDefault(w =>w.TimeSheetCategory =="MATPREP").HoursWorked,
DtHrs = g.Sum(s => s.DowntimeHours),
Indirect =g.Where(w =>w.ItemID.StartsWith("090")).Sum(h => h.ProcessDuration)
});
return query.ToList();
}
The first few bits just gather the data, it's the last query that is the "meat" of the procedure and takes the time.
I'm fairly sure I've done something horrid as the SQL it spits out is terrible, but for the life of me i can't see how to improve it.
Any hints greatly appreciated.
Gordon
Your expression gets optimized both in IQueriable compilation and SQL server query optimization and even here takes that long. It's highly probable that you have no column indexes needed for execution plan to be faster. Copy/paste your rendered SQL expression to SSMS, run it and see the actual plan. Optimize database structire if needed (put indexes). Otherwise, you got that really large amont of data that makes process slow.

Categories

Resources