Need Lambda query with if depending on the values - c#

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()
});

Related

Get the index of item in list based on value

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:

LINQ grouping without changing the order [duplicate]

Let's say I have following data:
Time Status
10:00 On
11:00 Off
12:00 Off
13:00 Off
14:00 Off
15:00 On
16:00 On
How could I group that using Linq into something like
[On, [10:00]], [Off, [11:00, 12:00, 13:00, 14:00]], [On, [15:00, 16:00]]
Create a GroupAdjacent extension, such as the one listed here.
And then it's as simple as:
var groups = myData.GroupAdjacent(data => data.OnOffStatus);
You could also do this with one Linq query using a variable to keep track of the changes, like this.
int key = 0;
var query = data.Select(
(n,i) => i == 0 ?
new { Value = n, Key = key } :
new
{
Value = n,
Key = n.OnOffFlag == data[i - 1].OnOffFlag ? key : ++key
})
.GroupBy(a => a.Key, a => a.Value);
Basically it assigns a key for each item that increments when the current item does not equal the previous item. Of course this assumes that your data is in a List or Array, otherwise you'd have to try a different method
Here is a hardcore LINQ solution by using Enumerable.Zip to compare contiguous elements and generate a contiguous key:
var adj = 0;
var t = data.Zip(data.Skip(1).Concat(new TimeStatus[] { null }),
(x, y) => new { x, key = (x == null || y == null || x.Status == y.Status) ? adj : adj++ }
).GroupBy(i => i.key, (k, g) => g.Select(e => e.x));
It can be done as.
Iterate over collection.
Use TakeWhile<Predicate> condition is text of first element of collection On or Off.
Iterate over the subset of from point one and repeat above step and concatenate string.
Hope it helps..
You could parse the list and assign a contiguous key e.g define a class:
public class TimeStatus
{
public int ContiguousKey { get; set; }
public string Time { get; set; }
public string Status { get; set; }
}
You would assign values to the contiguous key by looping through, maintaining a count and detecting when the status changes from On to Off and so forth which would give you a list like this:
List<TimeStatus> timeStatuses = new List<TimeStatus>
{
new TimeStatus { ContiguousKey = 1, Status = "On", Time = "10:00"},
new TimeStatus { ContiguousKey = 1, Status = "On", Time = "11:00"},
new TimeStatus { ContiguousKey = 2, Status = "Off", Time = "12:00"},
new TimeStatus { ContiguousKey = 2, Status = "Off", Time = "13:00"},
new TimeStatus { ContiguousKey = 2, Status = "Off", Time = "14:00"},
new TimeStatus { ContiguousKey = 3, Status = "On", Time = "15:00"},
new TimeStatus { ContiguousKey = 3, Status = "On", Time = "16:00"}
};
Then using the following query you can extract the Status and grouped Times:
var query = timeStatuses.GroupBy(t => t.ContiguousKey)
.Select(g => new { Status = g.First().Status, Times = g });

Returning a nested dictionary within another dictionary with Linq

I have a little problem with creating a complex Linq query. I have the following tables:
Activities
----------
- Date : DateTime
- ProjectId : int
Projects
--------
- ProjectId
- ProjectNumber
I need to construct a query that returns a dictionary with the distinct years within the Activities table as keys. The value should be another dictionary containing all distinct months within the current distinct year as keys and then for each distinct month, I need a list of strings containing all the project numbers for that month.
So I would end up with something like this:
- 2014 //First distinct year
- 1 //January
- Contoso-2014-01 //Project number
- 3 //March
- IBM-2014-06 //Project number
- 2016 //Second distinct year
- 4 //April
- HP-2016-02 //Project number
Basically, we would have a dictionary containing two keys: 2014 and 2016
The values for the key 2014 would be a Dictionary with two KeyValuePairs. One with the key 1 and another with the key 3. Key 1 would have a list of strings as it's values containing the project number "Contoso-2014-01" and key 3 would contain "IBM-2014-06". And so on...
So now to my question: Is it even possible to query a database and get a return type structure like this? If yes, how can I achieve this?
Considering you have classes such as these
public class Activities
{
public DateTime Date { get; set; }
public Project Project { get; set; }
}
public class Project
{
public int ProjectId { get; set; }
public string ProjectNumber { get; set; }
}
Then I think this should work
public static void GetData()
{
var mainDic = new Dictionary<int, Dictionary<int, List<string>>>();
List<Activities> acts = new List<Activities>(); // Your database context.
acts.Select(x => x.Date.Year).Distinct().ToList().ForEach(
year =>
{
var yearlyDic = new Dictionary<int, List<string>>();
acts.Where(x => x.Date.Year == year).Select(x => x.Date.Month).Distinct().ToList().ForEach(
month =>
{
var projects = acts.Where(x => x.Date.Year == year && x.Date.Month == month)
.Select(x => x.Project.ProjectNumber).ToList();
yearlyDic.Add(month, projects);
});
mainDic.Add(year, yearlyDic);
});
}
I am assuming you are using SQL Server.. Here filtering Year part and Month part in the db query itself.
select YEAR(ac.[Date]) as projectYear, MONTH(ac.[Date]) as projectMonth ,pr.ProjectNumber Info from Activities ac join Projects pr on ac.ProjectId=pr.ProjectId
class ProjectActivity
{
public int Year { get; set; }
public int Month { get; set; }
public string ProjectNumber { get; set; }
public static List<ProjectActivity> GetProjectActivities()
{
//You can use above query and construct the list.
var sampleProjectActivities = new List<ProjectActivity>();
var projActivitySamp1 = new ProjectActivity()
{
Year = 2014,
Month = 1,
ProjectNumber = "Contoso-2014-01"
};
sampleProjectActivities.Add(projActivitySamp1);
var projActivitySamp2 = new ProjectActivity()
{
Year = 2014,
Month = 3,
ProjectNumber = "Contoso-2014-03"
};
sampleProjectActivities.Add(projActivitySamp2);
var projActivitySamp3 = new ProjectActivity()
{
Year = 2016,
Month = 4,
ProjectNumber = "HP-2016-02"
};
sampleProjectActivities.Add(projActivitySamp3);
var projActivitySamp4 = new ProjectActivity()
{
Year = 2016,
Month = 4,
ProjectNumber = "AnotherHP-2016-04"
};
sampleProjectActivities.Add(projActivitySamp4);
return sampleProjectActivities;
}
}
And call the code like this
var sampleProjectActivities = ProjectActivity.GetProjectActivities();
var result = sampleProjectActivities.GroupBy(projectActivity => projectActivity.Year)
.ToDictionary(k => k.Key,
v =>
{
return v.GroupBy(val => val.Month).ToDictionary(a => a.Key, b => b.Select(x => x.ProjectNumber).ToArray());
});
Assuming that your classes are named as you have mentioned (in the tables)..
This lambda should give you the dictionary that you need..
var dictionary = acts.GroupBy(activity => activity.Date.Year) // gives you year-wise groups
.ToDictionary(yearGroup => yearGroup.Key,
yearGroup => yearGroup.ToDictionary(activity => activity.Date.Month, // gives you month-wise groups
activity => yearGroup.Where(a => a.Date.Month == activity.Date.Month)
.Select(a => a.Project.ProjectNumber)
.ToList() // all the project numbers under this year and month
));
This seems to be the most straight-forward way to go:
Dictionary<int, Dictionary<int, string>> query =
(
from a in activities
join p in projects on a.ProjectId equals p.ProjectId
group new
{
a.Date.Month,
p.ProjectNumber,
} by a.Date.Year into gaps
select new
{
gaps.Key,
Value = gaps.ToDictionary(x => x.Month, x => x.ProjectNumber),
}
).ToDictionary(x => x.Key, x => x.Value);

How to intersect results after GroupBy

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
}
}

How to merge two lists based on a property?

I have two lists, one fake and one real, like:
BEFORE
// fake (list 1)
{ ID = 1, Year = 2011, X = "" }
, { ID = 2, Year = 2012, X = "" }
, { ID = 3, Year = 2013, X = "" }
// real (list 2)
{ ID = 35, Year = 2011, X = "Information" }
, { ID = 77, Year = 2013, X = "Important" }
I want to merge them looking for the Year, the result should be:
AFTER
{ ID = 35, Year = 2011, X = "Information" }
, { ID = 2, Year = 2012, X = "" }
, { ID = 77, Year = 2013, X = "Important" }
It must remove elements with the same year on the first list and add the element with the equivalent Year on the second list to the first list, keeping the order.
How can I do it using Linq?
You should be able to do that using a "left join":
from f in fake
join r in real
on f.Year equals r.Year
into joinResult
from r in joinResult.DefaultIfEmpty()
select new
{
ID = r == null ? f.ID : r.ID,
Year = f.Year,
X = r == null ? f.X : r.X
};
Justin's query is the most efficient way to do it, but if you're concerned with keeping identical objects (and not creating new records from the query) you could do it like this:
var combined = from f in fake
let r = (from r1 in real
where r1.Year == f.Year
select r1).SingleOrDefault()
select r ?? f;
Using IEnumerable.Union and IEqualityComparer.
P.S. This would result in a different result when compared to left join if the real list had more elements (years that are not present in fake list). The left join would not return those results which could be a desired result (not clear from OP).
public class MyClass
{
public int ID {get; set;}
public int Year {get; set;}
public string X {get; set;}
}
public class MyClassEqualityComparer : IEqualityComparer<MyClass>
{
public bool Equals(MyClass x, MyClass y)
{
return x.Year == y.Year;
}
public int GetHashCode(MyClass obj)
{
return obj.ToString().ToLower().GetHashCode();
}
}
void Main()
{
var fake = new List<MyClass> {
new MyClass { ID = 1, Year = 2011, X = "" }
, new MyClass { ID = 2, Year = 2012, X = "" }
, new MyClass { ID = 3, Year = 2013, X = "" }
};
var real = new List<MyClass> {
new MyClass { ID = 35, Year = 2011, X = "Information" }
, new MyClass { ID = 77, Year = 2013, X = "Important" }
};
var merged = real.Union(fake, new MyClassEqualityComparer());
}
Instead of defining the fake list yourself, try having Linq do it for you:
Enumerable.Range(2011,3) //2011, 2012, 2013
//use the overload that provides a 0-based ordinal position of each element
.Select(x,i=> new {ID = i+1, Year = x, X = String.Empty)
//now you have your fake list; join with the "real" list based on Year fields,
//taking the real element wherever it exists and the fake one otherwise
.Join(real, l=>l.Year, r=>r.Year, (l,r) => r == null ? l : r);
This will produce exactly the result set you want. You will likely need to define a named type for the list items, though, as two separately-defined anonymous types cannot be implicitly converted even if they have all the same member types/names.

Categories

Resources