The scenario is for a football league table. I can order the list by match win percentage and then by goals scored to determine their position in the league. I then use this ordering to get teams position in the league table using the IndexOf function.
this.results = this.results.OrderByDescending(x => x.WinPercentage).ThenByDescending(x => x.Goals);
this.results.Foreach(x => x.Position = this.results.IndexOf(x));
The problem arises when two teams (should be joint #1) have the same match win percentage and goals scored but when getting the index one team will be assigned #1 and the other #2.
Is there a way to get the correct position?
var position = 1;
var last = result.First();
foreach(var team in results)
{
if (team.WinPercentage != last.WinPercentage || team.Goals != last.Goals)
++position;
team.Position = position;
last = team;
}
What you could do is group the items based on the win percentage and goals (if both are the same, the teams will be in the same group), then apply the same position number to every element in the same group:
this.results = this.results.OrderByDescending(x => x.WinPercentage).ThenByDescending(x => x.Goals);
var positionGroups = this.results.GroupBy(x => new { WinPercentage = x.WinPercentage, Goals = x.Goals });
int position = 1;
foreach (var positionGroup in positionGroups)
{
foreach (var team in positionGroup)
{
team.Position = position;
}
position++;
}
The code below code will work for you
this.results = this.results.OrderByDescending(x => x.WinPercentage).ThenByDescending(x => x.Goals);
this.results.Foreach(x =>
{
int index = this.results.FindIndex(y => y.Goals == x.Goals && y.WinPercentage == x.WinPercentage);
x.Position = index > 0 ? this.results[index - 1].Position + 1 : 0;
});
Here's my solution
Define a class:
public class ABC
{
public int A { get; set; }
public int B { get; set; }
public int R { get; set; }
}
Constructing numerical:
List<ABC> list = new List<ABC>();
for (var i = 0; i < 100; i++)
{
list.Add(new ABC()
{
A = i,
B = i > 50 && i < 70 ? i + 20 : i + 1
});
}
Ranking and print the values:
var result = list.OrderByDescending(d => d.B)
.GroupBy(d => d.B)
.SelectMany((g, `i`) => g.Select(e => new ABC()
{
A = e.A,
B = e.B,
R = i + 1
})).ToList();
foreach (var t in result)
{
Console.WriteLine(JsonConvert.SerializeObject(t));
}
Console.ReadLine();
the result:
Related
I would like to Lambda my code but am stuck.
Basically:
If the array object contains say 4 members with their own year specification and id's. The array can however contain many more members with the same and different Id's and year (never same Id and same year though).
Member array:
array[0]: Id 1 Year 2010
array[1]: Id 2 Year 2010
array[2]: Id 1 Year 2008
array[3]: Id 1 Year 2009
First -
I want to delete all array-members with a specific Id for the year 2010 if they also have another year in the array (same id, different year). So in this case I would like to delete the [0] but not the other members.
Secondly -
I want only to keep the next newest year after 2010 in this case for Id 1 the year 2009, meaning I want to delete [2] as well. (the years come as strings which is why I'm converting them into ints for the comparision in the code below)
Below is my code with for loops that work that I need expert Lambda help with to avoid the loops:
var x = Member.Length;
for (int i = 0; i < x; i++)
{
var y = Member[i].id;
for (int j = i; j < x; j++)
{
var z = Member[j].id;
if (i != j)
{
if (y == z)
{
if (Member[i].year == "2010")
{
Member = Member.Where(w => w != Member[i]).ToArray();
i--;
j--;
x--;
break;
}
var tempI = Convert.ToInt32(Member[i].year);
var tempJ = Convert.ToInt32(Member[j].year);
if (tempI > tempJ)
{
Member = Member.Where(w => w != Member[j]).ToArray();
i--;
j--;
x--;
break;
}
}
}
}
}
I agree that the requirement doesn't make a lot of sense but this is how I interpreted
var Member = new[]
{
new { id = 1, year = "2010" },
new { id = 2, year = "2010" } ,
new { id = 1, year = "2008" } ,
new { id = 1, year = "2009" }
};
var results = from item in Member.Select(x => new { x.id, Year = Convert.ToInt32(x.year), item = x })
group item by item.id into sameItems
let order = sameItems.OrderByDescending(x => x.Year)
let first = order.ElementAtOrDefault(0)
let second = order.ElementAtOrDefault(1)
select first.Year == 2010 && second != null ? second.item : first.item;
foreach (var item in results)
{
System.Console.WriteLine($"id:{item.id},year:{item.year}");
}
I tend to avoid using LINQ to change the underlying collection I'm querying.
The code below will select up to two of most recent entries for each member.
var result = new List<MemberClass>();
var groups = Member.OrderBy(m => m.Id).ThenByDescending(m => m.Year).GroupBy(m => m.Id).ToList();
groups.ForEach(c => result.AddRange(c.Take(2)));
Use result instead of the original array.
I don't know if performance is a consideration for you. The code above may become slow as your collection grows.
Your description and requirements are incompatible, but here's one interpretation:
public class Member
{
public int Id { get; set; }
public string Year { get; set; }
}
var items = (new List<Member>() {
new Member() { Id=1, Year="2010" },
new Member() { Id=2, Year="2010" },
new Member() { Id=1, Year="2008" },
new Member() { Id=1, Year="2009" }
}).ToArray();
// Group everythnig by year, then only keep the highest id
var firstFiltered = items
.GroupBy(
x => x.Year,
x => x.Id,
(year, ids) => new Member()
{
Id = ids.Last(),
Year = year
});
var secondFiltered = firstFiltered
// Only keep years before 2010
.Where(x => String.Compare(x.Year, "2010") == -1)
// Then order by Id then Year
.OrderBy(x => x.Id)
.ThenBy(x => x.Year)
// And only keep the last/most recent year
.GroupBy(
x => x.Id,
x => x.Year,
(id, years) => new Member()
{
Id = id,
Year = years.Last()
});
To illustrate my problem I have created this simple snippet. I have a class Item
public class Item
{
public int GroupID { get; set; }
public int StrategyID { get; set; }
public List<Item> SeedData()
{
return new List<Item>
{
new Item {GroupID = 1, StrategyID = 1 },
new Item {GroupID = 2, StrategyID = 1 },
new Item {GroupID = 3, StrategyID = 2 },
new Item {GroupID = 4, StrategyID = 2 },
new Item {GroupID = 5, StrategyID = 3 },
new Item {GroupID = 1, StrategyID = 3 },
};
}
}
And what I want to check is that this SeedData method is not returning any duplicated GroupID/StrategyID pairs.
So in my Main method I have this:
Item item = new Item();
var data = item.SeedData();
var groupByStrategyIdData = data.GroupBy(g => g.StrategyID).Select(v => v.Select(gr => gr.GroupID)).ToList();
for (var i = 0; i < groupByStrategyIdData.Count; i++)
{
for (var j = i + 1; j < groupByStrategyIdData.Count; j++)
{
Console.WriteLine(groupByStrategyIdData[i].Intersect(groupByStrategyIdData[j]).Any());
}
}
which is working fine but one of the problems is that I have lost the StrategyID so in my real-case scenario I won't be able to say for which StrategyID/GroupID pair I have duplication so I was wondering is it possible to cut-off the LINQ to here:
var groupByStrategyIdData = data.GroupBy(g => g.StrategyID)
and somehow perform the check on this result?
One of the very easy ways would be to do grouping using some identity for your Item. You can override Equals/GetHashCode for your Item or instead write something like:
Item item = new Item();
var data = item.SeedData();
var duplicates = data.GroupBy(x => string.Format("{0}-{1}", x.GroupID, x.StrategyID))
.Where(group => group.Count() > 1)
.Select(group => group.Key)
.ToList();
Please note, that using a string for identity inside of group by is probably not the best way to do grouping.
As of your question about "cutting" the query, you should also be able to do the following:
var groupQuery = data.GroupBy(g => g.StrategyID);
var groupList = groupQuery.Select(grp => grp.ToList()).ToList();
var groupByStrategyIdData = groupQuery.Select(v => v.Select(gr => gr.GroupID)).ToList();
You may be able to do it another way, as follows:
// Check for duplicates
if (data != null)
{
var grp =
data.GroupBy(
g =>
new
{
g.GroupID,
g.StrategyID
},
(key, group) => new
{
GroupID = key.GroupID,
StrategyId = key.StrategyID,
Count = group.Count()
});
if (grp.Any(c => c.Count > 1))
{
Console.WriteLine("Duplicate exists");
// inside the grp object, you can find which GroupID/StrategyID combo have a count > 1
}
}
I would like to get the gender for calculations, for example the male and female options are in one column. I would like to get all male or all female for calculation.
I have a "computed property" which gives me list of all the items along with calculation. Here is the code:
partial void MeanFemale_Compute(ref string result)
{
// Set result to the desired field value
int totalAge = 0;
int count = 0;
foreach (InsuranceQuotation i in his.DataWorkspace.ApplicationData.InsuranceQuotations)
{
totalAge += i.mAge;
count++;
}
if (count != 0)
{
result = (totalAge / count).ToString();
}
}
How do I get to filter the gender in this "computed property".
You can use LINQ. It would look something like this:
int averageAge = this.DataWorkspace.ApplicationData.InsuranceQuotations.
Where(iq => iq.Gender == Gender.Female).
Average(iq => iq.mAge);
Could you filter with an if statement?
partial void MeanFemale_Compute(ref string result)
{
// Set result to the desired field value
int totalAge = 0;
int count = 0;
foreach (InsuranceQuotation i in this.DataWorkspace.ApplicationData.InsuranceQuotations)
{
if(i.Female == true)
{
totalAge += i.mAge;
count++;
}
}
if (count != 0)
{
result = (totalAge / count).ToString();
}
}
Hope this would help someone else for filtering the Choice List
in _InitializeDataWorkspace:
// get count of females
double fgender = (from gender in InsuranceQuotations
where gender.mGender == "Female"
select gender).Count();
//get sum of females ages
double female = InsuranceQuotations.Where(x => x.mGender == "Female").Sum(t => t.mAge);
// get count males
double mgender = (from gender in InsuranceQuotations
where gender.mGender == "Male"
select gender).Count();
//get sum of males ages
double male = InsuranceQuotations.Where(x => x.mGender == "Male").Sum(t => t.mAge);
// MeanFmale amd MeanMmale - The fields that display
MeanFmale = (female / fgender).ToString();
MeanMmale = (male / mgender).ToString();
Or
double fmale = InsuranceQuotations.Where(x => x.mGender == "Female").Average(t => t.mAge);
double mmale = InsuranceQuotations.Where(x => x.mGender == "Male").Average(t => t.mAge);
// MeanFmale amd MeanMmale - The fields that display
MeanFmale = fmale.ToString();
MeanMmale = mmale.ToString();
I have the below class:
public class FactoryOrder
{
public string Text { get; set; }
public int OrderNo { get; set; }
}
and collection holding the list of FactoryOrders
List<FactoryOrder>()
here is the sample data
FactoryOrder("Apple",20)
FactoryOrder("Orange",21)
FactoryOrder("WaterMelon",42)
FactoryOrder("JackFruit",51)
FactoryOrder("Grapes",71)
FactoryOrder("mango",72)
FactoryOrder("Cherry",73)
My requirement is to merge the Text of FactoryOrders where orderNo are in sequence and retain the lower orderNo for the merged FactoryOrder
- so the resulting output will be
FactoryOrder("Apple Orange",20) //Merged Apple and Orange and retained Lower OrderNo 20
FactoryOrder("WaterMelon",42)
FactoryOrder("JackFruit",51)
FactoryOrder("Grapes mango Cherry",71)//Merged Grapes,Mango,cherry and retained Lower OrderNo 71
I am new to Linq so not sure how to go about this. Any help or pointers would be appreciated
As commented, if your logic depends on consecutive items so heavily LINQ is not the easiest appoach. Use a simple loop.
You could order them first with LINQ: orders.OrderBy(x => x.OrderNo )
var consecutiveOrdernoGroups = new List<List<FactoryOrder>> { new List<FactoryOrder>() };
FactoryOrder lastOrder = null;
foreach (FactoryOrder order in orders.OrderBy(o => o.OrderNo))
{
if (lastOrder == null || lastOrder.OrderNo == order.OrderNo - 1)
consecutiveOrdernoGroups.Last().Add(order);
else
consecutiveOrdernoGroups.Add(new List<FactoryOrder> { order });
lastOrder = order;
}
Now you just need to build the list of FactoryOrder with the joined names for every group. This is where LINQ and String.Join can come in handy:
orders = consecutiveOrdernoGroups
.Select(list => new FactoryOrder
{
Text = String.Join(" ", list.Select(o => o.Text)),
OrderNo = list.First().OrderNo // is the minimum number
})
.ToList();
Result with your sample:
I'm not sure this can be done using a single comprehensible LINQ expression. What would work is a simple enumeration:
private static IEnumerable<FactoryOrder> Merge(IEnumerable<FactoryOrder> orders)
{
var enumerator = orders.OrderBy(x => x.OrderNo).GetEnumerator();
FactoryOrder previousOrder = null;
FactoryOrder mergedOrder = null;
while (enumerator.MoveNext())
{
var current = enumerator.Current;
if (mergedOrder == null)
{
mergedOrder = new FactoryOrder(current.Text, current.OrderNo);
}
else
{
if (current.OrderNo == previousOrder.OrderNo + 1)
{
mergedOrder.Text += current.Text;
}
else
{
yield return mergedOrder;
mergedOrder = new FactoryOrder(current.Text, current.OrderNo);
}
}
previousOrder = current;
}
if (mergedOrder != null)
yield return mergedOrder;
}
This assumes FactoryOrder has a constructor accepting Text and OrderNo.
Linq implementation using side effects:
var groupId = 0;
var previous = Int32.MinValue;
var grouped = GetItems()
.OrderBy(x => x.OrderNo)
.Select(x =>
{
var #group = x.OrderNo != previous + 1 ? (groupId = x.OrderNo) : groupId;
previous = x.OrderNo;
return new
{
GroupId = group,
Item = x
};
})
.GroupBy(x => x.GroupId)
.Select(x => new FactoryOrder(
String.Join(" ", x.Select(y => y.Item.Text).ToArray()),
x.Key))
.ToArray();
foreach (var item in grouped)
{
Console.WriteLine(item.Text + "\t" + item.OrderNo);
}
output:
Apple Orange 20
WaterMelon 42
JackFruit 51
Grapes mango Cherry 71
Or, eliminate the side effects by using a generator extension method
public static class IEnumerableExtensions
{
public static IEnumerable<IList<T>> MakeSets<T>(this IEnumerable<T> items, Func<T, T, bool> areInSameGroup)
{
var result = new List<T>();
foreach (var item in items)
{
if (!result.Any() || areInSameGroup(result[result.Count - 1], item))
{
result.Add(item);
continue;
}
yield return result;
result = new List<T> { item };
}
if (result.Any())
{
yield return result;
}
}
}
and your implementation becomes
var grouped = GetItems()
.OrderBy(x => x.OrderNo)
.MakeSets((prev, next) => next.OrderNo == prev.OrderNo + 1)
.Select(x => new FactoryOrder(
String.Join(" ", x.Select(y => y.Text).ToArray()),
x.First().OrderNo))
.ToList();
foreach (var item in grouped)
{
Console.WriteLine(item.Text + "\t" + item.OrderNo);
}
The output is the same but the code is easier to follow and maintain.
LINQ + sequential processing = Aggregate.
It's not said though that using Aggregate is always the best option. Sequential processing in a for(each) loop usually makes for better readable code (see Tim's answer). Anyway, here's a pure LINQ solution.
It loops through the orders and first collects them in a dictionary having the first Id of consecutive orders as Key, and a collection of orders as Value. Then it produces a result using string.Join:
Class:
class FactoryOrder
{
public FactoryOrder(int id, string name)
{
this.Id = id;
this.Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
}
The program:
IEnumerable<FactoryOrder> orders =
new[]
{
new FactoryOrder(20, "Apple"),
new FactoryOrder(21, "Orange"),
new FactoryOrder(22, "Pear"),
new FactoryOrder(42, "WaterMelon"),
new FactoryOrder(51, "JackFruit"),
new FactoryOrder(71, "Grapes"),
new FactoryOrder(72, "Mango"),
new FactoryOrder(73, "Cherry"),
};
var result = orders.OrderBy(t => t.Id).Aggregate(new Dictionary<int, List<FactoryOrder>>(),
(dir, curr) =>
{
var prevId = dir.SelectMany(d => d.Value.Select(v => v.Id))
.OrderBy(i => i).DefaultIfEmpty(-1)
.LastOrDefault();
var newKey = dir.Select(d => d.Key).OrderBy(i => i).LastOrDefault();
if (prevId == -1 || curr.Id - prevId > 1)
{
newKey = curr.Id;
}
if (!dir.ContainsKey(newKey))
{
dir[newKey] = new List<FactoryOrder>();
}
dir[newKey].Add(curr);
return dir;
}, c => c)
.Select(t => new
{
t.Key,
Items = string.Join(" ", t.Value.Select(v => v.Name))
}).ToList();
As you see, it's not really straightforward what happens here, and chances are that it performs badly when there are "many" items, because the growing dictionary is accessed over and over again.
Which is a long-winded way to say: don't use Aggregate.
Just coded a method, it's compact and quite good in terms of performance :
static List<FactoryOrder> MergeValues(List<FactoryOrder> dirtyList)
{
FactoryOrder[] temp1 = dirtyList.ToArray();
int index = -1;
for (int i = 1; i < temp1.Length; i++)
{
if (temp1[i].OrderNo - temp1[i - 1].OrderNo != 1) { index = -1; continue; }
if(index == -1 ) index = dirtyList.IndexOf(temp1[i - 1]);
dirtyList[index].Text += " " + temp1[i].Text;
dirtyList.Remove(temp1[i]);
}
return dirtyList;
}
I am using NEST (ver 0.12) Elasticsearch (ver 1.0) and I'm facing a problem with the facets.
Basically I'm expecting the results to be something similar to this
Between 18 and 25 (10)
Between 26 and 35 (80)
Greater then 35 (10)
But what I'm actually getting is this
between (99)
and (99)
35 (99)
26 (99)
This is my code
namespace ElasticSerachTest
{
class Program
{
static void Main(string[] args)
{
var setting = new ConnectionSettings(new Uri("http://localhost:9200/"));
setting.SetDefaultIndex("customertest");
var client = new ElasticClient(setting);
var createIndexResult = client.CreateIndex("customertest", new IndexSettings
{
});
// put documents to the index using bulk
var customers = new List<BulkParameters<Customer>>();
for (int i = 1; i < 100; i++)
{
customers.Add(new BulkParameters<Customer>(new Customer
{
Id = i,
AgeBand = GetAgeBand(),
Name = string.Format("Customer {0}", i)
}));
}
var bp = new SimpleBulkParameters()
{
Replication = Replication.Async,
Refresh = true
};
//first delete
client.DeleteMany(customers, bp);
//now bulk insert
client.IndexMany(customers, bp);
// query with facet on nested field property genres.genreTitle
var queryResults = client.Search<Customer>(x => x
.From(0)
.Size(10)
.MatchAll()
.FacetTerm(t => t
.OnField(f => f.AgeBand)
.Size(30))
);
var yearFacetItems = queryResults.FacetItems<FacetItem>(p => p.AgeBand);
foreach (var item in yearFacetItems)
{
var termItem = item as TermItem;
Console.WriteLine(string.Format("{0} ({1})", termItem.Term, termItem.Count));
}
Console.ReadLine();
}
public static string GetAgeBand()
{
Random rnd = new Random();
var age = rnd.Next(18, 50);
if (Between(age, 18, 25))
{
return "Between 18 and 25";
}
else if (Between(age, 26, 35))
{
return "Between 26 and 35";
}
return "Greater then 35";
}
public static bool Between(int num, int lower, int upper)
{
return lower <= num && num <= upper;
}
[ElasticType(Name = "Customer", IdProperty = "id")]
public class Customer
{
public int Id
{
get;
set;
}
public string Name
{
get;
set;
}
[ElasticProperty(Index = FieldIndexOption.not_analyzed)]
public string AgeBand
{
get;
set;
}
}
}
}
Thanks
Based on the output you are seeing, I do not think your FieldIndexOption.not_analyzed is being applied to the AgeBand field. As those facet results look like like analyzed values. You need to apply the mapping during the index creating process as part of your index settings. Please try the following index creation code:
var createIndexResult = client.CreateIndex("customertest", s => s
.AddMapping<Customer>(m => m
.MapFromAttributes()
)
);
Please see the Nest Documentation on Mapping for some other ways to add the mapping to your index as well.