Changing GroupBy keys depending on Class Structure - c#

I have a list of items with multiple columns and would like to group them by some fields depending on a boolean:
I have the following class:
public class Item
{
public string Group { get; set; }
public string Person { get; set; }
public string Currency { get; set; }
public string Country { get; set; }
public string County { get; set; }
public string OtherAdd { get; set; }
public string Income { get; set; }
}
which is part of a List:
var results = items.ToList(); //items is IEnumerable<Item>
if int type = 1, then I want to group by more elements:
results = results
.GroupBy(e => new { e.Group, e.Person, e.Branch, e.Currency, e.Country, e.County, e.OtherAdd})
.Select(g => new Item
{
Group = g.Key.Group,
Person = g.Key.Person,
Currency = g.Key.Currency,
Currency = g.Key.Country,
Currency = g.Key.County,
Currency = g.Key.OtherAdd,
Income = g.Sum(p => double.Parse(p.Income, System.Globalization.CultureInfo.InvariantCulture)).ToString("0.00", System.Globalization.CultureInfo.InvariantCulture)
})
.ToList();
if int type = 2, then I want to group by fewer elements (e.g. because OtherAdd would be an empty String):
results = results
.GroupBy(e => new { e.Group, e.Person, e.Branch, e.Currency})
.Select(g => new Item
{
Group = g.Key.Group,
Person = g.Key.Person,
Currency = g.Key.Currency,
Income = g.Sum(p => double.Parse(p.Income, System.Globalization.CultureInfo.InvariantCulture)).ToString("0.00", System.Globalization.CultureInfo.InvariantCulture)
})
.ToList();
etc.
Is there a way for me to change the GroupBy key depending on my integer type without repeating the code?

Well, you could use the old SQL trick, conditional values:
.GroupBy(e => new { e.Group, Person = (e.Type == 1 ? e.Person : Guid.NewGuid().ToString()), ... }
While this will still include the columns in the group by, all the items will have unique keys, so it doesn't quite matter. Sadly, I don't think there's a way around generating the unique keys, unlike in SQL (where you could just use NULL).
A better way might be to implement your own grouping class, instead of using an anonymous type. You could then use your own equality and hashing semantics, to make sure whether you include all the fields or not. However, that is arguably going to be more work than just having the similar code repeated.
Or, you might want to revise your whole design. It doesn't sound like what you're trying to do makes much sense - it's already quite suspicious that you're using the same type for two different things, and using strings for all the fields doesn't help either. Maybe you could try a different object design?

Related

Transforming string of data into an object in c#

So I'm trying to solve this issue, which shouldn't be too hard, but I'm stuck on it for far too long now.
This is the data I'm working with var data = "2,6;2,7;4,14;5,20";
It's a string that shows <modifierGroup>,<modifier>;<modifierGroup>,<modifier>;...
This is the model I eventually want to get my data in:
public class ModifierGroup
{
public int Id { get; set; }
public List<Modifier> Modifiers { get; set; }
}
public class Modifier
{
public int Id { get; set; }
}
Right now I keep thinking I need to get my data in this format, so I can eventually push it into the model:
Key=2
Value=6
Value=7
Key=4
Value=14
Key=5
Value=20
But I could be wrong. I'd love to keep the code short. So I'd rather prevent loops in loops and doing if statements over and over. Best case scenario I get a 1 or 2-liner of code, but if it doesn't work, it doesn't work.
You could just use Split and GroupBy with a projection
var data = "2,6;2,7;4,14;5,20";
var result = data
.Split(";")
.Select(x => x.Split(",")
.Select(int.Parse)
.ToArray())
.GroupBy(x => x[0])
.Select(x => new ModifierGroup()
{
Id = x.Key,
Modifiers = x.Select(y => new Modifier() {Id = y[1]}).ToList()
});

Group IEnumerable into a string

I wonder if someone could spare me a few minutes to give me some advice please?
I've created an IEnumerable list:
public class EmailBlock
{
public int alertCategory { get; set; }
public string alertName { get; set; }
public string alertURL { get; set; }
public string alertSnippet { get; set; } //Need to work out the snippet
}
List<EmailBlock> myEmailData = new List<EmailBlock>();
Which I then loop through some data (Umbraco content - not that that's really relevant!) and add items to the list.
myEmailData.Add(new EmailBlock { alertCategory = category.Id, alertName = alert.GetPropertyValue("pageTitle"), alertURL = alert.NiceUrl });
What ultimately I'd like to do is group the list by the alertCategory and then load each 'group' (another loop occurs later to check what members have subscribed to what alert category) into a variable which I can then use as an email's content.
You could use Linq's GroupBy() to do this:
using System.Linq
...
//Create a type to hold your grouped emails
public class GroupedEmail
{
public int AlertCategory { get; set; }
public IEnumerable<EmailBlock> EmailsInGroup {get; set; }
}
var grouped = myEmailData
.GroupBy(e => e.alertCategory)
.Select(g => new GroupedEmail
{
AlertCategory = g.Key,
EmailsInGroup = g
});
You can select to an anonymous type if required and project your sequence into whatever structure you require.
Linq has a nice group by statement:
var emailGroup = emailList.GroupBy(e => e.alertCategory);
Then you can loop through each grouping and do whatever you want:
foreach(var grouping in emailGroup)
{
//do whatever you want here.
//note grouping will access the list of grouped items, grouping.Key will show the grouped by field
}
Edit:
To retrieve a group after you have grouped them, just use Where for more than one or First for just one:
var group = emailGroup.First(g => g.Key == "name you are looking for");
or
var groups = emailGroup.Where(g => listOfWantedKeys.Contains(g.Key));
this is a lot more efficient than looping through every time you need to find something.

ordering a list of addresses

I have created a list which contains a number of addresses. I want to sort the list so that they will appears numerically in order i.e.
1 Abbey Road
2 Abbey Road
3 Abbey Road
10 Abbey Road
I've tried list.sort - which does alphabetically but therefore 10 appears before 2.
My linq is limited so I was thinking there might be a way using that or using a regex.
Any ideas?
You can use Linq's OrderBy (http://msdn.microsoft.com/en-us/library/vstudio/bb549422%28v=vs.110%29.aspx) sorting:
list.OrderBy(street => street, comparer);
where street are the strings with the street name and comparer is an IComparer that sorts them the way you want. To sort string numerically you can take a look here:
Sorting a List of Strings numerically (1,2,...,9,10 instead of 1,10,2)
If you don't store the street-number in a separate field but you expect it to be in the first position, you could use this class with meaningful properties to extract all informations:
public class Address
{
public string FullAddress { get; set; }
public int Number { get; set; }
public string Street { get; set; }
}
Now basically you just have to use String.Split and int.Parse, for example in this LINQ query:
List<Address> addresses = strings.Select(s => new {
FullAddress = s.Trim(),
Tokens = s.Trim().Split()
})
.Where(x => x.Tokens.Length > 1 && x.Tokens[0].All(Char.IsDigit))
.Select(x => new Address {
FullAddress = x.FullAddress,
Street = String.Join(" ", x.Tokens.Skip(1)),
Number = int.Parse(x.Tokens[0])
})
.OrderBy(addr => addr.Number)
.ToList();
If you don't want to select this class but just your strings in the correct order, you have to change the end of the query to:
.OrderBy(addr => addr.Number)
.Select(addr => addr.FullAddress)
.ToList();
Note that the Where filters, so "invalid" addresses are skipped. If that's not desired you could use:
int number;
List<Address> addresses = strings.Select(s => new {
FullAddress = s.Trim(),
Tokens = s.Trim().Split()
})
.Select(x => new Address{
FullAddress = x.FullAddress,
Street = String.Join(" ", x.Tokens.Skip(1)),
Number = int.TryParse(x.Tokens[0], out number) ? number : int.MaxValue
})
.OrderBy(addr => addr.Number)
.ToList();
Try this:-
var sortedList = addr.OrderBy(x => x.ID).ToList();
Fiddle.
If you are looking to sort it by Sort method, then you need to implement IComparable interface:-
public class Address : IComparable<Address>
{
public int ID { get; set; }
public string Add { get; set; }
public int CompareTo(Address other)
{
return this.ID.CompareTo(other.ID);
}
}
Then you can do:-
addr.Sort();
SortFiddle.

How to make nested list operations?

I am trying to call nested list operation. I have two different list but Name is their common variable. I have two lists, here's is what i am trying :
selectedImage
public int Id { get; set; }
public int X { get; set; }
public int Y { get; set; }
public string Name { get; set; }
generatedEyeDistance
public string Name { get; set; }
public double eyeDistance { get; set; }
I want to call generetedEyeDistance's eyeDistance value, however Name should be same. I tried :
var asdasf = generatedEyeDistance.Where(f=> (f.Name) == (selectedImage.Select(name => name.Name))).ToList();
But this gives following error:
Operator '==' cannot be applied to operands of type 'string' and 'System.Collections.Generic.IEnumerable<string>'
And also my approach looks like wrong. Could anyone help me to fix this issue?
If you want to get generated distances for selected images, then join both collection on Name:
from d in generatedEyeDistance
join i in selectedImage
on d.Name equals i.Name
select d
Lambda syntax (I don't like it with joins, but..)
generatedEyeDistance.Join(selectedImage, d => d.Name, i => i.Name, (d,i) => d)
BTW You have problem, because selectedImage.Select(name => name.Name) returns sequence of names, and you are trying to compare sequence with name of distance. Actually you shoul check if there exists image with same name as distance name:
generatedEyeDistance.Where(d => selectedImage.Any(i => i.Name == d.Name))
But join is much more efficient, because it uses set instead of doing sub-queries for each distance.
UPDATE: One more option, if you have list of distances, and don't want to use query syntax - then you can create set of names and filter list of distances manually:
var names = new HashSet<string>(selectedImage.Select(i => i.Name));
var result = generatedEyeDistance.FindAll(d => names.Contains(d.Name));

Easiest way to merge two List<T>s

I've got two List<Name>s:
public class Name
{
public string NameText {get;set;}
public Gender Gender { get; set; }
}
public class Gender
{
public decimal MaleFrequency { get; set; }
public decimal MaleCumulativeFrequency { get; set; }
public decimal FemaleCumulativeFrequency { get; set; }
public decimal FemaleFrequency { get; set; }
}
If the NameText property matches, I'd like to take the FemaleFrequency and FemaleCumulativeFrequency from the list of female Names and the MaleFrequency and MaleCumulativeFrequency values from the list of male Names and create one list of Names with all four properties populated.
What's the easiest way to go about this in C# using .Net 3.5?
Are you attempting to sum each of the values when you merge the lists? If so, try something like this:
List<Name> list1 = new List<Name>();
List<Name> list2 = new List<Name>();
List<Name> result = list1.Union(list2).GroupBy(x => x.NameText).Select(x => new
Name
{
NameText = x.Key,
Gender = new Gender
{
FemaleCumulativeFrequency = x.Sum(y => y.Gender.FemaleCumulativeFrequency),
FemaleFrequency = x.Sum(y => y.Gender.FemaleFrequency),
MaleCumulativeFrequency = x.Sum(y => y.Gender.MaleCumulativeFrequency),
MaleFrequency = x.Sum(y => y.Gender.MaleFrequency)
}
}).ToList();
What this does is the following:
Unions the lists, creating an IEnumerable<Name> that contains the contents of both lists.
Groups the lists by the NameText property, so if there are duplicate Names with the same NameText, they'll show up in the same group.
Selects a set of new Name objects, with each grouped Name's properties summed... you can also use Average if that makes more sense.
Converts the entire query to a List<Name> by calling the "ToList()" method.
Edit: Or, as you've said below, you simply want to merge the two lists... do this:
List<Name> allNames = femaleNames.Union(maleNames).ToList();
This looks a lot like the census name frequency data, right? Gender is a bit of a misnomer for the class you have it's more like "FrequencyData".
In effect you want a Dictionary so you can lookup any name and get the four values for it. You could simply take the males and do ToDictionary(...) on it and then iterate over the females, if the name exists in the dictionary, replace the female probabilities on it, if it doesn't exist, create a new dictionary entry.
My own approach to this same data was to create a Table in a database with all four values attached.
Here's some code for your scenario ...
Dictionary<string, Gender> result;
result = males.ToDictionary(x => x.NameText, x => x.Gender);
foreach (var female in females)
{
if (result.ContainsKey(female.NameText))
{
result[female.NameText].FemaleCumulativeFrequency = female.Gender.FemaleCumulativeFrequency;
result[female.NameText].FemaleFrequency = female.Gender.FemaleFrequency;
}
else
result.Add(female.NameText, female.Gender);
}
I think this could be what you want although I'm not sure if it handles the cumulative frequencies as you'd expect:
var mergedNameList = maleNames
.Concat(femaleNames)
.GroupBy(n => n.NameText)
.Select(nameGroup => new Name
{
NameText = nameGroup.Key,
Gender = new Gender
{
MaleFrequency = nameGroup.Sum(n => n.Gender.MaleFrequency),
MaleCumulativeFrequency = nameGroup.Sum(n => n.Gender.MaleCumulativeFrequency),
FemaleFrequency = nameGroup.Sum(n => n.Gender.FemaleFrequency),
FemaleCumulativeFrequency = nameGroup.Sum(n => n.Gender.FemaleCumulativeFrequency)
}
}.ToList();

Categories