4

I'm Making a an ios application where you input 9 lettes and it will output anagrams of those 9 letters. It is like the target word, or 9 letter word in the paper. Like this link:

http://nineletterword.tompaton.com/

It doesn't just provide anagrams for 9 letters, it will do it for 4 letters, 5 letters, 6 letters... all of which contain at least the middle letter.

I want to make it an offline application, so I don't want to reference any websites or use an online json...

How would I go about checking if an array of the 9 letters can be rearranged into a word which is in an English dictionary which I have downloaded.

E.g. I have the input of (a,b,a,n,D,o,n,e,d): how would I get the output of 4 letters or more that are English words in an array called "English Dictionary" which must contains the middle letter "D" - like "abandon", "bond", "dead"...

Is the best method lots and lots of loops and if statements or is there something in xcode/ objective c which I can use to just get the list of 4 letters and then have all possible arrangements of it...

Cheers

falky
  • 589
  • 2
  • 11
  • 27

2 Answers2

3

Let me propose a different algorithm that depends on a lookup, not a search through an array.

Setup:

Iterate over the words in the dictionary. For each word, create a string with the same characters, sorted alphabetically. Using this string as a key, create a dictionary of arrays of the original words.

Usage:

Now You can do the check on any character combination very quickly: Just sort the characters like above and look the resulting key up in the map.

Example:

original array: ( bond, Mary, army )

anagram lookup map:

{
   bdno : ( bond ),
   amry : ( Mary, army ),
}

Using this map it's very fast to check anagrams of any word. No iteration over the dictionary array is needed.

Edit:

My proposed algorithm splits in three parts:

  1. A setup method to build the lookup map from a dictionary of objects: anagramMap
  2. A method to calculate the character-by-character sorted key: anagramKey
  3. An algorithm that finds all permutations of the characters contained in the nine letter word and looks up the words in the map: findAnagrams.

Here's an implementation of all three methods as a category on NSString:

@interface NSString (NSStringAnagramAdditions)
- (NSSet *)findAnagrams;
@end

@implementation NSString (NSStringAnagramAdditions)

+ (NSDictionary *)anagramMap
{
    static NSDictionary *anagramMap;
    if (anagramMap != nil)
        return anagramMap;

    // this file is present on Mac OS and other unix variants
    NSString *allWords = [NSString stringWithContentsOfFile:@"/usr/share/dict/words"
                                                   encoding:NSUTF8StringEncoding
                                                      error:NULL];

    NSMutableDictionary *map = [NSMutableDictionary dictionary];
    @autoreleasepool {
        [allWords enumerateLinesUsingBlock:^(NSString *word, BOOL *stop) {
            NSString *key = [word anagramKey];
            if (key == nil)
                return;
            NSMutableArray *keyWords = [map objectForKey:key];
            if (keyWords == nil) {
                keyWords = [NSMutableArray array];
                [map setObject:keyWords forKey:key];
            }
            [keyWords addObject:word];
        }];
    }

    anagramMap = map;
    return anagramMap;
}

- (NSString *)anagramKey
{
    NSString *lowercaseWord = [self lowercaseString];

    // make sure to take the length *after* lowercase. it might change!
    NSUInteger length = [lowercaseWord length];

    // in this case we're only interested in anagrams 4 - 9 characters long
    if (length < 4 || length > 9)
        return nil;

    unichar sortedWord[length];
    [lowercaseWord getCharacters:sortedWord range:(NSRange){0, length}];

    qsort_b(sortedWord, length, sizeof(unichar), ^int(const void *aPtr, const void *bPtr) {
        int a = *(const unichar *)aPtr;
        int b = *(const unichar *)bPtr;
        return b - a;
    });

    return [NSString stringWithCharacters:sortedWord length:length];
}

- (NSSet *)findAnagrams
{
    unichar nineCharacters[9];
    NSString *anagramKey = [self anagramKey];

    // make sure this word is not too long/short.
    if (anagramKey == nil)
        return nil;
    [anagramKey getCharacters:nineCharacters range:(NSRange){0, 9}];
    NSUInteger middleCharPos = [anagramKey rangeOfString:[self substringWithRange:(NSRange){4, 1}]].location;

    NSMutableSet *anagrams = [NSMutableSet set];

    // 0x1ff means first 9 bits set: one for each character
    for (NSUInteger i = 0; i <= 0x1ff; i += 1) {

        // skip permutations that do not contain the middle letter
        if ((i & (1 << middleCharPos)) == 0)
            continue;

        NSUInteger length = 0;
        unichar permutation[9];
        for (int bit = 0; bit <= 9; bit += 1) {
            if (i & (1 << bit)) {
                permutation[length] = nineCharacters[bit];
                length += 1;
            }
        }

        if (length < 4)
            continue;

        NSString *permutationString = [NSString stringWithCharacters:permutation length:length];
        NSArray *matchingAnagrams = [[self class] anagramMap][permutationString];

        for (NSString *word in matchingAnagrams)
            [anagrams addObject:word];
    }

    return anagrams;
}

@end

Assuming a test string in a variable called nineletters you would log the possible values using:

for (NSString *anagram in [nineletters findAnagrams])
    NSLog(@"%@", anagram);
Nikolai Ruhe
  • 81,520
  • 17
  • 180
  • 200
  • That's a great idea! But my only problem is I have to work out something to rearrange the letters in the 9 letter word. E.g. How do I go from "abandoned" to "bdno"? That would work really well If I could ork something out for that though! Thanks – falky Apr 08 '13 at 10:52
  • @falky I added a method that iterates over all possible combinations of the characters. See the `findAnagrams` implementation. – Nikolai Ruhe Apr 08 '13 at 14:34
  • Thanks very much for writing this method. I will try implementing it later on this afternoon. It seems much easier than looping over the dictionary and faster too! – falky Apr 08 '13 at 22:14
  • I tried implementing it but I have no idea how. I have put your code into a new category which I have imported into my main view controller file. I have an action method from a button which you press on the application. What do I put in there for it to then just simply nslog the list of anagrams? Like what should I type? I have the 9 letters in an nsstring called nineletters. Sorry, I'm new to this so I don't quite understand it. Thanks for helping though! – falky Apr 16 '13 at 05:32
  • YOU ARE A LEGEND! WISH I COULD UPVOTE YOU A MILLION TIMES MATE!! Thanks so much! – falky Apr 16 '13 at 09:23
  • 1
    I am probably wrong, but it seems to me that the algorithm checks that the word contains the middle letter of the *anagramKey* of the input word, instead of the middle letter of the input word. – Martin R Apr 16 '13 at 10:09
  • @NikolaiRuhe One very last thing I promise!! What is some very simple code (not in a class, or category), just objective c code to parse the English dictionary into an NSMutableDictionary, where the key is the the sorted word. I tried writing it but mine is very long and complicated. I also tried copying your class method, but that didn't work either! I have implemented it with 9 letters, and now all I am implementing just anagrams of a word like "stop" -> "pots", "tops", "post" etc. Any quick help would be really appreciated. :) – falky Apr 17 '13 at 01:07
  • @falky I'm not sure what you mean. The `anagramMap` method does exactly that: parsing an english dictionary file (one word per line) into an `NSDictionary`. First it reads the file into a string, then it iterates over the lines (== words), creating the anagram key. If that fails (because the word has less than four or more than 9 characters) it drops the word. Otherwise it puts it into the dictionary. What are you missing? Isn't that what you wanted to do? – Nikolai Ruhe Apr 17 '13 at 08:37
  • @NikolaiRuhe. Yes that does work perfectly and thank you very much for that. In an addition to that part of my application, I am making a separate view controller with a separate section which just finds anagrams for a particular word. All I really want to do and am asking you is how exactly do I call the AnagramMap class method and return an NSMutableDictionary of the words file, which I can then reference? Cheers – falky Apr 17 '13 at 08:57
  • The `anagramMap` method is specialized for the use case with four to nine letter words. If you want to allow shorter and longer words you have to change the `anagramKey` method (remove the lines containing `if (length < 4 || length > 9) return nil;`). This method is used from within the `anagramMap` method and filters the unwanted words by returning `nil`. – Nikolai Ruhe Apr 17 '13 at 09:02
  • @NikolaiRuhe. Thanks. Is that all I'd have to change? Would I have to change the 0x1ff code or the permutation[9] code? I have created a separate category with different names for the additional methods. – falky Apr 18 '13 at 10:28
  • @falky Oh, I see what you mean. Yes, you have to change all the hardcoded references to nine letters to a variable number of letters. And the bitmask for the loop has to be calculated like `(1 << length) - 1`. – Nikolai Ruhe Apr 18 '13 at 13:28
  • @NikolaiRuhe Thanks but I think I essentially solved it. I changed all the hardcoded nines into a variable but I changed the value of 0x1ff to 0x7fff which seemed to work, but your way of "(1 << length) - 1" is much better! Thank you! Working perfectly now! – falky Apr 18 '13 at 14:11
  • @NikolaiRuhe Btw i was going to ask, I know that the "/usr/share/dict/words" is hardcoded into the program. When I run it on the iOS simulator, it works properly, but when I run it on an external iPhone it obviously doesn't. I have added a .txt file into my xCode project called "words.txt". But when I change the "usr/...words" reference to the other file, it doesn't work. What can I do to change that so it will work on the external iPhone. Maybe something like "NSData *data = [NSData dataWithContentsOfFile:@"words.txt"];"?? I tried googling it but nothing came up! :) – falky Apr 18 '13 at 14:17
  • @falky You should ask that as a separate question on SO. My gut feeling is that you just got the path incorrect (try NSBundle's pathForResource methods). Or maybe your dict file has another format. – Nikolai Ruhe Apr 18 '13 at 15:06
  • It's ok I got it. thanks! I had a problem with a capital W for words when I used the NSBundle pathForResource method! Cheers – falky Apr 18 '13 at 23:20
2

First you need a method to check if one word is an anagram of a second word. There are many possible solutions (search for "Objective-C anagram"). This is essentially the method from https://stackoverflow.com/a/13465672/1187415, written slightly differently:

- (BOOL)does:(NSString*)longWord contain:(NSString *)shortWord
{
    NSMutableString *longer = [longWord mutableCopy];
    __block BOOL retVal = YES;
    // Loop over all characters (letters) in shortWord:
    [shortWord enumerateSubstringsInRange:NSMakeRange(0, [shortWord length])
                                  options:NSStringEnumerationByComposedCharacterSequences
                               usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
        // Check if letter occurs in longer word:
        NSRange letterRange = [longer rangeOfString:substring];
        if (letterRange.location != NSNotFound) {
            // Yes. Remove from longer word and continue.
            [longer deleteCharactersInRange:letterRange];
        } else {
            // No. Set return value to NO and quit the loop.
            retVal = NO;
            *stop = YES;
        }
    }];
    return retVal;
}

Examples:

  • [self does:@"abandoned" contain:@"bond"] = YES
  • [self does:@"abandoned" contain:@"sea"] = NO, because there is no "s" in the first word.
  • [self does:@"abandoned" contain:@"noon"] = NO, because "noon" has 2 letters "o", but the first word has only one "o".

Then you can proceed as follows:

NSArray *englishWords = ...; // Your array of english words

NSString *inputWord = @"abandoned"; // The input string
NSString *middleLetter = [inputWord substringWithRange:NSMakeRange([inputWord length]/2, 1)];

NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(NSString *word, NSDictionary *bindings) {
    // Word must have at least 4 letters:
    if ([word length] < 4)
        return NO;
    // Word must contain the middle letter:
    if ([word rangeOfString:middleLetter].location == NSNotFound)
        return NO;
    // Word must contain only letters of the input word:
    if (![self does:inputWord contain:word])
        return NO;
    return YES;
}];
NSArray *matchingWords = [englishWords filteredArrayUsingPredicate:predicate];
NSLog(@"%@", matchingWords);
Community
  • 1
  • 1
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • But will this allow me to go through all the possibilities of words in the bigger word... I don't quite understand the first part of your coding... Does that essentially go through every possibility of words in the greater 9 letter word?? Thanks very much for your help though!! – falky Apr 07 '13 at 07:35
  • 1
    @falky: I have added some comments and examples to the code. If I understand your question correctly, this should do exactly what you need. Why don't you try it and check if it works for you? - But of course you are welcome to ask more questions if required! – Martin R Apr 07 '13 at 08:06
  • Oh ok!!! Now I've got it. That makes much more sense. That's a really good idea actually! So your are very basically checking if a word from the dictionary is in the 9 letter word. Very smart! Thanks very much for your help with this and hte other question. I can upvote now! I'll test it out later on tonight! – falky Apr 07 '13 at 10:24
  • Wondering if you could help with this: http://stackoverflow.com/questions/15877151/parse-rtf-file-into-nsarray-objective-c – falky Apr 08 '13 at 10:47