Resolved NumberFormatter in TableView

Discussion in 'Mac Programming' started by Qaanol, Dec 21, 2013.

  1. Qaanol, Dec 21, 2013
    Last edited: Dec 25, 2013

    Qaanol macrumors 6502a

    Joined:
    Jun 21, 2010
    #1
    4th Update: Finally solved. See post #4.

    3rd Update: Partial improvement. See edit of next post.

    2nd Update: Not actually fully resolved. See next post.

    Update: I solved my problem. See edit below.

    I have a cell-based TableView where one column has a TextFieldCell equipped with a NumberFormatter. The NumberFormatter is configured to only allow non-negative integers.

    The problem I have is, when I type nonsense into the text field and then go to leave the text field, what I want to happen is for the nonsense to be replaced as if by atoi. So if I type “345abc” I want it to be replaced with “345”. (and if I type a negative number I want it to be replaced with 0, because I’m disallowing negatives.)

    But what actually happens is, if I type anything other than a positive integer, if there is even one non-numeric character, I cannot get out of the text field until I manually change it to an acceptable value. If I click outside the field, nothing happens. If I hit the tab key, nothing happens. If I hit the escape key, nothing happens.

    I am appalled that Apple allowed such a brutally user-unfriendly behavior to be the default, but whatever, that’s their decision to make. My question is, how do I fix it so it is user-friendly the way I want: if the user attempts to leave the TextFieldCell while it has a nonsense value, the text in the cell is parsed as if by atoi.

    The TextFieldCell does not have a delegate, so I cannot simply use the TextFieldDelegateProtocol methods.

    Ideas?

    Edit: Solved. I made a subclasses of NSNumberFormatter to act as my custom integer formatter. In it I implemented (overrode) getObjectValue:forString:errorDescription: to call [string intValue], check for negative numbers and replace them with 0, then construct a new NSString from that number and call [super getObjectValue:etc.].

    In IB I set the class of the NumberFormatter for the TextFieldCell to be my new subclass, and it works like a charm, except when the number format uses non-numeric characters.
     
  2. Qaanol, Dec 22, 2013
    Last edited: Dec 22, 2013

    Qaanol thread starter macrumors 6502a

    Joined:
    Jun 21, 2010
    #2
    Whoops, turns out that when I have the formatter configured to use comma separators, as for example “1,000” for one thousand, my previous attempt treats the comma as a non-numeric character and replaces the value with just the part before the comma, so “1” in the example.

    So…is there a good way to achieve what I want here?

    Edit:

    Okay, I took the incoming string, made a new string by replacing occurrences of [self groupingSeparator] with @"", then got the numeric value from that, made a new string from that value, and called the superclass’s implementation with that new string.

    This seems convoluted, and it probably still misses things like currency symbols and whatnot, so there must be a better way, right?
     
  3. chown33 macrumors 604

    Joined:
    Aug 9, 2009
    #3
    I can't tell what you want the numeric result to be. First you say you want it to act like atoi(), then when it does that by stopping at a comma, you say you don't want that. This seems like you haven't thought things through in sufficient detail, i.e. the behavior is underspecified.

    I think you should first make a clear list of characters you want recognized and those you don't. Then, describe exactly what happens when a character is not recognized. Does it halt the numeric parsing (which is what atoi() does)? Does it skip the character as if it weren't there? Does it do something else?

    Further, write down a few carefully crafted examples of strings, and the resulting values you want. For example, does "abc345$.6" produce 345, 345.6, 3456, or 0? Are grouping separators localized? What about currency marks? If you're ignoring everything not a digit, is that even necessary?

    One simple approach would be to eliminate every character that isn't a digit. So "abc345$.6" would produce the value 3456. It's fairly easy to iterate over a string, test a character for whether it's a digit, and build the digits-only string that results. You can then convert that to an integer.

    Or you might rethink, and simply prohibit unwanted characters in the text field. This would ensure that only digits are entered, so typing "abc345$.6" would only show "3456", and elicit a beep for each invalid character typed.
     
  4. Qaanol, Dec 25, 2013
    Last edited: Dec 25, 2013

    Qaanol thread starter macrumors 6502a

    Joined:
    Jun 21, 2010
    #4
    What I want to do is, essentially, let N be the greatest integer such that the first N characters of the user-entered text is considered valid by the basic NSNumberFormatter instance (with options specified in IB and/or programmatically.) If N=0, meaning the very first character is invalid, the result should be 0, otherwise the result should be the value found by the NSNumberFormatter class from the first N characters of the input string.

    So “abc345$.6” produces 0 because the first character is not valid.

    I definitely do not want there to be any beeps emitted, ever. I suppose I could implement partial string validation, if there is a way to make sure it does not beep.

    If I go that route, my question essentially amounts to, “Is there a simple way to query whether a basic NSNumberFormatter instance (with the specified options) would find the current string valid?”

    Edit: …and looking again at the documentation, it appears isPartialStringValid:newEditingString:errorDescription: might do exactly what I need.

    Edit 2: …and actually trying it out, it seems the default implementation returns YES all the time, even if the string is not valid and the cell refuses to end editing, so that’s not going to work.

    How the heck can I programmatically query whether a specific instance of NSNumberFormatter thinks the text currently in the NSTextFieldCell to which it belongs, is acceptable?

    Edit 3: Okay, I figured it out. The solution is, in the subclass implementation of getObjectValue:forString:range:error: to call [super getObjectValue:eek:bj forString:string range:rangep error:error] and check the result. In particular, the times that the cell refuses to end editing, are exactly when that method puts a null value in the obj pointer.

    Here is the code I currently have, which works. I will consider using partial string validation, but for now this works:

    Code:
    - (BOOL)getObjectValue:(out __autoreleasing id *)obj
                 forString:(NSString *)string
                     range:(inout NSRange *)rangep
                     error:(out NSError *__autoreleasing *)error
    {
        BOOL b = [super getObjectValue:obj
                             forString:string
                                 range:rangep
                                 error:error];
        if (b && [string length] && (!rangep || rangep->length)) {
            // If the input string is valid and non-empty, run with it
            return b;
        } else {
            // We have to truncate
            if (!rangep) {
                // Do not use [string length]-1 because that might be -1
                NSRange r = NSMakeRange(0, [string length]);
                rangep = &r;
            }
            
            while (rangep->length && ![super getObjectValue:obj
                                                  forString:string
                                                      range:rangep
                                                      error:nil]) {
                rangep->length--;   // Truncate, truncate, truncate...
            }
            
            if (rangep->length) {
                // We found a valid maximal non-empty initial substring
                return [super getObjectValue:obj
                                   forString:string
                                       range:rangep
                                       error:error];
            } else {
                // The first character was invalid
                if ([self minimum]) {
                    string = [self stringFromNumber:[self minimum]];
                } else if ([self maximum]) {
                    string = [self stringFromNumber:[self maximum]];
                } else {
                    string = [self stringFromNumber:[NSNumber numberWithInt:0]];
                }
                rangep->location = 0;
                rangep->length = [string length];
                return [super getObjectValue:obj
                                   forString:string
                                       range:rangep
                                       error:error];
            }
        }
    }
    Thanks chown, you were right that I had not fully specified my goals. When you called my attention to that fact, I was able to think it through clearly and figure out what I actually intended. It still was not trivial to find the solution, since the documentation does not explain when and how a formatter prevents its cell from finishing editing, but experimentation prevailed.
     

Share This Page