LINQ Group By Year to Form a Tree View - c#

I am trying to create a tree view that would essentially break down like so:
- Year
- Month
- Related Item
So we might have the Year 2022, that has several related items within the several months.
I have created the following model:
public class TreeYear
{
public string NodeYear { get; set; }
public DateTime CreatedDateTime { get; set; }
public List<TreeMonth> Months { get; set; }
}
public class TreeMonth
{
public int MonthID { get; set; }
public string MonthName { get; set; }
public quoteSummary QuoteSummary{ get; set; }
}
I have written some code in my controller which currently returns every item like so:
var allQuotes = QuoteSummary.ToList();
var tree = new TreeYear();
foreach (var quote in allQuotes)
{
tree.NodeYear= quote.CreatedTime.Year.ToString();
tree.CreatedDateTime = quote.CreatedTime;
tree.Months = new List<TreeMonth>()
{
new TreeMonth() {
MonthID = quote.CreatedTime.Month,
MonthName = getAbbreviatedName(quote.CreatedTime.Month),
QuoteSummary = quote
}
};
}
But obviously over here you can see that it has all 41 records of which none are grouped up by year.
I thought maybe I could write some linq something like but at the moment incorrect:
var groups = TheResponse.Details
.GroupBy(
d => Int32.Parse(d.NodeYear),
(key, g) => g.GroupBy(
d => d.Months.Select(x => x.MonthID)),
(key2, g2) => g2.GroupBy(d => d.CreatedDateTime)
)
);
Or would I need to change the model for this idea to work?

If I understood your question correctly, then you need to flatten the inner list and then group by months again.
var groups = TheResponse.Details
.GroupBy(d => Int32.Parse(d.NodeYear))
.Select(d => new
{
Year = d.Key,
MonthObj = d.SelectMany(m => m.Months)
.GroupBy(m => m.MonthID)
.Select(x => new
{
MonthID = x.Key,
RelatedItem = x.ToList()
})
});
I have simplified it by using anonymous types, but you can obviously tweek it based on your resp. Model.

Related

Joining two lists of object optimization

I am looking for a way of optimizing my LINQ query.
Classes:
public class OffersObject
{
public List<SingleFlight> Flights { get; set; }
public List<Offer> Offers { get; set; } = new List<Offer>();
}
public class SingleFlight
{
public int Id { get; set; }
public string CarrierCode { get; set; }
public string FlightNumber { get; set; }
}
public class Offer
{
public int ProfileId { get; set; }
public List<ExtraOffer> ExtraOffers { get; set; } = new List<ExtraOffer>();
}
public class ExtraOffer
{
public List<int> Flights { get; set; }
public string Name { get; set; }
}
Sample object:
var sampleObject = new OffersObject
{
Flights = new List<SingleFlight>
{
new SingleFlight
{
Id = 1,
CarrierCode = "KL",
FlightNumber = "1"
},
new SingleFlight
{
Id = 2,
CarrierCode = "KL",
FlightNumber = "2"
}
},
Offers = new List<Offer>
{
new Offer
{
ProfileId = 41,
ExtraOffers = new List<ExtraOffer>
{
new ExtraOffer
{
Flights = new List<int>{1},
Name = "TEST"
},
new ExtraOffer
{
Flights = new List<int>{2},
Name = "TEST"
},
new ExtraOffer
{
Flights = new List<int>{1,2},
Name = "TEST"
}
}
}
}
};
Goal of LINQ query:
List of:
{ int ProfileId, string CommercialName, List<string> fullFlightNumbers }
FullFlightNumber should by created by "Id association" of a flight. It is created like: {CarrierCode} {FlightNumber}
What I have so far (works correctly, but not the fastest way I guess):
var result = sampleObject.Offers
.SelectMany(x => x.ExtraOffers,
(a, b) => {
return new
{
ProfileId = a.ProfileId,
Name = b.Name,
FullFlightNumbers = b.Flights.Select(f => $"{sampleObject.Flights.FirstOrDefault(fl => fl.Id == f).CarrierCode} {sampleObject.Flights.First(fl => fl.Id == f).FlightNumber}").ToList()
};
})
.ToList();
Final note
The part that looks wrong to me is:
.Select(f => $"{sampleObject.Flights.FirstOrDefault(fl => fl.Id == f)?.CarrierCode} {sampleObject.Flights.FirstOrDefault(fl => fl.Id == f)?.FlightNumber}").ToList()
I am basically looking for a way of "joining" those two lists of the OffersObject by Flight's Id.
Any tips appreciated.
If there will only be a few flights defined in sampleObject.Flights, a sequential search using a numeric key is hard to beat.
However, if the number of flights times the number of offers is substantial (1000s or more), I would suggest loading the list of flights into a dictionary with Id as the key for efficient lookup. Something like:
var flightLookup = sampleObject.Flights.ToDictionary(f => f.Id);
And then calculate your FullFlightNumbers as
FullFlightNumbers = b.Flights
.Select(flightId => {
flightLookup.TryGetValue(flightId, out SingleFlight flight);
return $"{flight?.CarrierCode} {flight?.FlightNumber}";
})
.ToList()
TryGetValue above will quietly return a null value for flight if no match is found. If you know that a match will always be present, the lookup cold alternately be coded as:
SingleFlight flight = flightLookup[flightId];
The above also uses a statement lambda. In short, lambda functions can have either expression or statement blocks as bodies. See the C# reference for more information.
I'd suggest replacing the double .FirstOrDefault() approach with .IntersectBy(). It is available in the System.Linq namespace, starting from .NET 6.
.IntersectBy() basically filters sampleObject.Flights by matching the flight ID for each flight in sampleObject with flight IDs in ExtraOffers.Flights.
In the code below, fl => fl.Id is the key selector for sampleObject.Flights (i.e. fl is a SingleFlight).
var result = sampleObject.Offers
.SelectMany(x => x.ExtraOffers,
(a, b) => {
return new
{
ProfileId = a.ProfileId,
Name = b.Name,
FullFlightNumbers = sampleObject.Flights
.IntersectBy(b.Flights, fl => fl.Id)
.Select(fl => fl.FullFlightNumber) // alternative 1
//.Select(fl => $"{fl.CarrierCode} {fl.FlightNumber}") // alternative 2
.ToList()
};
})
.ToList();
In my suggestion I have added the property FullFlightNumber to SingleFlight so that the Linq statement looks slightly cleaner:
public class SingleFlight
{
public int Id { get; set; }
public string CarrierCode { get; set; }
public string FlightNumber { get; set; }
public string FullFlightNumber => $"{CarrierCode} {FlightNumber}";
}
If defining SingleFlight.FullFlightNumber is not possible/desirable for you, the second alternative in the code suggestion can be used instead.
Example fiddle here.

How to get Percentage of Sales for the Past 5 Years

In my ASP.NET Core-5 Entity Framework I have this model:
public class Sales
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Amount { get; set; }
public DateTime? SalesDate { get; set; }
}
DTO:
public class YearlyPercentDto
{
public decimal SalesTotal { get; set; }
public int SalesPercent { get; set; }
public string Year { get; set; }
}
public List<YearlyPercentDto> GetYearlySalesSummary()
{
var salesDetail = _context.sales
.GroupBy(o => new
{
Year = o.CreatedDate.Value.Year
})
.Select(u => new YearlyPercentDto
{
SalesPercent = u.Sum(x => x.Amount),
Year = u.Key.Year.ToString()
}).ToList();
return salesDetail;
}
I want to get the total_sales, percentage_sales for each year in the past 5 years as shown below:
Year (Past 5 Years) SalesTotal SalesPercent
2021 200000 18
2020 4300000
2019 1290000
2018 5400000
2017 3322220
How do I achieve this?
I think I'd just pull the totals from the DB and have C# work out the percentage:
public List<YearlyPercentDto> GetYearlySalesSummary()
{
var salesDetail = _context.sales
.Where(s => o.CreatedDate > DateTime.Now.AddYears(-5)
.GroupBy(o => o.CreatedDate.Value.Year)
.Select(u => new YearlyPercentDto
{
SalesTotal = u.Sum(x => x.Amount),
Year = u.Key.ToString() //why is Year a string?
}
).ToList();
//grand total
var tot = salesDetail.Sum(s => s.SalesTotal);
//apply percentage to each element
salesDetail.ForEach(s => s.SalesPercent = (int)(100.0 * s.SalesTotal/tot));
return salesDetail;
}
There seems little point in bullying the DB to provide this info when C# can quickly work it out - the extra hoops to jump through to get the DB to do it don't seem worth it

Group By query in mvc5

Hi i want to write sql Group by query in C# of my MVC5 application.
In the above image I have group by query which i wrote in sql . That I want to write in C# front end.
I tried to write query in front end. But I am getting error which is mentioned in the image. Now I want to write that Group By query in C# and want to display the each employee with count (output same as mentioned in the first image). Can anyone help me to resolve this issue?
My ViewModel(Dashnboard View model)
public class DashboardViewmodel
{
public List<CustomerTypeCountModel> CustomerTypesCountModels { get; set; }
public List<View_VisitorsForm> Visits { get; set; }
public CustomerTypeViewModel CustomerTypeViewModels { get; set; }
public int sizingcount { get; set; }
public int Processingcount { get; set; }
//here i declared two properties
public string EmployeeName { get; set; }
public string EmployeeCount { get; set; }
}
My Controller code
[HttpGet]
public ActionResult SalesVisit()
{
return View();
}
public ActionResult GetDatesFromSalesVisit(DashboardViewmodel dvm)
{
var fromdate = Convert.ToDateTime(dvm.CustomerTypeViewModels.FromDate);
var todate = Convert.ToDateTime(dvm.CustomerTypeViewModels.ToDate);
List<View_VisitorsForm> empcount = new List<View_VisitorsForm>();
if (DepartmentID == new Guid("47D2C992-1CB6-44AA-91CA-6AA3C338447E") &&
(UserTypeID == new Guid("106D02CC-7DC2-42BF-AC6F-D683ADDC1824") ||
(UserTypeID == new Guid("B3728982-0016-4562-BF73-E9B8B99BD501"))))
{
var empcountresult = db.View_VisitorsForm.GroupBy(G => G.Employee)
.Select(e => new
{
employee = e.Key,
count = e.Count()
}).ToList();
empcount = empcountresult ;//this line i am getting error
}
DashboardViewmodel obj = new DashboardViewmodel();
return View("SalesVisit", obj);
}
When you use a GroupBy you get an IEnumerable<IGrouping<Key,YourOriginalType>> so you do not have .Employee and .VisitingID properties.
Change as following:
public class EmployeeCount
{
public string Employee {get; set;}
public int Count {get; set;}
}
List<EmployeeCount> result = db.View_VisitorsForm
.Where(item => item.VisitingDate >= beginDate && item.VisitingDate < endDate)
.GroupBy(G => G.Employee)
.Select(e =>new EmployeeCount
{
employee = e.Key,
count = e.Count()
}).ToList();
//Now add the result to the object you are passing to the View
Also keep in mind that you are not instantiating objects of type View_VisitorsForm but an anonymous object so assigning the result to empcount yet alone with the added FirstOrDefault will not compile
To pass this structure to the View and present it check this question
hope this helps you
var query = db.View_VisitorsForm.Where(o => o.VisitingDate >= new DateTime(2016,10,01) && o.VisitingDate <= new DateTime(2016, 10, 30)).GroupBy(G => G.Employee)
foreach (var item in query)
{
Console.WriteLine($"Employee Id {item.Key} : Count :{item.Count()}");
}

Group list according to date with multiple dates per item

I have a list like follows :-
List
EDIT (Years can be different fot Birthdays and anniversaries)
- EmpGuid -265d317b-b819-4171-ba12-e64388746d81
Name - abc
Birthday - 15 Aug 2000
Anniversary- 12 july 1989
EmpGuid - 265d317b-b819-4171-ba12-e64388746d82
Name - xyz
Birthday - 24 Jan 2000
Anniversary- 15 Aug 1988
EmpGuid - 265d317b-b819-4171-ba12-e64388746d83
Name - mno
Birthday - 15 aug 2000
Anniversary- 24 Jan 1987
And I want to group the list according to the dates like so :-
12 July - abc anniversary
15 aug - abc Birthday
mno Birthday
xyz Anniversary
24 jan - xyz birthday
mno anniversary
I tried doing this :-
var groupedEmpList = FinalList.GroupBy(u => u.Birthdate)
.Select(grp =>new { GroupID =grp.Key, FinalList = grp.ToList()})
.ToList()
The the above does not give me the desired output. Any help on this would be appreciated
This solution uses SelectMany to get a flattened list of all the dates on which you can group by the items:
var result = FinalList.Select(item => new
{
Date = new [] { item.Birthday.ToString("ddMM"), item.Anniversary.ToString("ddMM") },
Item = item
})
.SelectMany(item => item.Date.Select(date => new { Date = date, Item = item.Item }))
.GroupBy(item => item.Date)
.Select(grouping => new { Date = grouping.Key, Events = grouping.ToList() }).ToList();
One can also perform the first select within the SelectMany - for the purpose of the answer I kept it separately
For adding the type of the event (and on the way removing the first select):
var result = FinalList.SelectMany(item => new List<dynamic>
{
new { Date = item.Birthday.ToString("ddMM"), Event = "Birthday", Item = item },
new { Date = item.Anniversary.ToString("ddMM"), Event = "Anniversary", Item = item }
})
.GroupBy(item => item.Date)
.Select(grouping => new { Date = grouping.Key, Events = grouping.ToList() }).ToList();
For outputing these results you can:
public enum EventTypes
{
Birthday,
Anniversary
}
public class Event
{
public string Date { get; set; }
public EventTypes Type { get; set; }
public IEnumerable<dynamic> Items { get; set; }
}
var result = FinalList.SelectMany(item => new List<dynamic>
{
new { Date = item.Birthday.ToString("ddMM"), Type = EventTypes.Birthday, Item = item },
new { Date = item.Anniversary.ToString("ddMM"), Type = EventTypes.Anniversary, Item = item }
})
.GroupBy(item => new { item.Date, item.Type })
.Select(grouping => new Event { Date = grouping.Key.Date, Type = grouping.Key.Type, Items = grouping.ToList() }).ToList();
Now the result list is List<Event> and inside you have also your oridinal objects in the Items (replace the dynamic of that list to your original class type - I just don't know it)
Since you need to duplicate items (to put same item into up to 2 groups based on different dates) you need to perform that step separately as LINQ does not duplicate items with basic commands.
Simple option - just have 2 lists combined first - one for birthdays and one for anniversary and extract date with its type to wrapping type similar to:
var mergedList =
FinalList.Select(x => new {
Date = x.Birthdate, Type = "Birthday", Value = x})
.Concat(
FinalList
.Where(x => x.Birthday != x.Anniversary) // if needed
.Select(x => new {
Date = x.Anniversary, Type = "Anniversary", Value = x});
// Now list have all unique dates - can group and extract any info
var grouped = mergedList
.GroupBy(u => u.Date)
.Select(grp => new {
Date = grp.Key,
ListOfNames = grp.Select(x => new {x.Value.Name, x.Type}).ToList()
})
.ToList();
Assuming that you have a class:
class Info
{
public Guid EmpGuid { get; set; }
public string Name { get; set; }
public DateTime Birthday { get; set; }
public DateTime Anniversary { get; set; }
}
You need to unpivot your class into the following class:
class UnpivotInfo
{
public Guid EmpGuid { get; set; }
public string Name { get; set; }
public DateTime Date { get; set; }
public string Type { get; set; }
}
where your Type is either "Birthday" or "Anniversary", by doing this:
var unpivoted = list.SelectMany(i => new[]
{
new UnpivotInfo {EmpGuid = i.EmpGuid, Date = i.Birthday, Type = "Birthday", Name = i.Name}
, new UnpivotInfo {EmpGuid = i.EmpGuid, Date = i.Anniversary, Type = "Anniversary", Name = i.Name}
});
You can then group your data by date:
var groups = unpivoted.GroupBy(p => p.Date);

Linq Get totals using group by

I have the following class:
class Item
{
public decimal TransactionValue { get; set; }
public string TransactionType { get; set; }
}
And I have this list:
var items = new List<Item>
{
new Item
{
TransactionValue = 10,
TransactionType = "Income"
},
new Item
{
TransactionValue = 10,
TransactionType = "Income"
},
new Item
{
TransactionValue = -5,
TransactionType = "Outgoing"
},
new Item
{
TransactionValue = -20,
TransactionType = "Outgoing"
}
};
And I am trying to get the sums based on ValueType, I have tried the below but it is adding everything and giving me one total which is -5, what I want is totals for each transaction type so I want to get a new class which is Totals class below and with this data: TotalIncoming : 20 and TotalOutgoing : - 25.
var r = items.Sum(x => x.TransactionValue);
class Totals
{
public decimal TotalIncoming { get; set; }
public decimal TotalOutgoing { get; set; }
}
Thanks
You can achieve your desired result with following query:-
Totals result = new Totals
{
TotalIncoming = items.Where(x => x.TransactionType == "Income")
.Sum(x => x.TransactionValue),
TotalOutgoing = items.Where(x => x.TransactionType == "Outgoing")
.Sum(x => x.TransactionValue)
};
But, as you can see with your Type Totals, we need to hard-code the TransactionType and we have no clue from the result that this Sum belongs to which type apart from the naming convention used.
I will create the below type instead:-
class ItemTotals
{
public string ItemType { get; set; }
public decimal Total { get; set; }
}
Here we will have the TransactionType along with its corresponding Total in the result, we can simply group by TransactionType & calculate the sum, here is the query for same:-
List<ItemTotals> query = items.GroupBy(x => x.TransactionType)
.Select(x => new ItemTotals
{
ItemType = x.Key,
Total = x.Sum(z => z.TransactionValue)
}).ToList();
Here is the Complete Working Fiddle, you can choose from both.
I'm sure there is a probably a clever way to do this in one line using Linq, but everything I could come up with was quite ugly so I went with something a bit more readable.
var results = items.GroupBy(x => x.TransactionType)
.ToDictionary(x => x.Key, x => x.Sum(y => y.TransactionValue));
var totals = new Totals
{
TotalIncoming = results["Income"],
TotalOutgoing = results["Outgoing"]
};

Categories

Resources