30

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.

Community
  • 1
  • 1
Chris Frederick
  • 5,482
  • 3
  • 36
  • 44
  • Yeah I verified the behavior, and I couldn't find anything that would indicate that it is being saved in the .xcdatamodeld/entities file incorrectly, so I would suspect that it isn't really xcode that is doing it. probably the runtime environment, perhaps you should file a bug report. Is there a special reason why you would need to use Decimal type? – Grady Player Jun 01 '11 at 20:31
  • @Grady Thanks, I'm glad that I'm not the only one running into this. To answer your question, I'm using Decimal numbers for the accuracy that they provide (base-10 math instead of floating-point precision), as discussed earlier [on Stack Overflow](http://stackoverflow.com/questions/421463/should-i-use-nsdecimalnumber-to-deal-with-money) [and elsewhere](http://www.cimgf.com/2008/04/23/cocoa-tutorial-dont-be-lazy-with-nsdecimalnumber-like-me/). – Chris Frederick Jun 01 '11 at 21:10
  • I guess you will just have to implement your own -(BOOL)validate:(id *)ioValue error:(NSError **)outError, would be closest to default behavior. – Grady Player Jun 01 '11 at 21:26
  • @Grady Thanks for your advice! I just updated my question to include your workaround. I had actually considered filing a bug report, but I wasn't sure precisely where and/or how to do so. Could you point me in the right direction? – Chris Frederick Jun 02 '11 at 05:04
  • http://developer.apple.com/bugreporter/ – Grady Player Jun 02 '11 at 07:32
  • @Grady Okay, I filed a bug report with Apple. I would link to it here, but it doesn't look like Apple provides public URLs for their bugs? For what it's worth, the problem ID is 9585842. – Chris Frederick Jun 10 '11 at 06:18
  • @Chris, cool let me know what you find out. – Grady Player Jun 10 '11 at 07:58
  • **UPDATE:** My problem appears to be a duplicate of problem ID 9405079 (not that I can view it...). – Chris Frederick Jun 18 '11 at 04:31
  • This seems to still be an issue. I set a minimum value on a `Decimal` to `0.01`, and validation succeeds when testing against a `Decimal` of `0`. – Nick Kohrn Jul 16 '17 at 21:45

3 Answers3

2

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;
}
Chris Frederick
  • 5,482
  • 3
  • 36
  • 44
Grady Player
  • 14,399
  • 2
  • 48
  • 76
  • That's interesting, I get the same behavior. You would think that `compare` would be commutative, but that doesn't appear to be the case. I wonder if in the first conditional statement the `NSNumber` is being converted into an `NSDecimalNumber`, and in the second conditional statement the `NSDecimalNumber` is being converted into an `NSNumber`—which is more prone to floating-point errors than `NSDecimalNumber` and thus fails the equality test... – Chris Frederick Jun 09 '11 at 07:10
  • Yeah, I don't know. Seems like the root of the problem, maybe if we file a bug report we will get a better answer – Grady Player Jun 09 '11 at 07:15
  • There shouldn't be a float problem with 1.2, I don't think anyway – Grady Player Jun 09 '11 at 07:17
  • Could you update your answer to include your suggestion to use `-(BOOL)validate:(id *)ioValue error:(NSError **)outError`? That's the workaround that I chose to use and I want to credit you for it. If/when I get an update from Apple about this bug, I'll edit my question to include that information, as well. – Chris Frederick Jul 04 '11 at 21:51
  • Thanks! I'll update my question later to remove the redundancy. – Chris Frederick Jul 06 '11 at 00:45
0

This is a very long shot but have you tried 1,2 instead of 1.2?

Mayoneez
  • 441
  • 5
  • 19
  • Oh, wouldn't that have been embarrassing... I just gave it a shot and the text field for **Max Value** automatically corrected my input to 1 – Chris Frederick May 27 '11 at 12:55
0

This is inportant: http://citeseer.ist.psu.edu/viewdoc/download;jsessionid=86013D0FEFFA6CD1A626176C5D4EF9E2?doi=10.1.1.102.244&rep=rep1&type=pdf

It a story about your problem - I think

And remember that decimal is not fractional - where would you put the decimal point ?

Floatingpoints has this inside called mantissa ...

MikeyKennethR
  • 600
  • 4
  • 16
  • From the [Apple documentation on decimal numbers](http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/NumbersandValues/Articles/DecimalNumbers.html#//apple_ref/doc/uid/20000176-CJBCAGDI) (bold formatting is mine): "`NSDecimalNumber` is an immutable subclass of `NSNumber` that provides an object-oriented wrapper for doing base-10 arithmetic. **An instance can represent any number that can be expressed as `mantissa x 10 exponent` where *mantissa* is a decimal integer up to 38 digits long, and *exponent* is an integer between -128 and 127**." – Chris Frederick Jun 06 '11 at 17:21
  • Exactly - no fractions - just a lot of digits in the decimal world – MikeyKennethR Jun 07 '11 at 11:59
  • I'm not sure that I follow what you mean. Could you provide some sample code illustrating your point with your answer? – Chris Frederick Jun 07 '11 at 18:16
  • It seems to be comma/point issue. I initially thought that it could concern floatpoint tolerances - I missed it ... – MikeyKennethR Jun 07 '11 at 19:40