I populate the following List from database, where I like to get the percentage of Good or Bad by only day (ignoring time).
I cannot group the data by data.GroupBy(p => p.DateTime.ToString("YYYY-MM-dd")) or String.Format("YYYY-MM-dd", p.DateTime) or count from it, let alone doing the .Where clause to match a Content to be Good or Bad and count from it.
It is even possible in LINQ?
I can use SQL where I had to format each DateTime, perform multiple Array and Unique conversion and multiple SQL calls to retrieve the count for each variables to calculate a ratio, but that is so dumb.
DateTime Content
-------------------------------------
2018-03-16 17:59:26.000 Good
2018-03-16 18:05:04.000 Bad
2018-03-16 19:23:26.000 Bad
2018-03-17 03:19:02.000 Good
2018-03-17 06:20:32.000 Bad
What I want to get
2018-03-16 Good: 33% Bad: 66%
2018-03-17 Good: 50% Bad: 50%
Basically you want to take advantage of DateTime.Date
DateTime.Date Property
Gets the date component of this instance.
Given
var list = new List<MyClass>()
{
new MyClass()
{
Content = "Good",
MyDateTimeField = DateTime.Now
},
new MyClass()
{
Content = "Good",
MyDateTimeField = DateTime.Now
},
new MyClass()
{
Content = "Bad",
MyDateTimeField = DateTime.Now
}
};
Group By
var results = list.GroupBy(x => x.MyDateTimeField.Date)
.Select(x =>
{
var count = x.Count();
var good = x.Count(y => y.Content == "Good");
var bad = x.Count(y => y.Content == "Bad");
var result = new
{
Date = x.Key,
Good = good / (decimal)count,
Bad = bad / (decimal)count
};
return result;
});
foreach (var item in results)
{
Console.WriteLine(string.Format("Date = {0}, Good = {1:P2}, Bad = {2:P2}", item.Date.Date, item.Good, item.Bad));
}
Output
Date = 3/24/2018 12:00:00 AM, Good = 66.67 %, Bad = 33.33 %
Full Demo Here
Note : i'm using Format Specifiers in C# P2 to format the percent, however you can apply your own rounding and logic to tidy it up
You could try something like
var result = from p in dataContext.Table
group p by p.DateTime.Date into g
select new {
Date = g.Key,
NumberOfGood = g.Count(p => p.Content == "Good"),
NumberOfBad = g.Count(p => p.Content == "Bad"),
PercentageGood = ((decimal)g.Count(p => p.Content == "Good") / ((decimal)g.Count(p => p.Content == "Good") + (decimal)g.Count(p => p.Content == "Bad"))),
PercentageBad = ((decimal)g.Count(p => p.Content == "Bad") / ((decimal)g.Count(p => p.Content == "Good") + (decimal)g.Count(p => p.Content == "Bad")))
};
Related
What I implemented with a for loop is this:
phraseSources2 = new List<PhraseSource2>();
for (int i = 0; i < phraseSources.Count; i++)
{
var ps = phraseSources[i];
if (i != phraseSources.Count - 1)
{
var psNext = phraseSources[i + 1];
if (psNext != null &&
ps.Kanji == psNext.Kanji &&
ps.Kana == psNext.Kana &&
ps.English.Length <= psNext.English.Length)
{
i++;
ps = phraseSources[i];
}
} else
{
ps = phraseSources[i];
}
phraseSources2.Add(new PhraseSource2()
{
Kanji = ps.Kanji,
Kana = ps.Kana,
Furigana = ps.Furigana,
English = ps.English,
});
}
Previously I had been using LINQ
phraseSources2 = (List<Data1.Model.PhraseSource2>)phraseSources
.Select(x => new PhraseSource2()
{
Kanji = x.Kanji,
Kana = x.Kana,
Furigana = x.Furigana,
English = x.English,
}).ToList();
I know LINQ can do a lot but can it look forward at the next row when doing a select?
If I understand your problem correcly I wouldn't "look forward" but use GroupBy instead and group by Kanji and Kana then Select the longest English as the value in the PhraseSource2 object.
Something like this:
var phraseSource2 = phraseSources
.GroupBy(x => new {Kanji = x.Kanji, Kana = x.Kana})
.Select(g => new PhraseSource2 {
Kanji = g.Key.Kanji,
Kana = g.Key.Kana,
Furigana = g.First().Furigana,
English = g.OrderByDescending(x => x.English.Length).First().English
});
If the source collection can be accessed by index than you can use an overload to the select which gives you the current index.
var source = new[] { 'a', 'b', 'c' };
var result = source.Select((x, i) => new { Current = x, Next = source.Length > i+1 ? source[i+1] : ' '});
All you have to do is just set up a variable inside a query where you can easily retrieve next or previous value like this:
phraseSources2 = (List<Data1.Model.PhraseSource2>)phraseSources
.Select((x, y) =>
var NextKanji = (List<Data1.Model.PhraseSource2>)phraseSources.Skip(y + 1).FirstOrDefault().Kanji;
new PhraseSource2()
{
Kanji = NextKanji,
Kana = x.Kana,
Furigana = x.Furigana,
English = x.English,
}).ToList();
If you want to check some conditions before, you can do it like this:
phraseSources2 = (List<Data1.Model.PhraseSource2>)phraseSources
.Where((x, y) =>
var NextEnglish = (List<Data1.Model.PhraseSource2>)phraseSources.Skip(y + 1).FirstOrDefault().English;
x.English.Length < NextEnglish.Length)
.Select(x =>
new PhraseSource2()
{
Kanji = x.Kanji,
Kana = x.Kana,
Furigana = x.Furigana,
English = x.English,
}).ToList();
There is no built-in method, but there are third-party libraries that offer this functionality. The MoreLinq is a respected and free .NET library that offers a WindowLeft extension method, that processes a sequence into a series of subsequences representing a windowed subset of the original. So you could use it to process your phraseSources in pairs, and discard the pairs that have two equal phrases. Finally select the first phrase of the pairs that survived.
using static MoreLinq.Extensions.WindowLeftExtension;
var phraseSources2 = phraseSources
.WindowLeft(size: 2)
.Where(phrases => // phrases is of type IList<PhraseSource2>
{
if (phrases.Count == 2) // All have size 2 except from the last
{
var ps = phrases[0];
var psNext = phrases[1];
return ps.Kanji != psNext.Kanji || ps.Kana != psNext.Kana ||
ps.English.Length > psNext.English.Length;
}
else // The last is a single phrase
{
return true;
}
})
.Select(window => window[0]) // Select the first phrase
.ToList();
I need to do this:
if(...)
{
var query2 = query.Select(i => new { Balance = i.Balance * i.RateValue, Date = i.Date });
}
Of course I may do it, but then I'll have a problem:
var list = query2.ToList();
How I can implement this? Maybe something like this:
if(accountName == "Safe" && currency == "Br")
{
query = query.Where(i => new { Balance = i.Balance * i.RateValue, Date = i.Date });
}
But this doesn't work.
Your question is lacking context. What is the type of i and which properties does it have more? Do you want to filter on account name and currency? Also Where and Select are completely different functions.
Or did you mean something like this?
var query = query.Where(i => i.AccountName == "Safe" && i.Currency == "Br")
.Select(i => new { Balance = i.Balance * i.RateValue, Date = i.Date });
I have a list containing integer or string-integer
like this
TagNo FTerminal
1000 1
1000 5
1000 2S6
how can i get the result like this
TagNo FTerminal
1000 1
5
6
I have this , but definately it gives me error on 2s6.
how can i change it to cover all?
var terminalList = sourceLists.Where(t => t.TagNo == tagList)
.Where(t=>t.FromTerminal.Length>0)
.Select(t => int.Parse(t.FromTerminal))
.OrderBy(t=>t)
.ToList();
Instead of using int.Parse in your LINQ statement, you need to write your own function.
Something like this:
int parseTerminal(string input) {
int result = -1;
if (!int.TryParse(input, out result)) {
result = -99;
}
return result;
}
That would make your LINQ to
var terminalList = sourceLists
.Where( t => t.TagNo == tagList && t.FromTerminal.Length > 0 )
.Select( t => parseTerminak(t.FromTerminal) )
.OrderBy( t=>t )
.ToList();
Result:
TagNo FTerminal
1000 -99
1
5
You need to handle the special case where FromTerminal is not a number yourself.
A naive implementation of the requirement one could think of is something like this:
int parseTerminal(string input) {
int result = -1;
if (!int.TryParse(input, out result)) {
var temporaryString = string.Empty;
var lastInt = -1;
input.ToList().ForEach( aChar => {
if ( aChar >= '0' && aChar <= '9' ) {
temporaryString += aChar;
} else {
if ( temporaryString.Length >= 0 ) {
int.TryParse( temporaryString, out lastInt );
temporaryString = string.Empty;
}
}
} );
if ( temporaryString.Length >= 0 ) {
if (!int.TryParse( temporaryString, out lastInt )) {
lastInt = -98;
}
}
result = lastInt;
}
return result;
}
Note: I would not consider this production ready and you should think about edge cases.
Without knowing much about your data structure I have written a code using some system types.
var tuples = new List<Tuple<int, string>>
{
new Tuple<int, string>(1000, "1"),
new Tuple<int, string>(1000, "5"),
new Tuple<int, string>(1000,"2s6")
};
var enumerable = tuples.GroupBy(t => t.Item1).
Select(g => new Tuple<int, List<int>>(g.Key, g.Select(e => int.Parse(Regex.Match(e.Item2, #"(?<=(\D|^))\d+(?=\D*$)").Value)).ToList()));
The error occurs because you try to handle the string '2S6' as an integer, using int.Parse. This would naturally cause an exception.
I would suggest following another approach, using regular expressions. I think regular expressions are better solution since the data you ask come after string manipulation of the already retrieved query results.
Using regular expressions to do this kind of staff, would make it also easier for you to maintain in the future. Think of the case, that in a week you don't want to retrieve the last digit of the string, but the second digit of the string.
You can use this online regular expression tester to test your regular expression. I suppose the regular expression \d(?!.*\d) would be a good choice, since it returns the last digit.
This article is a good guide in using regular expressions in .NET, including examples.
Hope I helped!
if last symbol always is int you may change your code like this
var terminalList = sourceLists.Where(t => t.TagNo == tagList)
.Where(t=>t.FromTerminal.Length>0)
.Select(t => int.Parse(t.FromTerminal.Last()))
.OrderBy(t=>t)
.ToList();
UPDATE
if last not only one numer that can use regex like this
var terminalList = sourceLists.Where(t => t.TagNo == tagList)
.Where(t=>t.FromTerminal.Length>0)
.Select(t => int.Parse(Regex.Match(t.FromTerminal, #"(\d+)$").Groups[1].Value))
.OrderBy(t=>t)
.ToList();
I am quiet confused what do you wants ... as your picture says you wants the combination of TagNO and FTerminal and in the other hand your query says you wants only FTerminals in certain order ..
Now if you wants the first one then
void Abc(int tagList)
{
var sourceLists = new List<Demo>
{
new Demo { FTerminal = "200", TagNo = 1000 },
new Demo { FTerminal = "300", TagNo = 1000 },
new Demo { FTerminal = "400", TagNo = 1000 }
};
var terminalList = sourceLists
.Where(t => t.TagNo == tagList && t.FTerminal.Length > 0)
.OrderBy(i=>i.FTerminal).GroupBy(i=>i.TagNo);
}
And the second one
void Abc(int tagList)
{
var sourceLists = new List<Demo>
{
new Demo { FTerminal = "200", TagNo = 1000 },
new Demo { FTerminal = "300", TagNo = 1000 },
new Demo { FTerminal = "400", TagNo = 1000 }
};
var terminalList =
from Demo d in sourceLists
where d.TagNo == tagList
let number = int.Parse(d.FTerminal)
orderby number ascending
select number).ToList();
}
But till if you did not get the your desired answer then please knock!!!!
Hmm ... instead of jumping through hoops, just use IsInt ... problem solved ...
:)
var terminalList = sourceLists.Where(t => t.TagNo == tagList)
.Where(t=>t.FromTerminal.Length>0)
.Where(t => t.FromTerminal.IsInt() )
.Select(t => int.Parse(t.FromTerminal))
.OrderBy(t=>t)
.ToList();
(So, just added this condition .Where(t => t.FromTerminal.IsInt() ) to your selection process)
I'm optimizing a method with a number of Linq queries. So far the execution time is around 3 seconds and I'm trying to reduce it. There is quite a lot of operations and calculations happening in the method, but nothing too complex.
I will appreciate any suggections and ideas how the performance can be improved and code optimized.
The whole code of the method(Below I'll point where I have the biggest delay):
public ActionResult DataRead([DataSourceRequest] DataSourceRequest request)
{
CTX.Configuration.AutoDetectChangesEnabled = false;
var repoKomfortaktion = new KomfortaktionRepository();
var komfortaktionen = CTX.Komfortaktionen.ToList();
var result = new List<AqGeplantViewModel>();
var gruppen = new HashSet<Guid?>(komfortaktionen.Select(c => c.KomfortaktionsGruppeId).ToList());
var hochgeladeneKomplettabzuege = CTX.Komplettabzug.Where(c => gruppen.Contains(c.KomfortaktionsGruppeId)).GroupBy(c => new { c.BetriebId, c.KomfortaktionsGruppeId }).Select(x => new { data = x.Key }).ToList();
var teilnehmendeBetriebe = repoKomfortaktion.GetTeilnehmendeBetriebe(CTX, gruppen);
var hochgeladeneSperrlistenPlz = CTX.SperrlistePlz.Where(c => gruppen.Contains(c.KomfortaktionsGruppeId) && c.AktionsKuerzel != null)
.GroupBy(c => new { c.AktionsKuerzel, c.BetriebId, c.KomfortaktionsGruppeId }).Select(x => new { data = x.Key }).ToList();
var hochgeladeneSperrlistenKdnr = CTX.SperrlisteKdnr.Where(c => gruppen.Contains(c.KomfortaktionsGruppeId) && c.AktionsKuerzel != null)
.GroupBy(c => new { c.AktionsKuerzel, c.BetriebId, c.KomfortaktionsGruppeId }).Select(x => new { data = x.Key }).ToList();
var konfigsProAktion = CTX.Order.GroupBy(c => new { c.Vfnr, c.AktionsId }).Select(c => new { count = c.Count(), c.Key.AktionsId, data = c.Key }).ToList();
foreach (var komfortaktion in komfortaktionen)
{
var item = new AqGeplantViewModel();
var zentraleTeilnehmer = teilnehmendeBetriebe.Where(c => c.TeilnahmeStatus.Any(x => x.KomfortaktionId == komfortaktion.Id && x.AktionsTypeId == 1)).ToList();
var lokaleTeilnehmer = teilnehmendeBetriebe.Where(c => c.TeilnahmeStatus.Any(x => x.KomfortaktionId == komfortaktion.Id && x.AktionsTypeId == 2)).ToList();
var hochgeladeneSperrlistenGesamt =
hochgeladeneSperrlistenPlz.Count(c => c.data.AktionsKuerzel == komfortaktion.Kuerzel && c.data.KomfortaktionsGruppeId == komfortaktion.KomfortaktionsGruppeId) +
hochgeladeneSperrlistenKdnr.Count(c => c.data.AktionsKuerzel == komfortaktion.Kuerzel && c.data.KomfortaktionsGruppeId == komfortaktion.KomfortaktionsGruppeId);
item.KomfortaktionId = komfortaktion.KomfortaktionId;
item.KomfortaktionName = komfortaktion.Aktionsname;
item.Start = komfortaktion.KomfortaktionsGruppe.StartAdressQualifizierung.HasValue ? komfortaktion.KomfortaktionsGruppe.StartAdressQualifizierung.Value.ToString("dd.MM.yyyy") : string.Empty;
item.LokalAngemeldet = lokaleTeilnehmer.Count();
item.ZentralAngemeldet = zentraleTeilnehmer.Count();
var anzHochgelandenerKomplettabzuege = hochgeladeneKomplettabzuege.Count(c => zentraleTeilnehmer.Count(x => x.BetriebId == c.data.BetriebId) == 1) +
hochgeladeneKomplettabzuege.Count(c => lokaleTeilnehmer.Count(x => x.BetriebId == c.data.BetriebId) == 1);
item.KomplettabzugOffen = (zentraleTeilnehmer.Count() + lokaleTeilnehmer.Count()) - anzHochgelandenerKomplettabzuege;
item.SperrlisteOffen = (zentraleTeilnehmer.Count() + lokaleTeilnehmer.Count()) - hochgeladeneSperrlistenGesamt;
item.KonfigurationOffen = zentraleTeilnehmer.Count() - konfigsProAktion.Count(c => c.AktionsId == komfortaktion.KomfortaktionId && zentraleTeilnehmer.Any(x => x.Betrieb.Vfnr == c.data.Vfnr));
item.KomfortaktionsGruppeId = komfortaktion.KomfortaktionsGruppeId;
result.Add(item);
}
return Json(result.ToDataSourceResult(request));
}
The first half (before foreach) takes half a second which is okay. The biggest delay is inside foreach statement in the first iteration and in particular in these lines, execution of zentraleTeilnehmer takes 1.5 second for the first time.
var zentraleTeilnehmer = teilnehmendeBetriebe.Where(c => c.TeilnahmeStatus.Any(x => x.KomfortaktionId == komfortaktion.Id && x.AktionsTypeId == 1)).ToList();
var lokaleTeilnehmer = teilnehmendeBetriebe.Where(c => c.TeilnahmeStatus.Any(x => x.KomfortaktionId == komfortaktion.Id && x.AktionsTypeId == 2)).ToList();
TeilnehmendeBetriebe has over 800 lines, where TeilnahmeStatus property has normally around 4 items. So, maximum 800*4 iterations, which is not a huge number afterall...
Thus, I'm mostly interected in optimizing these lines, hoping to reduce execution time to half a second or so.
What I tried:
Rewrite Linq to foreach: didn't help, same time... probably not surprising, but was worth a try.
foreach (var tb in teilnehmendeBetriebe) //836 items
{
foreach (var ts in tb.TeilnahmeStatus) //3377 items
{
if (ts.KomfortaktionId == komfortaktion.Id && ts.AktionsTypeId == 1)
{
testResult.Add(tb);
break;
}
}
}
Selecting particular columns for teilnehmendeBetriebe with .Select(). Didn't help either.
Neither helped other small manipulations I tried.
What is interesting - while the first iteration of foreach can take up to 2 seconds, the second and further take just milisecons, so .net is capable of optimizing or reusing calculation data.
Any advice on what can be changed in order to improve performance is very welcome!
Edit:
TeilnahmeBetriebKomfortaktion.TeilnahmeStatus is loaded eagerly in the method GetTeilnehmendeBetriebe:
public List<TeilnahmeBetriebKomfortaktion> GetTeilnehmendeBetriebe(Connection ctx, HashSet<Guid?> gruppen)
{
return ctx.TeilnahmeBetriebKomfortaktion.Include(
c => c.TeilnahmeStatus).ToList();
}
Edit2:
The query which is sent when executing GetTeilnehmendeBetriebe:
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[BetriebId] AS [BetriebId],
[Extent1].[MandantenId] AS [MandantenId],
[Extent1].[CreatedUser] AS [CreatedUser],
[Extent1].[UpdatedUser] AS [UpdatedUser],
[Extent1].[CreatedDate] AS [CreatedDate],
[Extent1].[UpdatedDate] AS [UpdatedDate],
[Extent1].[IsDeleted] AS [IsDeleted]
FROM [Semas].[TeilnahmeBetriebKomfortaktion] AS [Extent1]
WHERE [Extent1].[IsDeleted] <> cast(1 as bit)
My assumption is that TeilnahmeBetriebKomfortaktion.TeilnahmeStatus is a lazy loaded collection, resulting in the N + 1 problem. You should eagerly fetch that collection to improve your performance.
The following iterations of the foreach loop are fast, because after the first iteration those objects are no longer requested from the database server but are server from memory.
I've got a collection of object which contains data as follows:
FromTime Duration
2010-12-28 24.0000
2010-12-29 24.0000
2010-12-30 24.0000
2010-12-31 22.0000
2011-01-02 1.9167
2011-01-03 24.0000
2011-01-04 24.0000
2011-01-05 24.0000
2011-01-06 24.0000
2011-01-07 22.0000
2011-01-09 1.9167
2011-01-10 24.0000
In the "FromTime" column, there are data "gaps" i.e. 2011-01-01 and 2011-01-08 are "missing". So what I'd like to do is to loop through a range of dates (in this instance 2010-12-28 to 2011-01-10) and "fill in" the "missing" data with a duration of 0.
As I've just started with LINQ, I feel that it should be "fairly" easy but I can't quite get it right. I'm reading the book "LINQ in Action" but feel that I'm still quite a way off before I can resolve this particular issue. So any help would be much appreciated.
David
I'll define class like bellow:
public class DurDate
{
public DateTime date = DateTime.ToDay;
public decimal dure = 0;
}
and will wrote function like bellow:
private IEnumerable<DurDate> GetAllDates(IEnumerable<DurDate> lstDur)
{
var min = lstDur.Min(x => x.date).Date;
var max = lstDur.Max(x => x.date).Date;
var nonexistenceDates = Enumerable.Range(0, (int) max.Subtract(min).TotalDays)
.Where(x =>!lstDur.Any(p => p.date.Date == min.Date.AddDays(x)))
.Select(p => new DurDate {date = min.Date.AddDays(p), dure = 0});
return lstDur.Concat(nonexistenceDates).OrderBy(x=>x.date);
}
Sample test case:
List<DurDate> lstDur = new List<DurDate> { new DurDate { date = DateTime.Today, dure = 10 }, new DurDate { date = DateTime.Today.AddDays(-5), dure = 12 } };
Edit: It works simply, first I'll going to find min and max range:
var min = lstDur.Min(x => x.date).Date;
var max = lstDur.Max(x => x.date).Date;
What are the days not in the given range:
Where(x =>!lstDur.Any(p => p.date.Date == min.Date.AddDays(x)))
After finding this days, I'll going to select them:
Select(p => new DurDate {date = min.Date.AddDays(p), dure = 0})
At last concatenate the initial values to this list (and sort them):
lstDur.Concat(nonexistenceDates).OrderBy(x=>x.date);
Something like that. I didn't tested it, but I believe, that you will got the idea:
var data = new[]
{
new { Date = DateTime.Now.AddDays(-5), Duration = 3.56 },
new { Date = DateTime.Now.AddDays(-3), Duration = 3.436 },
new { Date = DateTime.Now.AddDays(-1), Duration = 1.56 },
};
Func<DateTime, DateTime, IEnumerable<DateTime>> range = (DateTime from, DateTime to) =>
{
List<DateTime> dates = new List<DateTime>();
from = from.Date;
to = to.Date;
while (from <= to)
{
dates.Add(from);
from = from.AddDays(1);
}
return dates;
};
var result = range(data.Min(e => e.Date.Date), data.Max(e => e.Date.Date))
.Join(data, e => e.Date.Date, e => e.Date, (d, x) => new {
Date = d,
Duration = x == null
? 0.0
: x.Duration
});
Also it would be better to replace this range lambda with some static method.