简体   繁体   中英

Comparing the contents of two lists with linq (C#) to see if one specific value is in a certain range

Ok, I have smashed in my head for the better of the day now.

I have two lists of custom objects with one property the same at both lists. I need to iterate through both lists and see if the property is the same.

I could do it with nested for-each loops, but I'm rather reluctant if I can do the same with LINQ (and I'm sure I can do that). I have tried almost everything but I simply can't get to the solution I am looking for.

Here is the code for the objects I am using for the Lists.

public class Game
{
    // Fields
    private short maxPlayers;
    private Team axis;
    private Team allies;

    // Properties
    public string Name { get; set; }
    public short MaxPlayers
    {
        get
        {
            return maxPlayers;
        }
        set
        {
            if (value > 8)
                maxPlayers = 8;
            else if (value < 2)
                maxPlayers = 2;
            else
                maxPlayers = value;
        }
    }
    public short CurrentPlayers
    {
        get
        {
            int players = axis.Players.Count + allies.Players.Count;
            return (short)players;
        }
    }
    public bool IsFull
    {
        get
        {
            if (CurrentPlayers == MaxPlayers)
                return true;
            else
                return false;
        }
    }
    public Team Axis { get; set; }
    public Team Allies { get; set; }
    public List<Player> Players
    {
        // Somehow this does not work either, so I had to stick with one single team in the for-each loops. Ideas to fix?
        get
        {
            if (allies.Players.Count == 0)
                return axis.Players.Concat(allies.Players).ToList();
            else
                return allies.Players.Concat(axis.Players).ToList();

        }
    }

    //Constructor
    public Game()
    {
        axis = new Team();
        allies = new Team();
    }
}

public class Team
{
    public List<Player> Players { get; set; }

    public EFaction Faction { get; set; }

    public enum EFaction
    {
        Allies,
        Axis,
        Random
    }

    public Team()
    {
        Players = new List<Player>();
        Faction = EFaction.Random;
    }
}

public class Player
{
    private int skillRange = 200;

    public string Name { get; set; }
    public int Skill { get; set; }
    public int SkillRange
    {
        get
        {
            return skillRange;
        }
        set
        {
            if (value >= 200)
                skillRange = value;
            else
                skillRange = 200;
        }
    }
}

At startup I populate the List from a database and do the same with the List. What I want to do is iterate through the game list and compare the skill property for each player in the game with the skill property of each player on a team. Here is the for-each loop I used. this worked. But you clearly can see why I want to cut it down so badly.

        // Loop through each player in the Automatch queue.
        foreach (Team team in match.TeamsInQueue)
        {
            // Loop through every game in the Atomatch queue.
            foreach (Game game in match.AvailableGames)
            {
                int teamPlayersInSkillRange = 0;

                // Loop through every player in the team and loop through every player in the game.
                foreach (Player teamPlayer in team.Players)
                {
                    int gamePlayersInSkillRange = 0;
                    foreach (Player gamePlayer in game.Allies.Players)
                    {
                        // Compare beoth skill values. If they are in a certain range increase the counter.
                        if (Math.Abs(teamPlayer.Skill - gamePlayer.Skill) <= 200) // The range is currently set for 200, but I want to make it variable later.
                            gamePlayersInSkillRange++;
                    }

                    // Check if the player in the team is in skill range of the game he wants to join. If yes increase the counter.
                    if (gamePlayersInSkillRange == game.Allies.Players.Count)
                        teamPlayersInSkillRange++;
                  }
                // Check if the whole team is in skill range of the game they want to join. If yes return true.
                if (teamPlayersInSkillRange == team.Players.Count)
                {
                    // ToDo: Implement join process here.
                }
            }
        }

Any help would be appreciated. Thanks.

For a given team and game, you want the team to join the game if all of the team's player's are in the skill range of all the game's players. This sounds like a job for the All() method!

// Loop through each player in the Automatch queue.
foreach (Team team in match.TeamsInQueue)
{
    // Loop through every game in the Atomatch queue.
    foreach (Game game in match.AvailableGames)
    {
        bool allInSkillRange = team.Players.All(t =>
            game.Allies.Players.All(g => Math.Abs(t.Skill - g.Skill) <= 200));
        if(allInSkillRange)
        {
            // ToDo: Implement join process here.
        }
    }
}

If you're interested in an automated way to convert code to LINQ, look at Resharper .

Here's how I came up with the solution. Using this process along with becoming familiar with LINQ's methods and acquiring experience can make refactoring much easier. I didn't just look at the code and immediately think of using All() . I started by refactoring smaller sections and then going from there. After reading over all of the code I focused on the inner-most loop.

int gamePlayersInSkillRange = 0;
foreach (Player gamePlayer in game.Allies.Players)
{
    // Compare beoth skill values. If they are in a certain range increase the counter.
    if (Math.Abs(teamPlayer.Skill - gamePlayer.Skill) <= 200) // The range is currently set for 200, but I want to make it variable later.
        gamePlayersInSkillRange++;
}

This counts the number of players that satisfy a condition, which can be refactored into a Count() call:

int gamePlayersInSkillRange = game.Allies.Players.Count(g => 
    (Math.Abs(teamPlayer.Skill - g.Skill) <= 200);

The following if statement checks if gamePlayersInSkillRange equals the number of items in game.Allies.Players, which is the list that we were originally counting. Oh, so we're checking if all of those list members satisfy the predicate! We can include that step in the LINQ call by changing Count() to All() . Here is what the next innermost loop looks like now:

foreach (Player teamPlayer in team.Players)
{
    bool allGamePlayersInRange = game.Allies.Players.All(g => 
        (Math.Abs(teamPlayer.Skill - g.Skill) <= 200);

    // Check if the player in the team is in skill range of the game he wants to join. If yes increase the counter.
    if (allGamePlayersInRange)
        teamPlayersInSkillRange++;

}

Now this loop looks like it could be refactored into a Count() call. But if we examine the code just after it closely, we see it is the exact same pattern as the loop we just refactored, meaning we can jump right into refactoring it into an All() call, completing the refactoring.

Try this:

foreach(Team team in match.TeamsInQueue)
{
    if(team.Players.Insersect(match
           .SelectMany(m => m.AvailableGames, g=> g.Allies.Players), 
            new PlayerSkillComparer().Count() == team.Players.Count()) {
         // ToDO: Implement join process here.
    }
}

Where PlayerSkillComparer implements IEqualityComparer<Player> and its Equals method returns true if the two given Player objects have a skill difference <= 200. See an example here .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM