2

I've seen this question asked in different ways (check if a list contains a certain string or whether a string contains any given character) but I need something else.

The programme I'm working on is Poker-related and displays list of poker hands in the format [rank][suit], e.g. AhKd5h3c, in multiple DataGridViews.

Right now, I have this rudimentary textbox filter in place which is working fine.

for (int i = 0; i < allFilteredRows.Count; i++)
{
     allFilteredRows[i] = new BindingList<CSVModel>(allRows[i].Where
    (x => x.Combo.Contains(txtHandFilter.Text)).ToList());          
} 

allFilteredRows is the data source for my DataGridViews. allRows is the unfiltered list of hands from an SQL database. Combo is an individual poker hand.

That only filters for the exact sequence of characters in the textbox, though. What I want is to filter for each rank and suit individually. So if the user types 'AK', all combos that contain (at least) one ace and one king should be displayed. If the input is 'sss', it should filter for all hands with at least three spades. The order of the characters should not matter ('KA' is equal to 'AK') but every character needs to be included and ranks and suits can be combined, e.g. AKh should filter for all hands with at least one ace and the king of hearts.

This goes beyond my knowledge of LINQ so I'd be grateful for any help.

7
  • 2
    Enumerable.All is your friend. Where(r => txtHandFilter.Text.All(cardCh => r.Combo.Contains(cardCh))) Commented Oct 5, 2021 at 21:21
  • So I've managed to make something like this work. The problem is however that this doesn't account for multiple occurrences of the same character. So 'AA98' displays all A98* hands, and 'ssss' displays all hands that contain at least one spade. Commented Oct 6, 2021 at 9:52
  • I decided this needed more code to handle e.g. 5h5 properly. See my answer. Also, what should 5h5 match? What should h5h5 match? Commented Oct 6, 2021 at 20:22
  • Also, should repeated cards be ignored or fail? e.g. 5h5h needs to match two 5h or just one? Commented Oct 6, 2021 at 20:34
  • 5h5 should display all hands containing the 5 of hearts and one more 5 of any suit. h5h5 ideally filters for the 5 of hearts, plus at least one additional 5 and one additional heart, e.g. Ah5h5s3d. 5hh would be the 5 of hearts plus at least one additional heart. 5h5h is not possible as there is only one 5h in the deck but I guess it can be ignored. If you're interested, here is additional syntax that other tools implement: propokertools.com/simulations/generic_syntax But this way beyond of what I need at this point. Thanks for your help! I'll have a look at your answer now. Commented Oct 6, 2021 at 21:02

3 Answers 3

1

It seems to me you have two split operations you need to perform. First, you need to split up your filter string so that it consists of the individual card filters in it - either rank, suit, or a particular card. You can do this using regular expressions.

First, create character sets representing possible ranks and possible suits:

var ranks = "[23456789TJQKA]";
var suits = "[hsdc]";

Then, create a regular expression to extract the individual card filters:

var aCardFilter = $"{ranks}{suits}";

Finally, using an extension method that returns the values of the matches from a regular expression (or inlining it), you can split the filter and then group the similar filters. You will end up with two filters, individual card filters and rank/suite filters:

var cardFilters = txtHandFilter.Text.Matches(aCardFilter).ToList();
var suitRankFilters = txtHandFilter.Text.GroupBy(filterCh => filterCh).ToList();

Now you need to split the hand string into a collection of cards. Since each card is two characters, you can just split on substrings at every 2nd position. I wrapped this in a local function to make the code clearer:

IEnumerable<string> handToCards(string hand) => Enumerable.Range(0, hand.Length / 2).Select(p => hand.Substring(2 * p, 2));

Now you can test a hand for matching the card filters by checking that each card occurs in the hand, and for matching the suite/rank filters by checking that each occurs at least as often in the hand as in the filters:

bool handMatchesCardFilters(string hand, List<string> filters)
    => filters.All(filter => handToCards(hand).Contains(filter));
bool handMatchesFilters(string hand, List<IGrouping<char, char>> filters)
    => filters.All(fg => handToCards(hand).Count(card => card.Contains(fg.Key)) >= fg.Count());

Finally you are ready to filter the rows:

for (int i = 0; i < allFilteredRows.Count; ++i)
    allFilteredRows[i] = new BindingList<CSVModel>(
        allRows[i].Where(row => handMatchesCardFilters(row.Combo, cardFilters) &&
                               handMatchesFilters(row.Combo, suitRankFilters))
                  .ToList());

The extension method needed is

public static class StringExt {
    public static IEnumerable<string> Matches(this string s, string sre) => Regex.Matches(s, sre).Cast<Match>().Select(m => m.Value);
}
Sign up to request clarification or add additional context in comments.

1 Comment

Works perfectly! It'll take me while to fully understand everything but this is a great starting point to implement additional filters/syntax in the future. Thanks a lot!
0
  1. Convert your filter string to List of cards (from what I see, each item is length of 2 if 2nd char is small, length of 1 otherwise).

  2. Use GroupBy to get count of each card, so you should have struct { "K", 3 } instead of "KKK". https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.groupby?view=net-5.0

  3. Repeat on each hand (I assume, that each hand is List instead of just "AhKd5h3c" string)

  4. Iterate through hands checking if it contains all items from p2 output (I assume that { "K", 3 } should return combo that has 4 "K" as well:

    public bool Match(object GroupedCombo, object filter)
    {
        foreach (var item in filter)
        {
            if (!GroupedCombo.Contains(x => x.Card==filter.Card && x.Count>=filter.Count))
                return false;
        }
        return true;
    }
    

2 Comments

I'm a little stuck at step 4. 'Contains' cannot be used with type object but the parameters I'm passing to Match() are of type anonymous. How can I make this work?
@GasPanic object should not be anonymous, but known class. They used anonymous, because it's not needed in group by. However, you should be able to use class constructor: new MyGroup() { } instead of just new { }.
0

It sounds like you want to evaluate multiple conditions for your filter...

The conditions that you need to evaluate look like they are going to require you to parse through a string and extrapolate representations of card combinations. Problem here is it doesn't have any delimiters.

For my suggestion you will need them:

for (int i = 0; i < allFilteredRows.Count; i++)
{
     allFilteredRows[i] = new BindingList<CSVModel>(allRows[i].Where
    (x => txtHandFilter.Text.Split(" ").All(y => x.Combo.Contains(y)).ToList());          
} 

It the textbox you should have the card combos separated by spaces.

You could find some other way to delimit the txtHandFilter if you like, but I think this answers your base question.

Edit: I can think of only two options for coming up with the string array from the text box that you want:

1: Delimit it.

2: Parse through it character by character to find strings that match a card type representation from a dictionary.(To me this seems more impractical)

As for how to count occurrences I think that Michał Woliński has the right idea.

using System;
using System.Linq;

var cards = from card in txtHandFilter.Text.Split(" ")
     group card by card
     into g
     select new { Card = g.Key, Count = g.Count() };

for (int i = 0; i < allFilteredRows.Count; i++)
{
     allFilteredRows[i] = new BindingList<CSVModel>(allRows[i]
          .Where(x => cards
          .All(y => x.Combo.Contains(String.Concat(Enumerable.Repeat(y.Card, y.Count)))
          .ToList());          
} 

1 Comment

The problem with this solution is, as you say, that you need a delimiter which isn't very practical but also that it doesn't filter characters that occur multiple times in the search string. So 'AA' doesn't show all hands that contain minimum two aces but all hands with at least one ace.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.