If person with specific name exists in People XML list - c#

How can find out result of this:
<People>
<Person name="John" />
<Person name="Andrew" />
</People>
I need to find out if person with specific name exists in People list.
Example:
if (Element("People").ForAny(person => person.name == "John")) // Returns True
if (Element("People").ForAny(person => person.name == "Amanda")) // Returns False
I am using Xml Linq library.
Thanks for any help!

You need to work on your separation of concerns: divide your problems into smaller problems that can be used independently. This make it easier to reuse your code for other problems, easier to test your code, make changes without breaking the code.
Your problem consists of two subproblems:
How to convert XML data to a sequence of Persons
How to extract Persons with a certain name from a sequence of Persons
How to detect if a sequence of Persons is not empty?
I don't often convert XML to IEnumerable, but this answer helped me.
class Person
{
public string Name {get; set;}
... // other properties
}
If in your XML a Person really only has a Name, you don't have to define a special class for it. But then again: are you sure that a Person is nothing more than a Name? Shouldn't that be a collection of Names?
Ok, so we have a Person class (or a string), and we need a function to convert the XML into a sequence of Persons:
Write this as an extension method, so we can let it look like LINQ. See extension methods demystified. The word this in the signature makes it an extension method. It allows you to use the method as if it was a method of the string class itself
public static IEnumerable<Person> ToPersons(this String xml)
{
return XElement.Parse(xml)
.Elements("Person")
.Select(xmlElement => new Person
{
Name = xmlElement.Element("Name").Value,
// if needed, fill other properties, for instance:
Id = Int32.Parse(xmlElement.Element("Id").Value),
});
}
Usage:
string xmlTxt = ...
IEnumerable<Person> persons = xmlTxt.ToPersons();
If you don't have a string with xml, but an XmlReader, consider:
public static IEnumerable<Person> ToPersons(this XmlReader xmlReader)
{
while(xmlReader.Read())
{
// TODO: read one Person from xmlReader
Person person = new Person() {...} // you know better how to do this than I
yield return person
}
Now that we have seperated xml conversion from Person handling, the rest is easy:
Requirement:
Given the name of a Person, and some XmlText that represents a sequence of Persons, tell me if a Person with this name is in this sequence
string personName = ...
string xmlText = ... // or Use the XmlReader
bool personAvailable = xmlText.ToPersons()
.Where(person => person.Name == personName)
.Any();
In words: convert the xmlText to a sequence of Persons. From this sequence keep only those Persons that have a Name equal to personName. Return true if there is any element left in the remaining sequence.
If you use peculiar characters, or want to ignore case. Consider:
IEqualityComparer<strong> personNameComparer = StringComparer.CurrentCultureIgnoreCase;
bool personAvailable = xmlText.ToPersons()
.Where(person => personNameComparer.Equals(person.Name, personName))
.Any();
The nice thing is, because you separated your concerns, you can use ToPersons also for other functions:
Requirement: give me all New York City Residents, born before 1960
var oldPersons = xmlText.ToPersons
.Where(person => person.Location == "New York City" && person.Birthday.Year < 1960);
Or if you get your Persons from a Json file, or a CSV-file, or a database, you can still get all all New York City Residents, born before 1960, while only writing a new ToPersons method.

You can simply navigate from the root element People.
In all its decendants Person, is there any with the attribute name is equal to the value you are looking for :
string input = #"<People>
<Person name=""John"" />
<Person name=""Andrew"" />
</People>";
XDocument doc = XDocument.Parse(input);
var isJohnHere = doc.Element("People")
.Descendants("Person")
.Any(x=> x.Attribute("name").Value == "John");
The same way getting the List<string> of people's names will be :
var names = doc.Element("People")
.Descendants("Person")
.Select(x=> x.Attribute("name").Value);
And even shorter " All the Persons no matter where there are" :
var isJohnHere2 = doc.Descendants("Person")
.Any(x=> x.Attribute("name").Value == "John");
Live demo

Related

Remove duplicates from array of objects

I have a class called Customer that has several string properties like
firstName, lastName, email, etc.
I read in the customer information from a csv file that creates an array of the class:
Customer[] customers
I need to remove the duplicate customers having the same email address, leaving only 1 customer record for each particular email address.
I have done this using 2 loops but it takes nearly 5 minutes as there are usually 50,000+ customer records. Once I am done removing the duplicates, I need to write the customer information to another csv file (no help needed here).
If I did a Distinct in a loop how would I remove the other string variables that are a part of the class for that particular customer as well?
Thanks,
Andrew
With Linq, you can do this in O(n) time (single level loop) with a GroupBy
var uniquePersons = persons.GroupBy(p => p.Email)
.Select(grp => grp.First())
.ToArray();
Update
A bit on O(n) behavior of GroupBy.
GroupBy is implemented in Linq (Enumerable.cs) as this -
The IEnumerable is iterated only once to create the grouping. A Hash of the key provided (e.g. "Email" here) is used to find unique keys, and the elements are added in the Grouping corresponding to the keys.
Please see this GetGrouping code. And some old posts for reference.
What's the asymptotic complexity of GroupBy operation?
What guarantees are there on the run-time complexity (Big-O) of LINQ methods?
Then Select is obviously an O(n) code, making the above code O(n) overall.
Update 2
To handle empty/null values.
So, if there are instances where the value of Email is null or empty, the simple GroupBy will take just one of those objects from null & empty each.
One quick way to include all those objects with null/empty value is to use some unique keys at the run time for those objects, like
var tempEmailIndex = 0;
var uniqueNullAndEmpty = persons
.GroupBy(p => string.IsNullOrEmpty(p.Email)
? (++tempEmailIndex).ToString() : p.Email)
.Select(grp => grp.First())
.ToArray();
I'd do it like this:
public class Person {
public Person(string eMail, string Name) {
this.eMail = eMail;
this.Name = Name;
}
public string eMail { get; set; }
public string Name { get; set; }
}
public class eMailKeyedCollection : System.Collections.ObjectModel.KeyedCollection<string, Person> {
protected override string GetKeyForItem(Person item) {
return item.eMail;
}
}
public void testIt() {
var testArr = new Person[5];
testArr[0] = new Person("Jon#Mullen.com", "Jon Mullen");
testArr[1] = new Person("Jane#Cullen.com", "Jane Cullen");
testArr[2] = new Person("Jon#Cullen.com", "Jon Cullen");
testArr[3] = new Person("John#Mullen.com", "John Mullen");
testArr[4] = new Person("Jon#Mullen.com", "Test Other"); //same eMail as index 0...
var targetList = new eMailKeyedCollection();
foreach (var p in testArr) {
if (!targetList.Contains(p.eMail))
targetList.Add(p);
}
}
If the item is found in the collection, you could easily pick (and eventually modify) it with:
if (!targetList.Contains(p.eMail))
targetList.Add(p);
else {
var currentPerson=targetList[p.eMail];
//modify Name, Address whatever...
}

How to quickly match names (fname, lname) in different order with full name c#

I have this linq query I am trying to optimize. I want to replace this query with a fast constant (preferably) retrieval of the value. I thought about a twin key dictionary but I have no idea which order the fname or lname will come first. I wanted to ask here if there is a fast way to do this.
I wanted to take a list of names, search through it for fname-lname the - is the delimeter and return all that match the full name that is searched. The list of people could be moderately large.
var nameList = from p in listOfPeople
where ((p.lname+"-"+p.fname == fullName)
|| (p.fname+"-"+p.lname == fullname))
select p;
Edit: listOfPeople can be any datatype, not necessarily a list.
Here's how you can create your dictionary.
var nameLookup = new Dictionary<Tuple<string,string>, List<Person>>();
foreach(var person in listOfPeople)
{
List<Person> people = null;
var firstLast = Tuple.Create(person.fname, person.lname);
if(nameLookup.TryGetValue(firstLast, out people))
{
people.Add(person);
}
else
{
nameLookup.Add(firstLast, new List<Person> { person });
}
// If the person's first and last name are the same we don't want to add them twice.
if(person.fname == person.lname)
{
continue;
}
var lastFirst = Tuple.Create(person.lname, person.fname);
if(nameLookup.TryGetValue(lastFirst, out people))
{
people.Add(person);
}
else
{
nameLookup.Add(lastFirst, new List<Person> { person });
}
}
Then your lookup would be
// split by the delimiter to get these if needed
var fullName = Tuple.Create(firstName, lastName);
List<Person> nameList = null;
if(!nameLookup.TryGetValue(fullName, out nameList))
{
nameList = new List<Person>();
}
It's important to keep the first and last names separate or you have to pick a delimiter that will not show up the the first or last name. Hyphen "-" could be part of a first or last name. If the delimiter is guaranteed to not be part of the first or last name you can just substitute the use of the Tuple.Create(x,y) with x + delimiter + y and change the dictionary to Dictionary<string, List<Person>>.
Additionally the reason for having a List<Person> as the value of the dictionary is to handle cases like "Gary William" and "William Gary" being two different people.
In your "P" definition, which I guess it's a "People" type, I would add a "FullName" property, which will be your comparator:
public string FullName {get {return fname + "-" + lname;}}
And modify your LINQ with:
Where string.Equals(p.FullName, fullName) .
If you REALLY want to use with ANY datatype, which would include just string or even DataTable, i really don't see any better way than the way you did...
I tested with Stopwatch and this appears to be a little more effective
var nameList = from n in(
from p in listOfPeople
select new{FullName = p.fname +"-"+ p.lname}
)
where n.FullName==fullName
select n;

Apply some field description using an XML

I need a verry especific format for each field of MyObject class
public class MyObject{
public Long Amount {get; set}
public Long FiscalAmount {get; set;}
// .... A lot more of fields
}
For example,
if MyObject.Amount is 5000 the expected result is 00005000.
if MyObject.FiscalAmount is 23 the expected result is 000023
In order to accomplished this, I have defined an XML file:
<FieldDescriptionXML>
<FielDefinition>
<Name>Amount</Name>
<FillWith>0</FillWith>
<Lenght>8</Lenght>
</FielDefinition>
<FielDefinition>
<Name>FiscalAmount</Name>
<FillWith>0</FillWith>
<Lenght>6</Lenght>
</FielDefinition>
.... A lot more of fields
<FieldDescriptionXML>
Then I use the following code to get MyObject fields with the desired output
MyObject myObject = new MyObject();
myObject.Amount = 5000;
...
// Gets the xml definition
// I deserialize the previous XML file and name it FieldDescription
List<FieldDescription> fieldDescriptionList = DeserializeXML(XMLFilePath);
FieldDescription fieldDescription = fieldDescriptionList.Find(x => x.Name == "Amount");
// Apply the field definition and returns a string.
// For this I use a private method called ApplyXMLFielDefinition
// where I got how many spaces the result should be filled
string result = ApplyXMLFielDefinition(myObject.Amount,fieldDescription);
Console.Writeline(result) // prints 00005000
// Now same code for MyObject.FiscalAmount
As you see, I got the desired output, but I had to do it one by one repeating the code.
Is there any other other better approached you can share?, thanks.
Environment: C# 4.0
If I am reading your question correctly you will have a list of elements you want populated from a large xml collection. Have you tried LinqToXml
var x = (from target in xmlDoc.Decendants("FielDefinition")
select (int) target.Element("FillWith").value()
where target.Element("Name").Value.tolower() == "amount")).ToList();
That will return a list<int>

Deserialize string of name=value format to object

I want to read files, each of which contains a person's details, as below, and convert it to a Person object.
Covert below
id=1
firstName=John
lastName=Smith
To:
public class Person
{
public int Id {get;set;}
public string FirstName{get;set;}
public string LastName{get;set;}
}
Are there .NET built-in methods to do that, or third party library. I cannot find it via google.
Update:
The file format CANNOT be changed.
.NET is really into XML, so you won't find build-in functionality for INI-like formats. But there are a bunch of libs that make it easy to read and write such files, e.g. ini-parser or nini, but you still have to do the mapping to and from objects manually.
You could parse the text with String.Split and LINQ:
Dictionary<string, string> dict = text
.Split(new[] { Environment.NewLine }, StringSplitOptions.None)
.Select(e => e.Split('='))
.ToDictionary(strings => strings[0], strings => strings[1]);
Then use something like Dictionary Adapter.
For example using File.ReadAllLines, a little bit of Linq and String.Substring?
var lines = File.ReadAllLines(path).Select(l => l.Trim());
var idLine = lines.FirstOrDefault(l => l.StartsWith("id=", StringComparison.OrdinalIgnoreCase));
var lNameLine = lines.FirstOrDefault(l => l.StartsWith("lastname=", StringComparison.OrdinalIgnoreCase));
var fNameLine = lines.FirstOrDefault(l => l.StartsWith("firstname=", StringComparison.OrdinalIgnoreCase));
if (idLine != null && lNameLine != null && fNameLine != null)
{
Person person = new Person()
{
Id = int.Parse(idLine.Substring(idLine.IndexOf("=") + 1)),
FirstName = fNameLine.Substring(fNameLine.IndexOf("=") + 1),
LastName = lNameLine.Substring(lNameLine.IndexOf("=") + 1)
};
}
(assuming that there's just one person per file)
But i would use a different format like XML (or a database of course).
I really think you should consider changing your input data format into something more standard (like XML or JSON).
But that does not mean you can't read your file at all. You should just read your text file by your own:
var people = new List<Person>();
using (var stream = File.OpenRead("Input.txt"))
{
using (var reader = new StreamReader(stream))
{
while (!reader.EndOfStream)
{
int id = int.Parse(reader.ReadLine().Substring(3));
string firstName = reader.ReadLine().Substring(10);
string lastName = reader.ReadLine().Substring(9);
var newPerson = new Person()
{
Id = id,
FirstName = firstName,
LastName = lastName
};
people.Add(newPerson);
}
}
}
If you have the data in a format like this:
<Person>
<Id>1</Id>
<FirstName>John</FirstName>
<LastName>Smith</LastName>
</Person>
Then this C# code will desrialise into an instance of Person
//assuming you have a string called "data" that contains the above XML.
XDocument xd=XDocument.Parse(data); //needs System.Xml.Linq for XDocument type.
using(var reader = xd.CreateReader())
{
using(XmlSerializer ser = new XmlSerializer(typeof(Person))
{
Person p = ser.Deserialize(reader) as Person;
//p will be null if it didn't work, so make sure to check it!
}
}
Note that the deserializer is case sensitive so you need to make sure the element cases match the casing of the properties in your class (You can get arond this by decorating your properties with Serializer attributes that tell the serialzer how to map them here)
The plain native serialzer is great for simple objects like this but can trip you up on some data types like char, bool, etc, so do checkout that link on the attributes.
If you wanted to do it from the format you gave in the question, you'd need to write a custom serialiser, in your case my advice would be to read from your file and generate XML from the data using XDocument Hope that helps.

LINQ Type expected

have the following linq code, trying to parse an xml file to a datatable but i get strange values in the resultant datatable all cell values show as
System.Xml.Ling.XContainer+<GetElements>d_11
Here is my LINQ
XDocument doc1 = XDocument.Load(#"D:\m.xml");
var q = from address in doc1.Root.Elements("Address")
let name = address.Elements("Name")
let street = address.Elements("Street")
let city = address.Elements("city")
select new
{
Name = name,
Street = street,
City = city
};
var xdt = new DataTable();
xdt.Columns.Add("Name", typeof(string));
xdt.Columns.Add("Street", typeof(string));
xdt.Columns.Add("City", typeof(string));
foreach (var address in q)
{
xdt.Rows.Add(address.Name, address.Street, address.City);
}
dataGrid1.ItemsSource = xdt.DefaultView;
here is my xml:
<PurchaseOrder PurchaseOrderNumber="99503" OrderDate="1999-10-20">
<Address Type="Shipping">
<Name>Ellen Adams</Name>
<Street>123 Maple Street</Street>
<City>Mill Valley</City>
<State>CA</State>
<Zip>10999</Zip>
<Country>USA</Country>
</Address>
<Address Type="Billing">
<Name>Tai Yee</Name>
<Street>8 Oak Avenue</Street>
<City>Old Town</City>
<State>PA</State>
<Zip>95819</Zip>
<Country>USA</Country>
</Address>
</PurchaseOrder>
and here is the result i get!
You forgot to regrieve the inner text of XElements. So you are selecting the whole element with attributes etc. Use this part of code:
var q = from address in doc1.Root.Elements("Address")
let name = address.Element("Name")
let street = address.Element("Street")
let city = address.Element("city")
select new
{
Name = name.Value,
Street = street.Value,
City = city.Value
};
address.Elements("Name") is a collection of all of the elements of type "Name". It so happens that in your case it's a collection of size one, but it's still a collection. You want to get the first item out of that collection (since you know it will be the only one) and then get the text value of that element. If you use Element instead of Elements you'll get the first item that matches, rather than a collection of items.
Now that you have your single element, you also need to get the value of that element, rather than the element itself (which also contains lots of other information in the general case, even though there really isn't anything else interesting about it in this particular case.
var q = from address in doc1.Root.Elements("Address")
select new
{
Name = address.Element("Name").Value,
Street = address.Element("Street").Value,
City = address.Element("City").Value
};
How does this work for you?
var q = from address in doc1.Root.Elements("Address")
let name = (string)(address.Element("Name"))
let street = (string)(address.Element("Street"))
let city = (string)(address.Element("city"))
//...
http://msdn.microsoft.com/en-us/library/bb155263.aspx
You are calling Elements which returns n elements wrapped in a helper class.
You probably mean to call Element which returns the first element as an XElement object.
Try this:
var q = from address in doc1.Root.Elements("Address")
let name = address.Element("Name").Value
let street = address.Element("Street").Value
let city = address.Element("City").Value
Change address.Elements("Name") to address.Elements("Name").FirstOrDefault() and so on.
The Elements method returns an IEnumerable. Therefore, your let variables point ato a sequence of elements, not a single element. You should take the single element returned, which will be an XElement, and then take its Value property to get the concatenated text of its contents. (As per documentation)
Instead of
select new
{
Name = name,
Street = street,
City = city
}
You should write:
select new
{
Name = name.Single().Value,
Street = street.Single().Value,
City = city.Single().Value
}
Either there, or directly in the let expressions. You may also find a helper method useful:
public static string StringValueOfElementNamed(XElement node, string elementName) {
return node.Elements(elementName).Single().Value;
}
Turn this helper method into an extension method if you wish to use member access syntax.
Edit: After reading concurrent answers, the better method to use would be:
public static string StringValueOfElementNamed(XElement node, string elementName) {
return node.Element(elementName).Value;
}
Element returns the first found element. Beware the null pointer returned when there is no element found.

Categories