LINQ sort dictionary by value - c#

I am trying to sort values Descending before pushing them into dictionary. The query works fine but i just need to add sorting functionality.
Following is my original LINQ query
List<Model> list = query1.Where(x => x.ItemsPrice.Any())
.GroupBy(x => new { Student = query2.FirstOrDefault(y => y.Id == x. Id).Student.Name })
.Select(x => new Model
{
StudentName = x.Key.Student,
ClassItems = x.SelectMany(y => y. ItemsPrice)
.GroupBy(y => y. Price.item.Name)
.ToDictionary(y => y.Key, y => y.Sum(z => z.TotalPrice()))
}).ToList();
I am trying the following code but it is giving this error
can not implicitly convert IOrderedEnumerables to System.Collection.Generic.Dictioanry.
.ToDictionary(y => y.Key, y => y.Sum(z => z.TotalPrice())).OrderByDescending(y => y.Value)

Something like this should work for you:
public class MyComparer : IComparer<int>
{
public int Compare(int x, int y)
{
return x.CompareTo(y);
}
}
// ...
private void DoStuff()
{
var dict = new Dictionary<int, int> {{3, 300}, {2, 200}};
var sortedDict = new SortedDictionary<int, int>(dict, new MyComparer());
}
Or in your specific scenario:
private class DescendingComparer : IComparer<string>
{
int IComparer<string>.Compare(string a, string b)
{
return StringComparer.InvariantCulture.Compare(b, a);
}
}
// ....
ClassItems = new SortedDictionary<string, float>(x.SelectMany(y => y. ItemsPrice)
.GroupBy(y => y. Price.item.Name)
.ToDictionary(y => y.Key, y => y.Sum(z => z.TotalPrice())),
new DescendingComparer());
Note: adjust the key and value type of the sorted dictionary as well as the comparer type according to your needs!

The order that items are stored in a dictionary is not guaranteed. Don't rely on it. When you retrieve items from the dictionary, you can sort it at that point.
"For purposes of enumeration, each item in the dictionary is treated as a KeyValuePair structure representing a value and its key. The order in which the items are returned is undefined."
-- MSDN
As Markus pointed out, if you really need order in your dictionary, use the OrderedDictionary<,> class or the SortedDictionary<,> class.

Related

Sort by most occurring string from json by key order and order contains array itself

I have JSON file which contains orders as arrays against same key as
[
{
"order":["Order1"]
},
{
"order":["Order2"]
},
{
"order":["Order2","Order3"]
},
{
"order":["Order1","Order2"]
},
{
"order":["Order2","Order3"]
}
]
I want it to order by most occurred orders combination.
Kindly help me out in this.
NOTE: It is not a simple array of string kindly look at the json before you mark it as probable duplicate.
This can be done as follows. First, introduce a data model for your orders as follows:
public class Order
{
public string[] order { get; set; }
}
Next, define the following equality comparer for enumerables:
public class IEnumerableComparer<TEnumerable, TElement> : IEqualityComparer<TEnumerable> where TEnumerable : IEnumerable<TElement>
{
//Adapted from IEqualityComparer for SequenceEqual
//https://stackoverflow.com/questions/14675720/iequalitycomparer-for-sequenceequal
//Answer https://stackoverflow.com/a/14675741 By Cédric Bignon https://stackoverflow.com/users/1284526/c%C3%A9dric-bignon
public bool Equals(TEnumerable x, TEnumerable y)
{
return Object.ReferenceEquals(x, y) || (x != null && y != null && x.SequenceEqual(y));
}
public int GetHashCode(TEnumerable obj)
{
// Will not throw an OverflowException
unchecked
{
return obj.Where(e => e != null).Select(e => e.GetHashCode()).Aggregate(17, (a, b) => 23 * a + b);
}
}
}
Now you can deserialize the JSON containing the orders listed above and sort the unique orders by descending frequency as follows:
var items = JsonConvert.DeserializeObject<List<Order>>(jsonString);
//Adapted from LINQ: Order By Count of most common value
//https://stackoverflow.com/questions/20046563/linq-order-by-count-of-most-common-value
//Answer https://stackoverflow.com/a/20046812 by King King https://stackoverflow.com/users/1679602/king-king
var query = items
//If order items aren't already sorted, you need to do so first.
//use StringComparer.OrdinalIgnoreCase or StringComparer.Ordinal or StringComparer.CurrentCulture as required.
.Select(i => i.order.OrderBy(s => s, StringComparer.Ordinal).ToArray())
//Adapted from writing a custom comparer for linq groupby
//https://stackoverflow.com/questions/37733773/writing-a-custom-comparer-for-linq-groupby
//Answer https://stackoverflow.com/a/37734601 by Gert Arnold https://stackoverflow.com/users/861716/gert-arnold
.GroupBy(s => s, new IEnumerableComparer<string [], string>())
.OrderByDescending(g => g.Count())
.Select(g => new Order { order = g.Key } );
var sortedItems = query.ToList();
Demo fiddle here.
Alternatively, if you want to preserve duplicates rather than merging them, you can do:
var query = items
//If order items aren't already sorted, you may need to do so first.
//use StringComparer.OrdinalIgnoreCase or StringComparer.Ordinal or StringComparer.CurrentCulture as required.
.Select(i => i.order.OrderBy(s => s, StringComparer.Ordinal).ToArray())
//Adapted from writing a custom comparer for linq groupby
//https://stackoverflow.com/questions/37733773/writing-a-custom-comparer-for-linq-groupby
//Answer https://stackoverflow.com/a/37734601 by Gert Arnold https://stackoverflow.com/users/861716/gert-arnold
.GroupBy(s => s, new IEnumerableComparer<string [], string>())
.OrderByDescending(g => g.Count())
.SelectMany(g => g)
.Select(a => new Order { order = a });
Demo fiddle #2 here.
Notes:
I define the equality comparer using two generic types IEnumerableComparer<TEnumerable, TElement> : IEqualityComparer<TEnumerable> where TEnumerable : IEnumerable<TElement> rather than just IEnumerableComparer<string> as shown in this answer to IEqualityComparer for SequenceEqual by Cédric Bignon in order to prevent the string [] sort key from being upcast to IEnumerable<string> via type inferencing in the .GroupBy(s => s, new IEnumerableComparer<string>()) lambda expression.
If you are sure the orders are already sorted, or ["Order3", "Order1"] differs from ["Order1", "Order3"], then replace i.order.OrderBy(s => s, StringComparer.Ordinal).ToArray() with just i.order.

order objects by given values

Given:
class C
{
public string Field1;
public string Field2;
}
template = new [] { "str1", "str2", ... }.ToList() // presents allowed values for C.Field1 as well as order
list = new List<C> { ob1, ob2, ... }
Question:
How can I perform Linq's
list.OrderBy(x => x.Field1)
which will use template above for order (so objects with Field1 == "str1" come first, than objects with "str2" and so on)?
In LINQ to Object, use Array.IndexOf:
var ordered = list
.Select(x => new { Obj = x, Index = Array.IndexOf(template, x.Field1)})
.OrderBy(p => p.Index < 0 ? 1 : 0) // Items with missing text go to the end
.ThenBy(p => p.Index) // The actual ordering happens here
.Select(p => p.Obj); // Drop the index from the result
This wouldn't work in EF or LINQ to SQL, so you would need to bring objects into memory for sorting.
Note: The above assumes that the list is not exhaustive. If it is, a simpler query would be sufficient:
var ordered = list.OrderBy(x => Array.IndexOf(template, x.Field1));
I think IndexOf might work here:
list.OrderBy(_ => Array.IndexOf(template, _.Field1))
Please note that it will return -1 when object is not present at all, which means it will come first. You'll have to handle this case. If your field is guaranteed to be there, it's fine.
As others have said, Array.IndexOf should do the job just fine. However, if template is long and or list is long, it might be worthwhile transforming your template into a dictionary. Something like:
var templateDict = template.Select((item,idx) => new { item, idx })
.ToDictionary(k => k.item, v => v.idx);
(or you could just start by creating a dictionary instead of an array in the first place - it's more flexible when you need to reorder stuff)
This will give you a dictionary keyed off the string from template with the index in the original array as your value. Then you can sort like this:
var ordered = list.OrderBy(x => templateDict[x.Field1]);
Which, since lookups in a dictionary are O(1) will scale better as template and list grow.
Note: The above code assumes all values of Field1 are present in template. If they are not, you would have to handle the case where x.Field1 isn't in templateDict.
var orderedList = list.OrderBy(d => Array.IndexOf(template, d.MachingColumnFromTempalate) < 0 ? int.MaxValue : Array.IndexOf(template, d.MachingColumnFromTempalate)).ToList();
I've actually written a method to do this before. Here's the source:
public static IOrderedEnumerable<T> OrderToMatch<T, TKey>(this IEnumerable<T> source, Func<T, TKey> sortKeySelector, IEnumerable<TKey> ordering)
{
var orderLookup = ordering
.Select((x, i) => new { key = x, index = i })
.ToDictionary(k => k.key, v => v.index);
if (!orderLookup.Any())
{
throw new ArgumentException("Ordering collection cannot be empty.", nameof(ordering));
}
T[] sourceArray = source.ToArray();
return sourceArray
.OrderBy(x =>
{
int index;
if (orderLookup.TryGetValue(sortKeySelector(x), out index))
{
return index;
}
return Int32.MaxValue;
})
.ThenBy(x => Array.IndexOf(sourceArray, x));
}
You can use it like this:
var ordered = list.OrderToMatch(x => x.Field1, template);
If you want to see the source, the unit tests, or the library it lives in, you can find it on GitHub. It's also available as a NuGet package.

LinQ ofType in Value

I have the following Dictionary:
public Dictionary<string,object> Items;
Now I need to get all Items where the Value of the Dictionary-item is from a specific type. (e.g. "int")
var intValues = Items.OfType<KeyValuePair<string,int>> simply does not work.
Code without LinQ would be something like:
var intValues=new Dictionary<string,int>()
foreach (var oldVal in Items) {
if (oldVal.Value is int) {
intValues.add(oldVal.Key, oldVal.Value);
}
}
(Update) my example should show the basic idea. But if possible I would avoid to create a new Dictionary as a result.
The direct translation of your foreach would be the following in LINQ:
var intValues = Items.Where(item => item.Value is int)
.ToDictionary(item => item.Key, item => (int)item.Value);
So basically, you filter first for where the item.Value is an int, and then you create a dictionary from it using ToDictionary where you cast those values to int to make sure that the resulting dictionary is a Dictionary<string, int>. Since we filtered non-integers already, this type cast will always succeed.
You can use the is operator on the Value property:
var intValues = Items.Where(x => x.Value is int);
If you want an actual Dictionary<string,int> at the end just add:
.ToDictionary(v=> v.Key, v=> (int)v.Value)
Try with this:
var intValue = Items
.Where(x => x.Value is int) // filter per Value is int
.ToDictionary(x => x.Key, x => (int)x.Value); // create a new dictionary converting Value to int
You can do
var result = Items.Where(x => x.Value is int)
.ToDictionary(x => x.Key, x => x.Value);

ToDictionary on anonymous types

I am trying to mimic a function that was already created in the code base I am working on. The first function works, but when I try to modify it to use strings in the dictionary it does not work. I get System.Linq.Enumerable+WhereSelectEnumerableIterator2[<>f__AnonymousType32[System.Int32,System.String],System.String] as a value for the comments. I know that the first one is using average which is an aggregate but I cannot figure out how to aggregate the comments as they are strings.
public static Dictionary<int, double> getRatingAverages(string EventID)
{
List<tbMultipurposeVertical> allMain = DynamicData.Vertical.getRecords(EventID, appcode, -2).ToList();
Dictionary<int, double> ratings;
using (FBCDBDataContext db = new FBCDBDataContext())
{
ratings = db.tbMultipurposeVerticals.Where(v => v.eventid == EventID & v.appcode == "ratinglabel" & v.label == "Rater")
.Select(v => new
{
AbstractID = v.parent,
Rating = int.Parse(db.tbMultipurposeVerticals.First(r => r.parent == v.id & r.label == "Rating").value)
})
.GroupBy(r => r.AbstractID).ToDictionary(k => k.Key, v => v.Select(r => r.Rating).Average());
}
return ratings;
}
public static Dictionary<int, string> getRatingComments(string EventID)
{
List<tbMultipurposeVertical> allMain = DynamicData.Vertical.getRecords(EventID, appcode, -2).ToList();
Dictionary<int, string> comments;
using (FBCDBDataContext db = new FBCDBDataContext())
{
comments = db.tbMultipurposeVerticals.Where(v => v.eventid == EventID & v.appcode == "ratinglabel" & v.label == "Rater")
.Select(v => new
{
AbstractID = v.parent,
Comment = db.tbMultipurposeVerticals.First(r => r.parent == v.id & r.label == "Comment").ToString()
})
.GroupBy(r => r.AbstractID).ToDictionary(k => k.Key, v => v.Select(r => r.Comment).ToString());
}
return comments;
}
In the first method, you are taking the average of the ratings (an aggregate method). For the second method, you are now treating it as a single comment.
It's not giving you what you expect because of the .GroupBy()
As Steve Greene suggests, either you can get the first comment (v.First().ToString() or v.FirstOrDefault().ToString()), or you can consider concatenating the comments (v.Concat()) if that makes sense in your application.
Otherwise you may want to make your dictionary to be of the form Dictionary<int, List<string>>

Convert List to Dictionary<string, string[]>

I have a simple custom object:
class CertQuestion
{
public string Field {get;set;}
public string Value {get;set;}
}
Subsequently I find myself with a List in some code. I'm trying to figure out how to format a list of CertQuestions into a corresponding Dictionary with similar Field names grouped together. For instance, given the following list:
List<CertQuestion> certQuestions = new List<CertQuestion>()
{
new CertQuestion("Key", "Value1"),
new CertQuestion("Key", "Value2"),
new CertQuestion("Key2", "Value"),
new CertQuestion("Key2", "Value2")
};
I would like to convert that (trying to use LINQ) into a Dictionary with two entries such as
{{"Key", "Value1, Value2"}, {"Key2", "Value, Value2"}}
Group the questions by field, then convert to dictionary by selecting key, then value. Value becomes the grouping's list.
certQuestions.GroupBy(c => c.Field)
.ToDictionary(k => k.Key, v => v.Select(f => f.Value).ToList())
Or for an array:
certQuestions.GroupBy(c => c.Field)
.ToDictionary(k => k.Key, v => v.Select(f => f.Value).ToArray())
Edit based on question in comment:
class CertTest
{
public string TestId {get;set;}
public List<CertQuestion> Questions {get;set;}
}
var certTests = new List<CertTest>();
You would use the SelectMany extension method. It is designed to aggregate a property list object that is in each element of the original list:
certTests.SelectMany(t => t.Questions)
.GroupBy(c => c.Field)
.ToDictionary(k => k.Key, v => v.Select(f => f.Value).ToList())
Your requirement was for a comma-separated list of values, that can be done like this:
var dict = certQuestions.GroupBy(c => c.Field)
.ToDictionary(k => k.Key, v => String.Join(", ", v.Select(x => x.Value)))
Live example: http://rextester.com/LXS58744
(You should consider whether what you actually want is the values to be an Array or List<string> - see other answers)

Categories