4

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.

andyvn22
  • 14,696
  • 1
  • 52
  • 74

5 Answers5

17

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
  • 17,664
  • 2
  • 43
  • 56
  • 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 Aug 07 '09 at 08:55
  • Where is the check function defined? – zekel May 18 '10 at 15:30
  • 1
    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 May 19 '10 at 07:04
  • 1
    I usually deal with the separation of the string list from the enum by having a 'MyEnumCount' at the end of the enum list, and then an assertion in the EnumToString method that the number of items in the array is equal to the 'MyEnumCount - 1'. That way if someone adds an enum and doesn't update the array, you get an immediate assertion failure. Compile time assertions instead of runtime assertions can make that even more self correcting. – Jon Hess Nov 26 '10 at 06:57
  • Peter, any idea where the static NSArray delcaration should go to make it compile? Placing the alloc/init block anywhere outside of a method declaration causes problems. – pchap10k Dec 01 '10 at 04:50
  • 1
    It goes in my .mm files, but note that I always compile everything as Objective-C++. C probably doesn't allow static initialisation with function calls, in which case you'll need to create a static accessor function with a static local variable that you initialise once. That is a static function with body static kManipulateWindowTargetTypeArray = nil; if ( ! kManipulateWindowTargetTypeArray ) kManipulateWindowTargetTypeArray = ...; return kManipulateWindowTargetTypeArray; Alternatively, switch to Objective-C++ (the only real down side is that clang doesn't work with C++ yet). – Peter N Lewis Dec 09 '10 at 06:44
5

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
  • 14,237
  • 1
  • 47
  • 51
5

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;
}
user96459
  • 446
  • 3
  • 4
0

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,282
  • 1
  • 23
  • 31
0

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
  • 164
  • 3