Wednesday
Jun092010

NSNumberFormatter

Next step for me is to collect information about NSNumberFormatter.

NSNumberFormatter is a subclass of NSFormatter. While it uses CFNumberFormatter, it is not toll-free bridged. A CFNumberFormatterRef is actually a member variable inside NSNumberFormatter. Since Mac OS X 10.4 CFNumberFormatter is internally using ICU.

- (NSString *)stringForObjectValue:(id)obj;

After checking, if the obj is actually an NSNumber (otherwise it simply returns nil). If obj is nil, the -nilSymbol is returned.

Otherwise -doubleValue is requested and checked for a real number.

  • FP_NAN, the -notANumberSymbol is returned
  • FP_SUBNORMAL or FP_ZERO returns the -zeroSymbol
  • FP_INFINITE either returns -negativeInfinitySymbol or -positiveInfinitySymbol.

If the NSNumber is a regular number, CFNumberFormatterCreateStringWithNumber is called with the CFNumberFormatter, which is part of the NSNumberFormatter.

- (NSAttributedString *)attributedStringForObjectValue:(id)obj withDefaultAttributes:(NSDictionary *)attrs;

This method simply calls -stringForObjectValue and applies the following text attributes, if necessary:

  • obj == nil adds the -textAttributesForNil attributes
  • FP_NAN adds the -textAttributesForNotANumber attributes
  • FP_SUBNORMAL and FP_ZERO are adding the -textAttributesForZero attributes
  • FP_INFINITE are adding either the -textAttributesForNegativeInfinity or -textAttributesForPositiveInfinity

If the number is a regular number, either -textAttributesForNegativeValues or -textAttributesForPositiveValues are applied.

- (BOOL)getObjectValue:(out id *)obj forString:(NSString *)string errorDescription:(out NSString **)error;

Internally this method uses the following method with a minor parsing change, but returns the -localizedDescription of the NSError and ignores the range.

NSNumberFormatter specific methods

- (BOOL)getObjectValue:(out id *)obj forString:(NSString *)string range:(inout NSRange *)rangep error:(out NSError **)error;

This method reports the used range of the string and an NSError in addition to the usual stuff from NSFormatter. The function behaves differently depending if the behavior is set to 10.0 or 10.4. I'll only discuss the 10.4 behavior here, before 10.4 the parser was not using the ICU library and the parsing was done in code inside the formatter.

  • If the string is the nilSymbol, nil is returned
  • If the string is the negativeInfinitySymbol, kCFNumberNegativeInfinity is returned (or [NSDecimalNumber minimumDecimalNumber], if generatesDecimalNumbers is set)
  • If the string is the positiveInfinitySymbol, kCFNumberPositiveInfinity is returned (or [NSDecimalNumber maximumDecimalNumber], if generatesDecimalNumbers is set)
  • If the decimalSeparator or the groupingSeparator is a non-breaking space (ASCII 160), all spaces from the first occurrence of digits to the last occurrence of digits string are replaced with the non-breaking space. Example: " 1 234.56 " will become " 1?234.56 " with the "?" being the non-breaking space. This seems like an internal adaption for ICU.
  • CFNumberFormatterCreateNumberFromString is called with the string (limited by the optional range)
  • If the application using this formatter was linked on 10.6 or later the leftover characters may not contain anything but whitespace Characters, otherwise the detection failed
  • If the detection failed, a second try is made with a modified format string for the CFNumberFormatter. The formatter string is based on the Unicode Technical Standard #35 and stripped down to only contain the symbols 0-9 and "#@.-+%‰¤". Groupings, exponent formatting, pad escape (but not the padding characters, which seems a bug) and subpattern (which also seems to look like a bug) are removed.
  • If the parsing still fails, the method returns an error
  • Otherwise the detected range is updated.
  • Then the minimum and maximum range is checked and an error returned if the value is outside of this range.
  • At the end an NSDecimalNumber is returned, if -generatesDecimalNumber is set.

- (NSString *)stringFromNumber:(NSNumber *)number;

This is a convenience method. This method returns nil, if number is nil, otherwise it calls -stringForObjectValue:. Therefore it behaves like -stringForObjectValue:, except it never returns the -nilSymbol

- (NSNumber *)numberFromString:(NSString *)string;

This is another convenience method. This method returns nil, if string is nil, otherwise it calls -getObjectValue:forString:errorDescription: and returns the object or nil, if the value couldn't be determined. The error message is ignored.

+ (NSString *)localizedStringFromNumber:(NSNumber *)num numberStyle:(NSNumberFormatterStyle)nstyle;

This is another convenience method. This method returns nil, if string is nil, otherwise it sets the temporarily created formatter behavior to 10.4, the number style to the provided nstyle and returns the -stringForObjectValue:

Wednesday
Jun092010

NSFormatter

I am in the process of writing a collection of NSFormatter classes. Before doing so, I'll try to find out as much as I could about the NSFormatter classes, which are provided with AppKit.

The NSFormatter class is the base class for all formatters. It has to conform to the NSCopying and NSCoding protocols, with -copyWithZone: just retaining the object. Simple enough.

Minimal implementation

These two methods are the minimal implementation for an NSFormatter.

- (NSString *)stringForObjectValue:(id)obj;

This one returns an NSString to be displayed to the user for a certain object. If not implemented, an NSInvalidArgumentException exception will be thrown by this class.

- (BOOL)getObjectValue:(out id *)obj forString:(NSString *)string errorDescription:(out NSString **)error;

This method is the reverse of the first one. It takes a string and tries to convert it into an object and returns YES if it was successful or NO – and an error if the input couldn't be converted into the object. If not implemented, it also throws an NSInvalidArgumentException exception.

Optional methods

The following methods are optional and allow custom attributs, editing and validation.

- (NSString *)editingStringForObjectValue:(id)obj;

This method allows to return a different string for editing the value. For example the € prefix or a % suffix could be stripped away for editing. If not implemented, the base class simply calls -stringForObjectValue:

- (NSAttributedString *)attributedStringForObjectValue:(id)obj withDefaultAttributes:(NSDictionary *)attrs;

If the displayed string should have a certain color (like red for negative numbers) or other attributes (blue underline for web links), this method will be used. NSCell is testing for this method first, before falling back to -stringForObjectValue. A common implementation would internally call -stringForObjectValue before doing a mutable copy of the attributes, adding the additional ones and then returning an NSAttributedString.

- (BOOL)isPartialStringValid:(NSString *)partialString newEditingString:(NSString **)newString errorDescription:(NSString **)error;

This is a compatibility method, in case the method below is not implemented. It takes the partial string as the input, filters the input for valid characters and returns YES, in case the input is valid, together with a new string, which e.g. can be formatted differently. If the input is not valid an error description can be returned, which is forwarded to a potential delegate of the control view. The problem with this method is, that the position of the cursor in the input is unknown and any modification will set the cursor to the end of the input.

The default implementation just returns YES, so any input is accepted.

- (BOOL)isPartialStringValid:(NSString **)partialStringPtr proposedSelectedRange:(NSRangePointer)proposedSelRangePtr originalString:(NSString*)origString originalSelectedRange:(NSRange)origSelRange errorDescription:(NSString **)error;

This method is called to check if a user input is valid before accepting the actual input. This allows to e.g. only accept valid digits or even validate the format of a social security number. It also allows — with limitations — automatic formatting of the input while typing.

If not implemented, the method above is called and if the result is YES and a new string is returned, it returns it as the partial string with the selection range set to the very end of the string and a selection length of 0, which positions the cursor behind the text.

While not required – NSFormatter can exist without any view – the method is usually called from NSCell from the method -textView:(NSTextView *)textView shouldChangeTextInRange:(NSRange)affectedCharRange replacementString:(NSString *)replacementString, if the cell actually has a formatter and a replacementString is provided.

  • A mutable copy of the original string is made, the affectedCharRange being replaced with the replacementString. This copy is provided as the partialStringPtr to the method.
  • The proposedSelRange points behind the replaced string with an empty selection length.
  • The original string, is the -string returned from the NSTextView.
  • The original selection range is the affectedCharRange from the calling method.
  • error is a pointer to nil, which has to be replaced by an NSString * in case of an error.

If this method returns NO, the delegate of the controlView of the cell -control:(NSControl *)control didFailToValidatePartialString:(NSString *)string errorDescription:(NSString *)error is called with the proposed (declined by the formatter) string and the error description. If the partialStringPtr is different from the one provided to the method, the user proposed changes will be reverted.