Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does XCode Floor "Min Value" and "Max Value" for Core Data Decimal Attributes?

Background

I, like scores of programmers before me, am working on an application that deals with money. I'm relatively new to Cocoa programming, but after reading through the manuals I decided that I would try to use Core Data because it provides a number of features that I want and should save me from re-inventing the wheel. Anyway, my question doesn't have anything to do with whether or not I should use Core Data: it has to do with the behavior of Core Data and XCode themselves.

UPDATE: I filed a bug report with Apple and was informed that it is a duplicate of problem ID 9405079. They are aware of the issue, but I have no idea when or if they are going to fix it.

The Problem

For some reason that I cannot understand, XCode floors the Min Value and Max Value constraints when I edit a Decimal property in my managed object model. (I'm using Decimal properties for the reasons described here.)

Assume that I have a Core Data entity with a Decimal attribute named value (this is merely for illustration; I've used other attribute names, as well). I want it to have a value greater than 0, but because XCode will only allow me to specify a minimum value (inclusive), I set Min Value equal to 0.01. Much to my surprise, this results in a validation predicate of SELF >= 0! I get the same result when I change the minimum value: all fractional values are truncated (the minimum value is floored). The maximum value has the same behavior.

By way of illustration, the value property in the following screenshot will result in validation predicates of SELF >= 0 and SELF <= 1.

value configured in XCode

Strangely enough, though, if I change the type of this property to either Double or Float, the validation predicates will change to SELF >= 0.5 and SELF <= 1.2, as expected. Stranger still, if I create my own data model following the Core Data Utility Tutorial, the validation predicates are set correctly even for decimal properties.

Original Workaround

Since I can't find any way to fix this problem in XCode's managed object model editor, I have added the following code—indicated by the begin workaround and end workaround comments—to my application delegate's managedObjectModel method (this is the same application delegate that XCode provides by default when you create a new project that uses Core Data). Note that I am adding a constraint to keep the Transaction entity's amount property greater than 0.

- (NSManagedObjectModel *)managedObjectModel {

    if (managedObjectModel) return managedObjectModel;

    managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];

    // begin workaround
    NSEntityDescription *transactionEntity = [[managedObjectModel entitiesByName] objectForKey:@"Transaction"];
    NSAttributeDescription *amountAttribute = [[transactionEntity attributesByName] objectForKey:@"amount"];
    [amountAttribute setValidationPredicates:[NSArray arrayWithObject:[NSPredicate predicateWithFormat:@"SELF > 0"]]
                      withValidationWarnings:[NSArray arrayWithObject:@"amount is not greater than 0"]];
    // end workaround

    return managedObjectModel;
}

Questions

  1. Is this really a bug in how XCode generates validation predicates for decimal properties in managed object models for Core Data?
  2. If so, is there a better way to work around it than the ones I have described here?

Repro Code

You should be able to reproduce this issue with the following sample code for a DebugController class, which prints out the constraints on every property in a managed object model to a label. This code makes the following assumptions.

  • You have an application delegate named DecimalTest_AppDelegate
  • Your application delegate has a managedObjectContext method
  • Your managed object model is named "Wallet"

Take the following steps to use this code.

  1. Instantiate the DebugController in Interface Builder.
  2. Connect the controller's appDelegate outlet to your application delegate.
  3. Add a wrapping label (NSTextField) to your user interface and connect the controller's debugLabel outlet to it.
  4. Add a button to your user interface and connect its selector to the controller's updateLabel action.
  5. Launch your application and press the button connected to the updateLabel action. This prints your managed object model's constraints to debugLabel and should illustrate the behavior that I've described here.

DebugController.h

#import <Cocoa/Cocoa.h>
// TODO: Replace 'DecimalTest_AppDelegate' with the name of your application delegate
#import "DecimalTest_AppDelegate.h"


@interface DebugController : NSObject {

    NSManagedObjectContext *context;

    // TODO: Replace 'DecimalTest_AppDelegate' with the name of your application delegate
    IBOutlet DecimalTest_AppDelegate *appDelegate;
    IBOutlet NSTextField *debugLabel;

}

@property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext;

- (IBAction)updateLabel:sender;

@end

DebugController.m

#import "DebugController.h"

@implementation DebugController

- (NSManagedObjectContext *)managedObjectContext
{
    if (context == nil)
    {
        context = [[NSManagedObjectContext alloc] init];
        [context setPersistentStoreCoordinator:[[appDelegate managedObjectContext] persistentStoreCoordinator]];
    }
    return context;     
}

- (IBAction)updateLabel:sender
{
    NSString *debugString = @"";

    // TODO: Replace 'Wallet' with the name of your managed object model
    NSEntityDescription *entity = [NSEntityDescription entityForName:@"Wallet" inManagedObjectContext:[self managedObjectContext]];
    NSArray *properties = [entity properties];

    for (NSAttributeDescription *attribute in properties)
    {
        debugString = [debugString stringByAppendingFormat:@"\n%@: \n", [attribute name]];
        NSArray *validationPredicates = [attribute validationPredicates];
        for (NSPredicate *predicate in validationPredicates)
        {
            debugString = [debugString stringByAppendingFormat:@"%@\n", [predicate predicateFormat]];
        }
    }
    //  NSPredicate *validationPredicate = [validationPredicates objectAtIndex:1];
    [debugLabel setStringValue:debugString];
}

@end

Thanks everyone.

like image 986
Chris Frederick Avatar asked May 17 '11 16:05

Chris Frederick


1 Answers

I did another test, and I suspect that it has to do with the compare: method of NSNumber and NSDecimalNumber.

NSDecimalNumber * dn = [NSDecimalNumber decimalNumberWithString:@"1.2"];

if ([dn compare:[NSNumber numberWithFloat:1.2]]==NSOrderedSame) {
        NSLog(@"1.2==1.2");
    }else{
        NSLog(@"1.2!=1.2");
    }

    if ([[NSNumber numberWithFloat:1.2] compare:dn]==NSOrderedSame) {
        NSLog(@"1.2==1.2");
    }else{
        NSLog(@"1.2!=1.2");
    }

Output is:

2011-06-08 14:39:27.835 decimalTest[3335:903] 1.2==1.2
2011-06-08 14:39:27.836 decimalTest[3335:903] 1.2!=1.2

Edit: The following workaround was originally a comment that I added to the question and was eventually adapted into the question body.

Using -(BOOL)validate<key>:(id *)ioValue error:(NSError **)outError you can implement behavior that is close to the default behavior (as described here).

For example (taken from the question body, written by OP Chris):

-(BOOL)validateAmount:(id *)ioValue error:(NSError **)outError {

    // Assuming that this is a required property...
    if (*ioValue == nil)
    {
        return NO;
    }

    if ([*ioValue floatValue] <= 0.0)
    {
        if (outError != NULL)
        {
            NSString *errorString = NSLocalizedStringFromTable(
                @"Amount must greater than zero", @"Transaction",
                @"validation: zero amount error");

            NSDictionary *userInfoDict = [NSDictionary dictionaryWithObject:errorString
                forKey:NSLocalizedDescriptionKey];

            // Assume that we've already defined TRANSACTION_ERROR_DOMAIN and TRANSACTION_INVALID_AMOUNT_CODE
            NSError *error = [[[NSError alloc] initWithDomain:TRANSACTION_ERROR_DOMAIN
                code:TRANSACTION_INVALID_AMOUNT_CODE
                userInfo:userInfoDict] autorelease];
            *outError = error;
        }

        return NO;
    }



  return YES;
}
like image 65
Grady Player Avatar answered Nov 12 '22 06:11

Grady Player