views:

1487

answers:

5

What's the cleanest way to store an enum in XML and read it back out again? Say I've got:

enum ETObjectType {ETNormalObjectType, ETRareObjectType, ETEssentialObjectType};

...and I want to take a variable, enum ETObjectType objectType = ETNormalObjectType;, and convert it to XML that looks like this: <objectType>ETNormalObjectType</objectType>.

Currently what I'm doing is something like this:

NSString* const ETObjectTypeAsString[] = {@"ETNormalObjectType",@"ETRareObjectType",@"ETEssentialObjectType"};

[anXMLElement addChild:[NSXMLElement elementWithName:@"objectType" stringValue:ETObjectTypeAsString[objectType]]];

...but that's not entirely ideal; I'm not happy about updating both lists every time I change my enum. But it's acceptable. Much, much worse is reading XML back in, for which I am currently doing this:

if ([[[anXMLElement childNamed:@"objectType"] stringValue] isEqualToString:@"ETRareObjectType"])
{
    [self initObjectType:ETRareObjectType];
}
else if ([[[anXMLElement childNamed:@"objectType"] stringValue] isEqualToString:@"ETEssentialObjectType"])
{
    [self initObjectType:ETEssentialObjectType];
}
else
{
    [self initObjectType:ETNormalObjectType];
}

Yuck! This disgusts me. There's got to be a cleaner way to read, at least, or perhaps a unified way to read and write?

I'm using Obj-C and Cocoa, but I wouldn't mind some pure C functions. I'd even use preprocessor stuff, if it's the only way.

+2  A: 

Here's how I typically write these styles of methods:

#define countof(array) (sizeof(array)/sizeof(array[0]))

enum {
    ETNormalObjectType,
    ETRareObjectType,
    ETEssentialObjectType
};
typedef NSInteger ETObjectType;

NSString *ETObjectTypesAsStrings[] = {[ETNormalObjectType] = @"ETNormalObjectType", 
                                      [ETRareObjectType] = @"ETRareObjectType", 
                                      [ETEssentialObjectType] = @"ETEssentialObjectType"};

NSString *ETStringFromObjectType(ETObjectType type) {
    return ETObjectTypesAsStrings[type];
}

ETObjectType ETObjectTypeFromString(NSString *string) {
    NSString *match = nil;
    for(NSInteger idx = 0; !match && (idx < countof(ETObjectTypesAsStrings)); idx += 1) {
        if ([string isEqualToString:ETObjectTypesAsStrings[idx]]) {
            match = ETObjectTypesAsStrings[idx];
        }
    }
    return match;
}

You end up having to put your enumeration values in two places, the original enumeration, and the array that maps integer values to their string names. The two functions that actually do the mapping don't have copies of the maps though.

Jon Hess
A: 

The great thing of XML is that it can transform to nearly anything, even to code. It just takes (a lot of) effort to make the translation once. I have worked in several projects where XML was translated to code. And this saved a lot of time. This technique is for instance, covered in chapter 12 of the book "XSLT Cookbook 2nd edition, S. Mangano, O'Reilley".

It's not an easy solution, but if you have a good mapping you - have a single point of definition (your xml) - can generate .h files with enum - can generate tables or functions to read/write values in xml

It depends on the number of enums and how often they change if it is worthwhile. Good luck!

Adriaan
+3  A: 

I haven't found a better way than duplicating the enum in a string. However, I do it slightly differently, namely:

typedef enum {
    kManipulateWindowTargetFrontWindow,
    kManipulateWindowTargetNamedWindow, 
    kManipulateWindowTargetWindowNameContaining, 
    kManipulateWindowTargetDEFAULT = kManipulateWindowTargetFrontWindow, 
} ManipulateWindowTargetType;
#define kManipulateWindowTargetTypeNamesArray @"FrontWindow", @"NamedWindow", @"WindowNameContaining", nil

then in the implementation:

static NSArray* kManipulateWindowTargetTypeArray = [[NSArray alloc] initWithObjects: kManipulateWindowTargetTypeNamesArray];

NSString* ManipulateWindowTargetTypeToString( ManipulateWindowTargetType mwtt )
{
    return [kManipulateWindowTargetTypeArray objectAtIndex:mwtt];
}

ManipulateWindowTargetType ManipulateWindowTargetTypeFromString( NSString* s )
{
    NSUInteger n = [kManipulateWindowTargetTypeArray indexOfObject:s];
    check( n != NSNotFound );
    if ( n == NSNotFound ) {
        n = kManipulateWindowTargetDEFAULT;
    }
    return (ManipulateWindowTargetType) n;
}

The reason I use the #define is to avoid declaring the array in the header file, but it would be insane to separate the definition of the enum from the definition of the sequence of strings, so this is the best compromise I've found.

Since the code is boilerplate, you can actually make them a category on NSArray.

@interface NSArray (XMLExtensions)

- (NSString*) stringWithEnum: (NSUInteger) e;
- (NSUInteger) enumFromString: (NSString*) s default: (NSUInteger) def;
- (NSUInteger) enumFromString: (NSString*) s;

@end

@implementation NSArray (XMLExtensions)

- (NSString*) stringWithEnum: (NSUInteger) e;
{
    return [self objectAtIndex:e];
}

- (NSUInteger) enumFromString: (NSString*) s default: (NSUInteger) def;
{
    NSUInteger n = [self indexOfObject:s];
    check( n != NSNotFound );
    if ( n == NSNotFound ) {
        n = def;
    }
    return n;
}

- (NSUInteger) enumFromString: (NSString*) s;
{
    return [self enumFromString:s default:0];
}


@end

and then:

NSLog( @"s is %@", [kManipulateWindowTargetTypeArray stringWithEnum:kManipulateWindowTargetNamedWindow] );
ManipulateWindowTargetType mwtt = (ManipulateWindowTargetType)[kManipulateWindowTargetTypeArray enumFromString:@"WindowNameContaining" default:kManipulateWindowTargetDEFAULT];
NSLog( @"e is %d", mwtt );
Peter N Lewis
Great idea for keeping the enum and string sequence both in the header file. Something bothers me about making those be NSArray methods, but I'll probably do it anyway. Thanks!
andyvn22
Where is the check function defined?
zekel
check is defined in AssertMacros.h, along with verify, require, and the variants _noerr, _action, _quiet, _string. They should be frequently sprinkled throughout your code so you find your bugs before your users do.
Peter N Lewis
+2  A: 

I echo Jon's solution, but you can use the dreaded X-macro to avoid repeating yourself at all. I don't know how to comment on Jon's answer with code formatting, so here it is as a new answer.

#define ETObjectTypeEntries \
ENTRY(ETNormalObjectType) \
ENTRY(ETRareObjectType) \
ENTRY(ETEssentialObjectType)

typedef enum ETObjectType {
#define ENTRY(objectType) objectType, 
    ETObjectTypeEntries
#undef ENTRY
} ETObjectType;

NSString *ETObjectTypesAsStrings[] = {
#define ENTRY(objectType) [objectType] = @"" # objectType, 
    ETObjectTypeEntries
#undef ENTRY
};

#define countof(array) (sizeof(array)/sizeof(array[0]))

NSString *ETStringFromObjectType(ETObjectType type) {
    return ETObjectTypesAsStrings[type];
}

NSString *ETObjectTypeFromString(NSString *string) {
    NSString *match = nil;
    for(NSInteger idx = 0; !match && (idx < countof(ETObjectTypesAsStrings)); idx += 1) {
        if ([string isEqualToString:ETObjectTypesAsStrings[idx]]) {
            match = ETObjectTypesAsStrings[idx];
        }
    }
    return match;
}
This is... truly disgusting. :D Thanks! (I'm going to use the category on NSArray strategy, but this is really fun to play with.)
andyvn22
"Truly disgusting"? I'd say unconventionally elegant.
Jon Hess
A: 

I'm experimenting with this solution -

static NSString *stepTypeEnum[kStepTypeCount] = {@"one",@"two",@"three",@"four"};

int enumFromStrings(NSString*findString,NSString *strings[],int enumMax){
    for (int i=0;i<enumMax;i++){
        if ([findString isEqual: strings[i]]) {
            return i;
        }
    }
    NSLog(@"enum was not found for string %@", findString);
    assert(false);
    return INT_MAX;
}

I like it because it checks the length of the string array at compile time, and the enumFromStrings function is generic and reusable. You call it like so:

-(void)setType:(NSString*)typeString{
    type = enumFromStrings(typeString,stepTypeEnum,kStepTypeCount);
}
smasher