How can I make this LINQ query cleaner? - c#

I have recently written a LINQ query to get a Dictionary containing the last 6 month's placement amounts.
It is returning a Dictionary of Month string - Decimal Amount pairs.
It seems kind of cludgey. Any of you LINQ masters out there able to help me refactor this to make a bit cleaner?
/// <summary>
/// Gets the last 6 months of Placement History totalled by Month
/// for all Agencies
/// </summary>
/// <returns></returns>
public Dictionary<string, decimal> getRecentPlacementHistory()
{
var placementHistoryByMonth = new Dictionary<string, decimal>();
using (DemoLinqDataContext db = new DemoLinqDataContext())
{
for (int i = 0; i < 6; i++)
{
Decimal monthTotal =
(from a in db.Accounts
where
(a.Date_Assigned.Value.Month == DateTime.Now.AddMonths(-i).Month &&
a.Date_Assigned.Value.Year == DateTime.Now.AddMonths(-i).Month)
select a.Amount_Assigned).Sum();
String currentMonth = DateTime.Now.AddMonths(-i).ToString("MMM");
placementHistoryByMonth.Add(currentMonth, monthTotal);
}
return placementHistoryByMonth;
}
}

First problem:
where (a.Date_Assigned.Value.Month == DateTime.Now.AddMonths(-i).Month &&
a.Date_Assigned.Value.Year == DateTime.Now.AddMonths(-i).Month)
Shouldn't the latter expression end with .Year rather than .Month? Surely you'll rarely get a year with a value of 1-12...
I would extract the idea of the "current month" as you're using it a lot. Note that you're also taking the current time multiple times, which could give odd results if it runs at midnight at the end of a month...
public Dictionary<string, decimal> getRecentPlacementHistory()
{
var placementHistoryByMonth = new Dictionary<string, decimal>();
using (DemoLinqDataContext db = new DemoLinqDataContext())
{
DateTime now = DateTime.Now;
for (int i = 0; i < 6; i++)
{
DateTime selectedDate = now.AddMonths(-i);
Decimal monthTotal =
(from a in db.Accounts
where (a.Date_Assigned.Value.Month == selectedDate.Month &&
a.Date_Assigned.Value.Year == selectedDate.Year)
select a.Amount_Assigned).Sum();
placementHistoryByMonth.Add(selectedDate.ToString("MMM"),
monthTotal);
}
return placementHistoryByMonth;
}
}
I realise it's probably the loop that you were trying to get rid of. You could try working out the upper and lower bounds of the dates for the whole lot, then grouping by the year/month of a.Date_Assigned within the relevant bounds. It won't be much prettier though, to be honest. Mind you, that would only be one query to the database, if you could pull it off.

Use Group By
DateTime now = DateTime.Now;
DateTime thisMonth = new DateTime(now.Year, now.Month, 1);
Dictionary<string, decimal> dict;
using (DemoLinqDataContext db = new DemoLinqDataContext())
{
var monthlyTotal = from a in db.Accounts
where a.Date_Assigned > thisMonth.AddMonths(-6)
group a by new {a.Date_Assigned.Year, a.Date_Assigned.Month} into g
select new {Month = new DateTime(g.Key.Year, g.Key.Month, 1),
Total = g.Sum(a=>a.Amount_Assigned)};
dict = monthlyTotal.OrderBy(p => p.Month).ToDictionary(n => n.Month.ToString("MMM"), n => n.Total);
}
No loop needed!

If you are not worried about missing months with no data,then I had a similar problem where I did the following : (translated to your variables)
DateTime startPeriod =
new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1).AddMonths(-6);
var query1 = from a in db.Accounts where a.Date_Assigned >= startPeriod
group a by new { a.Date_Assigned.Year ,a.Date_Assigned.Month } into result
select new
{
dt = new DateTime( result.Key.Year, result.Key.Month , 1),
MonthTotal = result.Sum(i => i.Amount_Assigned)
} ;
var dict = query1.OrderBy(p=> p.dt).ToDictionary(n => n.Dt.ToString("MMM") , n => n.MonthTotal );

Related

Split field a string and compare each value with string linq c#

I'm trying to search in DataTable with three string columns[start_date, end_date, expected_visits] using LINQ, but the third column has many lines as shown
So I split the third one with \n but I want to loop over date to compare it with text of txtSearchYear and return the result as DataTable, When I tried to use ForEach as shown in my code, An error appeared with me says A local or parameter named 'e' cannot be declared in this scope because that name is used in an enclosing local scope to define a local here is my code ...
var tblResult =
tbl
.AsEnumerable()
.Where(x =>
x.Field<string>("start_date").Substring(0, 4).Equals(txtSearchYear.Text.Trim())
|| x.Field<string>("end_date").Substring(0, 4).Equals(txtSearchYear.Text.Trim())
|| x.Field<string>("expected_visits").Split('\n').ToList().ForEach((e) =>
{
e.Substring(0, 4).Equals(txtSearchYear.Text.Trim())
}));
if (tblResult.Any())
{
GetData(tblResult.CopyToDataTable());
}
else
{
DGVData.Rows.Clear();
}
I would directly compare the year and use the year (int) in lambda and trying to handle the dates as List or Array of Dates instead of comparing substrings, e.g.
DateTime now = DateTime.Now;
int year = -1;
if (!Int32.TryParse(txtSearchYear.Text.Trim(), out year) || year < 1900)
{
throw new InvalidProgramException($"Year {year} isn't valid for us.");
}
List<DateTime> dates = new List<DateTime>();
dates.Add(now);
dates.Add(now.Subtract(new TimeSpan(0, 10, 10)));
dates.Add(now.Subtract(new TimeSpan(0, 20, 20)));
/* ... */
var myList = dates.AsEnumerable().Where(x => x.Year == year).ToList();
Action<DateTime> action = new Action<DateTime>(MyDelegateAction);
myList.ForEach(e => MyDelegateAction(e));
}
private static void MyDelegateAction(DateTime date) { /* do somewhat */; }
kind regards, heinrich.

Finding the month between a range

I have a datasource that returns dates and I have to find where the months falls within the month and day range buckets. The months and day range buckets are predefined so I put it in a Dictionary (not sure if that is even a good idea). I am using linq to find the min and Max dates and extracting the month from them. I need to find month from the dictionary where that month extracted falls within the range. For Example
Dictionary<int, int> MonthDayBuckets = new Dictionary<int, int>() { { 3,31 }, { 6,30 }, { 9,30 }, { 12,31 } };
var MinyDate = _dataSource.Min(x => x.values[0]);
var MaxDate = _dataSource.Max(x => x.values[0]);
var startMonth = Convert.ToDateTime(MinyDate).ToString("MM");
var endMonth = Convert.ToDateTime(MaxDate).ToString("MM");
Say startmonth return Jan so I want to be able to go to the dictionary and return only march (03.31) and if I get 10 for the Max (October) I am trying to return (12,31) December
If my understanding is correct, your MonthDayBuckets variable is meant to represent date ranges:
3/31 - 6/30
6/30 - 9/30
9/30 - 12/31
12/31 - 3/31
...and given a month, you're wanting to see what the end date is of the interval that the first of that month falls between? Like you gave the example of October returning 12/31.
This problem can be simplified since you'll get the same result saying "what's the next occurring date after this given date?" The next occurring date for 10/01 would be 12/31. So here's how you could rearrange your data:
var availableDates = new List<string> { "03/31", "06/30", "09/30", "12/31" };
Now you'll be able to find a match by finding the index of the first one that's greater than your given date. Note how I made the month/day combos lexicographical orderable.
var startMonth = Convert.ToDateTime(MinyDate).ToString("MM");
var startDate = startMonth + "/01";
var endMonth = Convert.ToDateTime(MaxDate).ToString("MM");
var endDate = endMonth + "/01";
// Wrap around to the first date if this falls after the end
var nextStartDate = availableDates.FirstOrDefault(d => d.CompareTo(startDate) >= 1) ?? availableDates[0];
var nextEndDate = availableDates.FirstOrDefault(d => d.CompareTo(endDate) >= 1) ?? availableDates[0];
You could use Linq for the purpose. For example,
var nearestKey = MonthDayBuckets.Keys.Where(x => x >= endMonth.Month).Min();
var nearestDate = new DateTime(DateTime.Now.Year,nearestKey,MonthDayBuckets[nearestKey]); // or whatever the year it needs to be represent
Though above query would get you the result, I would suggest you define a structure to store the Range itself, rather than using Dictionary
For example,
public class Range
{
public MonthDate StartRange{get;set;}
public MonthDate EndRange{get;set;}
public Range(MonthDate startRange,MonthDate endRange)
{
StartRange = startRange;
EndRange = endRange;
}
}
public class MonthDate
{
public MonthDate(int month,int date)
{
Month = month;
Date = date;
}
public int Month{get;set;}
public int Date{get;set;}
//Depending on if your Ranges are inclusive or not,you need to decide how to compare
public static bool operator >=(MonthDate source, MonthDate comparer)
{
return source.Month>= comparer.Month && source.Date>=comparer.Date;
}
public static bool operator <=(MonthDate source, MonthDate comparer)
{
return source.Month<= comparer.Month && source.Date<=comparer.Date;
}
}
Now you could define ranges as
var dateRanges = new Range[]
{
new Range(new MonthDate(12,31),new MonthDate(3,31)),
new Range(new MonthDate(3,31),new MonthDate(6,30)),
new Range(new MonthDate(6,30),new MonthDate(12,31)),
};
var result = dateRanges.First(x=>x.StartRange <= new MonthDate(endMonth.Month,endMonth.Day) && x.EndRange >= new MonthDate(endMonth.Month,endMonth.Day));

Get date of last seven days

I want to get date of last seven days from now.For example current date is
02-10-2016, get date of seven days like this
01-10-2016,30-09-2016,29-09-2016,28-09-2016,27-09-2016,26-09-2016
My code
string dt = DateTime.Now.ToString("yyyy-MM-dd");
DateTime lastWeek = dt.AddDays(-7.0);
AddDays is a part of DateTime, not of string.
You need to build your dates iteratively and then convert it to a string.
DateTime[] last7Days = Enumerable.Range(0, 7)
.Select(i => DateTime.Now.Date.AddDays(-i))
.ToArray();
foreach (var day in last7Days)
Console.WriteLine($"{day:yyyy-MM-dd}"); // Any manipulations with days go here
Without LINQ, with a simple loop:
DateTime dt = DateTime.Now;
for (int i=0;i<7;i++)
{
dt = dt.AddDays(-1);
Console.WriteLine(dt.Date.ToShortDateString());
}
Try using Linq:
var date = new DateTime(2016, 10, 2);
var result = Enumerable.Range(1, 7)
.Select(day => date.Date.AddDays(- day))
.ToArray(); // if you want to represent dates as an array
Test
// 01-10-2016,30-09-2016,29-09-2016,28-09-2016,27-09-2016,26-09-2016,25-09-2016
Console.Write(string.Join(",", result.Select(d => d.ToString("dd-MM-yyyy"))));
You are almost there, the AddDays method will add only a specific number of days to the given data and dives you the resulted date. But here in your case you need a list of dates, so you have to loop through those dates and get them as well. I hope the following method will help you to do this:
public static string GetLast7DateString()
{
DateTime currentDate = DateTime.Now;
return String.Join(",",Enumerable.Range(0, 7)
.Select(x => currentDate.AddDays(-x).ToString("dd-MM-yyyy"))
.ToList());
}
Note : If you want to exclude the current date means you have to take the range from 7 and the count should be 7. You can read more about Enumerable.Range here
If you call this method like the following means you will get the output as 24-10-2016,23-10-2016,22-10-2016,21-10-2016,20-10-2016,19-10-2016,18-10-2016
string opLast7Days = GetLast7DateString();
public static List<DateTime> getLastSevenDate(DateTime currentDate)
{
List<DateTime> lastSevenDate = new List<DateTime>();
for (int i = 1; i <= 7; i++)
{
lastSevenDate.Add(currentDate.AddDays(-i));
}
return lastSevenDate;
}

C# Best practice - Counting how many times each date occurs

I'm looking for a best practice for counting how many times each date occurs in a list.
For now, I have working code (just tested) but I think the way I did is not so good.
var dates = new List<DateTime>();
//Fill list here
var dateCounter = new Dictionary<DateTime, int>();
foreach (var dateTime in dates)
{
if (dateCounter.ContainsKey(dateTime))
{
//Increase count
dateCounter[dateTime] = dateCounter[dateTime] + 1;
}
else
{
//Add to dictionary
dateCounter.Add(dateTime, 1);
}
}
Anyone who knows a better solution?
dates.GroupBy(d => d).ToDictionary(g => g.Key, g => g.Count());

Access to LINQ results - what is a better way?

So I have a set of data from an API call that I need to use. I filter to correct subset and access a specific field with the code below. Is there a better way of getting currentDate and beforeCurrentDate?
DateTime beforeCurrentDate, currDate;
var curr = from c in GlobalResults<FinancialYear>.Results
where c.IsCurrentYear = true
select c;
var prev = from c in GlobalResults<FinancialYear>.Results
where c.ID < curr.FirstOrDefault().ID && c.YearEnd == curr.FirstOrDefault ().YearStart.AddDays(-1)
select c;
foreach (var cfy in curr)
{
currDate = cfy.YearEnd;
}
foreach (var pfy in prev)
{
beforeCurrentDate = pfy.YearStart.AddDays (-1);
}
I know the foreach is the wrong way, so what should I use?
EDIT: What the API results contain is a set of dates, with one having the IsCurrent field set to true. I want the EndDate field of the IsCurrent = true result, and the StartDate field of the previous result. Previous ito of the StartDate to EndDate period. The ID field is no use since a previous date range could be captured after the current date range.
var curr = GlobalResults<FinancialYear>.Results.FirstOrDefault(c => c.IsCurrentYear);
var prev = GlobalResults<FinancialYear>.Results.FirstOrDefault(c => c.ID < curr.ID && c.YearEnd == curr.YearStart.AddDays(-1));
var currDate = curr.YearEnd;
var beforeCurrentDate = prev.YearStart.AddDays(-1);
You can get the current Date for the financial year by constraining the year and choosing the Last(), which replaces your foreach loop:
var currYear = GlobalResults<FinancialYear>.Results.Where(p=>p.IsCurrentYear == true).Last();
var currDate = currYear.YearEnd;
Similarly, you can use the currYear to get the previous information:
var prevYear = GlobalResults<FinancialYear>.Results.Where(p=>p.YearEnd == currDate.AddDays(-1)).FirstOrDefault();
var beforeCurrentDate = prevYear.YearStart.AddDays(-1);

Categories