Query values from a C# Generic Dictionary using LINQ - c#

Here's the code i have:
Dictionary<double, long> dictionary = new Dictionary<double, long>();
dictionary.Add(99, 500);
dictionary.Add(98, 500);
dictionary.Add(101, 8000);
dictionary.Add(103, 6000);
dictionary.Add(104, 5);
dictionary.Add(105, 2000);
double price = 100;
the query i want is:
the key that is nearest price AND with the lowest value.
so in the above example it should return 99.
how do i code this in LINQ ?
i have seen alot of linq examples but i cannt adapt any of them to my needs b/c my query has 2 conditions.
thanks for any help.
edit:
based on comments from #nintendojunkie and #DmitryMartovoi i have had to rethink my approach.
if i prioritize key closest to price then resulting value may not be the lowest and if i prioritize value first then the key may be too far from price so the query will have to prioritize BOTH the key and value the same and give me the lowest value with the closest key to price. both key and value are equally important.
can anyone help on this?
thanks

Don't forget - you use dictionary. Dictionary has only unique keys. I think you consider this structure as List<KeyValuePair<double, long>>. If so - please look to this example:
var minimumKeyDifference = dictionary.Min(y => Math.Abs(y.Key - price));
var minimumItems = dictionary.Where(x => Math.Abs(x.Key - price).Equals(minimumKeyDifference));
var desiredKey = dictionary.First(x => x.Value.Equals(minimumItems.Where(y => y.Key.Equals(x.Key)).Min(y => y.Value))).Key;

You say that you need to find the closest price and the lowest value, but you don't define the rules for attributing precedence between two. In the below, I'm attributing them equal precedence: a price distance of 1 is equivalent to a value of 1.
var closest =
dictionary.OrderBy(kvp => Math.Abs(kvp.Key - price) + kvp.Value).First();
The OrderBy(…).First() should be replaced by a MinBy(…) operator, if available, for performance.
Edit: If the value is only meant to serve as a tiebreaker, then use this (also posted by Giorgi Nakeuri):
var closest =
dictionary.OrderBy(kvp => Math.Abs(kvp.Key - price))
.ThenBy(kvp => kvp.Value)
.First();

You can do it this way:
var result = dictionary.Select(c => new { c.Key, Diff = Math.Abs(price - c.Key) + Math.Abs(price - c.Value), c.Value }).OrderBy(c => c.Diff).FirstOrDefault();

The following works if you change your dictionary key's data type to decimal instead of double.
decimal price = 100;
decimal smallestDiff = dictionary.Keys.Min(n => Math.Abs(n - price));
var nearest = dictionary.Where(n => Math.Abs(n.Key - price) == smallestDiff)
.OrderBy(n => n.Value).First();
If you use double this may fail due to rounding issues, but decimal is preferred for anything having to do with money to avoid those issues.

var price = 100.0;
var nearestKey = (from pair in dictionary
let diff = Math.Abs(pair.Key - price)
select new {Key = pair.Key, Diff = diff}
order by diff desc).First().Key;
var minValue = dictionary[nearestKey];

Maybe you want a magic linq query but i suggest to try the in below.
public static class MyExtensions
{
public static double? GetNearestValue (this IDictionary<double, long> dictionary, double value)
{
if (dictionary == null || dictionary.Count == 0)
return null;
double? nearestDiffValue = null;
double? nearestValue = null;
foreach (var item in dictionary) {
double currentDiff = Math.Abs (item.Key - value);
if (nearestDiffValue == null || currentDiff < nearestDiffValue.Value) {
nearestDiffValue = currentDiff;
nearestValue = item.Value;
}
}
return nearestValue;
}
}
And call like this
Console.WriteLine (dictionary.GetNearestValue (100d));

var min = dictionary
.OrderBy(pair => pair.Value)
.Select(pair =>
new
{
k = pair.Key,
d = Math.Abs(pair.Key - price)
})
.OrderBy(t => t.d)
.Select(t => t.k)
.FirstOrDefault();

Related

How sort Dictionary by 2 criterias and subcriteria in c#

I want to sort myDictionary first by Key and then by Value.
Dictionary<int, double> myDictionary = new Dictionary<int, double>()
{
{1,514},
{2,509},
{3,510},
{4,509},
{5,517},
{6,512},
{7,514},
{8,511}
}
I need to find min and max Value, then to sort it by Key.
var orderedDict = myObjectDictionary
.OrderByDescending(a => a.Value)
.ThenBy(b => b.Key);
But I need to find this object with min Value farthest(using Key) from object with max Value. If there is two object with min/max Value to get farthest(using Key) one from opposite object(min/max).
After that I need to look for object near to min/max and if their Values are near to min/max Values and they are farthest to get them.
In this case:
Max= 5, 517
Min= 3, 510
Requirement: Can't get a min and a max if they are next to each other.
In the following solution I've assumed that myDictionary keys are incremented by 1.
You can have the desired output with the following code:
var groupedByValues = myDictionary.ToLookup(p => p.Value, p => p.Key);
var orderByValues = from item in groupedByValues
orderby item.Key ascending
select (Value:item.Key, Indexes: item.ToList());
var orderedByValues = orderByValues.ToList();
var maxCandidates = orderByValues.Last();
(int Key, double Value) max = (maxCandidates.Indexes.First(), maxCandidates.Value);
(int Key, double Value)? min = null;
foreach(var item in orderedByValues)
{
if (item.Indexes.Any(idx => Math.Abs(idx - max.Key) == 1))
continue;
else
{
min = (item.Indexes.First(), item.Value);
break;
}
};
var groupedByValues = myDictionary.ToLookup(p => p.Value, p => p.Key);
Here we create groups based on the value, the groupedByValues looks like this:
514: 1,7
509: 2,4
510: 3
517: 5
512: 6
511: 8
var orderByValues = from item in groupedByValues
orderby item.Key ascending
select (Value:item.Key, Indexes: item.ToList());
var orderedByValues = orderByValues.ToList();
Here we simply order the previous collection by its keys
The orderByValues is just a query, the orderedByValues is its materialized form:
509: 2,4
510: 3
511: 8
512: 6
514: 1,7
517: 5
var maxCandidates = orderByValues.Last();
(int Key, double Value) max = (maxCandidates.Indexes.First(), maxCandidates.Value);
(int Key, double Value)? min = null;
By issuing the orderByValues.Last command we get the highest value
The highest value can be present multiple times that's why we have Indexes
I've picked the first index from Indexes but you might need to alter this logic based on your requirements
We convert the maxCandidates into the expected output format
We create a min variable, which is nullable to be able to initialize it
foreach(var item in orderedByValues)
{
if (item.Indexes.Any(idx => Math.Abs(idx - max.Key) == 1))
continue;
else
{
min = (item.Indexes.First(), item.Value);
break;
}
};
Here we iterate through the orderedByValues collection in order to find the first min which satisfies all requirements
We are checking the distance between the max Key and the min candidate's indexes
If any of the indexes is next to the max key, then we continue searching
If none of the indexes is next to the max key, then we have found the min

C# Splitting a List<string> Value

I have a List with values {"1 120 12", "1 130 22", "2 110 21", "2 100 18"}, etc.
List<string> myList = new List<string>();
myList.Add("1 120 12");
myList.Add("1 130 22");
myList.Add("2 110 21");
myList.Add("2 100 18");
I need to count based on the first number (ID) is and sum the consequent values for this
IDs i.e. for ID = 1 -> 120+130=150 and 12+22=34 and so on... I have to return an array with these values.
I know I can get these individual values, add them to an array and split it by the empty space between them with something like:
string[] arr2 = arr[i].Split(' ');
and loop thru them to do the sum of each value, but... is there an easy way to do it straight using Lists or Linq Lambda expression?
You can do it in LINQ like this:
var result = myList.Select(x => x.Split(' ').Select(int.Parse))
.GroupBy(x => x.First())
.Select(x => x.Select(y => y.Skip(1).ToArray())
.Aggregate(new [] {0,0}, (y,z) => new int[] {y[0] + z[0], y[1] + z[1]}));
First, the strings are split and converted to int, then they are grouped by ID, then the ID is dropped, and in the end, they are summed together.
But I strongly recommend not doing it in LINQ, because this expression is not easy to understand. If you do it the classic way with a loop, it is quite clear what is going on at first sight. But put this code containing the loop into a separate method, because that way it won't distract you and you still only call a one-liner as in the LINQ solution.
To do it straight, no LINQ, perhaps:
var d = new Dictionary<string, (int A, int B)>();
foreach(var s in myList){
var bits = s.Split();
if(!d.ContainsKey(bits[0]))
d[bits[0]] = (int.Parse(bits[1]), int.Parse(bits[2]));
else {
(int A, int B) x = d[bits[0]];
d[bits[0]] = (x.A + int.Parse(bits[1]), x.B + int.Parse(bits[2]));
}
}
Using LINQ to parse the int, and switching to using TryGetValue, will tidy it up a bit:
var d = new Dictionary<int, (int A, int B)>();
foreach(var s in myList){
var bits = s.Split().Select(int.Parse).ToArray();
if(d.TryGetValue(bits[0], out (int A, int B) x))
d[bits[0]] = ((x.A + bits[1], x.B + bits[2]));
else
d[bits[0]] = (bits[1], bits[2]);
}
Introducing a local function to safely get either the existing nums in the dictionary or a (0,0) pair might reduce it a bit too:
var d = new Dictionary<int, (int A, int B)>();
(int A, int B) safeGet(int i) => d.ContainsKey(i) ? d[i]: (0,0);
foreach(var s in myList){
var bits = s.Split().Select(int.Parse).ToArray();
var nums = safeGet(bits[0]);
d[bits[0]] = (bits[1] + nums.A, bits[2] + nums.B);
}
Is it any more readable than a linq version? Hmm... Depends on your experience with Linq, and tuples, I suppose..
I know this question already has a lot of answers, but I have not seen one yet that focuses on readability.
If you split your code into a parsing phase and a calculation phase, we can use LINQ without sacrificing readability or maintainability, because each phase only does one thing:
List<string> myList = new List<string>();
myList.Add("1 120 12");
myList.Add("1 130 22");
myList.Add("2 110 21");
myList.Add("2 100 18");
var parsed = (from item in myList
let split = item.Split(' ')
select new
{
ID = int.Parse(split[0]),
Foo = int.Parse(split[1]),
Bar = int.Parse(split[2])
});
var summed = (from item in parsed
group item by item.ID into groupedByID
select new
{
ID = groupedByID.Key,
SumOfFoo = groupedByID.Sum(g => g.Foo),
SumOfBar = groupedByID.Sum(g => g.Bar)
}).ToList();
foreach (var s in summed)
{
Console.WriteLine($"ID: {s.ID}, SumOfFoo: {s.SumOfFoo}, SumOfBar: {s.SumOfBar}");
}
fiddle
If you want, but I think it will be much easier to edit and optimize using the usual value. I don't find using this kind of logic inside LINQ will stay that way for a long period of time. Usually, we need to add more values, more parsing, etc. Make it not really suitable for everyday use.
var query = myList.Select(a => a.Split(' ').Select(int.Parse).ToArray())
.GroupBy(
index => index[0],
amount => new
{
First = amount[1],
Second = amount[2]
},
(index, amount) => new
{
Index = index,
SumFirst = amount.Sum(a => a.First),
SumSecond = amount.Sum(a => a.Second)
}
);
fiddle
is there an easy way to do it straight using Lists or Linq Lambda expression?
Maybe, is it wise to do this? Probably not. Your code will be hard to understand, impossible to unit test, the code will probably not be reusable, and small changes are difficult.
But let's first answer your question as a one LINQ statement:
const char separatorChar = ' ';
IEnumerable<string> inputText = ...
var result = inputtext.Split(separatorChar)
.Select(text => Int32.Parse(text))
.Select(numbers => new
{
Id = numbers.First()
Sum = numbers.Skip(1).Sum(),
});
Not reusable, hard to unit test, difficult to change, not efficient, do you need more arguments?
It would be better to have a procedure that converts one input string into a proper object that contains what your input string really represents.
Alas, you didn't tell us if every input string contains three integer numbers, of that some might contain invalid text, and some might contain more or less than three integer numbers.
You forgot to tell use what your input string represents.
So I'll just make up an identifier:
class ProductSize
{
public int ProductId {get; set;} // The first number in the string
public int Width {get; set;} // The 2nd number
public int Height {get; set;} // The 3rd number
}
You need a static procedure with input a string, and output one ProductSize:
public static ProductSize FromText(string productSizeText)
{
// Todo: check input
const char separatorChar = ' ';
var splitNumbers = productSizeText.Split(separatorChar)
.Select(splitText => Int32.Parse(splitText))
.ToList();
return new ProductSize
{
ProductId = splitNumbers[0],
Width = splitNumbers[1],
Height = splitNumbers[2],
};
}
I need to count based on the first number (ID) is and sum the consequent values for this IDs
After creating method ParseProductSize this is easy:
IEnumerable<string> textProductSizes = ...
var result = textProductSizes.Select(text => ProductSize.FromText(text))
.Select(productSize => new
{
Id = productSize.Id,
Sum = productSize.Width + productSize.Height,
});
If your strings do not always have three numbers
If you don't have always three numbers, then you won't have Width and Height, but a property:
IEnumerable<int> Numbers {get; set;} // TODO: invent proper name
And in ParseProductSize:
var splitText = productSizeText.Split(separatorChar);
return new ProductSize
{
ProductId = Int32.Parse(splitText[0]),
Numbers = splitText.Skip(1)
.Select(text => Int32.Parse(text));
I deliberately keep it an IEnumerable, so if you don't use all Numbers, you won't have parsed numbers for nothing.
The LINQ:
var result = textProductSizes.Select(text => ProductSize.FromText(text))
.Select(productSize => new
{
Id = productSize.Id,
Sum = productSize.Numbers.Sum(),
});

Partition Keys in List<KeyValuePair> c#

I have a List of KeyValuePairs
var hitCoord = new List<KeyValuePair<int, double>>()
and sorted like this (descending by Key)
hitCoord.Sort((a, b) => (b.Key.CompareTo(a.Key)));
I can find the total highest Value with
hitCoord.Sort((a, b) => (b.Value.CompareTo(a.Value)));
(^ maybe that can be used for the following query?)
I would like to partition the Keys in my list such that I can find Values that meet a condition within the specified range of keys.
i.e. I would like to find the highest Value and Lowest Value in a range of (int)Keys
for (i=0; i<hitCoord.Count; i++)
{
if (hitCoord[i].Key > (int lowerbound) && hitCoord[i].Key < (int upperBound)
{
find highest Value?
}
}
Not sure if that is at all on the right track. I am new to programming and very new to KeyValuePairs. Any help you can offer on this matter is much appreciated! Thank you!
Finding the max value in a specified range of keys could be solved by using LINQ (using System.Linq;) like this:
hitCoord.Where(c => c.Key > lowerbound && c.Key < upperbound).Max(c => c.Value);
The approach:
Use Where to filter all items with key in range
Use Max to get the max value
You could adapt and extend the query also with more checks and constraints. Some basic queries are described in Basic LINQ Query Operations (C#).
You don't need to actually sort - you can do this with Linq (adding using System.Linq; to the top of your .cs file). You just want a Where to filter by key and a Max to get the highest value:
var maxValue = hitCoord.Where(hc => hc.Key > lowerbound && hc.Key < upperBound)
.Max(hc => hc.Value);
As others have suggested this is all pretty easy to do with linq. here's another sample of linq calls including how to create a partition lookup.
var hitCoord = new List<KeyValuePair<int, double>>()
{
new KeyValuePair<int, double>(1, 1.1),
new KeyValuePair<int, double>(1, 1.2),
new KeyValuePair<int, double>(2, 2.0),
new KeyValuePair<int, double>(2, 2.1)
};
var partitions = hitCoord.ToLookup(kvp => kvp.Key % 2);
var maxKvp = hitCoord.Max(kvp => kvp.Key);
var minKvp = hitCoord.Min(kvp => kvp.Key);
int lower = 1;
int higher = 2;
var maxInRange = hitCoord.Where(kvp => kvp.Key >= lower && kvp.Key <= higher).Max(kvp => kvp.Key);
That said if this is perfromance critical then you'll probably want to use something other than linq so you can optimize it and avoid going through the list multiple times.

select the min value using linq

I have a Dictionary
Dictionary<Location2D, int> h_scores = new Dictionary<Location2D, int>();
and I want to select the Key // which is Location2D by the minimum int value.
I tried
h_scores.Min().Key; // not working
h_scores.OrderBy(x => x.Value).Select(y=> y.Key).Min(); //error At least one object must implement IComparable.
so how can I select a key by the smallest int value?
You just need to use the right overload of Min:
val minKey = h_scores.Min(s => s.Value).Key;
Update
Didn't pay attention to the return value of the overload for Min. You are definitely looking for MinBy from Jon Skeet's morelinq:
val minKey = h_scores.MinBy(s => s.Value).Key;
Just for the sake of diversity, the solution which doesn't need external dependencies (e.g. MoreLinq) and is O(n) in contrast to OrderBy() solutions which are at least O(n*log(n)):
var minKey =
h_scores.Aggregate(h_scores.First(), (min, curr) => curr.Value < min.Value ? curr : min).Key;
If you order them by the Value the first one will be the one with the minimum
h_scores.OrderBy(x => x.Value).First().Select(y=> y.Key);
I don't know what a Location2D is but you can use the following example to do what you want. Just sub in your class instead of string. Also, because values are not guaranteed to be unique in a Dictionary (but may be in your case), you will likely want to do a .Single() on the keys enumeration.
[Test]
public void Test()
{
var dictionary = new Dictionary<string, int>
{
{ "first", 2 },
{ "second", 1 },
{ "third", 3 },
{ "fourth", 1 }
};
int min = dictionary.Values.Min();
IEnumerable<string> keys = dictionary.Keys.Where(key => dictionary[key] == min);
Assert.That(keys.Count(), Is.EqualTo(2));
Assert.That(keys.Contains("second"), Is.True);
Assert.That(keys.Contains("fourth"), Is.True);
}

Average extension method in Linq for default value

Anyone know how I can set a default value for an average? I have a line like this...
dbPlugins = (from p in dbPlugins
select new { Plugin = p, AvgScore = p.DbVersions.Average(x => x.DbRatings.Average(y => y.Score)) })
.OrderByDescending(x => x.AvgScore)
.Select(x => x.Plugin).ToList();
which throws an error becase I have no ratings yet. If I have none I want the average to default to 0. I was thinking this should be an extension method where I could specify what the default value should be.
There is: DefaultIfEmpty.
I 'm not sure about what your DbVersions and DbRatings are and which collection exactly has zero items, but this is the idea:
var emptyCollection = new List<int>();
var average = emptyCollection.DefaultIfEmpty(0).Average();
Update: (repeating what's said in the comments below to increase visibility)
If you find yourself needing to use DefaultIfEmpty on a collection of class type, remember that you can change the LINQ query to project before aggregating. For example:
class Item
{
public int Value { get; set; }
}
var list = new List<Item>();
var avg = list.Average(item => item.Value);
If you don't want to/can not construct a default Item with Value equal to 0, you can project to a collection of ints first and then supply a default:
var avg = list.Select(item => item.Value).DefaultIfEmpty(0).Average();
My advice would to create a reusable solution instead of a solution for this problem only.
Make an extension method AverageOrDefault, similar to FirstOrDefault. See extension methods demystified
public static class MyEnumerableExtensions
{
public static double AverageOrDefault(this IEnumerable<int> source)
{
// TODO: decide what to do if source equals null: exception or return default?
if (source.Any())
return source.Average();
else
return default(int);
}
}
There are 9 overloads of Enumerable.Average, so you'll need to create an AverageOrDefault for double, int?, decimal, etc. They all look similar.
Usage:
// Get the average order total or default per customer
var averageOrderTotalPerCustomer = myDbContext.Customers
.GroupJoin(myDbContext.Orders,
customer => customer.Id,
order => order.CustomerId,
(customer, ordersOfThisCustomer) => new
{
Id = customer.Id,
Name = customer.Name,
AverageOrder = ordersOfThisCustomer.AverageOrDefault(),
});
I don't think there's a way to select default, but how about this query
dbPlugins = (from p in dbPlugins
select new {
Plugin = p, AvgScore =
p.DbVersions.Any(x => x.DbRatings) ?
p.DbVersions.Average(x => x.DbRatings.Average(y => y.Score)) : 0 })
.OrderByDescending(x => x.AvgScore)
.Select(x => x.Plugin).ToList();
Essentially the same as yours, but we first ask if there are any ratings before averaging them. If not, we return 0.

Categories

Resources