Getting grouped date range using linq - c#

I have this data that I have to group by price , and check the range and continouity of data
date price
2014-01-01 10
2014-01-02 10
2014-01-03 10
2014-01-05 20
2014-01-07 30
2014-01-08 40
2014-01-09 50
2014-01-10 30
and the output should look like this
2014-01-01 2014-01-03 10
2014-01-05 2014-01-05 20
2014-01-07 2014-01-07 30
2014-01-08 2014-01-08 40
2014-01-09 2014-01-09 50
2014-01-10 2014-01-10 30
I tried so far
var result = list
.OrderBy(a => a.Date)
.GroupBy(a => a.Price)
.Select(x => new
{
DateMax = x.Max(a => a.Date),
DateMin = x.Min(a => a.Date),
Count = x.Count()
})
.ToList()
.Where(a => a.DateMax.Subtract(a.DateMin).Days == a.Count)
.ToList();
I am not really sure this takes care of continuous dates. All dates are unique!

So first of we'll use a helper method to group consecutive items. It'll take a function that will be given the "previous" and "current" item, and it will then determine if that item should be in the current group, or should start a new one.
public static IEnumerable<IEnumerable<T>> GroupWhile<T>(
this IEnumerable<T> source, Func<T, T, bool> predicate)
{
using (var iterator = source.GetEnumerator())
{
if (!iterator.MoveNext())
yield break;
List<T> list = new List<T>() { iterator.Current };
T previous = iterator.Current;
while (iterator.MoveNext())
{
if (predicate(previous, iterator.Current))
{
list.Add(iterator.Current);
}
else
{
yield return list;
list = new List<T>() { iterator.Current };
}
previous = iterator.Current;
}
yield return list;
}
}
Now we're able to use that method to group the items and then select out the information that we need:
var query = data.OrderBy(item => item.Date)
.GroupWhile((previous, current) =>
previous.Date.AddDays(1) == current.Date
&& previous.Price == current.Price)
.Select(group => new
{
DateMin = group.First().Date,
DateMax = group.Last().Date,
Count = group.Count(),
Price = group.First().Price,
});

As an alternative to Servy's answer, which I find more elegant and obviously much more resuable,
You could do something more bespoke in one sweep (after ordering.)
public class ContiguousValuePeriod<TValue>
{
private readonly DateTime start;
private readonly DateTime end;
private readonly TValue value;
public ContiguousValuePeriod(
DateTime start,
DateTime end,
TValue value)
{
this.start = start;
this.end = end;
this.value = value;
}
public DateTime Start { get { return this.start; } }
public DateTime End { get { return this.start; } }
public TValue Value { get { return this.value; } }
}
public IEnumerable<ContiguousValuePeriod<TValue>>
GetContiguousValuePeriods<TValue, TItem>(
this IEnumerable<TItem> source,
Func<TItem, DateTime> dateSelector,
Func<TItem, TValue> valueSelector)
{
using (var iterator = source
.OrderBy(t => valueSelector(t))
.ThenBy(t => dateSelector(t))
.GetEnumerator())
{
if (!iterator.MoveNext())
{
yield break;
}
var periodValue = valueSelector(iterator.Current);
var periodStart = dateSelector(iterator.Current);
var periodLast = periodStart;
var hasTail = false;
while (iterator.MoveNext())
{
var thisValue = valueSelector(iterator.Current);
var thisDate = dateSelector(iterator.Current);
if (!thisValue.Equals(periodValue) ||
thisDate.Subtract(periodLast).TotalDays > 1.0)
{
// Period change
yield return new ContiguousValuePeriod(
periodStart,
periodLast,
periodValue);
periodStart = thisDate;
periodValue = thisValue;
hasTail = false;
}
else
{
hasTail = true;
}
periodLast = thisDate;
}
}
if (hasTail)
{
yield return new ContiguousValuePeriod(
periodStart,
periodLast,
periodValue);
}
}
which you use like,
var result = yourList.GetContiguousValuePeriods(
a => a.Date,
a => a.Price);

Related

Calculate Percent Change From IGrouping Count in Linq/C#

I would like to have a "percent change for 'Investigations' and 'Breaches' by each quarter. I'm currently grouping by quarter and getting counts but I cannot figure out how to add percent change.
This is what I want to have a IEnumerable/List of:
public class StatusCountDto
{
public string Quarter { get; set; }
public int Investigations { get; set; }
public double InvestigationsChange { get; set; }
public int Breaches { get; set; }
public double BreachesChange { get; set; }
}
Currently I am grouping by the Quarter and getting counts but I cannot figure out how to get the percent change of Investigation counts and Breaches counts from the previous quarter.
The data is already sorted by Quarter. If the previous value doesn't exit (first index) then it should be 0.
This is what I have so far.
Metrics.GroupBy(m => m.Quarter )
.Select((g, index) => new StatusCountDto
{
Quarter = g.Key,
Investigations = g.Count(),
Breaches = g.Where(a => a.Breach == "Yes").Count()
})
.ToList();
Is there a way to use the index to calculate the percent change?
Using an extension method based on the APL scan operator, which is like Aggregate but returns the intermediate results, you can run through the data and refer back to previous counts.
// TRes combineFn(TRes PrevResult, T CurItem)
// First PrevResult is TRes seedFn(T FirstItem)
// FirstItem = items.First()
// First CurItem = items.Skip(1).First()
// output is seedFn(items.First()), combineFn(PrevResult, CurItem), ...
public static IEnumerable<TRes> Scan<T, TRes>(this IEnumerable<T> items, Func<T, TRes> seedFn, Func<TRes, T, TRes> combineFn) {
using (var itemsEnum = items.GetEnumerator()) {
if (itemsEnum.MoveNext()) {
var prev = seedFn(itemsEnum.Current);
for (; ; ) {
yield return prev;
if (!itemsEnum.MoveNext())
yield break;
prev = combineFn(prev, itemsEnum.Current);
}
}
}
}
Given this variation of Scan that uses a lambda to seed the result stream, you can use it to compute the whole stream:
var ans = Metrics
.GroupBy(m => m.Quarter)
.Select(g => new {
Quarter = g.Key,
Investigations = g.Count(),
Breaches = g.Count(a => a.Breach == "Yes")
})
.Scan(f => new StatusCountDto { // first result
Quarter = f.Quarter,
Investigations = f.Investigations,
Breaches = f.Breaches
},
(prev, cur) => new StatusCountDto { // subsequent results
Quarter = cur.Quarter,
Investigations = cur.Investigations,
InvestigationsChange = 100.0 * (cur.Investigations - prev.Investigations) / prev.Investigations,
Breaches = cur.Breaches,
BreachesChange = 100.0 * (cur.Breaches - prev.Breaches) / prev.Breaches
}
)
.ToList();

Returning Default Value from List<> when match is not found

I am trying to find a cleaner way of returning a default value when there is no match found. An example I have written to best demonstrate my question is shown below from LinqPad
So basically if given Age is not found in the list SingleOrDefault returns a null as normal. So instead of returning a null I pick the highest Threshold from the regardless of what the Age value is.
However instead of doing if or using ?? (null coalescing operator) is there a cleaner way of achieving this? Perhaps setting a default value inside the get and set of Age property inside the test class?
void Main()
{
var list = new List<test>()
{
new test ( 55, 27 ),
new test ( 56, 28),
new test ( 57, 29),
new test ( 59, 30),
new test ( 60, 31) //60+
};
var res = list.SingleOrDefault(x => x.Age == 61);
if (res == null)
{
list.Max(l => l.Threshold).Dump();
}
else
{
res.Threshold.Dump();
}
}
class test
{
public int Age
{
get;
set;
}
public int Threshold
{
get;
set;
}
public test(int age, int threshold)
{
Age = age;
Threshold = threshold;
}
}
You could use DefaultIfEmpty() of LINQ:
var res = list.Where(x => x.Age == 61)
.Select(t => t)
.DefaultIfEmpty(list.First(x => x.Threshold == list.Max(t => t.Threshold)))
.SingleOrDefault();
I guess you would like to have a LINQ method SingleOrMax, that you could use like this:
var res = list.SingleOrMax(x => x.Age == 61, x => x.Threshold);
The first expression is the predicate for SingleOrDefault, and the second expression selects the key that will be used for finding the max element, if needed.
Here it is:
public static TSource SingleOrMax<TSource, TMaxKey>(this IEnumerable<TSource> source,
Func<TSource, bool> predicate, Func<TSource, TMaxKey> maxKeySelector)
{
var result = source.SingleOrDefault(predicate);
if (result != default) return result;
var maxKeyComparer = Comparer<TMaxKey>.Default;
TSource max = default;
TMaxKey maxKey = default;
int count = 0;
foreach (var item in source)
{
var key = maxKeySelector(item);
if (count == 0 || maxKeyComparer.Compare(key, maxKey) > 0)
{
max = item;
maxKey = key;
}
count++;
}
// If you remove the line bellow, then rename this method to SingleOrMaxOrDefault
if (count == 0) throw new InvalidOperationException("Sequence contains no elements");
return max;
}
You could always go with an extension method, although it seems a bit overkill.
public static Test SingleAgeOrMaxThreshold(this IEnumerable<Test> items, int age)
{
Test max = null;
foreach (Test t in items)
{
if (t.Age == age)
return t;
if (max == null || t.Threshold > max.Threshold)
max = t;
}
return max;
}

How to use IEnumerable.GroupBy comparing multiple properties between elements?

How do I group "adjacent" Sites:
Given data:
List<Site> sites = new List<Site> {
new Site { RouteId="A", StartMilepost=0.00m, EndMilepost=1.00m },
new Site { RouteId="A", StartMilepost=1.00m, EndMilepost=2.00m },
new Site { RouteId="A", StartMilepost=5.00m, EndMilepost=7.00m },
new Site { RouteId="B", StartMilepost=3.00m, EndMilepost=5.00m },
new Site { RouteId="B", StartMilepost=11.00m, EndMilepost=13.00m },
new Site { RouteId="B", StartMilepost=13.00m, EndMilepost=14.00m },
};
I want result:
[
[
Site { RouteId="A", StartMilepost=0.00m, EndMilepost=1.00m },
Site { RouteId="A", StartMilepost=1.00m, EndMilepost=2.00m }
],
[
Site { RouteId="A", StartMilepost=5.00m, EndMilepost=7.00m }
],
[
Site { RouteId="B", StartMilepost=3.00m, EndMilepost=5.00m }
],
[
Site { RouteId="B", StartMilepost=11.00m, EndMilepost=13.00m },
Site { RouteId="B", StartMilepost=13.00m, EndMilepost=14.00m }
]
]
I tried using GroupBy with a custom comparer function checking routeIds match and first site's end milepost is equal to the next sites start milepost. My HashKey function just checks out routeId so all sites within a route will get binned together but I think the comparer makes an assumption like if A = B, and B = C, then A = C, so C won't get grouped with A,B,C since in my adjacency case, A will not equal C.
First, let Site class be (for debugging / demonstration)
public class Site {
public Site() { }
public string RouteId;
public Decimal StartMilepost;
public Decimal EndMilepost;
public override string ToString() => $"{RouteId} {StartMilepost}..{EndMilepost}";
}
Well, as you can see we have to break the rules: equality must be transitive, i.e. whenever
A equals B
B equals C
then
A equals C
It's not the case in your example. However, if we sort the sites by StartMilepost we, technically, can implement IEqualityComparer<Site> like this:
public class MySiteEqualityComparer : IEqualityComparer<Site> {
public bool Equals(Site x, Site y) {
if (ReferenceEquals(x, y))
return true;
else if (null == x || null == y)
return false;
else if (x.RouteId != y.RouteId)
return false;
else if (x.StartMilepost <= y.StartMilepost && x.EndMilepost >= y.StartMilepost)
return true;
else if (y.StartMilepost <= x.StartMilepost && y.EndMilepost >= x.StartMilepost)
return true;
return false;
}
public int GetHashCode(Site obj) {
return obj == null
? 0
: obj.RouteId == null
? 0
: obj.RouteId.GetHashCode();
}
}
then GroupBy as usual; please, note that OrderBy is required, since order of comparision matters here. Suppose we have
A = {RouteId="X", StartMilepost=0.00m, EndMilepost=1.00m}
B = {RouteId="X", StartMilepost=1.00m, EndMilepost=2.00m}
C = {RouteId="X", StartMilepost=2.00m, EndMilepost=3.00m}
Here A == B, B == C (so in case of A, B, C all items will be in the same group) but A != C (and thus in A, C, B will end up with 3 groups)
Code:
List<Site> sites = new List<Site> {
new Site { RouteId="A", StartMilepost=0.00m, EndMilepost=1.00m },
new Site { RouteId="A", StartMilepost=1.00m, EndMilepost=2.00m },
new Site { RouteId="A", StartMilepost=5.00m, EndMilepost=7.00m },
new Site { RouteId="B", StartMilepost=3.00m, EndMilepost=5.00m },
new Site { RouteId="B", StartMilepost=11.00m, EndMilepost=13.00m },
new Site { RouteId="B", StartMilepost=13.00m, EndMilepost=14.00m },
};
var result = sites
.GroupBy(item => item.RouteId)
.Select(group => group
// Required Here, since MySiteEqualityComparer breaks the rules
.OrderBy(item => item.StartMilepost)
.GroupBy(item => item, new MySiteEqualityComparer())
.ToArray())
.ToArray();
// Let's have a look
var report = string.Join(Environment.NewLine, result
.Select(group => string.Join(Environment.NewLine,
group.Select(g => string.Join("; ", g)))));
Console.Write(report);
Outcome:
A 0.00..1.00; A 1.00..2.00
A 5.00..7.00
B 3.00..5.00
B 11.00..13.00; B 13.00..14.00
Here are a couple of implementations where order of Site's does not matter. You can use the LINQ Aggregate function:
return sites.GroupBy(x => x.RouteId)
.SelectMany(x =>
{
var groupedSites = new List<List<Site>>();
var aggs = x.Aggregate(new List<Site>(), (contiguous, next) =>
{
if (contiguous.Count == 0 || contiguous.Any(y => y.EndMilepost == next.StartMilepost))
{
contiguous.Add(next);
}
else if (groupedSites.Any(y => y.Any(z => z.EndMilepost == next.StartMilepost)))
{
var groupMatchIndex = groupedSites.FindIndex(y => y.Any(z => z.EndMilepost == next.StartMilepost));
var el = groupedSites.ElementAt(groupMatchIndex);
el.Add(next);
groupedSites[groupMatchIndex] = el;
}
else
{
groupedSites.Add(contiguous);
contiguous = new List<Site>();
contiguous.Add(next);
}
return contiguous;
}, final => { groupedSites.Add(final); return final; });
return groupedSites;
});
Alternatively, just with foreach:
return sites.GroupBy(x => x.RouteId)
.SelectMany(x =>
{
var groupedSites = new List<List<Site>>();
var aggList = new List<Site>();
foreach (var item in x)
{
if (aggList.Count == 0 || aggList.Any(y => y.EndMilepost == item.StartMilepost))
{
aggList.Add(item);
continue;
}
var groupMatchIndex = groupedSites.FindIndex(y => y.Any(z => z.EndMilepost == item.StartMilepost));
if (groupMatchIndex > -1)
{
var el = groupedSites.ElementAt(groupMatchIndex);
el.Add(item);
groupedSites[groupMatchIndex] = el;
continue;
}
groupedSites.Add(aggList);
aggList = new List<Site>();
aggList.Add(item);
}
groupedSites.Add(aggList);
return groupedSites;
});
I was surprised that GroupBy doesn't have overload with Func<..., bool> for in-place grouping without hassle of implementing custom class.
So I've created one:
public static IEnumerable<IEnumerable<T>> GroupBy<T>(this IEnumerable<T> source, Func<T, T, bool> func)
{
var items = new List<T>();
foreach (var item in source)
{
if (items.Count != 0)
if (!func(items[0], item))
{
yield return items;
items = new List<T>();
}
items.Add(item);
}
if (items.Count != 0)
yield return items;
}
The usage:
var result = sites.GroupBy((x, y) => x.RouteId == y.RouteId &&
x.StartMilepost <= y.EndMilepost && x.EndMilepost >= y.StartMilepost).ToList();
This should produce wanted result.
Few words about implementation. In above extension method you have to supply delegate which should return true if x and y should be grouped. The method is dumb and will simply compare adjacent items in same order as they come. Your input is ordered, but you may have to use OrderBy/ThenBy before using it with something else.
Here is an extension method for grouping lists of the specific class (Site). It is implemented with an inner iterator function GetGroup that produces one group with adjacent sites. This function is called in a while loop to produce all groups.
public static IEnumerable<IEnumerable<Site>> GroupAdjacent(
this IEnumerable<Site> source)
{
var ordered = source
.OrderBy(item => item.RouteId)
.ThenBy(item => item.StartMilepost);
IEnumerator<Site> enumerator;
bool finished = false;
Site current = null;
using (enumerator = ordered.GetEnumerator())
{
while (!finished)
{
yield return GetGroup();
}
}
IEnumerable<Site> GetGroup()
{
if (current != null) yield return current;
while (enumerator.MoveNext())
{
var previous = current;
current = enumerator.Current;
if (previous != null)
{
if (current.RouteId != previous.RouteId) yield break;
if (current.StartMilepost != previous.EndMilepost) yield break;
}
yield return current;
}
finished = true;
}
}
Usage example:
var allGroups = sites.GroupAdjacent();
foreach (var group in allGroups)
{
foreach (var item in group)
{
Console.WriteLine(item);
}
Console.WriteLine();
}
Output:
A 0,00..1,00
A 1,00..2,00
A 5,00..7,00
B 3,00..5,00
B 11,00..13,00
B 13,00..14,00

How to select last value from each run of similar items?

I have a list. I'd like to take the last value from each run of similar elements.
What do I mean? Let me give a simple example. Given the list of words
['golf', 'hip', 'hop', 'hotel', 'grass', 'world', 'wee']
And the similarity function 'starting with the same letter', the function would return the shorter list
['golf', 'hotel', 'grass', 'wee']
Why? The original list has a 1-run of G words, a 3-run of H words, a 1-run of G words, and a 2-run of W words. The function returns the last word from each run.
How can I do this?
Hypothetical C# syntax (in reality I'm working with customer objects but I wanted to share something you could run and test yourself)
> var words = new List<string>{"golf", "hip", "hop", "hotel", "grass", "world", "wee"};
> words.LastDistinct(x => x[0])
["golf", "hotel", "grass", "wee"]
Edit: I tried .GroupBy(x => x[0]).Select(g => g.Last()) but that gives ['grass',
'hotel', 'wee'] which is not what I want. Read the example carefully.
Edit. Another example.
['apples', 'armies', 'black', 'beer', 'bastion', 'cat', 'cart', 'able', 'art', 'bark']
Here there are 5 runs (a run of A's, a run of B's, a run of C's, a new run of A's, a new run of B's). The last word from each run would be:
['armies', 'bastion', 'cart', 'art', 'bark']
The important thing to understand is that each run is independent. Don't mix-up the run of A's at the start with the run of A's near the end.
There's nothing too complicated with just doing it the old-fashioned way:
Func<string, object> groupingFunction = s => s.Substring(0, 1);
IEnumerable<string> input = new List<string>() {"golf", "hip", "..." };
var output = new List<string>();
if (!input.Any())
{
return output;
}
var lastItem = input.First();
var lastKey = groupingFunction(lastItem);
foreach (var currentItem in input.Skip(1))
{
var currentKey = groupingFunction(str);
if (!currentKey.Equals(lastKey))
{
output.Add(lastItem);
}
lastKey = currentKey;
lastItem = currentItem;
}
output.Add(lastItem);
You could also turn this into a generic extension method as Tim Schmelter has done; I have already taken a couple steps to generalize the code on purpose (using object as the key type and IEnumerable<T> as the input type).
You could use this extension that can group by adjacent/consecutive elements:
public static IEnumerable<IGrouping<TKey, TSource>> GroupAdjacent<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector)
{
TKey last = default(TKey);
bool haveLast = false;
List<TSource> list = new List<TSource>();
foreach (TSource s in source)
{
TKey k = keySelector(s);
if (haveLast)
{
if (!k.Equals(last))
{
yield return new GroupOfAdjacent<TSource, TKey>(list, last);
list = new List<TSource>();
list.Add(s);
last = k;
}
else
{
list.Add(s);
last = k;
}
}
else
{
list.Add(s);
last = k;
haveLast = true;
}
}
if (haveLast)
yield return new GroupOfAdjacent<TSource, TKey>(list, last);
}
public class GroupOfAdjacent<TSource, TKey> : IEnumerable<TSource>, IGrouping<TKey, TSource>
{
public TKey Key { get; set; }
private List<TSource> GroupList { get; set; }
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return ((System.Collections.Generic.IEnumerable<TSource>)this).GetEnumerator();
}
System.Collections.Generic.IEnumerator<TSource> System.Collections.Generic.IEnumerable<TSource>.GetEnumerator()
{
foreach (var s in GroupList)
yield return s;
}
public GroupOfAdjacent(List<TSource> source, TKey key)
{
GroupList = source;
Key = key;
}
}
Then it's easy:
var words = new List<string>{"golf", "hip", "hop", "hotel", "grass", "world", "wee"};
IEnumerable<string> lastWordOfConsecutiveFirstCharGroups = words
.GroupAdjacent(str => str[0])
.Select(g => g.Last());
Output:
string.Join(",", lastWordOfConsecutiveFirstCharGroups); // golf,hotel,grass,wee
Your other sample:
words=new List<string>{"apples", "armies", "black", "beer", "bastion", "cat", "cart", "able", "art", "bark"};
lastWordOfConsecutiveFirstCharGroups = words
.GroupAdjacent(str => str[0])
.Select(g => g.Last());
Output:
string.Join(",", lastWordOfConsecutiveFirstCharGroups); // armies,bastion,cart,art,bark
Demonstration
Try this algoritm
var words = new List<string> { "golf", "hip", "hop", "hotel", "grass", "world", "wee" };
var newList = new List<string>();
int i = 0;
while (i < words.Count - 1 && i <= words.Count)
{
if (words[i][0] != words[i+1][0])
{
newList.Add(words[i]);
i++;
}
else
{
var j = i;
while ( j < words.Count - 1 && words[j][0] == words[j + 1][0])
{
j++;
}
newList.Add(words[j]);
i = j+1;
}
}
You can use following extension method to split your sequence into groups (i.e. sub-sequnces) by some condition:
public static IEnumerable<IEnumerable<T>> Split<T, TKey>(
this IEnumerable<T> source, Func<T, TKey> keySelector)
{
var group = new List<T>();
using (var iterator = source.GetEnumerator())
{
if (!iterator.MoveNext())
yield break;
else
{
TKey currentKey = keySelector(iterator.Current);
var keyComparer = Comparer<TKey>.Default;
group.Add(iterator.Current);
while (iterator.MoveNext())
{
var key = keySelector(iterator.Current);
if (keyComparer.Compare(currentKey, key) != 0)
{
yield return group;
currentKey = key;
group = new List<T>();
}
group.Add(iterator.Current);
}
}
}
if (group.Any())
yield return group;
}
And getting your expected results looks like:
string[] words = { "golf", "hip", "hop", "hotel", "grass", "world", "wee" };
var result = words.Split(w => w[0])
.Select(g => g.Last());
Result:
golf
hotel
grass
wee
Because your input is a List<>, so I think this should work for you with an acceptable performance and especially it's very concise:
var result = words.Where((x, i) => i == words.Count - 1 ||
words[i][0] != words[i + 1][0]);
You can append ToList() on the result to get a List<string> if you want.
I went with
/// <summary>
/// Given a list, return the last value from each run of similar items.
/// </summary>
public static IEnumerable<T> WithoutDuplicates<T>(this IEnumerable<T> source, Func<T, T, bool> similar)
{
Contract.Requires(source != null);
Contract.Requires(similar != null);
Contract.Ensures(Contract.Result<IEnumerable<T>>().Count() <= source.Count(), "Result should be at most as long as original list");
T last = default(T);
bool first = true;
foreach (var item in source)
{
if (!first && !similar(item, last))
yield return last;
last = item;
first = false;
}
if (!first)
yield return last;
}

Using LinqToSql without load base in memory

I have one problem. If i'm using LinqToSql, my program load my database in memory.
little example:
//pageNumber = 1; pageSize = 100;
var result =
(
from a in db.Stats.AsEnumerable()
where (DictionaryFilter(a, sourceDictionary) && DateFilter(a, beginTime, endTime) && ErrorFilter(a, WarnLevel))
select a
);
var size = result.Count(); // size = 1007
var resultList = result.Skip((pageNumber-1)*pageSize).Take(pageSize).ToList();
return resultList;
DictionaryFilter, DateFilter and ErrorFilter are functions that filter my datebase.
after this my program use ~250Mb of Ram.
if i dont use:
var size = result.Count();
My program use ~120MB Ram.
Before use this code, my program use ~35MB Ram.
How can I use count and take functions not loading all my datebase in memory?
static bool DateFilter(Stat table, DateTime begin, DateTime end)
{
if ((table.RecordTime >= begin.ToFileTime()) && (table.RecordTime <= end.ToFileTime()))
{
return true;
}
return false;
}
static bool ErrorFilter(Stat table, bool[] WarnLevel)
{
if (WarnLevel[table.WarnLevel]) return true;
else return false;
}
static bool DictionaryFilter(Stat table, Dictionary<GetSourcesNameResult, bool> sourceDictionary)
{
foreach (var word in sourceDictionary)
{
if (table.SourceName == word.Key.SourceName)
{
return word.Value;
}
}
//
return false;
}
Simple: don't use .AsEnumerable(). That means "switch to LINQ-to-Objects". Before that, db.Stats was IQueryable<T>, which is a composable API, and would do what you expect.
That, however, means that you can't use C# methods like DictionaryFilter and DateFilter, and must instead compose things in terms of the Expression API. If you can illustrate what they do I can probably advise further.
With your edit, the filtering can be tweaked, for example:
static IQueryable<Stat> ErrorFilter(IQueryable<Stat> source, bool[] WarnLevel) {
// extract the enabled indices (match to values)
int[] levels = WarnLevel.Select((val, index) => new { val, index })
.Where(pair => pair.val)
.Select(pair => pair.index).ToArray();
switch(levels.Length)
{
case 0:
return source.Where(x => false);
case 1:
int level = levels[0];
return source.Where(x => x.WarnLevel == level);
case 2:
int level0 = levels[0], level1 = levels[1];
return source.Where(
x => x.WarnLevel == level0 || x.WarnLevel == level1);
default:
return source.Where(x => levels.Contains(x.WarnLevel));
}
}
the date filter is simpler:
static IQueryable<Stat> DateFilter(IQueryable<Stat> source,
DateTime begin, DateTime end)
{
var from = begin.ToFileTime(), to = end.ToFileTime();
return source.Where(table => table.RecordTime >= from
&& table.RecordTime <= to);
}
and the dictionary is a bit like the levels:
static IQueryable<Stat> DictionaryFilter(IQueryable<Stat> source,
Dictionary<GetSourcesNameResult, bool> sourceDictionary)
{
var words = (from word in sourceDictionary
where word.Value
select word.Key.SourceName).ToArray();
switch (words.Length)
{
case 0:
return source.Where(x => false);
case 1:
string word = words[0];
return source.Where(x => x.SourceName == word);
case 2:
string word0 = words[0], word1 = words[1];
return source.Where(
x => x.SourceName == word0 || x.SourceName == word1);
default:
return source.Where(x => words.Contains(x.SourceName));
}
}
and:
IQueryable<Stat> result = db.Stats;
result = ErrorFilter(result, WarnLevel);
result = DateFiter(result, beginTime, endTime);
result = DictionaryFilter(result, sourceDictionary);
// etc - note we're *composing* a filter here
var size = result.Count(); // size = 1007
var resultList = result.Skip((pageNumber-1)*pageSize).Take(pageSize).ToList();
return resultList;
The point is we're now using IQueryable<T> and Expression exclusively.
The following SO question might explain things: Understanding .AsEnumerable in Linq To Sql
.AsEnumerable() loads the entire table.

Categories