LINQ: Chain ID's from one row to another - c#

I have a table which has sort of a child->child->parent set up inside it. (It's a patchup im using on an existing old database so it's a little dodgy).
The class for the table:
public class Foo
{
int ID {get;set;}
int ParentID {get;set;}
int BaseParentID {get;set;}
}
Lets say i have a few records in there
ID: 10, ParentID: 5, BaseParentID: 5
ID: 05, ParentID: 1, BaseParentID: 5
ID: 01, ParentID: 1, BaseParentID: 0
What I want to do, is get each of the ParentID's until the the baseparentid is 0. So in a way, it's stepping through the table from one record to another and retrieving it into a list of ID's.
The end result should be a list: { 10, 5, 1 }
This is what I'm doing now (there is a limit of 4 at the moment, but i'd prefer it if there was no limit):
var list = new List<int?>();
var id = 10; // The first ID is given when this method is started.
list.Add(id);
int? pid = db.Foo.Where(w => w.ID == id).Single().BaseParentID; // i have this as a compiled query function
if (pid != 0) {
list.Add(pid);
pid = db.Foo.Where(w => w.ID == pid).Single().BaseParentID; // for the sake of this example i'm just using the query here
if (pid != null) {
list.Add(pid);
// And so on
}
}
As you can see, it's a bit of a crappy way to do this. But i'm not sure if there's a way to do this in a fancy linq query.
ps. The point of this is sort of a pseudo folder structure.

This is a good example of where you would write a separate iterator function:
IEnumerable<Foo> TraverseParents(Foo foo, IEnumerable<Foo> all)
{
while(foo != null)
{
yield return foo;
foo = (foo.pid == 0) ? null : all.FirstOrDefault(f => f.ID == foo.pid);
}
}
// In the calling code
var id = 10;
Foo root = db.Foo.FirstOrDefault(f => f.ID == id);
List<int> list = TraverseParents(root, db.Foo)
.Select(f => f.ID)
.ToList();

You can use the following method:
List<int> GetParentHierarchy(int startingId)
{
List<int> hierarchy = new List<int> { startingId };
using(Connection db = new Connection()) //change to your context
{
int parentId = startingId;
while(true)
{
var foo = db.Foo(x => x.Id == parentId).SingleOrDefault();
if(foo == null)
break;
parentId = foo.ParentId;
hierarchy.Add(foo.Id);
if(foo.BaseParentID == 0)
break;
}
}
return hierarchy;
}

Related

How to avoid using IF-Else and use inline if condition inside the .where() function?

q = q.Where(s =>
!matchingRecords.Contains(s.Id)
|| (s.SecId != null)
);
but the matchingrecords could be null or having 0 items in it since it's a list. So, in that case it would fail in the above code. I want to check this contains only if
the matching records is not null and have some elements else not.
One way is to put IF-Else block and repeat the code but I want to do it inline, how ?
So, if input conditions are:
matchingRecords is not null;
matchingRecords not empty (contains elements, .Count > 0);
no if-else usage allowed;
could it be done through ternary?
var list = matchingRecords?.Count > 0 ?
q.Where(s => !matchingRecords.Contains(s.Id) && s.SecId != null).ToList()
: new List<Record>();
matchingRecords? checks for null and .Count after checks for "not empty". If-else replaced with ternary, which would filter collection using Where or return new List<Record> on else case.
Sample:
class Program
{
private static List<int> matchingRecords; // It is null, we "forget" to initialize it
static void Main(string[] args)
{
var list = new List<Record>()
{
new Record { Id = 0, SecId ="Some SeqId" },
new Record { Id = 1, SecId = null },
new Record { Id = 2, SecId = "Another SeqId" },
};
var filteredRecords = FilterRecords(list);
}
static IEnumerable<Record> FilterRecords(IEnumerable<Record> q)
{
return matchingRecords?.Count > 0 ? // Checking for not null and not empty (if case)
q.Where(s => !matchingRecords.Contains(s.Id) && s.SecId != null)
: q; // else case
}
}
public class Record
{
public int Id { get; set; }
public string SecId { get; set; }
}
Not sure that properly reproduced your situation, so correct me if something is wrong.
q = q.Where(s => (matchingRecords != null && matchingRecords.Count > 0 &&
!matchingRecords.Contains(s.Id))
|| (s.SecId != null)
);
The condition matchingRecords != null && matchingRecords.Count > 0 will ensure !matchingRecords.Contains(s.Id) is executed only if matchingRecords has at least 1 record

How to find parent and the parent of the parent for a given child without recursion?

I have a very large in memory List, I'm looking for the most efficient algorithm to take a list of Items, and finding all of parents when child is provided.
List<Data> elements = new List<Data>
{
new Data {Id = 1, ParentId = null },
new Data {Id = 2, ParentId = 1},
new Data {Id = 3, ParentId = 2},
new Data {Id = 4, ParentId = 3}
};
var parents =
elements
.Where(x => x.ParentId != null)
.ToDictionary(x => x.Id, x => x.ParentId.Value);
IEnumerable<int> GetParents(int i) =>
parents.ContainsKey(i)
? new[] { parents[i] }.Concat(GetParents(parents[i]))
: Enumerable.Empty<int>();
var result = GetParents(3); //1,2
This works fine, but Its not efficient way.
How can the code be rewritten so that no recursive calls to Execute are made?
An non recursive solution is pretty straightforward:
var currentId = i;
while (parents.TryGetValue(currentId, out var parentId))
{
yield return parentId;
currentId = parentId;
}
Am I missing something?
As you know any recursive approach is not a good choice where you are dealing with a big amount of data. Because of recursive calls using heap stack and after a while, you will get StackOverFlowException. So just as you asked I provided a simple-designed non-recursive implementation for your question.
** In this sample, I'm getting deep into the hierarchy just for 7000 levels down because the recursive approach raises StackOverFlowException for more than that.
** The non-recursive approach including the first node which has null value as ParendId.
** The non-recursive execution time is much better than the recursive one.
public class Data
{
public int Id { get; set; }
public int? ParentId { get; set; }
}
static List<Data> elements = new List<Data>();
static void Main(string[] args)
{
//To fill up the list with huge number if items
elements.Add(new Data() { Id = 1, ParentId = null });
Enumerable.Range(2, 1000000).ToList().ForEach(x => elements.Add(new Data { Id = x, ParentId = x - 1 }));
//Making dictionary as you did it
var parents =elements.ToDictionary(x => x.Id, x => x.ParentId);
/*Non-Recursive Approach*/
IEnumerable<int?> GetNonRecursiveParents(int i)
{
List<int?> parentsList = new List<int?>();
if (parents.ContainsKey(i))
{
var parentNode = parents[i];
do
{
parentsList.Add(parentNode);
parentNode = parents[parentNode.Value];
}
while (parentNode != null);
}
return parentsList;
};
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
var r = GetNonRecursiveParents(7000);
stopwatch.Stop();
var elapsed1 = stopwatch.Elapsed;// Execution time: 00:00:00.0023625
//Making dictionary as you did it
parents = elements.Where(x => x.ParentId != null).ToDictionary(x => x.Id, x => x.ParentId);
/*Recursive Approach*/
IEnumerable<int?> GetParents(int i) =>
parents.ContainsKey(i)
? new[] { parents[i] }.Concat(GetParents(parents[i].Value))
: Enumerable.Empty<int?>();
stopwatch.Restart();
var result = GetParents(7000);
stopwatch.Stop();
var elapsed2= stopwatch.Elapsed;// Execution time: 00:00:00.0040636
}

LINQ OrderBy - Custom

I have some data like
ID Sequence customIndex
1 1 0
2 2 0
3 3 2
4 4 1
5 5 0
I need to use sequence in order by when customIndex is zero other wise use customIndex.
So result should be ID in order of 1,2,4,3,5.
I need LINQ implementation using Lambda. I tried some solution but could not implement.
Posting duplicate and deleting previous one, because of wrong formatting the meaning of question got changed and I received bunch of negative votes.
Added code at dotnet fiddle:
https://stable.dotnetfiddle.net/fChl40
The answer is based on assumption, that CustomIndex is greater or equals to zero:
var result =
data.OrderBy(x => x.CustomIndex==0 ? x.Sequence :
data.Where(y => y.CustomIndex==0 && y.Sequence < x.Sequence)
.Max(y => (int?)y.Sequence))
.ThenBy(x => x.CustomIndex);
This is working for provided test data:
l.OrderBy(a => a.customIndex != 0 ?
list.Where(b => b.Sequence < a.Sequence && b.customIndex == 0)
.OrderByDescending(c => c.Sequence)
.FirstOrDefault()
.Sequence : a.Sequence)
.ThenBy(c=>c.customIndex )
.ToList();
The idea is to order non zero values by first preceding zero valued rows, and then by non zero values itself.
This is something I wanted:
public static void Main()
{
List<Data> data = new List<Data>();
data.Add(new Data{ Id=1, Sequence=1, CustomIndex=0});
data.Add(new Data{ Id=5, Sequence=5, CustomIndex=0});
data.Add(new Data{ Id=6, Sequence=6, CustomIndex=2});
data.Add(new Data{ Id=2, Sequence=2, CustomIndex=0});
data.Add(new Data{ Id=3, Sequence=3, CustomIndex=2});
data.Add(new Data{ Id=4, Sequence=4, CustomIndex=1});
data.Add(new Data{ Id=7, Sequence=7, CustomIndex=1});
int o = 0;
var result = data
.OrderBy(x=>x.Sequence).ToList()
.OrderBy((x)=> myCustomSort(x, ref o) )
;
result.ToList().ForEach(x=> Console.WriteLine(x.Id));
}
public static float myCustomSort(Data x, ref int o){
if(x.CustomIndex==0){
o = x.Sequence;
return x.Sequence ;
}
else
return float.Parse(o + "."+ x.CustomIndex);
}
Sample code: https://stable.dotnetfiddle.net/fChl40
I will refine it further
Based on your question and reply to my comment, I understand you need to clusterize the items' collection, then consider Sequence and CustomIndex on all items of each cluster.
Once clustered (split into blocks depending on a specific criterion) you can merge them back into a unique collection, but while doing that you can manipulate each cluster independently the way you need.
public static class extCluster
{
public static IEnumerable<KeyValuePair<bool, T[]>> Clusterize<T>(this IEnumerable<T> self, Func<T, bool> clusterizer)
{
// Prepare temporary data
var bLastCluster = false;
var cluster = new List<T>();
// loop all items
foreach (var item in self)
{
// Compute cluster kind
var bItemCluster = clusterizer(item);
// If last cluster kind is different from current
if (bItemCluster != bLastCluster)
{
// If previous cluster was not empty, return its items
if (cluster.Count > 0)
yield return new KeyValuePair<bool, T[]>(bLastCluster, cluster.ToArray());
// Store new cluster kind and reset items
bLastCluster = bItemCluster;
cluster.Clear();
}
// Add current item to cluster
cluster.Add(item);
}
// If previous cluster was not empty, return its items
if (cluster.Count > 0)
yield return new KeyValuePair<bool, T[]>(bLastCluster, cluster.ToArray());
}
}
// sample
static class Program
{
public class Item
{
public Item(int id, int sequence, int _customIndex)
{
ID = id; Sequence = sequence; customIndex = _customIndex;
}
public int ID, Sequence, customIndex;
}
[STAThread]
static void Main(string[] args)
{
var aItems = new[]
{
new Item(1, 1, 0),
new Item(2, 2, 0),
new Item(3, 3, 2),
new Item(4, 4, 1),
new Item(5, 5, 0)
};
// Split items into clusters
var aClusters = aItems.Clusterize(item => item.customIndex != 0);
// Explode clusters and sort their items
var result = aClusters
.SelectMany(cluster => cluster.Key
? cluster.Value.OrderBy(item => item.customIndex)
: cluster.Value.OrderBy(item => item.Sequence));
}
}
It ain't pretty, but it exemplifies what you were asking for, I think:
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
public static void Main()
{
List<Data> data = new List<Data>();
data.Add(new Data { Id = 1, Sequence = 1, CustomIndex = 0 });
data.Add(new Data { Id = 2, Sequence = 2, CustomIndex = 0 });
data.Add(new Data { Id = 3, Sequence = 3, CustomIndex = 2 });
data.Add(new Data { Id = 4, Sequence = 4, CustomIndex = 1 });
data.Add(new Data { Id = 5, Sequence = 5, CustomIndex = 0 });
//List of items where the sequence is what counts
var itemsToPlaceBySequence = data.Where(x => x.CustomIndex == 0).OrderBy(x => x.Sequence).ToList();
//List of items where the custom index counts
var itemsToPlaceByCustomIndex = data.Where(x => x.CustomIndex != 0).OrderBy(x => x.CustomIndex).ToList();
//Array to hold items
var dataSlots = new Data[data.Count];
//Place items by sequence
foreach(var dataBySequence in itemsToPlaceBySequence) {
dataSlots[dataBySequence.Sequence - 1] = dataBySequence ;
}
//Find empty data slots and place remaining items in CustomIndex order
foreach (var dataByCustom in itemsToPlaceByCustomIndex) {
var index = dataSlots.ToList().IndexOf(null);
dataSlots[index] = dataByCustom ;
}
var result = dataSlots.ToList();
result.ForEach(x => Console.WriteLine(x.Id));
var discard = Console.ReadKey();
}
public class Data
{
public int Id { get; set; }
public int Sequence { get; set; }
public int CustomIndex { get; set; }
}
}
The ordering you want to do (order partly on CustomIndex and partly on Sequence) doesn't work like that. But this should be close to what you want. Order first by CustomIndex, and then by Sequence.
var result = data.OrderBy(x => x.CustomIndex).ThenBy(x => x.Sequence);

Tricky combination of two lists using Linq

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;}
}

how to get an ordered list with default values using linq

I have an ICollection of records (userID,itemID,rating) and an IEnumerable items
for a specific userID and each itemID from a set of itemIDs, i need to produce a list of the users rating for the items or 0 if no such record exists. the list should be ordered by the items.
example:
records = [(1,1,2),(1,2,3),(2,3,1)]
items = [3,1]
userID = 1
result = [0,2]
my attempt:
dataset.Where((x) => (x.userID == uID) & items.Contains(x.iID)).Select((x) => x.rating);
it does the job but it doesn't return 0 as default value and it isnt ordered...
i'm new to C# and LINQ, a pointer in the correct direction will be very appreciated.
Thank you.
This does the job:
var records = new int[][] { new int[] { 1, 1, 2 }, new int[] { 1, 2, 3 }, new int[] { 2, 3, 1 } };
var items = new int[] { 3, 1 };
var userId = 1;
var result = items.Select(i =>
{
// When there's a match
if (records.Any(r => r[0] == userId && r[1] == i))
{
// Return all numbers
return records.Where(r => r[0] == userId && r[1] == i).Select(r => r[2]);
}
else
{
// Just return 0
return new int[] { 0 };
}
}).SelectMany(r => r); // flatten the int[][] to int[]
// output
result.ToList().ForEach(i => Console.Write("{0} ", i));
Console.ReadKey(true);
How about:
dataset.Where((x) => (x.userID == uID)).Select((x) => items.Contains(x.iID) ? x.rating : 0)
This does the job. But whether it's maintainable/readable solution is topic for another discussion:
// using your example as pseudo-code input
var records = [(1,1,2),(1,2,3),(2,3,1)];
var items = [3,1];
var userID = 1;
var output = items
.OrderByDescending(i => i)
.GroupJoin(records,
i => i,
r => r.ItemId,
(i, r) => new { ItemId = i, Records = r})
.Select(g => g.Records.FirstOrDefault(r => r.UserId == userId))
.Select(r => r == null ? 0 : r.Rating);
How this query works...
ordering is obvious
the ugly GroupJoin - it joins every element from items with all records that share same ItemId into annonymous type {ItemId, Records}
now we select first record for each entry that matches userId - if none is found, null will be returned (thanks to FirstOrDefault)
last thing we do is check whether we have value (we select Rating) or not - 0
How about this. your question sounds bit like an outer join from SQL, and you can do this with a GroupJoin, SelectMany:
var record1 = new Record() { userID = 1, itemID = 1, rating = 2 };
var record2 = new Record() { userID = 1, itemID = 2, rating = 3 };
var record3 = new Record() { userID = 2, itemID = 3, rating = 1 };
var records = new List<Record> { record1, record2, record3 };
int userID = 1;
var items = new List<int> { 3, 1 };
var results = items
.GroupJoin( records.Where(r => r.userID == userID), item => item, record => record.itemID, (item, record) => new { item, ratings = record.Select(r => r.rating) } )
.OrderBy( itemRating => itemRating.item)
.SelectMany( itemRating => itemRating.ratings.DefaultIfEmpty(), (itemRating, rating) => rating);
To explain what is going on
For each item GroupJoin gets the list of rating (or empty list if no rating) for the specified user
OrderBy is obvious
SelectMany flattens the ratings lists, providing a zero if the ratings list is empty (by DefaultIfEmpty)
Hope this makes sense.
Be aware, if there is more than one rating for an item by a user, they will all appear in the final list.

Categories

Resources