I got a list like this:
class Article
{
...
Public DateTime PubTime{get;set}
...
}
List<Article> articles
Now I want to group this list with hour range :[0-5,6-11,12-17,18-23]
I know there is a cumbersome way to do this:
var firstRange = articles.Count(a => a.PubTime.Hour >= 0 && a.PubTime.Hour <= 5);
But I want to use a elegant way. How can I do that?Use Linq Or anything others?
Group by Hour / 6:
var grouped = articles.GroupBy(a => a.PubTime.Hour / 6);
IDictionary<int, int> CountsByHourGrouping = grouped.ToDictionary(g => g.Key, g => g.Count());
The key in the dictionary is the period (0 representing 0-5, 1 representing 6-11, 2 representing 12-17, and 3 representing 18-23). The value is the count of articles in that period.
Note that your dictionary will only contain values where those times existed in the source data, so it won't always contain 4 items.
You could write a CheckRange Function, which takes your values and returns a bool. To make your code more reusable and elegant.
Function Example:
bool CheckRange (this int number, int min, int max)
=> return (number >= min && number <= max);
You could now use this function to check if the PubTime.Hour is in the correct timelimit.
Implementation Example:
var firstRange = articles.Count(a => a.CheckRange(0, 5));
Related
This is what I have so far:
decimal? total = list.Sum(item => item.Score);
What I would like to do is to exclude the min and max value in the list and then get the total value.
Is it possible to do all that in one linq statement?
list.OrderBy(item => item.Score)
.Skip(1)
.Reverse()
.Skip(1)
.Sum(item => item.Score);
You can try ordering the list first, then skip first item (minimum) and take all but the last (maximum) from the rest:
decimal? total = list.OrderBy(x => x.Score)
.Skip(1)
.Take(list.Count - 2)
.Sum(x => x.Score);
This is not the nicest code imaginable, but it does have the benefits of
only enumerating through the entire collection once (though it does get the first value three times).
Not require any much more memory than that to hold the IEnumerator and two Tuple<int, int, long, long> objects (which you'd not have if using OrderBy, ToList and sorting, etc.). This lets it work with arbitrarily large IEnumerable collections.
A single Linq expression (which is what you wanted).
Handles the edge cases (values.Count() < 2) properly:
when there's no values, using Min() and Max() on an IEnumerable will throw an InvalidOperationException
when there's one value, naïve implementations will do something like Sum() - Min() - Max() on the IEnumerable which returns the single value, negated.
I know you've already accepted an answer, but here it is: I'm using a single call to Enumerable.Aggregate.
public static long SumExcludingMinAndMax(IEnumerable<int> values)
{
// first parameter: seed (Tuple<running minimum, running maximum, count, running total>)
// second parameter: func to generate accumulate
// third parameter: func to select final result
var result = values.Aggregate(
Tuple.Create<int, int, long, long>(int.MaxValue, int.MinValue, 0, 0),
(accumulate, value) => Tuple.Create<int, int, long, long>(Math.Min(accumulate.Item1, value), Math.Max(accumulate.Item2, value), accumulate.Item3 + 1, accumulate.Item4 + value),
accumulate => accumulate.Item3 < 2 ? 0 : accumulate.Item4 - accumulate.Item1 - accumulate.Item2);
return result;
}
If you want to exclude all min- and max-values, pre-calculate both values and then use Ènumerable.Where to exclude them:
decimal? min = list.Min(item => item.Score);
decimal? max = list.Max(item => item.Score);
decimal? total = list
.Where(item=> item.Score != min && item.Score != max)
.Sum(item => item.Score);
You should pre-process list before sum to exclude min and max.
I know this question has been asked before, but the other questions are only about finding the CLOSEST. I dont want that. I need the LOWEST between two values. For example if this is the list:
Code from How to get the closest number from a List<int> with LINQ?:
List<int> numbers = new List<int>();
numbers.Add(2);
numbers.Add(5);
numbers.Add(7);
numbers.Add(10)
and the number to find is 9, I want it to return the 7 item, not 10 even though its closer. This code finds the closest, but any time I change it to find the lowest in the range it breaks for other situations where the inputted number is one of the numbers in the list(for example 7 or 10):
list.Aggregate((x,y) => Math.Abs(x-number) < Math.Abs(y-number) ? x : y);
How do I alter that code to support what I need to do?
I know I could use binarysearch... I don't want to use that, I want to use linq if possible.
var numbers = new List<int> { 2, 5, 7, 10 };
var seven = numbers.Where(n => n <= 9).Max();
If you have to consider cases where the list will not any number closest, the code would look like,
private static int? GetClosest(List<int> numbers, int number)
{
var shorterEnumerable = numbers.Where(x => x <= number);
var shorterArray = shorterEnumerable as int[] ?? shorterEnumerable.ToArray();
if (shorterArray.Length > 1)
return shorterArray.Max();
return null;
}
even #danielnixon answer is good, this uses agregate
int? closerLow = (int?) list.Aggregate((x,y) => Math.Abs(x-number) < Math.Abs(y-number)
? (x > number ? y : x )
: (y > number ? x : y));
if (closerLow > number) closerLow = null;
Say I have a class like so:
public class Work
{
public string Name;
public double Time;
public Work(string name, double time)
{
Name = name;
Time = time;
}
}
And I have a List<Work> with about 20 values that are all filled in:
List<Work> workToDo = new List<Work>();
// Populate workToDo
Is there any possible way that I can group workToDo into segments where each segments sum of Time is a particular value? Say workToDo has values like so:
Name | Time
A | 3.50
B | 2.75
C | 4.25
D | 2.50
E | 5.25
F | 3.75
If I want the sum of times to be 7, each segment or List<Work> should have a bunch of values where the sum of all the Times is 7 or close to it. Is this even remotely possible or is it just a stupid question/idea? I am using this code to separate workToDo into segments of 4:
var query = workToDo.Select(x => x.Time)
.Select((x, i) => new { Index = i, Value = x})
.GroupBy(y => y.Index / 4)
.ToList();
But I am not sure how to do it based on the Times.
Here's a query that segments your data in groups where the times are near to 7, but not over:
Func<List<Work>,int,int,double> sumOfRange = (list, start, end) => list
.Skip(start)
.TakeWhile ((x, index) => index <= end)
.ToList()
.Sum (l => l.Time);
double segmentSize = 7;
var result = Enumerable.Range(0, workToDo.Count ())
.Select (index => workToDo
.Skip(index)
.TakeWhile ((x,i) => sumOfRange(workToDo, index, i)
<= segmentSize));
The output for your example data set is:
A 3.5
B 2.75
total: 6.25
B 2.75
C 4.25
total: 7
C 4.25
D 2.5
total: 6.75
D 2.5
total: 2.5
E 5.25
total: 5.25
F 3.75
total: 3.75
If you want to allow a segments to total over seven, then you could increase the segmentSize variable by 25% or so (i.e. make it 8.75).
This solution recurses through all combinations and returns the ones whose sums are close enough to the target sum.
Here is the pretty front-end method that lets you specify the list of work, the target sum, and how close the sums must be:
public List<List<Work>> GetCombinations(List<Work> workList,
double targetSum,
double threshhold)
{
return GetCombinations(0,
new List<Work>(),
workList,
targetSum - threshhold,
targetSum + threshhold);
}
Here is the recursive method that does all of the work:
private List<List<Work>> GetCombinations(double currentSum,
List<Work> currentWorks,
List<Work> remainingWorks,
double minSum,
double maxSum)
{
// Filter out the works that would go over the maxSum.
var newRemainingWorks = remainingWorks.Where(x => currentSum + x.Time <= maxSum)
.ToList();
// Create the possible combinations by adding each newRemainingWork to the
// list of current works.
var sums = newRemainingWorks
.Select(x => new
{
Works = currentWorks.Concat(new [] { x }).ToList(),
Sum = currentSum + x.Time
})
.ToList();
// The initial combinations are the possible combinations that are
// within the sum range.
var combinations = sums.Where(x => x.Sum >= minSum).Select(x => x.Works);
// The additional combinations get determined in the recursive call.
var newCombinations = from index in Enumerable.Range(0, sums.Count)
from combo in GetCombinations
(
sums[index].Sum,
sums[index].Works,
newRemainingWorks.Skip(index + 1).ToList(),
minSum,
maxSum
)
select combo;
return combinations.Concat(newCombinations).ToList();
}
This line will get combinations that sum to 7 +/- 1:
GetCombinations(workToDo, 7, 1);
What you are describing is a packing problem (where the tasks are being packed into 7-hour containers). Whilst it would be possible to use LINQ syntax in a solution to this problem, there is no solution inherent in LINQ that I am aware of.
Well,
int a = 20;
int b = 30;
int c = 40;
int d = 50;
if (a > b,c,d)
how would i approach this, i have no idea i fail at every turn, its been hours
If there is a short quantity of numbers, you can simply use the boolean logic:
if (a > b && a > c && a > d)
{
}
If you don't know in advance the quantity of numbers, what about creating a collection and compare the first number to the numbers from the collection through a loop?
var numbers = { 30, 40, 50 };
if (!numbers.Any(c => 20 <= c))
{
}
You can put them in an array:
int a = 20;
int[] others = { 30, 40, 50 };
if(others.All(o => a > o))
{
// do something
}
Put them all in a list and do this:
if(list.All(x=> a > x))
Or in one line:
if(new List<int>{a, b, c, d}.All(x=> a > x))
EDIT
I changed the Max() to All(x => a > x) because the a > x will not return a true when a == x whereas Max() will do that.
Non-LINQ example:
if (Math.Max(a, Math.Max(b, Math.Max(c, d))) == a)
{
}
If all you want to know is if the number x is greater than the other numbers, you could either compare them explicitly like if(x>b & b>c) or use something like if(list.All(x=> a > x))
as mentioned above. If you have many numbers and all you want is the higher number, you could sort the list using a quick sort that could be efficient and get the first item.
It's a bit different if you need to compare them and get different comparissons then probably the easiest thing is to loop through the list.
Lets say I have this amputated Person class:
class Person
{
public int Age { get; set; }
public string Country { get; set; }
public int SOReputation { get; set; }
public TimeSpan TimeSpentOnSO { get; set; }
...
}
I can then group on Age and Country like this:
var groups = aListOfPeople.GroupBy(x => new { x.Country, x.Age });
Then I can output all the groups with their reputation totals like this:
foreach(var g in groups)
Console.WriteLine("{0}, {1}:{2}",
g.Key.Country,
g.Key.Age,
g.Sum(x => x.SOReputation));
My question is, how can I get a sum of the TimeSpentOnSO property? The Sum method won't work in this case since it is only for int and such. I thought I could use the Aggregate method, but just seriously can't figure out how to use it... I'm trying all kinds properties and types in various combinations but the compiler just won't recognize it.
foreach(var g in groups)
Console.WriteLine("{0}, {1}:{2}",
g.Key.Country,
g.Key.Age,
g.Aggregate( what goes here?? ));
Have I completely missunderstood the Aggregate method? Or what is going on? Is it some other method I should use instead? Or do I have to write my own Sum variant for TimeSpans?
And to add to the mess, what if Person is an anonymous class, a result from for example a Select or a GroupJoin statement?
Just figured out that I could make the Aggregate method work if I did a Select on the TimeSpan property first... but I find that kind of annoying... Still don't feel I understand this method at all...
foreach(var g in groups)
Console.WriteLine("{0}, {1}:{2}",
g.Key.Country,
g.Key.Age,
g.Select(x => x.TimeSpentOnSO)
g.Aggregate((sum, x) => sum + y));
List<TimeSpan> list = new List<TimeSpan>
{
new TimeSpan(1),
new TimeSpan(2),
new TimeSpan(3)
};
TimeSpan total = list.Aggregate(TimeSpan.Zero, (sum, value) => sum.Add(value));
Debug.Assert(total.Ticks == 6);
g.Aggregate(TimeSpan.Zero, (i, p) => i + p.TimeSpentOnSO)
Basically, the first argument to Aggregate is an initializer, which is used as the first value of "i" in the function passed in the second argument. It'll iterate over the list, and each time, "i" will contain the total so far.
For example:
List<int> nums = new List<int>{1,2,3,4,5};
nums.Aggregate(0, (x,y) => x + y); // sums up the numbers, starting with 0 => 15
nums.Aggregate(0, (x,y) => x * y); // multiplies the numbers, starting with 0 => 0, because anything multiplied by 0 is 0
nums.Aggregate(1, (x,y) => x * y); // multiplies the numbers, starting with 1 => 120
A combination of Chris and Daniels answers solved it for me. I needed to initialize the TimeSpan, and I did things in the wrong order. The solution is:
foreach(var g in groups)
Console.WriteLine("{0}, {1}:{2}",
g.Key.Country,
g.Key.Age,
g.Aggregate(TimeSpan.Zero, (sum, x) => sum + x.TimeSpentOnSO));
Thanks!
And also... D'oh!
You could write TimeSpan Sum method...
public static TimeSpan Sum(this IEnumerable<TimeSpan> times)
{
return TimeSpan.FromTicks(times.Sum(t => t.Ticks));
}
public static TimeSpan Sum<TSource>(this IEnumerable<TSource> source,
Func<TSource, TimeSpan> selector)
{
return TimeSpan.FromTicks(source.Sum(t => selector(t).Ticks));
}
Alternatively, MiscUtil has generic-enabled Sum methods, so Sum should work on a TimeSpan just fine (since there is a TimeSpan+TimeSpan=>TimeSpan operator defined).
Just please don't tell me the number... it would scare me...
You could sum on one of the Total properties of the TimeSpan. For instance, you could get the represented TotalHours of time spent on SO like this:
g.Sum(x => x.SOReputation.TotalHours)
I believe this would give you the result you're looking for, but with the caveat that you'd have to put the units of measure according to what you need (hours, minutes, second, days, etc.)