Challenge: data structure

Hello,
having finished the challenge, I am not very happy with my data structures and wanted to know if someone has a more elegant solution?
In NSData+Speakable.m I am using a global array of NSString to simply encode the lower bits within encodeAsSpeakableString

NSString * const brandNames[]={ @"Camry", @"Nikon", @"Apple", ..., @"Pepsi" };

For dataWithSpeakableString I am using a NSDictionary

NSDictionary *brandValue = @{@"Camry":@0, @"Nikon":@1, @"Apple":@2, ... @"Pepsi":@31 };

It would be optimal if there was only one data structure for both applications…
Thanks for any suggestions

PS: I am not using the proposed NSCharacterSet for finding the next digit. I simply use

to get an array of NSStrings. I then iterate through it. If it decodes to an int thats for the upper bits, and together with the next NSString in the array will go into a local function to form the original byte…

If you mean your dictionary keys are duplicated in the brandNames array or vice versa, you can get an array of keys from the dictionary and use that as the brandNames array.

That’s it, Thanks a lot. Now I form the array out of the Dictionary and get rid of a possible source of error. The method allKeys did not deliver the keys in order. So I used keysSortedByValueUsingSelector

I tried different options declaring the brandValuesDictionary. Maybe someone is interested in my journey :slight_smile:

  1. Tried to declare it as a class extension, but experienced that this is not possible within a category (or is it?)
  2. Tried to put it in a property. Which again i think is not possible in a category.
  3. Tried to declare it as a global variable like

That gave me an error “initializer element is not a compile-time constant”.
4. I declared it as a local function. Worked (only had to add the parenthesis). But did not look nice…

  1. Tried number 3 again. This time using an init. Did not succeed. I would be interested if someone did it this way!
  2. Finally, stayed with 3, but do the initialization in a local function that is called at the beginning of encodeAsSpeakableString and dataWithSpeakableString

Since I loved this book, I am now very much looking forward to receiving the pre-ordered iOS Book. I recommend the two books to every iOS beginner.
THANKS

Since I posted many ways of not doing it, i wanted to share my solution as well.
I admit, it is not catching lots of possible errors. At least I may help someone avoid typing in the brand names :wink:
Here is NSData+Speakable.m

[code]#import “NSData+Speakable.h”

@implementation NSData (Speakable)

static NSDictionary *brandValuesDictionary;

void initbrandValues()
{
brandValuesDictionary = @{@“Camry”:@0,
@“Nikon”:@1,
@“Apple”:@2,
@“Ford”:@3,
@“Audi”:@4,
@“Google”:@5,
@“Nike”:@6,
@“Amazon”:@7,
@“Honda”:@8,
@“Mazda”:@9,
@“Buick”:@10,
@“Fiat”:@11,
@“Jeep”:@12,
@“Lexus”:@13,
@“Volvo”:@14,
@“Fuji”:@15,
@“Sony”:@16,
@“Delta”:@17,
@“Focus”:@18,
@“Puma”:@19,
@“Samsung”:@20,
@“Tivo”:@21,
@“Halo”:@22,
@“Sting”:@23,
@“Shrek”:@24,
@“Avatar”:@25,
@“Shell”:@26,
@“Visa”:@27,
@“Vogue”:@28,
@“Twitter”:@29,
@“Lego”:@30,
@“Pepsi”:@31
};
}

/*
leftmost 3 bits will be encoded as digits between 2 and 9
Attention: explicitly declared singleByte as unsigned.
Otherwise the right shift operator treats the leftmost bit as a sign!
*/
NSString *encodeByte(unsigned int singleByte)
{
NSArray *brandNamesArray = [brandValuesDictionary keysSortedByValueUsingSelector:@selector(compare:)];
unsigned int threeLeftmostBits = singleByte >> 5;
threeLeftmostBits+=2;

// rightmost 5 bits will be encoded as strings
unsigned int fiveRightmostBits = singleByte & 31; // AND 00011111
NSString *result = [NSString stringWithFormat:@"%d %@ ", threeLeftmostBits, brandNamesArray[fiveRightmostBits]];
return(result);

}

  • (NSString *)encodeAsSpeakableString
    {
    NSMutableString *result = [[NSMutableString alloc]init];
    initbrandValues();
    unsigned char *byteBuffer = (unsigned char *)[self bytes];
    for (char b=0; b<8; b++)
    {
    [result appendString:encodeByte(byteBuffer[b])];
    }
    return result;
    }

/*
takes the coded values and decodes them to its original HEX Value
eg. lm=5 rm=@“Fuji” returns 6f
*/
unsigned char decodeByte(unsigned int lm, NSString *rm, NSError **e) {
unsigned int result = 0;
// get the correct leftmost bits
result +=(lm-2);
result = result << 5;
// get the rightmost bits NSInteger
int rmbits = [brandValuesDictionary[rm] intValue];
if (!rmbits) {
if (e) {
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @“Unable to parse”};
*e = [NSError errorWithDomain:@“Speakable Bytes” code:1 userInfo];
return 0;
}
}
result = result | rmbits; // bitwise-OR
return result;
}

  • (NSData *)dataWithSpeakableString:(NSString *)s
    error:(NSError **)e
    {
    NSMutableData *resultData = [[NSMutableData alloc]init];
    unsigned char *aBuffer;
    initbrandValues();

    // Build an Array of substrings
    NSArray *wordGroups = [s componentsSeparatedByString:@" "];
    // Loop through array. If element is number then - together with the following element - decode them to byte
    for(int i=0; i<[wordGroups count]; i++) {
    unsigned int testInt = (unsigned int)[wordGroups[i] intValue];
    if (testInt) {
    unsigned int encodedLeftmostBits = (unsigned int)testInt;
    NSString *encodedRightmostBits = wordGroups[i+1];
    unsigned char byteToAppend = decodeByte(encodedLeftmostBits, encodedRightmostBits, e);
    if (byteToAppend) {
    // To add the byte to NSMutableArray the method appendByte is called. It needs a buffer instead of the byte itself
    // So, first a pointer to the byte is formed
    aBuffer = &byteToAppend;
    [resultData appendBytes:aBuffer length:1];
    } else {
    return nil;
    }
    }
    }
    return resultData;
    }

@end
[/code]

Hey jwgraz,

Why not just use a const char * array for your data structure? That way you can declare it outside of the two methods and you can quickly access the string by subscripting the number, and you can (a little less conveniently) access the number by iterating through the array until you find the string.

Hi PneumaPilot,
Well, actually, you are totally right. I guess I was thinking too complicated :slight_smile:

I first went the route of using a const char * array like mentioned above, but my understanding of how C arrays work was causing some headaches when needing to iterate over them and then having to convert them to an NSString became a hassle. For some reason, even though I had only 32 strings in the array, the sizeof(brands) method was always returning 256. I remembered something that Paul Hegarty did in his iOS Stanford course, where he suggested storing a commonly used collection as a class method that can be used by anyone, which I think applies in this case because I can see having access to all the possible brands as helpful to anyone using the Category.

In NSData+Speakable.h, define a new class method called speakableBrands which returns a pointer to an NSArray

In NSData+Speakable.m, implement it

+ (NSArray *)speakableBrands { return @[@"Camry", @"Nikon", @"Apple", @"Ford", @"Audi", @"Google", @"Nike", @"Amazon", @"Honda", @"Mazda", @"Buick", @"Fiat", @"Jeep", @"Lexus", @"Volvo", @"Fuji", @"Sony", @"Delta", @"Focus", @"Puma", @"Samsung", @"Tivo", @"Halo", @"Sting", @"Shrek", @"Avatar", @"Shell", @"Visa", @"Vogue", @"Twitter", @"Lego", @"Pepsi"]; }

Anywhere in your code that you need to access the list, you can invoke it like this:

[code]NSArray *brands = [NSData speakableBrands];

for (NSString *brand in brands) {
NSLog(@"%@", brand);
}[/code]