I have the following code
TimeSpan[] hours = new[] {
new TimeSpan(10,35,50),
new TimeSpan(10,36,48),
new TimeSpan(10,41,48),
new TimeSpan(10,47,58),
new TimeSpan(10,49,14),
new TimeSpan(11,22,15),
new TimeSpan(11,24,18),
new TimeSpan(11,25,25),
};
I want to group the hours and minutes by 5 minutes.
I want to get the result below
1st Group
**10:35:50
10:36:48
10:41:48**
2nd Group
**10:47:58
10:49:14**
3rd Group
**11:22:15
11:24:18
11:25:25**
I have tried the following code but I cannot get the exact result that I need
var query = (from x in hours select x)
.GroupBy(r=> r.Ticks / TimeSpan.FromMinutes(5).Ticks));
You can try to implement a simple loop instead of Linq:
Code:
private static IEnumerable<TimeSpan[]> MyGrouping(IEnumerable<TimeSpan> source,
double minutes) {
List<TimeSpan> list = new List<TimeSpan>();
foreach (TimeSpan item in source.OrderBy(x => x)) {
// shall we start a new group?
if (list.Any() && (item - list.Last()).TotalMinutes > minutes) {
// if yes, return the previous
yield return list.ToArray();
list.Clear();
}
list.Add(item);
}
if (list.Any())
yield return list.ToArray();
}
Demo:
var result = MyGrouping(hours, 5)
.Select((list, index) => $"Group {index + 1}: [{string.Join(", ", list)}]");
Console.Write(string.Join(Environment.NewLine, result));
Outcome:
Group 1: [10:35:50, 10:36:48, 10:41:48]
Group 2: [10:47:58, 10:49:14]
Group 3: [11:22:15, 11:24:18, 11:25:25]
You could do this by using Aggregate to determine which "group" each timespan would belong to and then GroupBy that group
TimeSpan[] hours = new[] {
new TimeSpan(10,35,50),
new TimeSpan(10,36,48),
new TimeSpan(10,41,48),
new TimeSpan(10,47,58),
new TimeSpan(10,49,14),
new TimeSpan(11,22,15),
new TimeSpan(11,24,18),
new TimeSpan(11,25,25),
};
var query = hours.Aggregate(new List<(int group, TimeSpan ts)>(), (acc,curr) => {
if(acc.Count == 0)
acc.Add((0,curr));
else
{
var (lastGroup,lastTs) = acc.Last();
if(curr.Subtract(lastTs).TotalMinutes <= 5)
acc.Add((lastGroup,curr));
else
acc.Add((lastGroup+1,curr));
}
return acc;
}).GroupBy(x => x.group, y => y.ts);
foreach(var item in query)
Console.WriteLine(String.Join(", ", item));
Output is
10:35:50, 10:36:48, 10:41:48
10:47:58, 10:49:14
11:22:15, 11:24:18, 11:25:25
Live example: https://dotnetfiddle.net/zXhw0g
Good question here on Aggregate explained to help you understand it.
You can write a method to group the items like so:
public static IEnumerable<List<TimeSpan>> GroupItemsWithin(IEnumerable<TimeSpan> times, TimeSpan maxDelta)
{
var previous = TimeSpan.MinValue;
var spans = new List<TimeSpan>();
foreach (var span in times)
{
if (previous == TimeSpan.MinValue || (span - previous) <= maxDelta)
{
spans.Add(span);
}
else if (spans.Count > 0)
{
yield return spans;
spans = new List<TimeSpan>{ span };
}
previous = span;
}
if (spans.Count > 0)
yield return spans;
}
Then you can use it like this:
public static void Main()
{
TimeSpan[] hours = new[] {
new TimeSpan(10, 35, 50),
new TimeSpan(10, 36, 48),
new TimeSpan(10, 41, 48),
new TimeSpan(10, 47, 58),
new TimeSpan(10, 49, 14),
new TimeSpan(11, 22, 15),
new TimeSpan(11, 24, 18),
new TimeSpan(11, 25, 25),
};
foreach (var group in GroupItemsWithin(hours, TimeSpan.FromMinutes(5)))
{
Console.WriteLine(string.Join(", ", group));
}
}
Output:
10:35:50, 10:36:48, 10:41:48
10:47:58, 10:49:14
11:22:15, 11:24:18, 11:25:25
If you want to use Linq. Then this is the simplest solution Demo
TimeSpan[] hours = new[]
{ new TimeSpan(10, 35, 50),
new TimeSpan(10, 36, 48),
new TimeSpan(10, 41, 48),
new TimeSpan(10, 47, 58),
new TimeSpan(10, 49, 14),
new TimeSpan(11, 22, 15),
new TimeSpan(11, 24, 18),
new TimeSpan(11, 25, 25), };
int groupID = -1;
var result = hours.OrderBy(h => h).Select((item, index) =>
{
if (index == 0 || (!(Math.Abs(hours[index - 1].Subtract(item).TotalMinutes) <= 5)))
++groupID;
return new { group = groupID, item = item };
}).GroupBy(item => item.group);
Related
I am stuck in a requirement.
I get two flags from the database, namely lastMonthNumber and lastMonthName, these can range from 1 to 12 and January to December.
Now I have a requirement such that if lastMonthName="March" and lastMonthNumner=12, then the parent list should be as below:
1, April
2, May
3, June
4, July
5, August
6, September
7, October
8, November
9, December
10, January
11, February
12, March
if lastMonthName="April" and lastMonthNumber=6, then the list should be:
7, November
8, December
9, January
10, February
11, March
12, April
This lastMonthNumber can range from 1 to 12 and lastMonthName too can range from Jan to Dec. The parent list needs to be dynamic.
If lastMonthNumber=6 and lastMonthName="April", then the list needs to have 6 elements with April as 12 and backtrack to have total 6 elements.
The parent list can be a dictionary, such as:
var monthsDictionary=new Dictionary<int, string>();
I am trying something as below, but not able to visualize further:
var monthsDictionary = new Dictionary<int, string>();
var numbers = new List<int> { 1,2,3,4,5,6,7,8,9,10,11,12};
var months = new List<string> {"January","February","March","April","May","June","July","August","September","October","November","December" };
foreach (var month in months.Select())
{
if (month == lastMonthName)
{
}
}
Please help. Pointers will be very helpful.
Have a constant with month names
private readonly string[] monthNames = { "Januar" ..., "December" };
and an accessor method that is able to cycle the index:
private string GetMonthName(int index)
{
if (index < 0) return monthNames[monthNames.Length - 1 - (index % monthNames.Length)];
return monthNames[index % monthNames.Length];
}
Then create the list:
int indexOfLastMonthName = Array.IndexOf(monthNames, lastMonthName);
var parentData = Enumerable
// Create as many items as last month number
.Range(0, lastMonthNumber)
// Create the list in reverse order, start on LastMonthName and 12,
// then go back with every item
.Select(x => new
{
Number = 12 - x,
Name = GetMonthName(indexOfLastMonthName - x)
}
// Turn it back
.Reverse();
Then fill it to an appropriate data structure. e.g. like this:
List<Tuple<int, string>> parentList = parentData
.Select(x => Tuple.Create(x.Number, x.Name)))
.ToList();
If you prefer a classic solution (using the GetMonthName method from above):
int indexOfLastMonthName = Array.IndexOf(monthNames, lastMonthName);
List<Tuple<int, string>> parentList = new List<Tuple<int, string>>();
for(int i = 0; i < lastMonthNumber; i++)
{
parentList.Add(
Tuple.Create(
12 - i,
GetMonthName(indexOfLastMonthName - i));
}
parentList.Reverse();
Try this linq query
var months = new List<string> {"January","February","March","April","May","June","July","August","September","October","November","December" };
var lastMonthNumber = ...;
var lastMonthName = ....;
var rows = months.Concat(months)
.Reverse()
.SkipWhile(m => m != lastMonthName)
.Take(lastMonthNumber)
.Reverse()
.Select((m, i) => new { id = i + 12 - lastMonthNumber + 1, m})
.ToArray();
It duplicate a months list, reverse it and skip items until lastMonthName found. After that this code limit result items count to lastMonthNumber and reverse list back
try the below fiddle.
https://dotnetfiddle.net/GYP2Go
private static Dictionary<int, string> GetRequiredResult(int lastMonthNumber, string lastMonthName)
{
var indx = months.IndexOf(lastMonthName);
// here this list will have months in required order that ends with lastMonthName
var revisedMonthList = new List<string>();
revisedMonthList.AddRange(months.Skip(indx + 1).Take(12));
revisedMonthList.AddRange(months.Take(indx + 1));
// get count = lastMonthNumber element from last using index, and then convert them to dictionary.
return revisedMonthList
.Select((mn, index) => new {index, mn})
.Where(c => c.index >= months.Count - lastMonthNumber)
.ToDictionary(c=>c.index + 1, c=>c.mn);
}
var monthsDictionary = new Dictionary<int, string>();
var numbers = new List<int> { 1,2,3,4,5,6,7,8,9,10,11,12};
var months = new List<string> {"January","February","March","April","May","June","July","August","September","October","November","December" };
int flag=0;
int items=12;
var numbersList = new List<int>();
var monthsList = new List<string>();
foreach (var month in months)
{
if(flag==0){
monthsList.Insert(items--,month);
if (month == lastMonthName)
{
flag=1;
}
}
else if(flag==1)
{
monthsList.add(month);
}
}
flag=0;
try following:
var months = Enumerable.Range(1, 12).Select(i => new
{
I = i,
M = System.Globalization.DateTimeFormatInfo.CurrentInfo.GetMonthName(i)
});
//string lastMonthName = "March"; int lastMonthNumber = 12;
//string lastMonthName = "April"; int lastMonthNumber = 6;
var selMonthInt = months.Where(x => x.M == lastMonthName).Select(y => y.I).FirstOrDefault();
int endCount = lastMonthNumber + selMonthInt;
if (endCount >= 12) { endCount = selMonthInt; }
var lst1 = months.Where(x => x.I > endCount).Select(z => z.M);
var lst2 = months.Where(x => x.I <= selMonthInt).Select(z => z.M);
var lst = lst1.Union(lst2).ToArray();
var selMonths = Enumerable.Range(0, lastMonthNumber).Select(i => new { I = (13 - lastMonthNumber + i), M = lst[i] });
Given an array of integers (with +ve and -ve numbers which are unordered),
what is the LINQ statement to find those two numbers whose sum is closest to 0
E.g int[] a = new int[]{2, 56, -11, 15, 12, 10, 43, -59, -13}
In the above set of integers {-11,10} and {12,-13} are the two set of two integers which is closest to 0
I could not get much, except the following basic query of LINQ as i was not sure how to proceed,
var res = from x in a
WHERE //i am stuck in the logic what to write here
select x
If it can be any two values in the set then you can do this...
var set = new int[] { 2, 56, -11, 15, 12, 10, 43, -59, -13 };
var ret = (from a in set
from b in set
orderby Math.Abs(a + b)
select new
{
a,
b
}).First();
Console.WriteLine(ret); // {a: -11, b:12 }
If you want slightly better performance for large sets (and can assume there are negative and positive values mixed) you can do this.
var set = new int[] { 2, 56, -11, 15, 12, 10, 43, -59, -13 };
var ret = (from a in set.Where(x => x >= 0)
from b in set.Where(y => y < 0)
orderby Math.Abs(a + b)
select new
{
a,
b
}).First();
Console.WriteLine(ret); // { a= 12, b= -11}
And since you now want the entire set of matches excluding matching to one's self...
var set = new int[] { 2, 56, -11, 15, 12, 10, 43, -59, -13 };
var ret = from a in set
from b in set
where a != b
let c = new { a, b }
group c by Math.Abs(c.a + c.b);
var minset = ret.First(i => i.Key == ret.Min(j => j.Key))
.Select(s=>s);
Console.WriteLine(minset.Aggregate(new StringBuilder(),
(sb,v)=>sb.Append(v)
.AppendLine()
));
/*
{ a = -11, b = 12 }
{ a = -11, b = 10 }
{ a = 12, b = -11 }
{ a = 12, b = -13 }
{ a = 10, b = -11 }
{ a = -13, b = 12 }
*/
And to dedup....
var set = new[] { 2, 56, -11, 15, 12, 10, 43, -59, -13 };
var ret = from a in set
from b in set
where a != b
let c = new { a, b }
group c by Math.Abs(c.a + c.b);
var minset = ret.First(i => i.Key == ret.Min(j => j.Key))
.Select(s => new { a = Math.Min(s.a, s.b), b = Math.Max(s.a, s.b) })
.Distinct();
Console.WriteLine(minset.Aggregate(new StringBuilder(),
(sb, v) => sb.Append(v)
.AppendLine()));
/*
{ a = -11, b = 12 }
{ a = -11, b = 10 }
{ a = -13, b = 12 }
*/
var data = new [] { 2, 56, -11, 15, 12, 10, 43, -59, -13 };
// TODO : assert data.Length >= 2
var query = from i in Enumerable.Range (0, data.Length - 2)
from j in Enumerable.Range (i + 1, data.Length - 1 - i)
let x = data[i] let y = data[j]
group new { x, y } by Math.Abs (x + y) into g
orderby g.Key select g;
Console.WriteLine (string.Join ("\n", query.First ()));
{ x = -11, y = 12 }
{ x = -11, y = 10 }
{ x = 12, y = -13 }
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 6 years ago.
Improve this question
The following code sorts by object.ToString() and thus what I thought was working, was not.
My question is whether there's a way to accomplish the goal of sorting a list of multi-value rows (of unknown column type).
I only need to support int, double, string, DateTime, and TimeSpan. Sorting is always Ascending.
DataTable table = new DataTable("TableFu")
{ Columns = { "Name", "Age", "Grade", "BirthDay", "HowLong" } };
table.Rows.Add("Abe", 2, 1.3, new DateTime(2016, 1, 13), new TimeSpan(0, 30, 30));
table.Rows.Add("Abe", 1, 2.3, new DateTime(2016, 10, 13), new TimeSpan(1, 30, 30));
table.Rows.Add("Abe", 1, 2.3, new DateTime(2016, 2, 13), new TimeSpan(1, 30, 30));
table.Rows.Add("Abe", 1, 1.3, new DateTime(2016, 1, 13), new TimeSpan(1, 30, 30));
table.Rows.Add("Abe", 10, 1.3, new DateTime(2016, 1, 13), new TimeSpan(2, 30, 30));
table.Rows.Add("Abe", 1, 1.3, new DateTime(2016, 1, 13), new TimeSpan(1, 35, 30));
table.Rows.Add("Betty", 1, 2.3, new DateTime(2016, 1, 13), new TimeSpan(1, 30, 30));
table.Rows.Add("Betty", 1, 2.4, new DateTime(2014, 1, 13), new TimeSpan(1, 30, 30));
table.Rows.Add("Betty", 1, 10.4, new DateTime(2015, 1, 13), new TimeSpan(1, 30, 30));
using (DataView view = new DataView(table)) {
view.Sort = "Name,Age,Grade,BirthDay,HowLong";
DataTable result = view.ToTable(); //sorted by those columns in Ascending order
}
Per #InBetween and #PeterDuniho, here's what I came up with.
class ListComparer : IComparer<List<object>> {
int IComparer<List<object>>.Compare(List<object> x, List<object> y) {
return CompareList(x, y);
}
private int CompareList(List<object> first, List<object> other) {
for (int i = 0; i < first.Count; ++i) {
var firstitem = first[i] as IComparable;
var otheritem = other[i] as IComparable;
if (firstitem == null)
throw new Exception("first item does not implement IComparable");
if (otheritem == null)
throw new Exception("other item does not implement IComparable");
if (firstitem.CompareTo(otheritem) != 0) {
return firstitem.CompareTo(otheritem);
}
}
return 0;
}
}
static void Main(string[] args) {
if (1 as IComparable != null) Console.WriteLine("integer is IComparable");
if (2.3 as IComparable != null) Console.WriteLine("double is IComparable");
if ("a string" as IComparable != null) Console.WriteLine("string is IComparable");
if (new DateTime(2016, 10, 30) as IComparable != null) Console.WriteLine("DateTime is IComparable");
if (new TimeSpan(10, 23, 16) as IComparable != null) Console.WriteLine("TimeSpan is IComparable");
List<List<Object>> list = new List<List<object>>();
list.Add(new List<object>() { 2, 3.5, new DateTime(2016, 10, 10) });
list.Add(new List<object>() { 1, 3.5, new DateTime(2016, 1, 1) });
list.Add(new List<object>() { 10, 3.5, new DateTime(2016, 1, 1) });
list.Add(new List<object>() { 2, 10.5, new DateTime(2016, 1, 1) });
list.Add(new List<object>() { 1, 3.6, new DateTime(2016, 1, 1) });
list.Add(new List<object>() { 10, 3.4, new DateTime(2016, 1, 1) });
list.Add(new List<object>() { 2, 3.5, new DateTime(2016, 3, 10) });
list.Add(new List<object>() { 1, 3.5, new DateTime(2016, 1, 1) });
list.Add(new List<object>() { 10, 3.5, new DateTime(2016, 1, 1) });
Console.WriteLine("\nUnsorted list of objects\n");
foreach (var sublist in list) {
foreach (var item in sublist) {
Console.Write(item + "\t\t");
}
Console.WriteLine();
}
list.Sort(new ListComparer());
Console.WriteLine("\nSorted list of objects\n");
foreach (var sublist in list) {
foreach (var item in sublist) {
Console.Write(item + "\t\t");
}
Console.WriteLine();
}
Console.ReadLine();
}
The ordering you are seeing is not the ordering that you think is happening. The view is not sorting by the actual type of the object, its simply ordering by the string representation of the object, that is, what object.ToString() returns.
An easy way to see this is simply adding in your second example a row with Name 10. The ordering you will get is not numerical 1, 1, ... , 2, 2, ... , 10. What you'll get is 1, 1, ... , 10, 2, ...
So this is probably not what you want.
As a pointer to posible solutions, the way Enumerable.OrderBy works for example is that for any Enumerable<T> it checks if the type implements IComparable<T> or IComparable (in that order) and uses the corresponding CompareTo implementation.
Its interesting to note that in case of an IEnumerable<object> it will only check for the non generic IComparable. The following will not work:
class MyComparable: IComparable<MyComparable> { ... }
var objectList = new List<object>() { myComparable1, myComparable2, ... };
var ordered = objectList.OrderBy(o => o); //Throws, MyComparable does not impement `IComparable`
The reason of this behavior is that there is no easy or reasonably performant way of figuring out if an object with unknown type implements IComparable<himself> and then leveraging that information:
var t = o.GetType();
var genericIComparableOfItself = t.GetInterfaces()
.Where(i => i.IsGenericType &&
i.GetGenericTypeDefinition().IsAssignableFrom(typeof(IComparable<>)) &&
i.GetGenericArguments().First() == t)
.FirstOrDefault();
Func<object, int> compareTo = other => (int)comp.GetMethod("CompareTo", new Type[] { t }).Invoke(o, new object[] { other }); //yuck!
This is one reason why it is always a good idea to implement IComparable when implementing IComparable<T>.
I am trying to work out if the following can be done in a LINQ to Objects statement.
I have a dictionary with the key as a DateTime (keys are values that are on multiple days) and a double value. I have too much data to plot on a graph so would like to the average value of each 5 minutes.
Sample Input
01/01/2012 23:53 5
01/01/2012 23:54 2
01/01/2012 23:55 1
01/01/2012 23:56 3
01/01/2012 23:57 4
01/01/2012 23:58 5
01/01/2012 23:59 6
02/01/2012 00:00 2
02/01/2012 00:01 4
02/01/2012 00:02 5
Expected Output
01/01/2012 23:55 3
02/01/2012 00:00 4.4
Using this helper method:
static DateTime RoundToNearestInterval(DateTime dt, TimeSpan d)
{
int f=0;
double m = (double)(dt.Ticks % d.Ticks) / d.Ticks;
if (m >= 0.5)
f=1;
return new DateTime(((dt.Ticks/ d.Ticks)+f) * d.Ticks);
}
it's as simple as
var result = from kvp in data
let key = RoundToNearestInterval(kvp.Key, TimeSpan.FromMinutes(5))
group kvp by key into g
select new { g.Key, Value = g.Average(x => x.Value) };
or
var result = data.GroupBy(kvp => RoundToNearestInterval(kvp.Key, TimeSpan.FromMinutes(5)), kvp => kvp.Value)
.Select(g => new { g.Key, Value = g.Average() });
LINQPad example:
void Main()
{
var tmp = new Dictionary<string, int>
{
{"01/01/2012 23:53", 5},
{"01/01/2012 23:54", 2},
{"01/01/2012 23:55", 1},
{"01/01/2012 23:56", 3},
{"01/01/2012 23:57", 4},
{"01/01/2012 23:58", 5},
{"01/01/2012 23:59", 6},
{"02/01/2012 00:00", 2},
{"02/01/2012 00:01", 4},
{"02/01/2012 00:02", 5}
};
var data = tmp.ToDictionary(d => DateTime.Parse(d.Key), d=>d.Value);
var result = from kvp in data
let key = RoundToNearestInterval(kvp.Key, TimeSpan.FromMinutes(5))
group kvp by key into g
select new {g.Key, Value = g.Average (x => x.Value) };
result.ToDictionary(r => r.Key, v => v.Value).Dump();
}
Here's a LINQ query that will do what you want, you can test this in LINQPad:
void Main()
{
var points = new[]
{
new { dt = new DateTime(2012, 1, 1, 23, 53, 00), value = 5 },
new { dt = new DateTime(2012, 1, 1, 23, 54, 00), value = 2 },
new { dt = new DateTime(2012, 1, 1, 23, 55, 00), value = 1 },
new { dt = new DateTime(2012, 1, 1, 23, 56, 00), value = 3 },
new { dt = new DateTime(2012, 1, 1, 23, 57, 00), value = 4 },
new { dt = new DateTime(2012, 1, 1, 23, 58, 00), value = 5 },
new { dt = new DateTime(2012, 1, 1, 23, 59, 00), value = 6 },
new { dt = new DateTime(2012, 1, 2, 00, 00, 00), value = 2 },
new { dt = new DateTime(2012, 1, 2, 00, 01, 00), value = 4 },
new { dt = new DateTime(2012, 1, 2, 00, 01, 00), value = 5 }
};
var interval = TimeSpan.FromMinutes(5);
var averageByInterval =
from point in points
let intervalStart = new DateTime(((int)((point.dt.Ticks + interval.Ticks / 2) / interval.Ticks)) * interval.Ticks)
group point.value by intervalStart into g
select new { g.Key, average = g.Average() };
averageByInterval.Dump();
}
Output:
Looks like your dictionary contains the ordered elements so we can do something like this:
var firstDate = yourDict.First().Key;
var output = yourDict.GroupBy(e=> (int)(e.Key - firstDate).TotalMinutes / 5)
.ToDictionary(g => g.First().Key
.AddMinutes(g.Average(e=>(e.Key - g.First().Key).TotalMinutes)),
g => g.Average(e=>e.Value));
NOTE: The input data of the OP uses a different cutlure than en-US, the month goes after the day. That's the noticeable point to take some test. otherwise the test won't be correct.
Try this:
var results =
data
.GroupBy(
x => (x.Key.Ticks / TimeSpan.TicksPerMinute + 2) / 5,
x => x.Value)
.Select(x => new
{
Key = new DateTime(x.Key * TimeSpan.TicksPerMinute * 5),
Value = x.Average()
});
var data = new Dictionary<DateTime, double>();
data.Add(new DateTime(2012, 1, 1, 23, 53, 0), 5);
data.Add(new DateTime(2012, 1, 1, 23, 54, 0), 2);
data.Add(new DateTime(2012, 1, 1, 23, 55, 0), 1);
data.Add(new DateTime(2012, 1, 1, 23, 56, 0), 3);
data.Add(new DateTime(2012, 1, 1, 23, 57, 0), 4);
data.Add(new DateTime(2012, 1, 1, 23, 58, 0), 5);
data.Add(new DateTime(2012, 1, 1, 23, 59, 0), 6);
data.Add(new DateTime(2012, 1, 2, 0, 0, 0), 2);
data.Add(new DateTime(2012, 1, 2, 0, 1, 0), 4);
data.Add(new DateTime(2012, 1, 2, 0, 2, 0), 5);
var result = data.GroupBy(kvp =>
{
var dt = kvp.Key;
var nearest5 = (int)Math.Round(dt.Minute / 5.0) * 5;
//Add the minutes after inital date creation to deal with minutes=60
return new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, 0, 0).AddMinutes(nearest5);
})
.Select(g =>
{
return new KeyValuePair<DateTime, double>(g.Key, g.Average(row => row.Value));
});
foreach (var r in result)
{
Console.WriteLine(r.Key + " " + r.Value);
// 1/01/2012 11:55:00 PM 3
// 2/01/2012 12:00:00 AM 4.4
}
I am looking to extract ranges from an list of integers using linq:
for example I am looking to split the following list:
List<int> numberList = new List<int>() { 30, 60, 90, 120, 150, 180, 270, 300, 330 };
into a list of integer ranges that will look like:
{ 30, 180 }
{ 270, 330 }
ie: where the next seq is greater than 30
another example :
List<int> numberList = new List<int>() { 30, 60, 120, 150, 270, 300, 330 };
into a list of integer ranges that will look like:
{ 30, 60 }
{ 120, 150 }
{ 270, 330 }
I have tried with for loops to find the best way possible however I don't
know where to start trying to use a linq query to do this.
You could write a method to handle the split:
IEnumerable<IList<int>> SplitValues(IList<int> input, int difference = 30)
{
List<int> results = new List<int>();
int last = input.First();
foreach(var value in input)
{
if (value - last > difference)
{
yield return new[] {results.First(), results.Last()};
results = new List<int>();
}
results.Add(value);
last = value;
}
yield return new[] {results.First(), results.Last()};
}
This matches your specifications as described, returning:
{ 30, 60 }
{ 120, 150 }
{ 270, 330 }
Note that a single value within the collection without a range will be duplicated. For example, { 30, 120, 150 } will return:
{ 30, 30 }
{ 120, 150 }
You can do this in one linq statement:
var numberList = new List<int>() { 30, 60, 120, 150, 270, 300, 330 };
var section = 0;
var result = numberList
.Select( (x, i) => new {value = x, section = (i == 0 ? 0 : ((x - numberList[i - 1]) > 30 ? ++section : section))})
.GroupBy(x => x.section)
.Select(x => x.Select(v => v.value).ToList()).ToList();
Well. There are many ways to do so and all have their pros and cons.
So here's yet another solution, hope it will be helpful to someone.
public static IEnumerable<TSource[]> ToRanges<TSource>(
this IEnumerable<TSource> source, Func<TSource, TSource, TSource, bool> isNear)
{
List<TSource[]> result = source./*OrderBy(value => value).*/Aggregate(
new List<TSource[]> { new[] { source.First(), source.First() } },
(ranges, currentValue) => {
TSource[] currentRange = ranges.Last();
TSource previousValue = currentRange[1];
if (isNear(currentRange[0], previousValue, currentValue))
currentRange[1] = currentValue;
else
ranges.Add(new[] { currentValue, currentValue});
return ranges;
}
);
return result;
}
Example usage:
List<int> numbers = new List<int>() { 30, 60, 90, 120, 150, 180, 270, 300, 330 };
// split by max difference
numberList.ToRanges(
(first, previous, current) => current - previous <= 30).ToArray();
// { 30, 180 }
// { 270, 330 }
// split by max range
numberList.ToRanges(
(first, previous, current) => current - first <= 90).ToArray();
// { 30, 120 }
// { 150, 180 }
// { 270, 330 }
In addition, you can split not only integers but also, for example, words by their first letter. Or DateTime/TimeSpan. Or whatever you want.
You must use LINQ? If not, what about:
List<int> numberList = new List<int>() { 30, 60, 120, 150, 270, 300, 330 };
Dictionary<int, int> result = new Dictionary<int, int>();
int lastStart = numberList.First();
for(int i=1; i < numberList.Count; i++)
{
if(numberList[i] >= lastStart + 30)
{
result.Add(lastStart, numberList[i]);
if (i == numberList.Count - 1) break;
lastStart = numberList[i + 1];
i++;
}
}
foreach (var item in result)
{
Console.WriteLine("{{{0}, {1}}}", item.Key, item.Value);
}
You can use TakeWhile and add the result to another list
void SplitByRange()
{
List<int> numberList = new List<int>() { 30, 60, 120, 150, 270, 300, 330 };
IEnumerable<int> aux = new List<int>();
int n = numberList.First();
int skip = 0;
List<List<int>> output = new List<List<int>>();
while ((aux = numberList.Skip(skip).TakeWhile(o => { bool r = (o - n) <= 30; n = o; return r; })).Count() > 0)
{
output.Add(aux.ToList());
skip += aux.Count();
}
}
At the end numberList will be empty and output will be a list of lists.
output[0] // { 30, 60 }
...
The current code will required at least 1 element on the list, and if you have
{ 30, 100 }
It will return as two lists with 1 element in each
{ 30 }
{ 100 }
Try this:
private static List<int[]> GetGroups(List<int> numberList)
{
List<List<int>> groups = new List<List<int>>();
numberList.Zip(numberList.Skip(1), (a, b) =>
{
if ((b - a) == 30)
{
if (groups.Count == 0)
groups.Add(new List<int>());
groups[groups.Count - 1].Add(a);
}
else if (a == b)
{
groups[groups.Count - 1].Add(a);
}
else
{
groups[groups.Count - 1].Add(a);
groups.Add(new List<int>());
}
return a;
}).ToList();
groups[groups.Count - 1].Add(numberList.Last());
return groups.Select(g => new[] { g.First(), g.Last() }).ToList();
}
Sample usage:
//List<int> numberList = new List<int>() { 30, 60, 90, 120, 150, 180, 270, 300, 330 };
List<int> numberList = new List<int>() { 30, 60, 120, 150, 270, 300, 330 };
var result = GetGroups(numberList);