Click here to Skip to main content
15,885,366 members
Articles / Programming Languages / C#

Scrabble Word Finder

Rate me:
Please Sign up or sign in to vote.
4.51/5 (9 votes)
5 Oct 2017CPOL2 min read 15.9K   245   6   4
Test your Linq-fu with anonymous types, grouping, null continuation and coalescing operators

Introduction

I recently knew that I had a valid 8 letter word (my 7 letters plus a letter on the board) -- Word With Friends word finder told me I could write the 8 letter word -- but I didn't know what the word was.

I had these letters to work with, including the 's' on the board:

C#
List<char> myLetters = new List<char>() { 't', 'r', 'o', 'i', 'l', 'b', 's', 's' };

Using the More Words site with the search "8 letter words starting with s" I got 3480 words that fit that criteria. But which one matched?

First Pass

Given that I knew the word began and ended with an 's', I did this:

C#
string[] words = File.ReadAllLines("words.txt");
var f1 = words.Where(w => w[0] == 's' && w[7] == 's');

var f2 = (from word in f1
where myLetters.All(c => word.Any(wc => wc == c))
select word).ToList();

f2.ForEach(w => Console.WriteLine(w));

Presto, there's my word: "strobils" -

1. A cone of a gymnosperm or of a seedless vascular plant such as a horsetail or a club moss.
2. Any of various similar structures, such as the female inflorescence of a hop plant, which is composed of small flowers obscured by overlapping green bracts.

But I was pre-filtering the entire word list knowing the beginning and ending letters. If I didn't use that filter:

C#
var f2 = (from word in <font color="#FF0000">words</font>
where myLetters.All(c => word.Any(wc => wc == c))
select word).ToList();

I got this list:

sorbitol
strobila
strobile
strobili
strobils

Notice that there are words with duplicate letters and words that have other letters that I don't have.

Second Pass

So how I do match exactly on the letters that are available? Well, count them and make sure that the count of each letter in a possible word matches the count of letters I have available to me. This gives me a count of my letters:

C#
var occurences = myLetters.Select(c => new { letter = c, count = myLetters.Count(o => o == c) });

Image 1

Notice that we have two entries for 's'. Let's clean that up:

C#
var occurences = myLetters.Distinct().
  Select(c => new { letter = c, count = myLetters.Count(o => o == c) });

Similarly, we do the same thing for each possible word (the ToList() makes the debugger readable):

C#
var wordLetterCounts = words.Select(word =>
 new
 {
   word = word,
   counts =
     word.GroupBy(c => c).Select((a, b) =>
     new
     {
       letter = a.Key,
       count = word.Count(c => c == a.Key)
     }).ToList()
 });

The first two words in the list of words:

Image 2

Now we can match counts of letters in the test word with the letters available to me:

C#
var matches = wordLetterCounts.Where(wlc => 
  wlc.counts.All(letterCount => 
    occurences.Single(o => o.letter == letterCount.letter).count == letterCount.count));

var listOfMatches = matches.ToList();

Oops:

Image 3

The problem here is that my list of occurrences (elephant, I misspelled "occurrences" in the code) of each letter may not contain the letter in the test word.

Third Pass

Null continuation and null coalescing operators to the rescue (fixed the spelling error too):

C#
var matches = wordLetterCounts.Where(wlc => 
  wlc.counts.All(letterCount => 
    (occurrences.SingleOrDefault(o => 
       o.letter == letterCount.letter)?.count ?? 0) == letterCount.count));

Success!

Image 4

Alternate Implementation

An alternate Linq statement that is "simpler" combines the pieces together without creating the temporary anonymous type wordLetterCounts:

C#
var matches = words.Select(word =>
{
  return new { word = word, matches =
  word.GroupBy(c => c).Select((a, b) => new { letter = a.Key, count = word.Count(c => c == a.Key) })
  .All(q => (occurrences.FirstOrDefault(o => o.letter == q.letter)?.count ?? 0) == q.count) };
}).Where(word => word.matches);

A Simple UI

Image 5

C#
using System;
using System.Data;
using System.Linq;
using System.Windows.Forms;

namespace WordFinderUI
{
  public partial class Form1 : Form
  {
    public Form1()
    {
      InitializeComponent();
      tbWordList.MaxLength = Int32.MaxValue;
    }

  private void btnFind_Click(object sender, EventArgs e)
  {
    var myLetters = tbMyLetters.Text.ToList();
    var occurrences = myLetters.
       Distinct().
       Select(c => new { letter = c, count = myLetters.Count(o => o == c) });
    var words = tbWordList.Text.Split(new char[] { '\r' }).Select(w=>w.Trim());

    var matches = words.Select(word =>
    {
      return new
      {
        word = word,
        matches =
        word.GroupBy(c => c).
          Select((a, b) => new { letter = a.Key, count = word.Count(c => c == a.Key) })
        .All(q => (occurrences.FirstOrDefault(o => o.letter == q.letter)?.count ?? 0) == q.count)
      };
    }).Where(word => word.matches);

    tbMatches.Text = matches.Count() == 0 ? 
      "No Matches!" : String.Join("\r\n", matches.Select(m => m.word));  }
  }
}

The only thing of interest to note here is:

C#
tbWordList.MaxLength = Int32.MaxValue;

The default length of a TextBox is 32767 characters, and some of these word lists are a lot longer. This gives you 2GB of characters!

Conclusion

Now I can "cheat" when I'm trying to find 7 letter words!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
QuestionIdea to create sense-worthy words Pin
AndyHo7-Oct-17 3:33
professionalAndyHo7-Oct-17 3:33 
AnswerRe: Idea to create sense-worthy words Pin
Marc Clifton7-Oct-17 7:38
mvaMarc Clifton7-Oct-17 7:38 
QuestionActually, the GroupBy is unneccessary Pin
Marc Clifton5-Oct-17 5:49
mvaMarc Clifton5-Oct-17 5:49 
AnswerRe: Actually, the GroupBy is unneccessary Pin
John Brett9-Oct-17 5:00
John Brett9-Oct-17 5:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.