Create the hierarchical structure parent-child starting from string definition - c#

I have a list of object defined in the following way
public class MyObject
{
public int Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
}
where the Id is the PK, and code contains a string made of numbers which defines the parent-child relationship between the objects:
id | Code
1 10 15
2 10 15 10
3 10 15 20
4 10 15 30
5 10 15 30 10
6 10 15 30 20
7 10 20
8 10 20 30
9 10 20 30 40
In the above example, object 1 is a root, 2,3,4 are children of 1; 5 and 6 are children of 4; 7 is a root; 8 is a child of 7 and 9 is a child of 8.
I'm trying to write an efficient algorithm that can fill in an additional field in the object, called ParentId, which defines the relation parent-children, to have something like this:
id | Code | ParentId
1 10 15 NULL
2 10 15 10 1
3 10 15 20 1
4 10 15 30 1
5 10 15 30 10 4
6 10 15 30 20 4
7 10 20 NULL
8 10 20 30 7
9 10 20 30 40 8
The list could contain up to 10000 objects, so it's quite large.
I'd like to avoid parsing the Code field as it could slow down a lot the loop.
Is there an alternative way?

public class MyObject
{
public int Id { get; set; }
public string Code { get; set; }
}
public class Result
{
public int Id { get; set; }
public string Code { get; set; }
public int? ParentId { get; set; }
}
var source = new List<MyObject> {
new MyObject { Id = 1, Code = "10 15" },
new MyObject { Id = 2, Code = "10 15 10" },
new MyObject { Id = 3, Code = "10 15 20" },
new MyObject { Id = 4, Code = "10 15 30" },
new MyObject { Id = 5, Code = "10 15 30 10" },
new MyObject { Id = 6, Code = "10 15 30 20" },
new MyObject { Id = 7, Code = "10 20" },
new MyObject { Id = 8, Code = "10 20 30" },
new MyObject { Id = 9, Code = "10 20 30 40" }
};
var result = new List<Result>(source.Count);
result.Add(new Result { Id = source[0].Id, Code = source[0].Code, ParentId = null });
for (int i = 1; i < source.Count; i++)
{
var cur = source[i];
if (cur.Code.Length < source[i - 1].Code.Length)
{
result.Add(new Result { Id = cur.Id, Code = cur.Code, ParentId = null });
continue;
}
for (int j = i - 1; j >= 0; j--)
{
if (cur.Code.StartsWith(source[j].Code))
{
result.Add(new Result { Id = cur.Id, Code = cur.Code, ParentId = source[j].Id });
break;
}
}
}
foreach (var x in result)
Console.WriteLine($"{x.Id} {x.Code,-12} {x.ParentId?.ToString() ?? "NULL"}");

Related

Return Result Set That Populates Class With Multiple Collections In Dapper

I am looking to return a result set that populates a class with multiple collections in Dapper (if possible). There are plenty of good examples with a single collection and a splitOn, but I would like to see if it is possible to populate more than one collection.
3 simplified classes:
class QCLogData
{
public int QCLogId { get; set; }
public string Stage { get; set; }
public bool PassQCTesting { get; set; }
public ICollection<QCLogPerson> Testers { get; set; }
public ICollection<QCInstrument> Instruments { get; set; }
}
class QCLogPerson
{
public int PersonId { get; set; }
}
class QCInstrument
{
public string InstrumentNumb { get; set; }
}
QCLogId Stage PassQCTesting TesterUserId InstrumentNumb
------- ----- ------------- ------------ --------------
1920 1 0 null null
1925 4 1 2 null
1926 2 0 null 77
1927 6 0 4 null
1927 6 0 5 null
1931 1 1 2 88
1931 1 1 2 94
1931 1 1 6 88
1931 1 1 6 94
2206 5 0 20 80
2206 5 0 20 88
2206 5 0 20 93
2206 5 0 25 80
2206 5 0 25 88
2206 5 0 25 93
Data filled:
QCLogId: 1920, Stage: 1, PassQCTesting: 0
Testers [] ; Instruments []
QCLogId: 1925, Stage: 4, PassQCTesting: 1
Testers [2] ; Instruments []
QCLogId: 1926, Stage: 2, PassQCTesting: 0
Testers [] ; Instruments [77]
QCLogId: 1927, Stage: 6, PassQCTesting: 0
Testers [4, 5] ; Instruments []
QCLogId: 1931, Stage: 1, PassQCTesting: 1
Testers [2, 6] ; Instruments [88, 94]
QCLogId: 2206, Stage: 5, PassQCTesting: 0
Testers [20, 25] ; Instruments [80, 88, 93]
Tried in Dapper, clearly not the correct approach for multiple collections.
using (var con = new SqlConnection(connectionString))
{
var sql = #"select QCL.QCLogId, Stage, PassQCTesting, TesterUserId as PersonId, InstrumentNumb
from dbo.QCLog QCL
left join dbo.QCLogTester QCT on QCL.QCLogId = QCT.QCLogId
left join dbo.QCLogInstrument QCINSTR on QCL.QCLogId = QCINSTR.QCLogId;";
var LogItemWithTesters = con.Query<QCLogData, QCLogPerson, QCInstrument, QCLogData>(sql, (qcData, qcTester, qcInstrument) =>
{
var tester = new QCLogPerson { PersonId = qcTester.PersonId };
qcData.Testers.Add(tester);
var instrument = new QCInstrument { InstrumentNumb = qcInstrument.InstrumentNumb };
qcData.Instruments.Add(instrument);
return qcData;
}, splitOn: "PersonId, InstrumentNumb");
var result = LogItemWithTesters.GroupBy(p => p.QCLogId).Select(g =>
{
var groupedQCLog = g.First();
groupedQCLog.Testers = g.Select(p => p.Testers.Single()).ToList();
return groupedQCLog;
});
return result.ToList();
}
Any help would be greatly appreciated.

C# Linq GroupBy - With Specific criteria

I have the below table which I want to group ,
Id NameId ValueId
1 1 4
1 10 18
1 9 15
2 1 4
2 10 17
2 9 0
3 1 5
3 9 16
3 10 18
4 1 5
4 10 18
4 9 16
5 1 4
5 10 17
5 9 0
The result should be grouped with Id having similar ValueId for all the corresponding NameId
Output
GroupId Id ValueId
fed1afcc-a778-48ef-9ee5-4b70886ce67c 1 4,18,15
a31055df-2e4e-472e-9301-e0e0a4e99f1e 2,5 4,17,0
8b9b3dca-4ce0-4cae-a870-1d1026bd608a 3,4 5,18,16
I actually don't need them concatenated, it is just a representation of how the output should be grouped.
I have done a working implementation using a nested for loop, which compare each entity to the subsequent entity and does a check for all the values. Am I over doing something which can be simply achieved in Linq?
Or how can this be done in Linq ?
A minimal version of my current code
var tableResult = _repository.GetData().OrderBy(x =>x.NameId).ToList();
var ids = tableResult.Select(x => x.id).Distinct().ToList();
var listOfProductGroup = new List<ProductGroup>();
for (int i = 0; i < ids.Count; i++)
{
var currentId = ids[i];
var currentEntity = tableResult.Where(x => x.id == currentId).ToList();
var productGroup = new ProductGroup(Guid.NewGuid(), currentProductId);
for (int j = i + 1; j < ids.Count; j++)
{
var subsequentId = ids[j];
var subsequentEntity = tableResult.Where(x => x.id == subsequentId ).ToList();
//This is my extension method which does the comparison
if(currentEntity.EqualsAll(subsequentEntity))
{
productGroup.Id.Add(subsequentId );
//am removing the product to avoid extra loop
ids.RemoveAt(j);
//adjust the index to match the removed product
j = j - 1;
}
}
}
listOfProductGroup.Add(productGroup);
public class ProductGroup
{
public ProductGroup(Guid groupId, int id)
{
GroupId= groupId;
Id= new List<int>()
{
id
};
}
public Guid GroupId{ get; set; }
public IList<int> Id { get; set; }
}
Something like this, perhaps:
class DictionaryComparer : IEqualityComparer<Dictionary<int, int>>
{
public bool Equals(Dictionary<int, int> x, Dictionary<int, int> y)
{
return x.Count == y.Count && !x.Except(y).Any();
}
public int GetHashCode(Dictionary<int, int> obj)
{
int hash = 0;
foreach (var kvp in obj.OrderBy(x => x.Key))
{
hash = hash ^ EqualityComparer<KeyValuePair<int, int>>.Default.GetHashCode(kvp);
}
return hash;
}
}
class Thing
{
public int Id { get; set; }
public int NameId { get; set; }
public int ValueId { get; set; }
public Thing(int id, int nameId, int valueId)
{
Id = id;
NameId = nameId;
ValueId = valueId;
}
}
class Program
{
static void Main(string[] args)
{
var data = new Thing[]
{
new Thing(1, 1, 4),
new Thing(1, 10, 18),
new Thing(1, 9, 15),
new Thing(2, 1, 4),
new Thing(2, 10, 17),
new Thing(2, 9, 0),
new Thing(3, 1, 5),
new Thing(3, 9, 16),
new Thing(3, 10, 18),
new Thing(4, 1, 5),
new Thing(4, 10, 18),
new Thing(4, 9, 16),
new Thing(5, 1, 4),
new Thing(5, 10, 17),
new Thing(5, 9, 0),
};
var #as = data.GroupBy(x => x.Id)
.Select(x => new {Id = x.Key, Data = x.ToDictionary(t => t.NameId, t => t.ValueId)})
.GroupBy(x => x.Data, x => x.Id, new DictionaryComparer());
foreach (var a in #as)
{
Console.WriteLine("{0} {1}", string.Join(",", a), string.Join(",", a.Key.Values));
}
Console.ReadKey();
}
}

C# LINQ - Ranking Multiple Criteria

For example, I have the following list of sales personnel, and their scores for two Key Performance Indicators (KPI):
SalesmanID KPI1 KPI2
Alice 20 4
Betty 50 6
Cindy 40 8
Doris 70 2
Emily 30 3
First, we rank the sales personnel based on KPI1 in descending order as follows.
SalesmanID KPI1 KPI1_Rank
Doris 70 1
Betty 50 2
Cindy 40 3
Emily 30 4
Alice 20 5
Next, we rank the sales personnel based on KPI2 in descending order as follows.
SalesmanID KPI2 KPI2_Rank
Cindy 8 1
Betty 6 2
Alice 4 3
Emily 3 4
Doris 2 5
Finally, we put them together to compute the Overall_Rank as the average of KPI1_Rank and KPI2_Rank (i.e. Overall_Score = (KPI1_Rank + KPI2_Rank) / 2)
SalesmanID KPI1_Rank KPI2_Rank Overall_Score
Alice 5 3 4
Betty 2 2 2
Cindy 3 1 2
Doris 1 5 6
Emily 4 4 4
We then proceed to rank the sales personnel according to the Overall_Score in descending order.
SalesmanID Overall_Score Overall_Rank
Doris 6 1
Alice 4 2 (Tie)
Emily 4 2 (Tie)
Cindy 2 4 (Tie)
Betty 2 4 (Tie)
Would this be possible with C# LINQ?
Long code, but it is for educational purposes.
using System.Linq;
namespace ConsoleApplication
{
class Program
{
static void Main (string[] args)
{
var salesmanList = new Salesman[]
{
new Salesman ("Betty", 50, 6),
new Salesman ("Cindy", 40, 8),
new Salesman ("Doris", 70, 2),
new Salesman ("Emily", 30, 3),
};
var rankByKPI1 = salesmanList.OrderByDescending (x => x.KPI1)
.Select ((x, index) => new SalesmanKpiRank (x, index + 1))
.ToArray (); // for debugging only
var rankByKPI2 = salesmanList.OrderByDescending (x => x.KPI2)
.Select ((x, index) => new SalesmanKpiRank (x, index + 1))
.ToArray (); // for debugging only
var overallScoreQuery = from salesman in salesmanList
let kpi1rank = rankByKPI1.Single (x => x.Salesman.Equals (salesman)).Rank
let kpi2rank = rankByKPI2.Single (x => x.Salesman.Equals (salesman)).Rank
select new SalesmanOverallScore (salesman, kpi1rank, kpi2rank);
var rankByOverallScore = overallScoreQuery.OrderByDescending (x => x.Score)
.Select ((x , index) => new { SalesmanOverallScore = x, OverallRank = index + 1});
var result = rankByOverallScore.ToArray ();
}
}
class Salesman
{
public Salesman (string id, int kpi1, int kpi2)
{
ID = id;
KPI1 = kpi1;
KPI2 = kpi2;
}
public string ID { get; }
public int KPI1 { get; }
public int KPI2 { get; }
public override bool Equals (object obj) =>ID == ((Salesman) obj).ID; // put some logic here
public override string ToString () => $"{ID} KPI1 = {KPI1}, KPI2 = {KPI2}";
}
class SalesmanKpiRank
{
public SalesmanKpiRank (Salesman salesman, int rank)
{
Salesman = salesman;
Rank = rank;
}
public Salesman Salesman { get; }
public int Rank { get; }
public override string ToString () => $"{Salesman} KPI Rank = {Rank}"; // kpi1 or kpi2
}
class SalesmanOverallScore
{
public SalesmanOverallScore (Salesman salesman, int kpi1rank, int kpi2rank)
{
Salesman = salesman;
KPI1Rank = kpi1rank;
KPI2Rank = kpi2rank;
}
public Salesman Salesman { get; }
public int KPI1Rank { get; }
public int KPI2Rank { get; }
public double Score => (KPI1Rank + KPI2Rank) / 2d;
public override string ToString () => $"{Salesman.ID} {Score}";
}
}

Dynamic Pivoting in Linq c# MVC

I have below data in my linq list.
StatusID Count MonthYear
======== ===== =========
1 0 Jan 2014
2 1 Feb 2013
1 2 Jan 2013
3 1 Dec 2014
2 0 Nov 2014
5 6 Jun 2015
Now my requirement is i need above list in below format. Where MonthYear column data is not fix. It can be any month and year data.
StatusID Jan 2013 Feb 2013 Jan 2014 Nov 2014 Dec 2014 Jun 2015
======== ======== ======== ======== ======== ======== ========
1 2 0 0 0 0 0
2 0 1 0 0 0 0
3 0 0 0 0 1 0
5 0 0 0 0 0 6
I read lots of solution on stackoverflow and even tried my own way but i was not get success. Below is code which i tried last.
var pvtData = new PivotData(new[] { "StatusID", "MonthYear" }, new SumAggregatorFactory("count"));
pvtData.ProcessData(finalList, (o, f) =>
{
var custData = (MontlyChartModified)o;
switch (f)
{
case "StatusID": return custData.StatusID;
case "MonthYear":
return custData.MonthYear;
case "count": return custData.count;
}
return null;
});
Please help me if you have any idea how to do dynamic pivoting.
Below is my code which i implemented. but when i debug and put watch on result var it shows me error "result The name 'result' does not exist in the current context"
public class MontlyChartModified
{
public int? StatusID { get; set; }
public int count { get; set; }
public string MonthYear { get; set; }
}
List<MontlyChartModified> finalList = new List<MontlyChartModified>();
finalList = (from x in Okdata
select new MontlyChartModified
{
StatusID = x.StatusID,
count = x.count,
MonthYear = x.Month.ToString() + " " + x.Year.ToString()
}).ToList();
var columns = new[] { "StatusID" }.Union(finalList.Select(a => a.MonthYear).OrderBy(a => a.Split(' ')[1]).ThenBy(a => a)).ToList();
var result = finalList.GroupBy(g => g.StatusID).OrderBy(g => g.Key).Select(g => columns.Select(c =>
{
if (c == "StatusID") return g.Key;
var val = g.FirstOrDefault(r => r.MonthYear == c);
return val != null ? val.count : 0;
}).ToList()).ToList();
my final result.
I managed to do it with grouping:
First, I create list of columns, sorted by year and month alphabetically:
var columns = new[] { "StatusID" }.Union(lst.Select(a => a.MonthYear).OrderBy(a => a.Split(' ')[1]).ThenBy(a => a)).ToList();
Second, results are groupped by StatusID and for each group I create List of values for each MonthYear column:
var result = lst.GroupBy(g => g.StatusID).OrderBy(g => g.Key).Select(g => columns.Select(c =>
{
if (c == "StatusID") return g.Key;
var val = g.FirstOrDefault(r => r.MonthYear == c);
return val != null ? val.Count : 0;
}).ToList()).ToList();
Input list is defined as follows:
var lst = new List<MontlyChartModified>()
{
new MontlyChartModified(){StatusID = 1, Count = 0, MonthYear = "Jan 2014"},
new MontlyChartModified(){StatusID = 2, Count = 1, MonthYear = "Feb 2013"},
new MontlyChartModified(){StatusID = 1, Count = 2, MonthYear = "Jan 2013"},
new MontlyChartModified(){StatusID = 3, Count = 1, MonthYear = "Dec 2014"},
new MontlyChartModified(){StatusID = 2, Count = 0, MonthYear = "Nov 2014"},
new MontlyChartModified(){StatusID = 5, Count = 6, MonthYear = "Jun 2015"},
};
class MontlyChartModified
{
public int StatusID { get; set; }
public int Count { get; set; }
public string MonthYear { get; set; }
}
result is List<List<int>>

Joining two lists together

If I have two lists of type string (or any other type), what is a quick way of joining the two lists?
The order should stay the same. Duplicates should be removed (though every item in both links are unique). I didn't find much on this when googling and didn't want to implement any .NET interfaces for speed of delivery.
You could try:
List<string> a = new List<string>();
List<string> b = new List<string>();
a.AddRange(b);
MSDN page for AddRange
This preserves the order of the lists, but it doesn't remove any duplicates (which Union would do).
This does change list a. If you wanted to preserve the original lists then you should use Concat (as pointed out in the other answers):
var newList = a.Concat(b);
This returns an IEnumerable as long as a is not null.
The way with the least space overhead is to use the Concat extension method.
var combined = list1.Concat(list2);
It creates an instance of IEnumerable<T> which will enumerate the elements of list1 and list2 in that order.
The Union method might address your needs. You didn't specify whether order or duplicates was important.
Take two IEnumerables and perform a union as seen here:
int[] ints1 = { 5, 3, 9, 7, 5, 9, 3, 7 };
int[] ints2 = { 8, 3, 6, 4, 4, 9, 1, 0 };
IEnumerable<int> union = ints1.Union(ints2);
// yields { 5, 3, 9, 7, 8, 6, 4, 1, 0 }
Something like this:
firstList.AddRange (secondList);
Or, you can use the 'Union' extension method that is defined in System.Linq.
With 'Union', you can also specify a comparer, which can be used to specify whether an item should be unioned or not.
Like this:
List<int> one = new List<int> { 1, 2, 3, 4, 5 };
List<int> second=new List<int> { 1, 2, 5, 6 };
var result = one.Union (second, new EqComparer ());
foreach( int x in result )
{
Console.WriteLine (x);
}
Console.ReadLine ();
#region IEqualityComparer<int> Members
public class EqComparer : IEqualityComparer<int>
{
public bool Equals( int x, int y )
{
return x == y;
}
public int GetHashCode( int obj )
{
return obj.GetHashCode ();
}
}
#endregion
targetList = list1.Concat(list2).ToList();
It's working fine I think so. As previously said, Concat returns a new sequence and while converting the result to List, it does the job perfectly. Implicit conversions may fail sometimes when using the AddRange method.
If some item(s) exist in both lists you may use
var all = list1.Concat(list2).Concat(list3) ... Concat(listN).Distinct().ToList();
As long as they are of the same type, it's very simple with AddRange:
list2.AddRange(list1);
var bigList = new List<int> { 1, 2, 3 }
.Concat(new List<int> { 4, 5, 6 })
.ToList(); /// yields { 1, 2, 3, 4, 5, 6 }
The AddRange method
aList.AddRange( anotherList );
List<string> list1 = new List<string>();
list1.Add("dot");
list1.Add("net");
List<string> list2 = new List<string>();
list2.Add("pearls");
list2.Add("!");
var result = list1.Concat(list2);
one way: List.AddRange() depending on the types?
One way, I haven't seen mentioned that can be a bit more robust, particularly if you wanted to alter each element in some way (e.g. you wanted to .Trim() all of the elements.
List<string> a = new List<string>();
List<string> b = new List<string>();
// ...
b.ForEach(x=>a.Add(x.Trim()));
See this link
public class ProductA
{
public string Name { get; set; }
public int Code { get; set; }
}
public class ProductComparer : IEqualityComparer<ProductA>
{
public bool Equals(ProductA x, ProductA y)
{
//Check whether the objects are the same object.
if (Object.ReferenceEquals(x, y)) return true;
//Check whether the products' properties are equal.
return x != null && y != null && x.Code.Equals(y.Code) && x.Name.Equals(y.Name);
}
public int GetHashCode(ProductA obj)
{
//Get hash code for the Name field if it is not null.
int hashProductName = obj.Name == null ? 0 : obj.Name.GetHashCode();
//Get hash code for the Code field.
int hashProductCode = obj.Code.GetHashCode();
//Calculate the hash code for the product.
return hashProductName ^ hashProductCode;
}
}
ProductA[] store1 = { new ProductA { Name = "apple", Code = 9 },
new ProductA { Name = "orange", Code = 4 } };
ProductA[] store2 = { new ProductA { Name = "apple", Code = 9 },
new ProductA { Name = "lemon", Code = 12 } };
//Get the products from the both arrays
//excluding duplicates.
IEnumerable<ProductA> union =
store1.Union(store2);
foreach (var product in union)
Console.WriteLine(product.Name + " " + product.Code);
/*
This code produces the following output:
apple 9
orange 4
lemon 12
*/
The two options I use are:
list1.AddRange(list2);
or
list1.Concat(list2);
However I noticed as I used that when using the AddRange method with a recursive function, that calls itself very often I got an SystemOutOfMemoryException because the maximum number of dimensions was reached.
(Message Google Translated)
The array dimensions exceeded the supported range.
Using Concat solved that issue.
I just wanted to test how Union works with the default comparer on overlapping collections of reference type objects.
My object is:
class MyInt
{
public int val;
public override string ToString()
{
return val.ToString();
}
}
My test code is:
MyInt[] myInts1 = new MyInt[10];
MyInt[] myInts2 = new MyInt[10];
int overlapFrom = 4;
Console.WriteLine("overlapFrom: {0}", overlapFrom);
Action<IEnumerable<MyInt>, string> printMyInts = (myInts, myIntsName) => Console.WriteLine("{2} ({0}): {1}", myInts.Count(), string.Join(" ", myInts), myIntsName);
for (int i = 0; i < myInts1.Length; i++)
myInts1[i] = new MyInt { val = i };
printMyInts(myInts1, nameof(myInts1));
int j = 0;
for (; j + overlapFrom < myInts1.Length; j++)
myInts2[j] = myInts1[j + overlapFrom];
for (; j < myInts2.Length; j++)
myInts2[j] = new MyInt { val = j + overlapFrom };
printMyInts(myInts2, nameof(myInts2));
IEnumerable<MyInt> myUnion = myInts1.Union(myInts2);
printMyInts(myUnion, nameof(myUnion));
for (int i = 0; i < myInts2.Length; i++)
myInts2[i].val += 10;
printMyInts(myInts2, nameof(myInts2));
printMyInts(myUnion, nameof(myUnion));
for (int i = 0; i < myInts1.Length; i++)
myInts1[i].val = i;
printMyInts(myInts1, nameof(myInts1));
printMyInts(myUnion, nameof(myUnion));
The output is:
overlapFrom: 4
myInts1 (10): 0 1 2 3 4 5 6 7 8 9
myInts2 (10): 4 5 6 7 8 9 10 11 12 13
myUnion (14): 0 1 2 3 4 5 6 7 8 9 10 11 12 13
myInts2 (10): 14 15 16 17 18 19 20 21 22 23
myUnion (14): 0 1 2 3 14 15 16 17 18 19 20 21 22 23
myInts1 (10): 0 1 2 3 4 5 6 7 8 9
myUnion (14): 0 1 2 3 4 5 6 7 8 9 20 21 22 23
So, everything works fine.

Categories