Tricky combination of two lists using Linq - c#

I hope somebody will be able to guide me in right direction here...
public class SubmissionLog
{
public int PKId {get;set;}
public int SubmissionId {get;set;}
public DateTime Created {get;set;}
public int StatusId {get;set;}
}
And this is the data:
1, 123, '1/24/2013 01:00:00', 1
2, 456, '1/24/2013 01:30:00', 1
3, 123, '1/25/2013 21:00:00', 2
4, 456, '1/25/2013 21:30:00', 2
5, 123, '2/25/2013 22:00:00', 1
6, 123, '2/26/2013 21:00:00', 2
7, 123, '2/16/2013 21:30:00', 1
What I am trying to is following:
I'd like to know the the average time span from StatusId 1 to StatusId 2 on a given day.
So, let's say date is 2/26/2013, then what I thought would make sense if first get the list like this:
var endlingList = (from sl in db.SubmissionLogs
where (DateTime.Now.AddDays(days).Date == sl.Created.Date) // days = passed number of days to make it 2/26/2013
&& (sl.StatusId == 2)
select sl).ToList();
var endingLookup = endlingList.ToLookup(a => a.SubmissionId, a => a.Created); // thought of using lookup because Dictionary doesn't allow duplicates
After that I thought I'd figure out starting points
var startingList = (from sl in db.SubmissionLogs
where endingList.Select(a => a.SubmissionId).ToArray().Contains(sl.QuoteId)
&& sl.StatusId == 1
select sl).ToList();
And then what I did was following:
var revisedList = endingLookup.Select(a =>
new SubmissionInterval {
SubmissionId = a.Key,
EndDateTime = endingLookup[a.Key].FirstOrDefault(), //This is where the problem is. This will only grab the first occurance.
StartDateTime = startLookup[a.Key].FirstOrDefault() //This is where the problem is. This will only grab the first occurance.
});
And then what I do to get average is following (again, this will only include the initial or first ocurances of status 1 and status 2 of some submission id Submission Log):
return revisedList.Count() > 0 ? revisedList.Select(a=> a.EndDateTime.Subtract(a.StartDateTime).TotalHours).Average() : 0;
So, I hope somebody will understand what my problem here is first of all... To re-cap, I want to get timespan between each status 1 and 2. I pass the date in, and then I have to look up 2's as that ensures me that I will find 1's. If I went the other way around and looked for 1's, then 2's may not exist (don't want that anyway).
At the end I wanna be able to average stuff out...
So let's say if some submission first went from 1 to 2 in a time span of 5h (the code that I left, will get me up to this point), then let's say it got reassigned to 1 and then it went back to 2 in a new time span of 6h, I wanna be able to get both and do the average, so (5+6)/2.
Thanks

I think I understand what you're trying to do. Does thishelp
void Main()
{
var list = new List<SubmissionLog>
{
new SubmissionLog(1, 123, "1/24/2013 01:00:00", 1),
new SubmissionLog(2, 456, "1/24/2013 01:30:00", 1),
new SubmissionLog(3, 123, "1/25/2013 21:00:00", 2),
new SubmissionLog(4, 456, "1/25/2013 21:30:00", 2),
new SubmissionLog(5, 123, "2/25/2013 22:00:00", 1),
new SubmissionLog(6, 123, "2/26/2013 21:00:00", 2),
new SubmissionLog(7, 123, "2/16/2013 21:30:00", 1),
};
// split out status 1 and 2
var s1s = list.Where (l => l.StatusId == 1).OrderBy (l => l.Created);
var s2s = list.Where (l => l.StatusId == 2).OrderBy (l => l.Created);
// use a sub-query to get the first s2 after each s1
var q = s1s.Select (s1 => new
{
s1,
s2 = s2s.FirstOrDefault (s2 =>
s1.SubmissionId == s2.SubmissionId &&
s2.Created >= s1.Created
)
}
).Where (s => s.s1.PKId < s.s2.PKId && s.s2 != null);
// extract the info we need
// note that TotalSecond is ok in Linq to Object but you'll
// probably need to use SqlFunctions or equivalent if this is to
// run against a DB.
var q1 = q.Select (x => new
{
Start=x.s1.Created,
End=x.s2.Created,
SubmissionId=x.s1.SubmissionId,
Seconds=(x.s2.Created - x.s1.Created).TotalSeconds
}
);
// group by submissionId and average the time
var q2 = q1.GroupBy (x => x.SubmissionId).Select (x => new {
x.Key,
Count=x.Count (),
Start=x.Min (y => y.Start),
End=x.Max (y => y.End),
Average=x.Average (y => y.Seconds)});
}
public class SubmissionLog
{
public SubmissionLog(int id, int submissionId, string date, int statusId)
{
PKId = id;
SubmissionId = submissionId;
Created = DateTime.Parse(date, CultureInfo.CreateSpecificCulture("en-US"));
StatusId = statusId;
}
public int PKId {get;set;}
public int SubmissionId {get;set;}
public DateTime Created {get;set;}
public int StatusId {get;set;}
}

Related

Need Lambda query with if depending on the values

I would like to Lambda my code but am stuck.
Basically:
If the array object contains say 4 members with their own year specification and id's. The array can however contain many more members with the same and different Id's and year (never same Id and same year though).
Member array:
array[0]: Id 1 Year 2010
array[1]: Id 2 Year 2010
array[2]: Id 1 Year 2008
array[3]: Id 1 Year 2009
First -
I want to delete all array-members with a specific Id for the year 2010 if they also have another year in the array (same id, different year). So in this case I would like to delete the [0] but not the other members.
Secondly -
I want only to keep the next newest year after 2010 in this case for Id 1 the year 2009, meaning I want to delete [2] as well. (the years come as strings which is why I'm converting them into ints for the comparision in the code below)
Below is my code with for loops that work that I need expert Lambda help with to avoid the loops:
var x = Member.Length;
for (int i = 0; i < x; i++)
{
var y = Member[i].id;
for (int j = i; j < x; j++)
{
var z = Member[j].id;
if (i != j)
{
if (y == z)
{
if (Member[i].year == "2010")
{
Member = Member.Where(w => w != Member[i]).ToArray();
i--;
j--;
x--;
break;
}
var tempI = Convert.ToInt32(Member[i].year);
var tempJ = Convert.ToInt32(Member[j].year);
if (tempI > tempJ)
{
Member = Member.Where(w => w != Member[j]).ToArray();
i--;
j--;
x--;
break;
}
}
}
}
}
I agree that the requirement doesn't make a lot of sense but this is how I interpreted
var Member = new[]
{
new { id = 1, year = "2010" },
new { id = 2, year = "2010" } ,
new { id = 1, year = "2008" } ,
new { id = 1, year = "2009" }
};
var results = from item in Member.Select(x => new { x.id, Year = Convert.ToInt32(x.year), item = x })
group item by item.id into sameItems
let order = sameItems.OrderByDescending(x => x.Year)
let first = order.ElementAtOrDefault(0)
let second = order.ElementAtOrDefault(1)
select first.Year == 2010 && second != null ? second.item : first.item;
foreach (var item in results)
{
System.Console.WriteLine($"id:{item.id},year:{item.year}");
}
I tend to avoid using LINQ to change the underlying collection I'm querying.
The code below will select up to two of most recent entries for each member.
var result = new List<MemberClass>();
var groups = Member.OrderBy(m => m.Id).ThenByDescending(m => m.Year).GroupBy(m => m.Id).ToList();
groups.ForEach(c => result.AddRange(c.Take(2)));
Use result instead of the original array.
I don't know if performance is a consideration for you. The code above may become slow as your collection grows.
Your description and requirements are incompatible, but here's one interpretation:
public class Member
{
public int Id { get; set; }
public string Year { get; set; }
}
var items = (new List<Member>() {
new Member() { Id=1, Year="2010" },
new Member() { Id=2, Year="2010" },
new Member() { Id=1, Year="2008" },
new Member() { Id=1, Year="2009" }
}).ToArray();
// Group everythnig by year, then only keep the highest id
var firstFiltered = items
.GroupBy(
x => x.Year,
x => x.Id,
(year, ids) => new Member()
{
Id = ids.Last(),
Year = year
});
var secondFiltered = firstFiltered
// Only keep years before 2010
.Where(x => String.Compare(x.Year, "2010") == -1)
// Then order by Id then Year
.OrderBy(x => x.Id)
.ThenBy(x => x.Year)
// And only keep the last/most recent year
.GroupBy(
x => x.Id,
x => x.Year,
(id, years) => new Member()
{
Id = id,
Year = years.Last()
});

LINQ grouping without changing the order [duplicate]

Let's say I have following data:
Time Status
10:00 On
11:00 Off
12:00 Off
13:00 Off
14:00 Off
15:00 On
16:00 On
How could I group that using Linq into something like
[On, [10:00]], [Off, [11:00, 12:00, 13:00, 14:00]], [On, [15:00, 16:00]]
Create a GroupAdjacent extension, such as the one listed here.
And then it's as simple as:
var groups = myData.GroupAdjacent(data => data.OnOffStatus);
You could also do this with one Linq query using a variable to keep track of the changes, like this.
int key = 0;
var query = data.Select(
(n,i) => i == 0 ?
new { Value = n, Key = key } :
new
{
Value = n,
Key = n.OnOffFlag == data[i - 1].OnOffFlag ? key : ++key
})
.GroupBy(a => a.Key, a => a.Value);
Basically it assigns a key for each item that increments when the current item does not equal the previous item. Of course this assumes that your data is in a List or Array, otherwise you'd have to try a different method
Here is a hardcore LINQ solution by using Enumerable.Zip to compare contiguous elements and generate a contiguous key:
var adj = 0;
var t = data.Zip(data.Skip(1).Concat(new TimeStatus[] { null }),
(x, y) => new { x, key = (x == null || y == null || x.Status == y.Status) ? adj : adj++ }
).GroupBy(i => i.key, (k, g) => g.Select(e => e.x));
It can be done as.
Iterate over collection.
Use TakeWhile<Predicate> condition is text of first element of collection On or Off.
Iterate over the subset of from point one and repeat above step and concatenate string.
Hope it helps..
You could parse the list and assign a contiguous key e.g define a class:
public class TimeStatus
{
public int ContiguousKey { get; set; }
public string Time { get; set; }
public string Status { get; set; }
}
You would assign values to the contiguous key by looping through, maintaining a count and detecting when the status changes from On to Off and so forth which would give you a list like this:
List<TimeStatus> timeStatuses = new List<TimeStatus>
{
new TimeStatus { ContiguousKey = 1, Status = "On", Time = "10:00"},
new TimeStatus { ContiguousKey = 1, Status = "On", Time = "11:00"},
new TimeStatus { ContiguousKey = 2, Status = "Off", Time = "12:00"},
new TimeStatus { ContiguousKey = 2, Status = "Off", Time = "13:00"},
new TimeStatus { ContiguousKey = 2, Status = "Off", Time = "14:00"},
new TimeStatus { ContiguousKey = 3, Status = "On", Time = "15:00"},
new TimeStatus { ContiguousKey = 3, Status = "On", Time = "16:00"}
};
Then using the following query you can extract the Status and grouped Times:
var query = timeStatuses.GroupBy(t => t.ContiguousKey)
.Select(g => new { Status = g.First().Status, Times = g });

Count groups using linq

I have an object called simulation results.
public SimulationResult
{
public Horse Winner {get;set;}
public Horse Second {get;set;}
public Horse Third {get;set;}
public Horse Fourth {get;set;}
}
public Horse
{
public Guid Id{get;set;}
}
So, I have a list of 50000 SimulationResult. How can I determine the top 50 most common results.
I tried using LINQ groupBy but the horseId appears in each object and it doesn't allow multiple occurrences of one value.
EDIT
Sorry, thought it was clear.
So we have 8 horses total. Say horse id is 1-8.
So in simulation result 1 the winner is 1, second is 2, third is 3, fourth is 4.
In simulation result 2 first is 5, second is 6 , third is 7, fourth is 8.
In simulation result 3 first is 1, second is 2, third is 3, fourth is 4.
So result set 1 and result set 3 are equal. So in this sample, winner 1 second 2 third 3 fourth 4 is the most common result.
I tried using LINQ groupBy but the horseId appears in each object and it doesn't allow multiple occurrences of one value.
If you mean using anonymous type as explained in Grouping by Composite Keys, although most of the time we can let the compiler infer the names of us, we can always (and here it's necessary to) specify them explicitly:
var topResults = simulationResults
.GroupBy(r => new
{
WinnerId = r.Winner.Id,
SecondId = r.Second.Id,
ThirdId = r.Third.Id,
FourthId = r.Fourth.Id,
})
.OrderByDescending(g => g.Count())
.Select(g => g.First()) // or new { Result = g.First(), Count = g.Count() } if you need the count
.Take(50)
.ToList();
Simplest answer to your question:
class ExactResult {
public String CombinedId { get; set; }
public int Count { get; set; }
}
resultList.Select(l => {
var combinedId = l.Winner.Id.ToString() + l.Second.Id.ToString() + l.Third.ToString() + l.Fourth.ToString();
return new ExactResult() { CombinedId = combinedId), Count = l.Count(c => c.Winner.Id.ToString() + c.Second.Id.ToString() + c.Third.ToString() + c.Fourth.ToString();)}
}).OrderByDescending(e => e.Count).Take(50)
The answer is meaningless though. If what you're really going for is the most likely 4 winners from a bunch of results, this is not the way to go about it. This will just display the most results with the EXACT same 4 winners.
What you're probably looking for is statistical analysis or maybe spread. Anyway things more complicated than what you're actually asking.
Maybe this is what you're looking for:
var q = (from x in mySimulationResultList
group x by x into g
let count = g.Count()
orderby count descending
select new { Value = g.Key, Count = count }).Take(50);
foreach (var x in q)
{
Console.WriteLine($"Value: {x.Value.ToString()} Count: {x.Count}");
}
If you want meaningful output for the Console.WriteLine you would need to override ToString for Horse and SimulationResult
You must override Equals and GetHashCode for SimulationResult, something like this:
public override bool Equals(object obj)
{
SimulationResult simResult = obj as SimulationResult;
if (simResult != null)
{
if (Winner == simResult.Winner
&& Second == simResult.Second
&& Third == simResult.Third
&& Fourth == simResult.Fourth)
{
return true;
}
}
return false;
}
public override int GetHashCode()
{
int hash = 12;
hash = hash * 5 + Winner.GetHashCode();
hash = hash * 5 + Second.GetHashCode();
hash = hash * 5 + Third.GetHashCode();
hash = hash * 5 + Fourth.GetHashCode();
return hash;
}
Sources here (group by query) and here (comparing objects against eachother)

Getting wrong sum and count in linq

This is my table structure
ID A B C D
1 null 10 5 null
2 3 5 null D2
3 8 null 2 D2
4 null 4 3 D1
5 4 6 1 D2
This is c# class and its property to store query result.
public class GrillTotals
{
public int? SumOfA {get; set;}
public int? SumOfB{get; set;}
public int? SumOfC{get; set;}
public int? CountOfD1{get; set;}
public int? CountOfD2{get; set;}
}
What I expect is:
SumOfA = 15
SumOfB = 20
SumOfC = 11
CountOfD1 = 1
CountOfD2 = 3
What I am getting is :
SumOfA = null,
SumOfB = null,
SumOfC = null,
CountOfD1 = 0,
CountOfD2 = 0
Here is a code what I have tried.
var _FinalResult = from s in dbContext.tblSchedules
group s by new
{
s.A,
s.B,
s.C,
s.D,
} into gt
select new GrillTotals
{
SumOfA = gt.Sum(g => g.A),
SumOfB = gt.Sum(g => g.B),
SumOfC = gt.Sum(g => g.C),
CountOfD1 = gt.Count(g => g.D == "D1"),
CountOfD2 = gt.Count(g => g.D == "D2"),
};
Try to correct me if I am doing something wrong or incorrectly.Any help will be appreciated.
You should not be grouping by the fields you want to calculate aggregates. When you group by them, every aggregate (Sum, Min, Max etc) will return the value itself (and Count 1 or 0 depending of the condition).
From what I see you are trying to return several aggregates with single SQL query. If that's correct, it's possible by using group by constant technique.
Just replace
group s by new
{
s.A,
s.B,
s.C,
s.D,
} into gt
with
group s by 1 // any constant
into gt
Try this:
var _FinalResult = from s in dbContext.tblSchedules
group s by new
{
s.A,
s.B,
s.C,
s.D,
} into gt
select new GrillTotals
{
SumOfA = gt.Sum(g => g.A ?? 0),
SumOfB = gt.Sum(g => g.B ?? 0),
SumOfC = gt.Sum(g => g.C ?? 0),
CountOfD1 = gt.Count(g => g.D == "D1"),
CountOfD2 = gt.Count(g => g.D == "D2"),
};

How to merge two lists based on a property?

I have two lists, one fake and one real, like:
BEFORE
// fake (list 1)
{ ID = 1, Year = 2011, X = "" }
, { ID = 2, Year = 2012, X = "" }
, { ID = 3, Year = 2013, X = "" }
// real (list 2)
{ ID = 35, Year = 2011, X = "Information" }
, { ID = 77, Year = 2013, X = "Important" }
I want to merge them looking for the Year, the result should be:
AFTER
{ ID = 35, Year = 2011, X = "Information" }
, { ID = 2, Year = 2012, X = "" }
, { ID = 77, Year = 2013, X = "Important" }
It must remove elements with the same year on the first list and add the element with the equivalent Year on the second list to the first list, keeping the order.
How can I do it using Linq?
You should be able to do that using a "left join":
from f in fake
join r in real
on f.Year equals r.Year
into joinResult
from r in joinResult.DefaultIfEmpty()
select new
{
ID = r == null ? f.ID : r.ID,
Year = f.Year,
X = r == null ? f.X : r.X
};
Justin's query is the most efficient way to do it, but if you're concerned with keeping identical objects (and not creating new records from the query) you could do it like this:
var combined = from f in fake
let r = (from r1 in real
where r1.Year == f.Year
select r1).SingleOrDefault()
select r ?? f;
Using IEnumerable.Union and IEqualityComparer.
P.S. This would result in a different result when compared to left join if the real list had more elements (years that are not present in fake list). The left join would not return those results which could be a desired result (not clear from OP).
public class MyClass
{
public int ID {get; set;}
public int Year {get; set;}
public string X {get; set;}
}
public class MyClassEqualityComparer : IEqualityComparer<MyClass>
{
public bool Equals(MyClass x, MyClass y)
{
return x.Year == y.Year;
}
public int GetHashCode(MyClass obj)
{
return obj.ToString().ToLower().GetHashCode();
}
}
void Main()
{
var fake = new List<MyClass> {
new MyClass { ID = 1, Year = 2011, X = "" }
, new MyClass { ID = 2, Year = 2012, X = "" }
, new MyClass { ID = 3, Year = 2013, X = "" }
};
var real = new List<MyClass> {
new MyClass { ID = 35, Year = 2011, X = "Information" }
, new MyClass { ID = 77, Year = 2013, X = "Important" }
};
var merged = real.Union(fake, new MyClassEqualityComparer());
}
Instead of defining the fake list yourself, try having Linq do it for you:
Enumerable.Range(2011,3) //2011, 2012, 2013
//use the overload that provides a 0-based ordinal position of each element
.Select(x,i=> new {ID = i+1, Year = x, X = String.Empty)
//now you have your fake list; join with the "real" list based on Year fields,
//taking the real element wherever it exists and the fake one otherwise
.Join(real, l=>l.Year, r=>r.Year, (l,r) => r == null ? l : r);
This will produce exactly the result set you want. You will likely need to define a named type for the list items, though, as two separately-defined anonymous types cannot be implicitly converted even if they have all the same member types/names.

Categories

Resources