Challenge


#1

This was a daunting task upon first read, but was not very difficult to sort out…

encodeAsSpeakableString was certainly the easiest to create (as mentioned in the book), but dataWithSpeakableString was not too difficult. For dataWithSpeakableString, I did not work with breaking the data into ranges, but broke the datastring into an NSStrings array and then just went through the parts. When the length of the part was one character, I got the value, and converted it. If the part was longer, it was a name bit, so looked for the index that belonged to the name. Then mashed the two bits together (with some manipulation to revert earlier changes) and stuffed it into an NSMutableArray…

Here is my solution…

#import "NSData+Speakable.h"

@implementation NSData (Speakable)

const uint FIRST_BYTE_MIN = 0x02;
const uint FIRST_BYTE_MAX = 0x09;
const uint BRAND_NAME_COUNT = 32;
const uint SHIFT_COUNT = 3;
const char * brandNameArray[BRAND_NAME_COUNT] = { "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"};

- (NSString *) encodeAsSpeakableString
{
    BOOL success = YES;
    
    NSMutableString *result = [NSMutableString string];
    const char *bytes = [self bytes];
    for (int index = 0; index < [self length]; index++)
    {
        // first three bits represented as digits 2 > 9 inclusive
        unsigned char firstByte = ((unsigned char) bytes[index] & 0x7) + FIRST_BYTE_MIN;
        unsigned char brandNameIndex = ((unsigned char) bytes[index]) >> SHIFT_COUNT;
    
        // check values:
        success = firstByte >= FIRST_BYTE_MIN &&
                  firstByte <= FIRST_BYTE_MAX &&
                  brandNameIndex < BRAND_NAME_COUNT;
    
        if (success)
        {
            // using white space to separate the elements
            [result appendFormat:@"%d %s ", firstByte, brandNameArray[brandNameIndex]];
        }
        else
        {
            NSLog(@"encodeAsSpeakableString error");
        }
    }
    
    return success ? result : nil;
}

+ (NSData *) dataWithSpeakableString: (NSString *) s error:(NSError **) e
{
    BOOL success = YES;
    
    NSMutableArray static * brandNames  = nil;
    
    // Used to pull apart incoming NSString into parts
    NSCharacterSet * spaces = [NSCharacterSet whitespaceCharacterSet];
    NSArray * partsOfString = [s componentsSeparatedByCharactersInSet:spaces];
    
    // used to assemble parts into data
    unsigned char firstBit = 0;
    unsigned char secondBit = 0;
    unsigned char combinedBits = 0;
 
    // used to compile parts into data
    NSMutableData * result = [[NSMutableData alloc] initWithLength: 0];
    
    if (!brandNames)
    {
        // instantiate mutable array of names and fill with names from c strings
        brandNames = [[NSMutableArray alloc] init];
        
        for (uint loop = 0; loop < BRAND_NAME_COUNT; loop++)
        {
            [brandNames addObject:[NSString stringWithFormat:@"%s", brandNameArray[loop]]];
        }
    }
    
    for (NSString * part in partsOfString)
    {
        // parts consist of single character (a digit) or many characters
        if ([part length] == 1)
        {
            // get the value of the part
            firstBit = [part intValue];
            
            if (firstBit >= FIRST_BYTE_MIN) // not a zero (a non digit or too low of a number)
            {
                // shift back to what it was before
                firstBit -= FIRST_BYTE_MIN;
            }
            else
            {
                success = NO;
                break;
            }
        }
        else if ([part length] > 1)
        {
            // parse brandNameArray looking for this string
            NSUInteger index = [brandNames indexOfObject:part];//<< SHIFT_COUNT;

            if (index != NSNotFound)
            {
                // set second bit and combine together
                secondBit = index << SHIFT_COUNT;
                combinedBits = firstBit | secondBit;
                
                [result appendData:[[NSData alloc] initWithBytes:&combinedBits length:sizeof(combinedBits)]];
            }
            else // empty
            {
                success = NO;
                break;
            }
        }
    }
    
    if (!success)
    {
        if (e)
        {
            NSDictionary *userInfo = @{NSLocalizedDescriptionKey : @"Unable to parse"};
            *e = [NSError errorWithDomain:@"SpeakableBytes" code:1 userInfo];
        }
        return nil;
    }
    else
    {
        return result;
    }
}

@end

#2

Excellent thinking.

Also, why not let the compiler calculate the value of BRAND_NAME_COUNT:

const char * brandNameArray[] = { "Camry", "Nikon", "Apple", ...};

const uint BRAND_NAME_COUNT = sizeof (brandNameArray)/sizeof (brandNameArray [0]);

#3

I used an explicit count for the array as we needed exactly 32 elements. We could not add or subtract from the array without changing the workings of the other workings of the program. We could use

or

but it hides the fact that we really want only a 32 count array.

And if we change the names around, the compiler will catch any deviation from 32 during compile not runtime.

(I originally coded for unspecified length of array, but nailed it down as I worked on it.)


#4

So, I am having a bit of trouble with this one. If you do not mind, could one of you walk me through what is going on in your encodeAsSpeakableString line by line? For some reason, I just cannot seem to wrap my mind around what you are doing here:

[code]// first three bits represented as digits 2 > 9 inclusive
unsigned char firstByte = ((unsigned char) bytes[index] & 0x7) + FIRST_BYTE_MIN;
unsigned char brandNameIndex = ((unsigned char) bytes[index]) >> SHIFT_COUNT;

    // check values:
    success = firstByte >= FIRST_BYTE_MIN &&
    firstByte <= FIRST_BYTE_MAX &&
    brandNameIndex < BRAND_NAME_COUNT;[/code]

I understand what the bitwise operations do but I have no grasp whatsoever on how to use them. I will jump back a couple chapters to see if I missed some big concept. If you could find the time to respond, it would be a tremendous help! Thank you in advance.


#5

Let’s see if I can break it down… Sorry for lack of better comments…

When I get firstByte, I use & 0x7 to strip out the first three bits (0x1+0x2+0x4) of the data, then simply add a value to make it start at two.

When I get brandNameIndex I use >> to shift the byte to loose the three bytes extracted above. Taken together, the two parts, include all the data the original byte contained, but now in two parts.

The test for success verifies that the firstByte is within the expected limits, and that brandNameIndex is less than the count of brand names.