I'm looking to group a list based on a list within that list itself, given the following data structure:
public class AccDocumentItem
{
public string AccountId {get;set;}
public List<AccDocumentItemDetail> DocumentItemDetails {get;set;}
}
And
public class AccDocumentItemDetail
{
public int LevelId {get;set;}
public int DetailAccountId {get;set;}
}
I now have a List<AccDocumentItem> comprised of 15 items, each of those items has a list with variable number of AccDocumentItemDetail's, the problem is that there may be AccDocumentItems that have identical AccDocumentItemDetails, so I need to group my List<AccDocumentItem> by it's AccDocumentItemDetail list.
To make it clearer, suppose the first 3 (of the 15) elements within my List<AccDocumentItem> list are:
1:{
AccountId: "7102",
DocumentItemDetails:[{4,40001},{5,40003}]
}
2:{
AccountId: "7102",
DocumentItemDetails:[{4,40001},{6,83003},{7,23423}]
}
3:{
AccountId: "7102",
DocumentItemDetails:[{4,40001},{5,40003}]
}
How can I group my List<AccDocumentItem> by it's DocumentItemDetails list such that row 1 and 3 are in their own group, and row 2 is in another group?
Thanks.
You could group by the comma separated string of detail-ID's:
var query = documentItemList
.GroupBy(aci => new{
aci.AccountId,
detailIDs = string.Join(",", aci.DocumentItemDetails
.OrderBy(did => did.DetailAccountId)
.Select(did => did.DetailAccountId))
});
Another, more ( elegant,efficient,maintainable ) approach is to create a custom IEqualityComparer<AccDocumentItem>:
public class AccDocumentItemComparer : IEqualityComparer<AccDocumentItem>
{
public bool Equals(AccDocumentItem x, AccDocumentItem y)
{
if (x == null || y == null)
return false;
if (object.ReferenceEquals(x, y))
return true;
if (x.AccountId != y.AccountId)
return false;
return x.DocumentItemDetails
.Select(d => d.DetailAccountId).OrderBy(i => i)
.SequenceEqual(y.DocumentItemDetails
.Select(d => d.DetailAccountId).OrderBy(i => i));
}
public int GetHashCode(AccDocumentItem obj)
{
if (obj == null) return int.MinValue;
int hash = obj.AccountId.GetHashCode();
if (obj.DocumentItemDetails == null)
return hash;
int detailHash = 0;
unchecked
{
foreach (var detID in obj.DocumentItemDetails.Select(d => d.DetailAccountId))
detailHash = detailHash * 23 + detID;
}
return hash + detailHash;
}
}
Now you can use it for GroupBy:
var query = documentItemList.GroupBy(aci => aci, new AccDocumentItemComparer());
You can use that for many other Linq extension methods like Enumerable.Join etc. also.
Related
I have an empty List (objPassenger) with passengers spaces incoming, and another list with data of passengers confirmed (objFilePassenger). I can't start from the 0 index in foreach.
Demo in web
foreach (var filePassenger in objFilePassengers)
{
//TypeId == 1 is ADULT
//TypeId == 2 is CHILD
var passenger = objPassengers.FirstOrDefault(p => (
(p.TypeId == filePassenger.Type && p.TypeId == "1") || (p.TypeId != "1" && filePassenger.Type != "1" && p.Age == filePassenger.Age)));
if (passenger != null)
{
passenger.PassengerId = filePassenger.PassengerID;
passenger.TypeId = filePassenger.Type;
passenger.FirstName = filePassenger.Name.ToTitleCase();
passenger.LastName = filePassenger.LastName.ToTitleCase();
passenger.Age = filePassenger.Age;
}
}
How can I skip the object if p.TypeId== 1 && filePassenger.Type == 2? This deferred object I need to skip and go next, until the type is equal like TypeId == 2 and Type == 2 (CHILD).
Updated Answer
Have you considered using a custom Collection to hold your passengers? You can build all the logic into the collection and make it enumerable to iterate through the passengers.
Here's a basic example built from your code:
Two data objects to represent a passenger. I use records because they make equality checks and cloning easy, and you get immutability. Just use the class object when you want to edit the record.
public record PassengerRecord(Guid PassengerId, string PassengerName, int PassengerType);
public class Passenger
{
public Guid PassengerId { get; set; } = Guid.NewGuid();
public string PassengerName { get; set; } = "Not Set";
public int PassengerType { get; set; }
public PassengerRecord AsRecord => new(PassengerId, PassengerName, PassengerType);
public Passenger() { }
public Passenger(PassengerRecord record)
{
this.PassengerId = record.PassengerId;
this.PassengerName = record.PassengerName;
this.PassengerType = record.PassengerType;
}
}
And then a very basic PassengerList which implements IEnumerable, restricts the number of passengers and checks if they are already on the list. Build your own logic and functionality into this.
public class PassengerList : IEnumerable<PassengerRecord>
{
private List<PassengerRecord> _items = new List<PassengerRecord>();
private int _maxRecords;
public PassengerList(int maxRecords)
=> _maxRecords = maxRecords;
public bool TryAddItem(Passenger passenger)
=> addItem(passenger.AsRecord);
public bool TryAddItem(PassengerRecord passenger)
=> addItem(passenger);
public bool addItem(PassengerRecord passenger)
{
// Doi whatever checks you want to do
if (!_items.AsQueryable().Any(item => item == passenger) && _items.Count < _maxRecords)
_items.Add(passenger);
return true;
}
public PassengerRecord this[int index]
=> _items[index];
public IEnumerator<PassengerRecord> GetEnumerator()
=> _items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> this.GetEnumerator();
}
This is how I solved it. The issue was 2 lists, one with the requested places, and another with the pre-loaded passengers. The first list could contain, for example, 6 places (3 adults and 3 minors) and the second list could have 2 adult passengers and 2 minor passengers. To skip the third adult (or the amount that was), as there is no others adults and the incomming passanger is a minor, it must be left blank. I solved it as follows.
var count = 0;
for (int i = 0; i < objFilePassengers.Count; i++)
{
if ((objFilePassengers[i].Type == "1" && objPassengers[i].TypeId == "1") || (objPassengers[i].TypeId == "2" && objFilePassengers[i].Type == "2" && objFilePassengers[i].Age < 18))
{
objPassengers[i].PassengerId = objFilePassengers[i].PassengerID;
objPassengers[i].TypeId = objFilePassengers[i].Type;
objPassengers[i].FirstName = objFilePassengers[i].Name.ToTitleCase();
objPassengers[i].LastName = objFilePassengers[i].LastName.ToTitleCase();
objPassengers[i].Age = objFilePassengers[i].Age;
}
else if ((objPassengers[i].TypeId == "1" && objFilePassengers[i].Type == "2"))
{
objFilePassengers.Insert(count + 1, new clsEnix_Passenger
{
PassengerID = objPassengers[i].PassengerId,
Name = "",
LastName = "",
});
count++;
}
}
Example result
There is a very simple class:
public class LinkInformation
{
public LinkInformation(string link, string text, string group)
{
this.Link = link;
this.Text = text;
this.Group = group;
}
public string Link { get; set; }
public string Text { get; set; }
public string Group { get; set; }
public override string ToString()
{
return Link.PadRight(70) + Text.PadRight(40) + Group;
}
}
And I create a list of objects of this class, containing multiple duplicates.
So, I tried using Distinct() to get a list of unique values.
But it does not work, so I implemented
IComparable<LinkInformation>
int IComparable<LinkInformation>.CompareTo(LinkInformation other)
{
return this.ToString().CompareTo(other.ToString());
}
and then...
IEqualityComparer<LinkInformation>
public bool Equals(LinkInformation x, LinkInformation y)
{
return x.ToString().CompareTo(y.ToString()) == 0;
}
public int GetHashCode(LinkInformation obj)
{
int hash = 17;
// Suitable nullity checks etc, of course :)
hash = hash * 23 + obj.Link.GetHashCode();
hash = hash * 23 + obj.Text.GetHashCode();
hash = hash * 23 + obj.Group.GetHashCode();
return hash;
}
The code using the Distinct is:
static void Main(string[] args)
{
string[] filePath = { #"C:\temp\html\1.html",
#"C:\temp\html\2.html",
#"C:\temp\html\3.html",
#"C:\temp\html\4.html",
#"C:\temp\html\5.html"};
int index = 0;
foreach (var path in filePath)
{
var parser = new HtmlParser();
var list = parser.Parse(path);
var unique = list.Distinct();
foreach (var elem in unique)
{
var full = new FileInfo(path).Name;
var file = full.Substring(0, full.Length - 5);
Console.WriteLine((++index).ToString().PadRight(5) + file.PadRight(20) + elem);
}
}
Console.ReadKey();
}
What has to be done to get Distinct() working?
You need to actually pass the IEqualityComparer that you've created to Disctinct when you call it. It has two overloads, one accepting no parameters and one accepting an IEqualityComparer. If you don't provide a comparer the default is used, and the default comparer doesn't compare the objects as you want them to be compared.
If you want to return distinct elements from sequences of objects of some custom data type, you have to implement the IEquatable generic interface in the class.
here is a sample implementation:
public class Product : IEquatable<Product>
{
public string Name { get; set; }
public int Code { get; set; }
public bool Equals(Product other)
{
//Check whether the compared object is null.
if (Object.ReferenceEquals(other, null)) return false;
//Check whether the compared object references the same data.
if (Object.ReferenceEquals(this, other)) return true;
//Check whether the products' properties are equal.
return Code.Equals(other.Code) && Name.Equals(other.Name);
}
// If Equals() returns true for a pair of objects
// then GetHashCode() must return the same value for these objects.
public override int GetHashCode()
{
//Get hash code for the Name field if it is not null.
int hashProductName = Name == null ? 0 : Name.GetHashCode();
//Get hash code for the Code field.
int hashProductCode = Code.GetHashCode();
//Calculate the hash code for the product.
return hashProductName ^ hashProductCode;
}
}
And this is how you do the actual distinct:
Product[] products = { new Product { Name = "apple", Code = 9 },
new Product { Name = "orange", Code = 4 },
new Product { Name = "apple", Code = 9 },
new Product { Name = "lemon", Code = 12 } };
//Exclude duplicates.
IEnumerable<Product> noduplicates =
products.Distinct();
If you are happy with defining the "distinctness" by a single property, you can do
list
.GroupBy(x => x.Text)
.Select(x => x.First())
to get a list of "unique" items.
No need to mess around with IEqualityComparer et al.
Without using Distinct nor the comparer, how about:
list.GroupBy(x => x.ToString()).Select(x => x.First())
I know this solution is not the answer for the exact question, but I think is valid to be open for other solutions.
I have a method from which I need to return a group, i.e.:
public static MyData BinSearch(MyData searchDate)
{
// First doing a binary search to get the index
if (index >= 0)
{
return recordList[index];
}
index = ~index;
if (index == 0 || index == recordList.Count)
{
return null;
}
int newIndex = (((index-1)+index)/2)+1;
string pointer = recordList[newIndex].TaxDet;
var groupData = recordList.GroupBy(p => p.TaxDet)
.ToDictionary(g => g.Key);
var output = groupData[pointer];
return (output); // Here I want to return a group of data
}
But I'm getting an error:
Cannot implicitly convert type
'System.Linq.IGrouping' to
'ConsoleApplication1.MyData'. An explicit conversion exists (are you
missing a cast?)
EDIT:
public class MyData
{
public string TaxDet{ get; set; }
public string empDetails { get; set; }
}
Since you are combining GroupBy and ToDictionary, by doing groupData[pointer] you are getting value from dictionary, which is IGrouping. If you need to extract data from grouping, you need 1 more index getter groupData[pointer][taxDet]
Probably .ToDictionary(g => g.Key); is redundant in this case
This is my linq query and I get lots of duplicates with school names.
so I created a regex function to trim the text:
public static string MyTrimmings(string str)
{
return Regex.Replace(str, #"^\s*$\n", string.Empty, RegexOptions.Multiline).TrimEnd();
}
the text gets trimed alright, however, the dropdown values are all duplicates! please help me eliminate duplicates, oh Linq joy!!
ViewBag.schools = new[]{new SelectListItem
{
Value = "",
Text = "All"
}}.Concat(
db.Schools.Where(x => (x.name != null)).OrderBy(o => o.name).ToList().Select(s => new SelectListItem
{
Value = MyTrimmings(s.name),
Text = MyTrimmings(s.name)
}).Distinct()
);
Distinct is poor, GroupBy for the win:
db.Schools.GroupBy(school => school.name).Select(grp => grp.First());
Assuming you have a School class you can write an IEqualityComparer
class SchoolComparer : IEqualityComparer<School>
{
public bool Equals(School x, School y)
{
//Check whether the compared objects reference the same data.
if (Object.ReferenceEquals(x, y)) return true;
//Check whether any of the compared objects is null.
if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
return false;
//Check whether the school' properties are equal.
return x.Name == y.Name;
}
// If Equals() returns true for a pair of objects
// then GetHashCode() must return the same value for these objects.
public int GetHashCode(School school)
{
//Check whether the object is null
if (Object.ReferenceEquals(school, null)) return 0;
//Get hash code for the Name field if it is not null.
int hashSchoolName = school.Name == null ? 0 : school.Name.GetHashCode();
//Calculate the hash code for the school.
return hashSchoolName;
}
}
Then your linq query would look like this:
db.Schools.Where(x => x.name != null)
.OrderBy(o => o.name).ToList()
.Distinct(new SchoolComparer())
.Select(s => new SelectListItem
{
Value = MyTrimmings(s.name),
Text = MyTrimmings(s.name)
});
You could make your class implement the IEquatable<T> interface, so Distinct will know how to compare them. Like this (basic example):
public class SelectListItem : IEquatable<SelectListItem>
{
public string Value { get; set; }
public string Text { get; set; }
public bool Equals(SelectListItem other)
{
if (other == null)
{
return false;
}
return Value == other.Value && Text == other.Text;
}
public override int GetHashCode()
{
unchecked
{
int hash = 17;
if (Value != null)
{
hash = hash * 23 + Value.GetHashCode();
}
if (Text != null)
{
hash = hash * 23 + Text.GetHashCode();
}
return hash;
}
}
}
(GetHashCode taken fron John Skeet's answer here: https://stackoverflow.com/a/263416/249000)
I'm having a List of String like
List<string> MyList = new List<string>
{
"A-B",
"B-A",
"C-D",
"C-E",
"D-C",
"D-E",
"E-C",
"E-D",
"F-G",
"G-F"
};
I need to remove duplicate from the List i.e, if "A-B" and "B-A" exist then i need to keep only "A-B" (First entry)
So the result will be like
"A-B"
"C-D"
"C-E"
"D-E"
"F-G"
Is there any way to do this using LINQ?
Implement IEqualityComparer witch returns true on Equals("A-B", "B-A"). And use Enumerable.Distinct method
This returns the sequence you look for:
var result = MyList
.Select(s => s.Split('-').OrderBy(s1 => s1))
.Select(a => string.Join("-", a.ToArray()))
.Distinct();
foreach (var str in result)
{
Console.WriteLine(str);
}
In short: split each string on the - character into two-element arrays. Sort each array, and join them back together. Then you can simply use Distinct to get the unique values.
Update: when thinking a bit more, I realized that you can easily remove one of the Select calls:
var result = MyList
.Select(s => string.Join("-", s.Split('-').OrderBy(s1 => s1).ToArray()))
.Distinct();
Disclaimer: this solution will always keep the value "A-B" over "B-A", regardless of the order in which the appear in the original sequence.
You can use the Enumerable.Distinct(IEnumerable<TSource>, IEqualityComparer<TSource>) overload.
Now you just need to implement IEqualityComparer. Here's something for you to get started:
class Comparer : IEqualityComparer<String>
{
public bool Equals(String s1, String s2)
{
// will need to test for nullity
return Reverse(s1).Equals(s2);
}
public int GetHashCode(String s)
{
// will have to implement this
}
}
For a Reverse() implementation, see this question
You need to implement the IEqualityComparer like this:
public class CharComparer : IEqualityComparer<string>
{
#region IEqualityComparer<string> Members
public bool Equals(string x, string y)
{
if (x == y)
return true;
if (x.Length == 3 && y.Length == 3)
{
if (x[2] == y[0] && x[0] == y[2])
return true;
if (x[0] == y[2] && x[2] == y[0])
return true;
}
return false;
}
public int GetHashCode(string obj)
{
// return 0 to force the Equals to fire (otherwise it won't...!)
return 0;
}
#endregion
}
The sample program:
class Program
{
static void Main(string[] args)
{
List<string> MyList = new List<string>
{
"A-B",
"B-A",
"C-D",
"C-E",
"D-C",
"D-E",
"E-C",
"E-D",
"F-G",
"G-F"
};
var distinct = MyList.Distinct(new CharComparer());
foreach (string s in distinct)
Console.WriteLine(s);
Console.ReadLine();
}
}
The result:
"A-B"
"C-D"
"C-E"
"D-E"
"F-G"
Very basic, but could be written better (but it's just working):
class Comparer : IEqualityComparer<string>
{
public bool Equals(string x, string y)
{
return (x[0] == y[0] && x[2] == y[2]) || (x[0] == y[2] && x[2] == y[0]);
}
public int GetHashCode(string obj)
{
return 0;
}
}
var MyList = new List<String>
{
"A-B",
"B-A",
"C-D",
"C-E",
"D-C",
"D-E",
"E-C",
"E-D",
"F-G",
"G-F"
}
.Distinct(new Comparer());
foreach (var s in MyList)
{
Console.WriteLine(s);
}
int checkID = 0;
while (checkID < MyList.Count)
{
string szCheckItem = MyList[checkID];
string []Pairs = szCheckItem.Split("-".ToCharArray());
string szInvertItem = Pairs[1] + "-" + Pairs[0];
int i=checkID+1;
while (i < MyList.Count)
{
if((MyList[i] == szCheckItem) || (MyList[i] == szInvertItem))
{
MyList.RemoveAt(i);
continue;
}
i++;
}
checkID++;
}