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.
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
.
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.
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;
}
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.
DecimalTest_AppDelegate
managedObjectContext
methodTake the following steps to use this code.
DebugController
in Interface Builder.appDelegate
outlet to your application delegate.NSTextField
) to your user interface and connect the controller's debugLabel
outlet to it.updateLabel
action.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.
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;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With