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