Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 6 years ago.
Improve this question
I have a file like this:
City|Country|Phone Number
Name
City|Country|Phone Number
Name
City|Country|Phone Number
Name
and so on...
I have made a class as:
class Person
{
string city, country, phone, name;
}
After reading this big file, I want to make a List and place all the values in their respective fields.
My work so far
List<PersonObj> objectsList = new List<PersonObj>();
string[] peopleFile = System.IO.File.ReadAllLines(fileName);
var oddLines = peopleFile.Where((src, index) => index % 2 != 0);
var evenLines = peopleFile.Where((src, index) => index % 2 == 0);
and its successfully retrieving address and name lines separately in oddLines and evenLines respectively.
What I Want
I should have objectsList populated using LINQ rather than populating them one by one using loop
Thanks a lot for your help
This should do what you want
var people = File.ReadLines(fileName)
.Select((l,i) => new { Line = l.Split('|'), LineNo = i })
.GroupBy(x => x.LineNo/2)
.Select(grp => new Person
{
city = grp.First().Line[0],
country = grp.First().Line.Skip(1).FirstOrDefault(),
phone = grp.First().Line.Skip(2).FirstOrDefault(),
name = grp.Last().Line[0]
})
.ToList();
Basically you use the overload of Select that gives you the index and that will allow you to group by the line number so you get groups of the first 2 lines, the next 2 lines and so on. Then just pull from the first or last line in the group (there should only be 2) and the index of the array from doing the split.
Note that this will give incorrect results if the file doesn't match the format, such as the name and city being the same for the last entry if the file has an odd number of lines or the country or phone being null if the odd lines do not have at least two pipe characters.
Also I used File.ReadLines instead of File.ReadAllLines to avoid an unneeded intermediate array being created.
You can do this using Zip. I assume that evenLines contains the cities etc. and oddLines the names.
var persons = oddLines.Zip(
evenLines.Select(line => line.Split('|')),
(name, data) => new Person {name = name, city = data[0], country = data[1], phone = data[2]});
Zip combines each line of oddLines with the corresponding line of evenLines. This second line is split by | and for each combination a new Person object is generated and filled with its data.
Of course there should be a little more error handling as this may throw exceptions if there are values missing in your file.
For better separation of concern, you could first combine the two results in oddLines and evenLines to create a complete string:
var lines = from o in oddLines
from ev in evenLines
select o + "|" + ev;
and then use double LINQ Select:
objectsList = lines.Select(x => x.Split('|'))
.Select(y => new PersonObj() {
city = y[0],
country = y[1],
phone = y[2],
name = y[3],
}).ToList();
The first Select will be used to split each row in the file to string[] with 4 elements and the second Select is used to create PersonObj item from them.
Note that you have to make your fields (city, country, phone, name) to public rather than private to do this.
Here is a LINQ way:
class Person
{
public string Name { get; set; }
public string Country { get; set; }
public string City { get; set; }
public string PhoneNumber { get; set; }
}
class Program
{
static void Main(string[] args)
{
string[] lines = File.ReadAllLines("data.txt");
List<Person> people =
lines
.Select((line, index) =>
new
{
Index = index / 2,
RawData = line
}
)
.GroupBy(obj => obj.Index)
.Select(group =>
{
var rawPerson = group.ToArray();
string name = rawPerson[1].RawData;
string[] rawDetails = rawPerson[0].RawData.Split('|');
return
new Person()
{
Name = name,
City = rawDetails[0],
Country = rawDetails[1],
PhoneNumber = rawDetails[2]
};
}
)
.ToList();
}
}
Related
I have two lists, both containing models that share a common field, ID(String value). I am comparing the ID's for duplication.
I currently have a LINQ statement in place to determine the duplicated ID values, which stores them into a list of strings:
List<string> duplicateRecords = testData.TestRecords.GroupBy(aa => aa.ID).Where(x => x.Count() > 1).Select(y => y.Key).ToList();
And a second LINQ statement that maps a List of respected models based off the duplicated ID LINQ result:
List<Model> modelRecords = testData.Models.Where(x => duplicateRecords.Any(y => x.ID == y)).ToList();
These two LINQ statements do exactly what I expected them to do which is great. But now there is a recent request to determine duplicate ID's based off of their minimum N characters during a comparison. This minimum N comparison must happen for the last N characters in a string.
EX)
ID1: 123 == ID2: 123
ID1: 0123 == ID2: 123
ID1: 123 == ID2: 0123
ID1: 1230 != ID2: 123
ID1: 123 != ID2: 1230
ID1: 122110123 == ID2: 123
Hopefully those examples give some insight into the problem I am trying to solve. This could be done using foreach loops but I have come to experience the code becomes very messy and unmanageable on complex list query's.
So my question is this: How can I use the last N characters of the smaller of the two comparing strings to determine duplicates using LINQ?
Note: I am also very open to more elegant ways of solving this problem, would really appreciate excluding any for or foreach solutions.
I assume that when the input contains 123 and 0123 you want the result to have both of them
var input = new List<Model>()
{
new Model {ID = "123"},
new Model {ID = "0123"},
new Model {ID = "1230"},
new Model {ID = "12"},
new Model {ID = "122110123"}
};
var result = input.Where(x => input.Any(y => y != x && (y.ID.EndsWith(x.ID) || x.ID.EndsWith(y.ID)))).ToList();
\\this will return 123, 0123 and 122110123
If you want to check agains existing duplicateRecords list then this should work:
List<Model> modelRecords = testData.Models.Where(x => duplicateRecords.Any(y => x.ID.EndsWith(y) || y.EndsWith(x.ID))).ToList();
In order to efficiently find the duplicates you need to sort the IDs by length so you can minimize the comparisons necessary. (The sort adds some overhead, but greatly decreases the comparisons that must be done - in my test where 9 IDs have and 3 are duplicates of 8 values, it is 15 comparisons sorted versus 42 unsorted.) Once you have them sorted by length, just compare each one to the ones that are equal to or longer (in case of complete duplicates) to find which short IDs need to be kept, marking any matches so you can skip them and then find all the Models that end with the found matches.
Create the List of IDs ordered by their length:
var orderedIDs = testData.TestRecords.Select(tr => tr.ID).OrderBy(id => id.Length).ToList();
I don't think there is any way to do this efficiently with LINQ, but a nested for loop skipping previous matches optimizes the search for duplicates.
First, variables to keep track of IDsand whichID`s have already matched:
var dupRecordSubIDs = new List<string>();
var alreadyMatched = new bool[testData.TestRecords.Count];
Now loop through the IDs and save the shorter matching IDs:
// foreach ID in length order
for (int n1 = 0; n1 < testData.TestRecords.Count-1; ++n1) {
// skip the ones that already matched a shorter ID
if (!alreadyMatched[n1]) {
// remember if the shorter ID was alrady added
var added_n1 = false;
// compare the ID to all greater than or equal length IDs
for (int n2 = n1 + 1; n2 < testData.TestRecords.Count; ++n2) {
// if not previously matched, see if we have a new match
if (!alreadyMatched[n2] && orderedIDs[n2].EndsWith(orderedIDs[n1])) {
// only add the shorter ID once for new matches
if (!added_n1) {
dupRecordSubIDs.Add(orderedIDs[n1]);
added_n1 = true;
}
// remember which longer IDs are already matched
alreadyMatched[n2] = true;
}
}
}
}
Now find all the Models that match one of the IDs with a duplicate:
var modelRecords = testData.Models.Where(m => dupRecordSubIDs.Any(d => m.ID.EndsWith(d))).ToList();
I assume ID is string. If so, you can do this :
string match = "123";
var duplicate = list.Where(x=> x.Substring(x.Length - match.Length) == match).ToList();
If I understand your question correctly, it looks to be just a matter of chopping off the last N characters in each ID property while grouping.
Something like this:
using System;
using System.Linq;
public class TestRecord
{
public string ID { get; set; }
}
public class TestModel
{
public string ID { get; set; }
}
public class Program
{
public static void Main()
{
var N = 3; // This is where you define the desired N length
var rand = new Random();
var testRecords = new TestRecord[]
{
new TestRecord {ID = "123"},
new TestRecord {ID = "0123"},
new TestRecord {ID = "1230"},
new TestRecord {ID = "122110123"},
};
var testModels = new TestModel[]
{
new TestModel {ID = "123"},
new TestModel {ID = "0123"},
new TestModel {ID = "1230"},
new TestModel {ID = "122110123"},
};
bool SortEm(string a, string b) => a.Length < b.Length ? b.EndsWith(a) : a.EndsWith(b);
var models = testRecords
.Where(record => testRecords.Any(target => record.ID != target.ID && SortEm(target.ID, record.ID)))
.ToDictionary(
key => key,
key => testModels.Where(testModel => SortEm(key.ID, testModel.ID)).ToArray());
foreach (var kvp in models)
{
System.Console.WriteLine($"For duplicate key ({kvp.Key.ID}) found models: \r\n\t{string.Join("\r\n\t", kvp.Value.Select(x => x.ID))}");
}
}
}
I have a list of objects I want to group.
Objects have a List parameter, and during grouping I want to make the sum of the lists like this :
for(int i=0;i<MyList1.Count();i++)
{
StatutOperations[i]=StatutOperations1[i]+StatutOperations2[i]...
}
For now using linq I have the following :
liste_rep = liste_rep.GroupBy(l => l.Nom)
.Select(cl => new Repere
{
Quantite = cl.Sum(c => c.Quantite),
IdAff = cl.First().IdAff,
ID = 0,
ListeOperations = cl.First().ListeOperations,
StatutOperations = cl.Zip(StatutOperations)//First().StatutOperations
}).ToList();
The line making problem is the last one, I found how to use Zip function to summ two tables, but what if I want to use it grouping Lists?
Edit : StatusOperations is a list of integers, concretely liste_rep is a list of details, details have a list of n operations, and StatusOperations determines how much details have been operated for each operation.
Example :
ListOperations = CUT, DRILL, PAINT
StatusOperations = 20,20,10
This means 20 details are cut, 20 are drilled and 10 are painted
I want to group the list of details getting totals for each operation.
Edit 2 :
For now I only could manage to do it making myself the grouping :
liste_rep = liste_rep.OrderBy(p => p.Nom).ToList();
if (liste_rep.Count()>1)
{
totalStatut = liste_rep[0].StatutOperations.ConvertAll(s => s = 0);
string oldRep = "";
Repere repere = new Repere();
foreach (Repere rep in liste_rep)
{
if (rep.Nom!=oldRep)
{
newListRep.Add(repere);
repere = new Repere();
repere.Nom = rep.Nom;
repere.StatutOperations = rep.StatutOperations;
}
else
{
repere.StatutOperations=repere.StatutOperations.Zip(rep.StatutOperations, (x, y) => x + y).ToList();
}
oldRep = rep.Nom;
}
}
You can use this
if StatutOperations is a list of int).
Use this at last line.
StatutOperations= cl.Aggregate((opl1, opl2) =>
{ return opl1.StatutOperations.Zip(opl2.StatutOperations, (opin1,opin2)=>opin1+opin2).ToList(); });
in above code Aggregate runs through two elements and aggregate as sum (op1+op2).
Note : Remember use aggregate if and only if list contains more than one element
.
Edit:
Sorry the above code is incorrect as this is applying aggregate on repere type object and hence the expected return value would be of Repere type.
Edited my code now it should work fine now.
liste_rep.GroupBy(l => l.Nom)
.Select(cl => new Repere
{
Quantite = cl.Sum(c => c.Quantite),
IdAff = cl.First().IdAff,
ID = 0,
ListeOperations = cl.First().ListeOperations,
StatutOperations = cl
.Select(x=>x.StatutOperations)
.Aggregate((x,y)=> x.Zip(y,(p,q)=>p+q).ToList());
}).ToList();
I've list of object which looks like below
public class test
{
public int ID{ get; set; }
public string Name { get; set; }
public Dictionary<string, string> SampleXML { get; set; }
}
I want to group the list based on values in 'SampleXML'. e.g. It contains values like price. I want to group List based on price.
How to implement the same. I tried below code but it just seperates the list by key. I need unique records.
Dictionary<string, List<test>> result = Obj.LstTest
.GroupBy(x => x.SampleXML["Price"])
.ToDictionary(g => g.Key,
g => g.ToList());
The above code generates the list based on price. e.g. If there are 6 records with 3 different prices, above code return 3 results,but again the list contains two records each.
EDIT
If the price is blank then it needs to be ignored in grouping that is if there are 3 records and all doesn't have prices then list will contain 3 item(as it is).
Any help is appreciated.
Edit: Based on Vicky S edit
To still have the result as a List of test:
List<test> list = new List<test>();
List<test> result = new List<test>();
result = list.GroupBy(x => x.SampleXML["Price"]).Select(g=>g.FirstOrDefault());
Edit: To include tests that have SampleXML with empty Price value or no Price Key. Do the following:
First we need to separate tests with "No Price" and include them to our final result, from test that have "Price" value to group them.
var emptyPrice = list.Where(l => !l.SampleXML.ContainsKey("Price") || l.SampleXML["Price"] == string.Empty).ToList();
var withoutEmptyPrice = list.Where(l => l.SampleXML.ContainsKey("Price") && !string.IsNullOrEmpty(l.SampleXML["Price"]));
Then we will group tests with "Price" value as my first answer.
var resultWithEmptyPrice = withoutEmptyPrice.GroupBy(x => x.SampleXML["Price"]).Select(g => g.FirstOrDefault()).ToList();
Finally, we will add the "No Price" tests to our result.
resultWithEmptyPrice.AddRange(emptyPrice);
I have this class:
public class RecipeLine
{
public List<string> PossibleNames { get; set; }
public string Name { get; set; }
public int Index { get; set; }
}
I have a list of multiple RecipeLine objects. For example, one of them looks like this:
Name: apple
PossibleNames: {red delicious, yellow delicious, ... }
Index = 3
I also have a table in my db which is called tblFruit and has 2 columns: name and id. the id isn't the same as the index in the class.
What I want to do is this:
for the whole list of RecipeLine objects, find all the records in tblFruit whose name is in PossibleNames, and give me back the index of the class and the id in the table. So we have a list in a list (a list of RecipeLine objects who have a list of strings). How can I do this with Linq in c#?
I'm pretty sure there isn't going to be a LINQ statement that you can construct for this that will create a SQL query to get the data exactly how you want. Assuming tblFruit doesn't have too much data, pull down the whole table and process it in memory with something like...
var result = tblFruitList.Select((f) => new {Id = f.id, Index = recipeLineList.Where((r) => r.PossibleNames.Contains(f.name)).Select((r) => r.Index).FirstOrDefault()});
Keeping in mind that Index will be 0 if there isn't a recipeLine with the tblFruit's name in it's PossibleNames list.
A more readable method that doesn't one-line it into a nasty linq statement is...
Class ResultItem {
int Index {get;set;}
int Id {get;set;}
}
IEnumerable<ResultItem> GetRecipeFruitList(IEnumerable<FruitItem> tblFruitList, IEnumerable<RecipeLine> recipeLineList) {
var result = new List<ResultItem>();
foreach (FruitItem fruitItem in tblFruitList) {
var match = recipeLineList.FirstOrDefault((r) => r.PossibleNames.Contains(fruitItem.Name));
if (match != null) {
result.Add(new ResultItem() {Index = match.Index, Id = fruitItem.Id});
}
}
return result;
}
If tblFruit has a lot of data you can try and pull down only those items that have a name in the RecipeLine list's of PossibleName lists with something like...
var allNames = recipeLineList.SelectMany((r) => r.PossibleNames).Distinct();
var tblFruitList = DbContext.tblFruit.Where((f) => allNames.Contains(f.Name));
To get all the fruits within your table whose Name is in PossibleNames use the following:
var query = myData.Where(x => myRecipeLines.SelectMany(y => y.PossibleNames).Contains(x.Name));
I don't think you can do this in a single step.
I would first create a map of the possible names to indexes:
var possibleNameToIndexMap = recipes
.SelectMany(r => r.PossibleNames.Select(possibleName => new { Index = r.Index, PossbileName = possibleName }))
.ToDictionary(x => x.PossbileName, x => x.Index);
Then, I would retrieve the matching names from the table:
var matchingNamesFromTable = TblFruits
.Where(fruit => possibleNameToIndexMap.Keys.Contains(fruit.Name))
.Select(fruit => fruit.Name);
Then you can use the names retrieved from the tables as keys into your original map:
var result = matchingNamesFromTable
.Select(name => new { Name = name, Index = possibleNameToIndexMap[name]});
Not fancy, but it should be easy to read and maintain.
I have an object (KS), which holds ID and Title (which has a number as part of the Title).
All I'm trying to do is sort it into descending order. The object has:
ID Title
1 1 Outlook VPN
2 2 Outlook Access
3 4 Access VBA
4 3 Excel Automation
So when order by Title, it should read:
ID Title
3 4 Access VBA
4 3 Excel Automation
2 2 Outlook Access
1 1 Outlook VPN
The code I'm using to sort it is:
IEnumerable<KS> query = results.OrderByDescending(x => x.Title);
However, query still has the objects in the original order!
Is there something to do with having numbers at the start of Title that I'm missing?
EDIT
I've added the code from the controller for clarity:
[HttpPost]
// [ValidateAntiForgeryToken]
// id is a string of words eg: "outlook access vpn"
// I split the words and want to check the Title to see how many words appear
// Then sort by the most words found
public JsonResult Lookup(string id)
{
List<string> listOfSearch = id.Split(' ').ToList();
var results = db.KS.Where(x => listOfSearch.Any(item => x.Title.Contains(item)));
// search each result, and count how many of the search words in id are found
// then add the count to the start of Title
foreach (KS result in results)
{
result.KSId = 0;
foreach (string li in listOfSearch)
{
if (result.Title.ToLower().Contains(li.ToLower()))
{
result.KSId += 1;
}
}
result.Title = result.KSId.ToString() + " " + result.Title;
}
// sort the results based on the Title - which has number of words at the start
IEnumerable<KS> query = results.OrderByDescending(x => x.Title).ToList();
return Json(query, JsonRequestBehavior.AllowGet);
}
Here is a screenshot after query has been populated showing Titles in the order: 1, 2, 1, 1:
Model for the object if it helps is:
public class KS
{
public int KSId { get; set; }
public string KSSol { get; set; }
public string Title { get; set; }
public string Fix { get; set; }
}
As I said in a comment, put a .ToList() where you declare your results variable. That is:
var results = db.KS.Where(x => listOfSearch.Any(item => x.Title.Contains(item)))
.ToList();
If you don't do that, the foreach loop will modify objects that might not be the same as the objects you sort later, because the database query is run again each time you enumerate your IQueryable<>.
You can always just ignore the strange behavior and go the safe way:
List<KS> query = results.ToList();
query.Sort((a, b) => a.Whatever.CompareTo(b.Whatever));
return Json(query, blah);
I simple did this and it worked for me :-
var sortedOrder = Query.OrderBy(b => b.Title.Substring(b.Title.IndexOf(" ")));
All I have done is SubString the Title at the index of of the blank space when ordering the objects in the sequence, that way, the OrderBy is looking at the first character in the title rather than the number at the beginning.
Old question, but maybe this will help someone using C#. I used the following expressions to sort a list of objects based on their quantity parameter in ascending or descending order. Can modify it to compare text as the original question was concerned with.
Ascending Order:
locationMaterials.Sort((x, y) => x.Quantity.CompareTo(y.Quantity));
Descending Order:
locationMaterials.Sort((x, y) => y.Quantity.CompareTo(x.Quantity));
You are missing .ToList()
IEnumerable<KS> query = results.OrderByDescending(x => x.Title).ToList();
results.OrderByDescending(x => x.Title) is a query, and it has no data.
ToList() forces the query to be executed.
[EDIT]
My answer assumes that your results has acually not been materialized, and that that is the source of your problem.