I have a Linq query where I am getting response values to a set of questions. I group the response data by question, and then perform various aggregations on the responsedata, such as averaging them. I also count the proportion of responses that could be classified as "high", "middle" and "low", based on specifying response option value ranges.
var result = ItemResponses
.Where (ir => ir.ItemID < 4 && ir.AssessmentInstance.ProjectID == 5)
.Select (ir => ir)
.GroupBy (ir => new {
ir.ItemID
}).Select (grouped => new {
ItemID = grouped.Key.ItemID,
Average = (double)grouped.Average (g => g.OptionValue),
ProportionHighScore =
(double)grouped.Where(g => g.OptionValue == 5 || g.OptionValue == 6).Count()
/ (double)grouped.Count(),
});
I would like to move the code where I specify which optionvalues should be combined into a "high" response category away from the Linq query, and was considering to set up an extension method to do this. In the extension method I can then specify different combinations of response option values that I can score into a "high" score, across scenarious (for example, if the max response was not 6 but 10, then I would count options 8, 9, and 10 towards a "high" response category.
The extension method might look something like this:
public static double ProportionHighScore(this IGrouping<a,b> values, int ResponseOptionID)
{
double ret = 0;
switch (ResponseOptionID)
{
case 1:
//code here to combine response options 5 and 6, and divide by total
break;
case 2:
//code here to combine response options 8, 9 and 10 and divide by total
break;
case 3:
//etc..
break;
default:
break;
}
return ret;
}
But the question I have then is: how to I go about passing the Linq grouping values as a parameter into the extension method? The type of the IGrouping b is an anonymous type.
Update:
I like the idea of just doing GroupBy (ir => it.ItemID) so that I get access to "IGrouping<int, ItemResponse>". But in the code here I simplified a bit. In my actual code there are a few more things going on, such as reversing the OptionValue scores if an item is flagged as "IsReversed".
var result2 = ItemResponses
.Where (ir => ir.ItemID < 4 && ir.AssessmentInstance.ProjectID == 5)
.Select (ir => new {
ItemID = ir.ItemID,
OptionValue =
(
//Reverse option value of items that are flagged to require reverse scoring
ir.Item.IsReversed == 0 ? ir.OptionValue :
((ir.ResponseScaleOption.ResponseScale.MaxValue + 1) - ir.OptionValue)
),
})
.GroupBy (g => new {g.ItemID})
.Select (grouped => new {
ItemID = grouped.Key.ItemID,
Average = (double)grouped.Average (g => g.OptionValue),
ProportionHighScore =
(double)grouped.Where(g => g.OptionValue == 5 || g.OptionValue == 6).Count()
/ (double)grouped.Count(),
});
In some versions of this query I also include fields from some joined tables as well. So the need to reverse the OptionValues is one reason why I assumed I needed an anonymous type. Perhaps I need to create a new class that I can project into ("ItemResponseForAggregation", or some such name), and then be able to do IGrouping<int, ItemResponseForAggregation> for my extension parameter?
You can't pass anonymous types in this way. They have to be in the immediate execution context to treat them as if they were strong types. Create a lightweight type so you can pass this in, and then add a parameter constraint on parameter 'b' to enforce that it must be of the type you've created.
I did a mock up in LINQPad of what I think you're looking for (if I am understanding the requirements correctly):
void Main()
{
var ItemResponses = new List<ItemResponse>();
var result = ItemResponses
.Where(ir => ir.ItemID < 4 && ir.AssessmentInstance.ProjectID == 5)
.GroupBy(ir => ir.ItemID)
.Select(
grouped => new {
ItemID = grouped.Key,
Average = (double)grouped.Average(g => g.OptionValue),
ProportionHighScore = grouped.ProportionHighScore(1, 2, 3, 4, 5)
}
);
result.Dump();
}
public class ItemResponse
{
public int ItemID { get; set; }
public int OptionValue { get; set; }
public AssessmentInstanceItem AssessmentInstance { get; set; }
}
public class AssessmentInstanceItem
{
public int ProjectID { get; set; }
}
public static class ItemResponseExtensions
{
public static double ProportionHighScore(this IGrouping<int, ItemResponse> values, params int[] ResponseOptionID)
{
double count = 0;
double total = values.Count();
foreach (int r in ResponseOptionID)
count += (double)values.Where(g => g.OptionValue == r).Count();
return count / total;
}
}
In the extension method, the params parameter allows you to specify as many options as you need. Then I am just looping through the options, adding the count for each response option.
Related
I have two lists, both containing models that share a common field, ID(String value). I am comparing the ID's for duplication.
I currently have a LINQ statement in place to determine the duplicated ID values, which stores them into a list of strings:
List<string> duplicateRecords = testData.TestRecords.GroupBy(aa => aa.ID).Where(x => x.Count() > 1).Select(y => y.Key).ToList();
And a second LINQ statement that maps a List of respected models based off the duplicated ID LINQ result:
List<Model> modelRecords = testData.Models.Where(x => duplicateRecords.Any(y => x.ID == y)).ToList();
These two LINQ statements do exactly what I expected them to do which is great. But now there is a recent request to determine duplicate ID's based off of their minimum N characters during a comparison. This minimum N comparison must happen for the last N characters in a string.
EX)
ID1: 123 == ID2: 123
ID1: 0123 == ID2: 123
ID1: 123 == ID2: 0123
ID1: 1230 != ID2: 123
ID1: 123 != ID2: 1230
ID1: 122110123 == ID2: 123
Hopefully those examples give some insight into the problem I am trying to solve. This could be done using foreach loops but I have come to experience the code becomes very messy and unmanageable on complex list query's.
So my question is this: How can I use the last N characters of the smaller of the two comparing strings to determine duplicates using LINQ?
Note: I am also very open to more elegant ways of solving this problem, would really appreciate excluding any for or foreach solutions.
I assume that when the input contains 123 and 0123 you want the result to have both of them
var input = new List<Model>()
{
new Model {ID = "123"},
new Model {ID = "0123"},
new Model {ID = "1230"},
new Model {ID = "12"},
new Model {ID = "122110123"}
};
var result = input.Where(x => input.Any(y => y != x && (y.ID.EndsWith(x.ID) || x.ID.EndsWith(y.ID)))).ToList();
\\this will return 123, 0123 and 122110123
If you want to check agains existing duplicateRecords list then this should work:
List<Model> modelRecords = testData.Models.Where(x => duplicateRecords.Any(y => x.ID.EndsWith(y) || y.EndsWith(x.ID))).ToList();
In order to efficiently find the duplicates you need to sort the IDs by length so you can minimize the comparisons necessary. (The sort adds some overhead, but greatly decreases the comparisons that must be done - in my test where 9 IDs have and 3 are duplicates of 8 values, it is 15 comparisons sorted versus 42 unsorted.) Once you have them sorted by length, just compare each one to the ones that are equal to or longer (in case of complete duplicates) to find which short IDs need to be kept, marking any matches so you can skip them and then find all the Models that end with the found matches.
Create the List of IDs ordered by their length:
var orderedIDs = testData.TestRecords.Select(tr => tr.ID).OrderBy(id => id.Length).ToList();
I don't think there is any way to do this efficiently with LINQ, but a nested for loop skipping previous matches optimizes the search for duplicates.
First, variables to keep track of IDsand whichID`s have already matched:
var dupRecordSubIDs = new List<string>();
var alreadyMatched = new bool[testData.TestRecords.Count];
Now loop through the IDs and save the shorter matching IDs:
// foreach ID in length order
for (int n1 = 0; n1 < testData.TestRecords.Count-1; ++n1) {
// skip the ones that already matched a shorter ID
if (!alreadyMatched[n1]) {
// remember if the shorter ID was alrady added
var added_n1 = false;
// compare the ID to all greater than or equal length IDs
for (int n2 = n1 + 1; n2 < testData.TestRecords.Count; ++n2) {
// if not previously matched, see if we have a new match
if (!alreadyMatched[n2] && orderedIDs[n2].EndsWith(orderedIDs[n1])) {
// only add the shorter ID once for new matches
if (!added_n1) {
dupRecordSubIDs.Add(orderedIDs[n1]);
added_n1 = true;
}
// remember which longer IDs are already matched
alreadyMatched[n2] = true;
}
}
}
}
Now find all the Models that match one of the IDs with a duplicate:
var modelRecords = testData.Models.Where(m => dupRecordSubIDs.Any(d => m.ID.EndsWith(d))).ToList();
I assume ID is string. If so, you can do this :
string match = "123";
var duplicate = list.Where(x=> x.Substring(x.Length - match.Length) == match).ToList();
If I understand your question correctly, it looks to be just a matter of chopping off the last N characters in each ID property while grouping.
Something like this:
using System;
using System.Linq;
public class TestRecord
{
public string ID { get; set; }
}
public class TestModel
{
public string ID { get; set; }
}
public class Program
{
public static void Main()
{
var N = 3; // This is where you define the desired N length
var rand = new Random();
var testRecords = new TestRecord[]
{
new TestRecord {ID = "123"},
new TestRecord {ID = "0123"},
new TestRecord {ID = "1230"},
new TestRecord {ID = "122110123"},
};
var testModels = new TestModel[]
{
new TestModel {ID = "123"},
new TestModel {ID = "0123"},
new TestModel {ID = "1230"},
new TestModel {ID = "122110123"},
};
bool SortEm(string a, string b) => a.Length < b.Length ? b.EndsWith(a) : a.EndsWith(b);
var models = testRecords
.Where(record => testRecords.Any(target => record.ID != target.ID && SortEm(target.ID, record.ID)))
.ToDictionary(
key => key,
key => testModels.Where(testModel => SortEm(key.ID, testModel.ID)).ToArray());
foreach (var kvp in models)
{
System.Console.WriteLine($"For duplicate key ({kvp.Key.ID}) found models: \r\n\t{string.Join("\r\n\t", kvp.Value.Select(x => x.ID))}");
}
}
}
I'm currently working on an application in ASP.NET MVC 4.5. I need to write a LINQ query to order a list of Projects by different StatusIds.
Given is a list of Projects with this ViewModel:
public class ProjectVm
{
public int ProjectId { get; set; }
public string Title { get; set; }
public Status StatusId { get; set; }
}
My project status enum:
public enum Status : byte
{
Draft = 1,
Pending = 2,
Validated = 3,
Refused = 4
}
The idea is to bring the List<ProjectVm> in a certain order, thus first ordered by 1 Draft, second by 2 Pending, third by 4 Refused and fourth by 3 Validated.
My current query looks like this:
projects = projects.OrderBy(x => x.StatusId).ToList();
Unfortunately this query doesn't respect the desired order (4 comes before 3).
Do you know how to apply a condition on this query to bring the projects into the right order (1, 2, 4, 3) ?
Thanks!
Just use several orderings, the first one with OrderByDescending, then the rest with ThenByDescending:
projects = projects
.OrderByDescending(p => p.StatusId == Status.Draft)
.ThenByDescending(p => p.StatusId == Status.Pending)
.ThenByDescending(p => p.StatusId == Status.Refused)
.ThenByDescending(p => p.StatusId == Status.Validated)
.ToList();
There's not a clean way to do it completely inline - you could do something like:
projects.OrderBy(x => x == Status.Validated ? int.MaxValue : (int)x.StatusId)
to force Validated to be at the end, but I would write a function:
private int CustomOrder(Status status)
{
switch(status)
{
// force Validated to the end
case Status.Validated:
return int.MaxValue;
default:
return (int)status;
}
}
and call it from the query:
projects.OrderBy(x => CustomOrder(x))
Since you can add comments and organize the code to make it clearer what your intent is.
Another option would be to put the values in array in the order you want, then order by their position in the array:
Status[] order = new [] {Draft, Pending, Refused, Validated};
projects.OrderBy(x => Array.IndexOf(order,x));
Try this:
public static int MyCustomOrder (Status status)
{
switch (status)
{
case Status.Draft : return 1;
case Status.Pending : return 2;
case Status.Validated : return 4;
case Status.Refused : return 3;
default: return -1;
}
}
And now:
var result = projects.OrderBy (x => MyCustomOrder (x.StatusId));
Not really pretty, but should work:
projects.OrderBy(x => x.StatusId).ThenBy(c => c.StatusId == Status.Validated ? 1 : 0).ToList();
Otherwise you need to provide your own Comparer:
class StatusComparer : IComparer<Status>
{
public int Compare(Status x, Status y)
{
if (x.Equals(y)) return 0;
return (x > y || x.Equals(Status.Validated)) ? 1 : -1;
}
}
And then call:
projects.OrderBy(x => x.StatusId, new StatusComparer()).ToList();
Or do something like the other people here proposed ;)
projects.OrderBy(x => x.StatusId == Status.Validated).ThenBy(x => x.StatusId)
Puts all the pending at the end, then sorts within that rule by StatusID. Two simple operations and likely to be processed well by any provider.
projects.OrderBy(x => x.StatusId == Status.Validated ? int.MaxValue : (int)x.StatusId)
A single operation, which is hence likely faster, that re-assigns the 3 for Pending to int.MaxValue before the sort.
I'd try the second first as the likely more efficient, but the second is worth noting as a general approach too.
Try this
var projectList= projects.OrderBy(x => (int)x.StatusId).ToList();
Below is my Enumerator List:
public enum StatusEnum
{
Open = 1,
Rejected = 2,
Accepted = 3,
Started = 4,
Completed = 5,
Cancelled = 6,
Assigned = 7
}
I need to bind this to a Combobox, but, only show a few specific statuses and ignore the rest.
This is what I have so far:
public static List<Activity.StatusEnum> StatusList()
{
IEnumerable<Activity.StatusEnum> query = Enum.GetValues(typeof(Activity.StatusEnum)).Cast<Activity.StatusEnum>()
.Where(x => x == Activity.StatusEnum.Open
|| x == Activity.StatusEnum.Rejected
|| x == Activity.StatusEnum.Accepted
|| x == Activity.StatusEnum.Started);
return query.ToList();
}
However, I feel that the code is little messy and is not a correct approach to bind filtered Enum list to a Combobox.
Can anyone suggest a more robust way of doing this?
Update
I might need to change the Order of selection. So I need a generic solution which doesn't only get the first X number of statuses.
return Enum.GetValues(typeof(Activity.StatusEnum)).Cast<Activity.StatusEnum>().Where((n, x) => x < 4);
If you want to be able to change the list of items, just add them into a List<Activity.StatusEnum> and use Contains:
var listValid = new List<Activity.StatusEnum>() { Activity.StatusEnum.Open, Activity.StatusEnum.Rejected, Activity.StatusEnum.Accepted, Activity.StatusEnum.Started };
return Enum.GetValues(typeof(Activity.StatusEnum)).Cast<Activity.StatusEnum>().Where(n => listValid.Contains(n));
Well if you're going to hard code the items that should be in the list anyway, why not just do this:
public static List<Activity.StatusEnum> StatusList()
{
return new List<Activity.StatusEnum>
{
Activity.StatusEnum.Open,
Activity.StatusEnum.Rejected,
Activity.StatusEnum.Accepted,
Activity.StatusEnum.Started
};
}
You could also dispose of the List<T> and just return the array itself. As long as you know these are the items you want, then there's no need for Linq.
Steps:
Get the enum values and cast the results to the type of the enum
Sort the enum values by their integer values (otherwise they sort
naturally by unsigned magnitude)
Take the first 4
Code:
return Enum.GetValues(typeof(Activity.StatusEnum))
.Cast<Activity.StatusEnum>()
.OrderBy(se =>(int)se)
.Take(4);
Output:
Open Rejected Accepted Started
First, if possible, I'd make your enum values powers of 2, so they could be OR'd together.
public enum StatusEnum
{
Open = 1,
Rejected = 2,
Accepted = 4,
Started = 8,
Completed = 16,
Cancelled = 32,
Assigned = 64
}
Then you could do something like this:
public static List<Activity.StatusEnum> StatusList()
{
var statusesToShow = Activity.StatusEnum.Open | Activity.StatusEnum.Rejected | Activity.StatusEnum.Accepted | Activity.StatusEnum.Started;
return Enum
.GetValues(typeof(Activity.StatusEnum))
.Cast<Activity.StatusEnum>()
.Where(x => (x & statusesToShow) == x)
.ToList();
}
EDIT: In light of the fact that you can't change the enum values, I'd just recommend you use something like:
public static List<Activity.StatusEnum> StatusList()
{
return new List<Activity.StatusEnum> {
Activity.StatusEnum.Open,
Activity.StatusEnum.Rejected,
Activity.StatusEnum.Accepted,
Activity.StatusEnum.Started
};
}
". . . only show the first 4 statuses and ignore the rest."
To get the first n elements of an IEnumerable<T>, use the Take method:
return Enum.GetValues(typeof(Activity.StatusEnum))
.Cast<Activity.StatusEnum>()
.Take(4)
.ToList();
How about something along the lines of:
.Where(x => x <= Activity.StatusEnum.Started)
I'm reviewing a piece of code I wrote not too long ago, and I just hate the way I handled the sorting - I'm wondering if anyone might be able to show me a better way.
I have a class, Holding, which contains some information. I have another class, HoldingsList, which contains a List<Holding> member. I also have an enum, PortfolioSheetMapping, which has ~40 or so elements.
It sort of looks like this:
public class Holding
{
public ProductInfo Product {get;set;}
// ... various properties & methods ...
}
public class ProductInfo
{
// .. various properties, methods...
}
public class HoldingsList
{
public List<Holding> Holdings {get;set;}
// ... more code ...
}
public enum PortfolioSheetMapping
{
Unmapped = 0,
Symbol,
Quantitiy,
Price,
// ... more elements ...
}
I have a method which can invoke the List to be sorted depending on which enumeration the user selects. The method uses a mondo switch statement that has over 40 cases (ugh!).
A short snippet below illustrates the code:
if (frm.SelectedSortColumn.IsBaseColumn)
{
switch (frm.SelectedSortColumn.BaseColumn)
{
case PortfolioSheetMapping.IssueId:
if (frm.SortAscending)
{
// here I'm sorting the Holding instance's
// Product.IssueId property values...
// this is the pattern I'm using in the switch...
pf.Holdings = pf.Holdings.OrderBy
(c => c.Product.IssueId).ToList();
}
else
{
pf.Holdings = pf.Holdings.OrderByDescending
(c => c.Product.IssueId).ToList();
}
break;
case PortfolioSheetMapping.MarketId:
if (frm.SortAscending)
{
pf.Holdings = pf.Holdings.OrderBy
(c => c.Product.MarketId).ToList();
}
else
{
pf.Holdings = pf.Holdings.OrderByDescending
(c => c.Product.MarketId).ToList();
}
break;
case PortfolioSheetMapping.Symbol:
if (frm.SortAscending)
{
pf.Holdings = pf.Holdings.OrderBy
(c => c.Symbol).ToList();
}
else
{
pf.Holdings = pf.Holdings.OrderByDescending
(c => c.Symbol).ToList();
}
break;
// ... more code ....
My problem is with the switch statement. The switch is tightly bound to the PortfolioSheetMapping enum, which can change tomorrow or the next day. Each time it changes, I'm going to have to revisit this switch statement, and add yet another case block to it. I'm just afraid that eventually this switch statement will grow so big that it is utterly unmanageable.
Can someone tell me if there's a better way to sort my list?
You're re-assigning the sorted data straight back to your pf.Holdings property, so why not bypass the overhead of OrderBy and ToList and just use the list's Sort method directly instead?
You could use a map to hold Comparison<T> delegates for all the supported sortings and then call Sort(Comparison<T>) with the appropriate delegate:
if (frm.SelectedSortColumn.IsBaseColumn)
{
Comparison<Holding> comparison;
if (!_map.TryGetValue(frm.SelectedSortColumn.BaseColumn, out comparison))
throw new InvalidOperationException("Can't sort on BaseColumn");
if (frm.SortAscending)
pf.Holdings.Sort(comparison);
else
pf.Holdings.Sort((x, y) => comparison(y, x));
}
// ...
private static readonly Dictionary<PortfolioSheetMapping, Comparison<Holding>>
_map = new Dictionary<PortfolioSheetMapping, Comparison<Holding>>
{
{ PortfolioSheetMapping.IssueId, GetComp(x => x.Product.IssueId) },
{ PortfolioSheetMapping.MarketId, GetComp(x => x.Product.MarketId) },
{ PortfolioSheetMapping.Symbol, GetComp(x => x.Symbol) },
// ...
};
private static Comparison<Holding> GetComp<T>(Func<Holding, T> selector)
{
return (x, y) => Comparer<T>.Default.Compare(selector(x), selector(y));
}
You could try reducing the switch to something like this:
private static readonly Dictionary<PortfolioSheetMapping, Func<Holding, object>> sortingOperations = new Dictionary<PortfolioSheetMapping, Func<Holding, object>>
{
{PortfolioSheetMapping.Symbol, h => h.Symbol},
{PortfolioSheetMapping.Quantitiy, h => h.Quantitiy},
// more....
};
public static List<Holding> SortHoldings(this List<Holding> holdings, SortOrder sortOrder, PortfolioSheetMapping sortField)
{
if (sortOrder == SortOrder.Decreasing)
{
return holdings.OrderByDescending(sortingOperations[sortField]).ToList();
}
else
{
return holdings.OrderBy(sortingOperations[sortField]).ToList();
}
}
You could populate sortingOperations with reflection, or maintain it by hand. You could also make SortHoldings accept and return an IEnumerable and remove the ToList calls if you don't mind calling ToList in the caller later. I'm not 100% sure that OrderBy is happy receiving an object, but worth a shot.
Edit: See LukeH's solution to keep things strongly typed.
Have you looked into Dynamic LINQ
Specifically, you could simply do something like:
var column = PortFolioSheetMapping.MarketId.ToString();
if (frm.SelectedSortColumn.IsBaseColumn)
{
if (frm.SortAscending)
pf.Holdings = pf.Holdings.OrderBy(column).ToList();
else
pf.Holdings = pf.Holdings.OrderByDescending(column).ToList();
}
Note: This does have the constraint that your enum match your column names, if that suits you.
EDIT
Missed the Product property the first time. In these cases, DynamicLINQ is going to need to see, for example, "Product.ProductId". You could reflect the property name or simply use a 'well-known' value and concat with the enum .ToString(). At this point, I'm just really forcing my answer to your question so that it at least is a working solution.
how about:
Func<Holding, object> sortBy;
switch (frm.SelectedSortColumn.BaseColumn)
{
case PortfolioSheetMapping.IssueId:
sortBy = c => c.Product.IssueId;
break;
case PortfolioSheetMapping.MarketId:
sortBy = c => c.Product.MarketId;
break;
/// etc.
}
/// EDIT: can't use var here or it'll try to use IQueryable<> which doesn't Reverse() properly
IEnumerable<Holding> sorted = pf.Holdings.OrderBy(sortBy);
if (!frm.SortAscending)
{
sorted = sorted.Reverse();
}
?
Not exactly the fastest solution, but it's reasonably elegant, which is what you were asking for!
EDIT:
Oh, and with the case statement, it probably needs refactoring to a seperate function that returns a Func, not really a nice way to get rid of it entirely, but you can at least hide it away from the middle of your procedure !
It seems to me that there are two immediate improvements we can make:
the logic that uses frm.SortAscending to decide between OrderBy and OrderByDesccending is duplicated in every case, and can be pulled out to after the switch if the cases are changed to do nothing more than establishing the sort key and putting it in a Func
that still leaves the switch itself of course - and this can be replaced by a static map (in a Dictionary, say) from PortfolioSheetMapping to a Func taking a Holding and returning the sort key. eg
You could implement a custom IComparer class which uses reflection. However this would be slower.
Here's a class, which I once used:
class ListComparer : IComparer
{
private ComparerState State = ComparerState.Init;
public string Field {get;set;}
public int Compare(object x, object y)
{
object cx;
object cy;
if (State == ComparerState.Init)
{
if (x.GetType().GetProperty(pField) == null)
State = ComparerState.Field;
else
State = ComparerState.Property;
}
if (State == ComparerState.Property)
{
cx = x.GetType().GetProperty(Field).GetValue(x,null);
cy = y.GetType().GetProperty(Field).GetValue(y,null);
}
else
{
cx = x.GetType().GetField(Field).GetValue(x);
cy = y.GetType().GetField(Field).GetValue(y);
}
if (cx == null)
if (cy == null)
return 0;
else
return -1;
else if (cy == null)
return 1;
return ((IComparable) cx).CompareTo((IComparable) cy);
}
private enum ComparerState
{
Init,
Field,
Property
}
}
Then use it like this:
var comparer = new ListComparer() {
Field= frm.SelectedSortColumn.BaseColumn.ToString() };
if (frm.SortAscending)
pf.Holding = pf.Holding.OrderBy(h=>h.Product, comparer).ToList();
else
pf.Holding = pf.Holding.OrderByDescending(h=>h.Product, comparer).ToList();
If the properties in the Holding class (symbol, price etc) are the same type you can do the following:
var holdingList = new List<Holding>()
{
new Holding() { Quantity = 2, Price = 5 },
new Holding() { Quantity = 7, Price = 2 },
new Holding() { Quantity = 1, Price = 3 }
};
var lookup = new Dictionary<PortfolioSheetMapping, Func<Holding, int>>()
{
{ PortfolioSheetMapping.Price, new Func<Holding, int>(x => x.Price) },
{ PortfolioSheetMapping.Symbol, new Func<Holding, int>(x => x.Symbol) },
{ PortfolioSheetMapping.Quantitiy, new Func<Holding, int>(x => x.Quantity) }
};
Console.WriteLine("Original values:");
foreach (var sortedItem in holdingList)
{
Console.WriteLine("Quantity = {0}, price = {1}", sortedItem.Quantity, sortedItem.Price);
}
var item = PortfolioSheetMapping.Price;
Func<Holding, int> action;
if (lookup.TryGetValue(item, out action))
{
Console.WriteLine("Values sorted by {0}:", item);
foreach (var sortedItem in holdingList.OrderBy(action))
{
Console.WriteLine("Quantity = {0}, price = {1}", sortedItem.Quantity, sortedItem.Price);
}
}
which then displays:
Original values:
Quantity = 2, price = 5
Quantity = 7, price = 2
Quantity = 1, price = 3
Values sorted by Price:
Quantity = 7, price = 2
Quantity = 1, price = 3
Quantity = 2, price = 5
I'm trying to select a subgroup of a list where items have contiguous dates, e.g.
ID StaffID Title ActivityDate
-- ------- ----------------- ------------
1 41 Meeting with John 03/06/2010
2 41 Meeting with John 08/06/2010
3 41 Meeting Continues 09/06/2010
4 41 Meeting Continues 10/06/2010
5 41 Meeting with Kay 14/06/2010
6 41 Meeting Continues 15/06/2010
I'm using a pivot point each time, so take the example pivot item as 3, I'd like to get the following resulting contiguous events around the pivot:
ID StaffID Title ActivityDate
-- ------- ----------------- ------------
2 41 Meeting with John 08/06/2010
3 41 Meeting Continues 09/06/2010
4 41 Meeting Continues 10/06/2010
My current implementation is a laborious "walk" into the past, then into the future, to build the list:
var activity = // item number 3: Meeting Continues (09/06/2010)
var orderedEvents = activities.OrderBy(a => a.ActivityDate).ToArray();
// Walk into the past until a gap is found
var preceedingEvents = orderedEvents.TakeWhile(a => a.ID != activity.ID);
DateTime dayBefore;
var previousEvent = activity;
while (previousEvent != null)
{
dayBefore = previousEvent.ActivityDate.AddDays(-1).Date;
previousEvent = preceedingEvents.TakeWhile(a => a.ID != previousEvent.ID).LastOrDefault();
if (previousEvent != null)
{
if (previousEvent.ActivityDate.Date == dayBefore)
relatedActivities.Insert(0, previousEvent);
else
previousEvent = null;
}
}
// Walk into the future until a gap is found
var followingEvents = orderedEvents.SkipWhile(a => a.ID != activity.ID);
DateTime dayAfter;
var nextEvent = activity;
while (nextEvent != null)
{
dayAfter = nextEvent.ActivityDate.AddDays(1).Date;
nextEvent = followingEvents.SkipWhile(a => a.ID != nextEvent.ID).Skip(1).FirstOrDefault();
if (nextEvent != null)
{
if (nextEvent.ActivityDate.Date == dayAfter)
relatedActivities.Add(nextEvent);
else
nextEvent = null;
}
}
The list relatedActivities should then contain the contiguous events, in order.
Is there a better way (maybe using LINQ) for this?
I had an idea of using .Aggregate() but couldn't think how to get the aggregate to break out when it finds a gap in the sequence.
Here's an implementation:
public static IEnumerable<IGrouping<int, T>> GroupByContiguous(
this IEnumerable<T> source,
Func<T, int> keySelector
)
{
int keyGroup = Int32.MinValue;
int currentGroupValue = Int32.MinValue;
return source
.Select(t => new {obj = t, key = keySelector(t))
.OrderBy(x => x.key)
.GroupBy(x => {
if (currentGroupValue + 1 < x.key)
{
keyGroup = x.key;
}
currentGroupValue = x.key;
return keyGroup;
}, x => x.obj);
}
You can either convert the dates to ints by means of subtraction, or imagine a DateTime version (easily).
In this case I think that a standard foreach loop is probably more readable than a LINQ query:
var relatedActivities = new List<TActivity>();
bool found = false;
foreach (var item in activities.OrderBy(a => a.ActivityDate))
{
int count = relatedActivities.Count;
if ((count > 0) && (relatedActivities[count - 1].ActivityDate.Date.AddDays(1) != item.ActivityDate.Date))
{
if (found)
break;
relatedActivities.Clear();
}
relatedActivities.Add(item);
if (item.ID == activity.ID)
found = true;
}
if (!found)
relatedActivities.Clear();
For what it's worth, here's a roughly equivalent -- and far less readable -- LINQ query:
var relatedActivities = activities
.OrderBy(x => x.ActivityDate)
.Aggregate
(
new { List = new List<TActivity>(), Found = false, ShortCircuit = false },
(a, x) =>
{
if (a.ShortCircuit)
return a;
int count = a.List.Count;
if ((count > 0) && (a.List[count - 1].ActivityDate.Date.AddDays(1) != x.ActivityDate.Date))
{
if (a.Found)
return new { a.List, a.Found, ShortCircuit = true };
a.List.Clear();
}
a.List.Add(x);
return new { a.List, Found = a.Found || (x.ID == activity.ID), a.ShortCircuit };
},
a => a.Found ? a.List : new List<TActivity>()
);
Somehow, I don't think LINQ was truly meant to be used for bidirectional-one-dimensional-depth-first-searches, but I constructed a working LINQ using Aggregate. For this example I'm going to use a List instead of an array. Also, I'm going to use Activity to refer to whatever class you are storing the data in. Replace it with whatever is appropriate for your code.
Before we even start, we need a small function to handle something. List.Add(T) returns null, but we want to be able to accumulate in a list and return the new list for this aggregate function. So all you need is a simple function like the following.
private List<T> ListWithAdd<T>(List<T> src, T obj)
{
src.Add(obj);
return src;
}
First, we get the sorted list of all activities, and then initialize the list of related activities. This initial list will contain the target activity only, to start.
List<Activity> orderedEvents = activities.OrderBy(a => a.ActivityDate).ToList();
List<Activity> relatedActivities = new List<Activity>();
relatedActivities.Add(activity);
We have to break this into two lists, the past and the future just like you currently do it.
We'll start with the past, the construction should look mostly familiar. Then we'll aggregate all of it into relatedActivities. This uses the ListWithAdd function we wrote earlier. You could condense it into one line and skip declaring previousEvents as its own variable, but I kept it separate for this example.
var previousEvents = orderedEvents.TakeWhile(a => a.ID != activity.ID).Reverse();
relatedActivities = previousEvents.Aggregate<Activity, List<Activity>>(relatedActivities, (items, prevItem) => items.OrderBy(a => a.ActivityDate).First().ActivityDate.Subtract(prevItem.ActivityDate).Days.Equals(1) ? ListWithAdd(items, prevItem) : items).ToList();
Next, we'll build the following events in a similar fashion, and likewise aggregate it.
var nextEvents = orderedEvents.SkipWhile(a => a.ID != activity.ID);
relatedActivities = nextEvents.Aggregate<Activity, List<Activity>>(relatedActivities, (items, nextItem) => nextItem.ActivityDate.Subtract(items.OrderBy(a => a.ActivityDate).Last().ActivityDate).Days.Equals(1) ? ListWithAdd(items, nextItem) : items).ToList();
You can properly sort the result afterwards, as now relatedActivities should contain all activities with no gaps. It won't immediately break when it hits the first gap, no, but I don't think you can literally break out of a LINQ. So it instead just ignores anything which it finds past a gap.
Note that this example code only operates on the actual difference in time. Your example output seems to imply that you need some other comparison factors, but this should be enough to get you started. Just add the necessary logic to the date subtraction comparison in both entries.