CS193P - Adding floating point button to iOS calculator - objective-c

I recently started following the online course on iPhone development from Stanford University on iTunes U.
I'm trying to do the homework assignments now for the first couple of lectures. I followed through the walkthrough where I built a basic calculator, but now I'm trying the first assignment and I can't seem to work it out. It's a follows:
Your calculator already works with floating point numbers (e.g. if you press 3 / 4 =
it will properly show the resulting value of 0.75), however, there is no way for the user
to enter a floating point number. Remedy this. Allow only legal floating point
numbers to be entered (e.g. “192.168.0.1” is not a legal floating point number).
First of all, I'm not sure whether a floating point counts as digitPressed or operationPressed. I even tried with a new method called floatingPointPressed but that didn't really work out. Could someone provide pointers on this?
When I saw it as digitPressed, I tried something like this:
if (hasFloatingPoint) {
NSRange range = [[display text] rangeOfString:#"."];
if (range.location == NSNotFound)
{
[display setText:[[display text] stringByAppendingFormat:digit]];
hasFloatingPoint = YES;
}
}
else {
[display setText:[[display text] stringByAppendingFormat:digit]];
}
But I'm missing the key concept here, I think.
I have also tried another solution which I have, sadly, undone already so I can't provide the code but what happend was this: I could press a number, say 5 and then a floating point and 3 so I would end up with 5.3. I then managed to 'disable' the floating point for the remainder of the input.. But I guess I was a little too strict on it: I couldn't, for example, put in 5.3.2 but after pressing an operation button (+, etc), it still wouldn't let me press the floating point button. I guess I should reset the bool I used for this?
I'm not looking for a completely written out solution here, but could someone be so kind to provide some basic advice on how to tackle this problem? Some kind of step-by-step overview of what I should do and think about, without actually providing a code solution.
Thanks in advance.

Okay. I solved this. Was fairly easy once I managed to wrap my head around it:
-(IBAction)digitPressed:(UIButton *)sender
{
NSString *digit = [[sender titleLabel] text];
NSRange range = [[display text] rangeOfString:#"."];
if (userIsInTheMiddleOfTypingANumber)
{
if ( ! ([digit isEqual:#"."] && (range.location != NSNotFound))) {
[display setText:[[display text] stringByAppendingFormat:digit]];
}
}
else
{
if ([digit isEqual:#"."]) {
[display setText: #"0."];
}
else {
[display setText: digit];
}
userIsInTheMiddleOfTypingANumber = YES;
}
}

Since you want a hint of where to look, have a look at attaching a NSNumberFormatter to the input fields.
This is the proper way of validating input, rather than manually checking each character the user enters yourself.
edit
I've just read the assignment that you are trying to work through. I think my answer here, although still the right way to do it, not the best way to do what the assignment is trying to get you to do.
Since you are reading the digits one by one as they are added to the view and appending them to the number you are operating on you may be on the right track with your original answer:
Keep accepting numbers as they are added
Add a decimalEntered flag - initialise it to 'NO'
If a decimal is entered, check the flag, if it is no, accept it and set the flag to YES
If a decimal is entered and the flag is YES, don't accept it.
Whenever you press an operand, set this flag back to NO.
When you learn a bit more, you'll realise that another way to do this is to let the user type in the field. Hook up a formatter to the field to validate that the input is a valid number, but it will also be able to turn the string into a number for you. And when an operand is pressed, this number will be kept aside as one of the numbers upon which the operand will operate.

-(IBAction)floatingPoint:(UIButton *)sender {
NSString *digit = sender.currentTitle;
NSRange range = [self.display.text rangeOfString:#"."];
if(range.location == NSNotFound){
self.display.text = [self.display.text stringByAppendingString:digit];
self.userIsInTheMiddleOfEnteringANumber = YES;
}
}

Your can also use scanners to solve this problem. The code is simpler and I think it also localized.
if (userIsInTheMiddleOfTypingANumber) {
// Allow floating point numbers to be input. Malformed numbers are replaced by 0
NSScanner *scanner = [NSScanner scannerWithString:display.text];
double d;
BOOL wellFormedDouble = [scanner scanDouble:&d];
if (wellFormedDouble && [scanner isAtEnd]) {
brain.operand=d;
}
else {
brain.operand=0;
}
userIsInTheMiddleOfTypingANumber = NO;
}

Hey I think I have a simple solution that works :)
- (IBAction)digitPressed:(UIButton *)sender
{
NSString *digit = [[sender titleLabel] text];
NSRange range = [[display text] rangeOfString:#"."];
if (range.location != NSNotFound && [digit isEqual:#"."]) {
return;
}
if (userIsInTheMiddleOfTypingANumber)
{
[display setText:[[display text] stringByAppendingString:digit]];
}
else
{
[display setText:[digit isEqual:#"."] ? #"0." : digit];
userIsInTheMiddleOfTypingANumber = YES;
}
}

i saw that it suggests to add a function to handle the decimal after i started in on that bit, and ended up with this. the decimal flag is reset in operation pressed. and to really complete it, it should probably disallow leading 0's too.
if (userIsInTheMiddleOfTypingANumber) {
if ([digit isEqual:#"."]) {
if (!userIsInTheMiddleOfTypingADecimal) {
[display setText:[[display text] stringByAppendingString:digit]];
userIsInTheMiddleOfTypingADecimal = YES;
}
}else {
[display setText:[[display text] stringByAppendingString:digit]];
}
}
else//new number
{
if ([digit isEqual:#"."]) {
if (!userIsInTheMiddleOfTypingADecimal) {// this is a superfluous check as if it's a new number it is also not a decimal.
[display setText:#"0."];
userIsInTheMiddleOfTypingADecimal = YES;
userIsInTheMiddleOfTypingANumber = YES;
}
}else {
[display setText:digit];
userIsInTheMiddleOfTypingANumber = YES;
}
}

I found that I did not need to use NSRange. My code can do the operations beginning with a decimal number or a floating number.
-(IBAction)digitPressed:(UIButton *)sender
{
NSString *digit = [[sender titleLabel] text];
if(userIsInTheMiddleOfTypingANumber)
{
[display setText:[[display text] stringByAppendingFormat:digit]];
}
else
{
if([digit isEqual:#"."]) //check for the decimal and append to the digits
[display setText:#"."];
else
[display setText:digit];
userIsInTheMiddleOfTypingANumber = YES;
}
}

I couldn't get any of the above code to work with the CS193P Fall 2011 Calculator.
Here is how I finally got it to work.
-(IBAction)digitPressed:(UIButton *)sender
{ // Begin ACTION
NSString *digit = sender.currentTitle;
if (self.userIsInTheMiddleOfEnteringNumber)
{
//Check to see if a decimal was entered
if ([digit isEqual:#"."])
{
//Check to see if a decimal was entered before
if (self.decimalEntered)
{
[self Display];
}
else
{
self.Display.text = [self.Display.text stringByAppendingString: digit];
self.decimalEntered = YES;
}
}
else
{
self.Display.text = [self.Display.text stringByAppendingString: digit];
}
}
else
{
if ([digit isEqual:#"."])
{
//First Decimal Entered";
self.Display.text = digit;
self.decimalEntered = YES;
}
else
{
//First Number Entered";
self.Display.text = digit;
}
// This tells the Brain that the user is typing
self.userIsInTheMiddleOfEnteringNumber = YES;
}
} // End ACTION

I believe this is simpler.
- (IBAction)digitPressed:(UIButton *)sender
{
NSString *digit =[[sender titleLabel] text];
NSRange range =[[display text] rangeOfString:#"."];
if (([digit isEqual:#"."])&&(range.location==NSNotFound)){midtype=YES;}
if (midtype){
if (([digit isEqual:#"."])&&(range.location!=NSNotFound))
{[display setText:[display text]];}
else {[display setText:[[display text]stringByAppendingString:digit]];}
}
else {[display setText:digit];
midtype =YES;
}
}

This is my first solution and doesn't look objective-C style but it works...
- (IBAction)digitPressed:(UIButton *)sender
{
NSString *digit = [sender currentTitle];
if ([digit isEqualToString:#"."]) {
self.numberOfDot ++;
}
if (self.userIsInTheMiddleOfEnteringANumber){
if (self.numberOfDot < 2) {
self.display.text = [self.display.text stringByAppendingString:digit];
}
else
if (![digit isEqualToString:#"."])
self.display.text = [self.display.text stringByAppendingString:digit];
}
else {
self.display.text = digit;
self.userIsInTheMiddleOfEnteringANumber = YES;
}
}

Related

Always returning null in NSString return function

I have the following code where I want to convert decimal odds to fractional odds. However the function findNearestWholeInteger always returns null.
- (NSString *)displayOddWithFormat:(NSString *)decimalOdd {
if ([[NSUserDefaults standardUserDefaults] boolForKey:#"FractionalOdds"] == YES) {
float oddsF = decimalOdd.floatValue;
oddsF = oddsF - 1.0f;
return [self findNearestWholeInteger:oddsF andInitial:oddsF andBottom:1];
} else {
return odd;
}
}
- (NSString *)findNearestWholeInteger:(float)odds andInitial:(float)initial andBottom:(float)bottom {
NSNumber *numberValue = [NSNumber numberWithFloat:odds];
NSString *floatString = [numberValue stringValue];
NSArray *floatStringComps = [floatString componentsSeparatedByString:#"."];
if (floatStringComps.count == 1) {
return [NSString stringWithFormat:#"%.f/%.f", odds, bottom];
} else {
bottom += 1;
odds += initial;
[self findNearestWholeInteger:odds andInitial:initial andBottom:bottom];
return nil;
}
}
Any ideas where I need to adapt my code? Thanks in advance!
Don't you want:
return [self findNearestWholeInteger:odds andInitial:initial andBottom:bottom];
//return nil;
(not that I really understand what the method is doing).

Can't get negative sign in calculator to disappear

I'm working on an iOS 5 calculator that contains the following code:
- (IBAction)digitPressed:(UIButton *)sender
{
//Enter digit, avoiding two-decimal-point case.
NSString *digit = sender.currentTitle;
NSRange range = [self.display.text rangeOfString:#"."];
if (range.location==NSNotFound || (![digit isEqualToString:#"."]))
if (self.userIsEnteringNumber)
{
self.display.text = [self.display.text stringByAppendingString:digit];
}
else
{
if (![sender.currentTitle isEqualToString:#"."])
{
self.display.text = digit;
}
else
{
self.display.text = #"0.";
}
self.userIsEnteringNumber = YES;
}
}
and
- (IBAction)changeSignPressed
{
//Reverse sign from positive to negative or vice versa.
if (self.numberIsNegative) self.numberIsNegative = NO;
if (!(self.numberIsNegative)) self.numberIsNegative = YES;
//Correct display to reflect change in sign.
if (self.numberIsNegative)
{
if (![[self.display.text substringToIndex:1] isEqualToString:#"-"])
{
self.display.text = [#"-" stringByAppendingString:self.display.text];
}
}
if (!self.numberIsNegative)
{
/*This isn't working--why not?
if ([[self.display.text substringToIndex:1] isEqualToString:#"-"])
{
NSLog(#"Okay."); //This doesn't log, so obv. the problem is in the previous line.
self.display.text = [self.display.text substringFromIndex:1];
}
*/
}
}
I can't get the commented-out code in changeSignPressed to work no matter how I fiddle with it. Any ideas about how I can fix this?
It's probably this code:
//Reverse sign from positive to negative or vice versa.
if (self.numberIsNegative) self.numberIsNegative = NO;
if (!(self.numberIsNegative)) self.numberIsNegative = YES;
There is no else so if self.numberIsNegative is initially YES it will become NO; it will then be immediately seen as NO on the following line where it is changed back to YES.
In any case, that code is more complex than it needs to be; you can just say this to invert a flag:
self.numberIsNegative = ! self.numberIsNegative;

search method not working after second hit

I've a problem with my UISearchDisplayController, the search is not working properly.
This is my code:
- (void)filterContentForSearchText:(NSString*)searchText
scope:(NSString*)scope
{
[self.searchResults removeAllObjects];
for (int i = 0; i < [temp_category count]; i++) {
BOOL foundResult = FALSE;
if ([[temp_category objectAtIndex:i] rangeOfString:searchText].location != NSNotFound) {
foundResult = TRUE;
}
if ([[price_producttitle objectAtIndex:i] rangeOfString:searchText].location != NSNotFound) {
foundResult = TRUE;
}
if ([[price_type objectAtIndex:i] rangeOfString:searchText].location != NSNotFound) {
foundResult = TRUE;
}
if ([[price_description objectAtIndex:i] rangeOfString:searchText].location != NSNotFound) {
foundResult = TRUE;
}
if (foundResult) {
NSNumber *result = [NSNumber numberWithInt:i];
if ([self searchResults] == nil) {
NSMutableArray *array = [[NSMutableArray alloc] init];
[self setSearchResults:array];
[array release];
}
[searchResults addObject:result];
}
}
NSLog (#"array = %i", [searchResults count]);
NSLog(#"%#",searchResults);
}
-(BOOL)searchDisplayController:(UISearchDisplayController *)controller
shouldReloadTableForSearchString:(NSString *)searchString
{
[self filterContentForSearchText:searchString
scope:[[self.searchDisplayController.searchBar scopeButtonTitles]
objectAtIndex:[self.searchDisplayController.searchBar
selectedScopeButtonIndex]]];
return YES;
}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller
shouldReloadTableForSearchScope:(NSInteger)searchOption
{
[self filterContentForSearchText:[self.searchDisplayController.searchBar text]
scope:[[self.searchDisplayController.searchBar scopeButtonTitles]
objectAtIndex:searchOption]];
return YES;
}
But I'm still confused, because when I start a search with the first letter, it gives the correct hits. But when I enter the second letter, it only shows one result (while there are more, as far as I know from my data sample). I'm doing something incorrectly. I think it has something to do with when the user enters text, but I'm confused which method I should use.
The code I now have is a combination of:
this tutorial and
this SO question.
Can someone give me a hint in the good direction? Displaying the results is fine, only this aspect bothers me. I think it has something to do with firing the method and [self.searchResults removeAllObjects];.
I would like to add my code, but the thing is that I still use the code I have above, but I'm manually implementing the UISearchBar (which I found somewhere else in a tutorial) instead of using SearchDisplayController. I also had difficulties with the navigationbar which dissappears when using SearchDisplayController, which gave me enough reason to implement it myself instead of using SearchDisplayController. It gives you more freedom.
At first it seemed a lot of work, so I choose to use SearchDisplayController, but I really advise anyone who needs some modification, or who wants more freedom, please do it manually with UISearchBar and a UITableView :)

NSRange not matching with NSNotFound

I have the following method:
- (IBAction)digitPressed:(UIButton *)sender {
NSLog(#"%#", sender.currentTitle);
if (self.userTypingNumber) {
if (![sender.currentTitle isEqualToString:#"."])
self.display.text = [self.display.text stringByAppendingString:sender.currentTitle];
else {
NSRange range = [sender.currentTitle rangeOfString:#"."];
if (range.location == NSNotFound) {
self.display.text = [self.display.text stringByAppendingString:sender.currentTitle];
}
}
} else {
self.display.text = sender.currentTitle;
self.userTypingNumber = YES;
}
}
My problem is that my program never enters de NSNotFound if. I'm making a calculator and it should accept floating point numbers, but whenever I press the . it just passes right over the if. Any idea what could be wrong?
Looking at your logic, sender.currentTitle is always going to be #"." in your else statement, and so your range is always going to have a location of 0.

Check that a input to UITextField is numeric only

How do I validate the string input to a UITextField? I want to check that the string is numeric, including decimal points.
You can do it in a few lines like this:
BOOL valid;
NSCharacterSet *alphaNums = [NSCharacterSet decimalDigitCharacterSet];
NSCharacterSet *inStringSet = [NSCharacterSet characterSetWithCharactersInString:myInputField.text];
valid = [alphaNums isSupersetOfSet:inStringSet];
if (!valid) // Not numeric
-- this is for validating input is numeric chars only. Look at the documentation for NSCharacterSet for the other options. You can use characterSetWithCharactersInString to specify any set of valid input characters.
There are a few ways you could do this:
Use NSNumberFormatter's numberFromString: method. This will return an NSNumber if it can parse the string correctly, or nil if it cannot.
Use NSScanner
Strip any non-numeric character and see if the string still matches
Use a regular expression
IMO, using something like -[NSString doubleValue] wouldn't be the best option because both #"0.0" and #"abc" will have a doubleValue of 0. The *value methods all return 0 if they're not able to convert the string properly, so it would be difficult to distinguish between a legitimate string of #"0" and a non-valid string. Something like C's strtol function would have the same issue.
I think using NSNumberFormatter would be the best option, since it takes locale into account (ie, the number #"1,23" in Europe, versus #"1.23" in the USA).
I use this code in my Mac app, the same or similar should work with the iPhone. It's based on the RegexKitLite regular expressions and turns the text red when its invalid.
static bool TextIsValidValue( NSString* newText, double &value )
{
bool result = false;
if ( [newText isMatchedByRegex:#"^(?:|0|[1-9]\\d*)(?:\\.\\d*)?$"] ) {
result = true;
value = [newText doubleValue];
}
return result;
}
- (IBAction) doTextChanged:(id)sender;
{
double value;
if ( TextIsValidValue( [i_pause stringValue], value ) ) {
[i_pause setTextColor:[NSColor blackColor]];
// do something with the value
} else {
[i_pause setTextColor:[NSColor redColor]];
}
}
If you want a user to only be allowed to enter numerals, you can make your ViewController implement part of UITextFieldDelegate and define this method:
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
NSString *resultingString = [textField.text stringByReplacingCharactersInRange: range withString: string];
// The user deleting all input is perfectly acceptable.
if ([resultingString length] == 0) {
return true;
}
NSInteger holder;
NSScanner *scan = [NSScanner scannerWithString: resultingString];
return [scan scanInteger: &holder] && [scan isAtEnd];
}
There are probably more efficient ways, but I find this a pretty convenient way. And the method should be readily adaptable to validating doubles or whatever: just use scanDouble: or similar.
#pragma mark - UItextfield Delegate
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
if ([string isEqualToString:#"("]||[string isEqualToString:#")"]) {
return TRUE;
}
NSLog(#"Range ==%d ,%d",range.length,range.location);
//NSRange *CURRANGE = [NSString rangeOfString:string];
if (range.location == 0 && range.length == 0) {
if ([string isEqualToString:#"+"]) {
return TRUE;
}
}
return [self isNumeric:string];
}
-(BOOL)isNumeric:(NSString*)inputString{
BOOL isValid = NO;
NSCharacterSet *alphaNumbersSet = [NSCharacterSet decimalDigitCharacterSet];
NSCharacterSet *stringSet = [NSCharacterSet characterSetWithCharactersInString:inputString];
isValid = [alphaNumbersSet isSupersetOfSet:stringSet];
return isValid;
}
Here are a few one-liners which combine Peter Lewis' answer above (Check that a input to UITextField is numeric only) with NSPredicates
#define REGEX_FOR_NUMBERS #"^([+-]?)(?:|0|[1-9]\\d*)(?:\\.\\d*)?$"
#define REGEX_FOR_INTEGERS #"^([+-]?)(?:|0|[1-9]\\d*)?$"
#define IS_A_NUMBER(string) [[NSPredicate predicateWithFormat:#"SELF MATCHES %#", REGEX_FOR_NUMBERS] evaluateWithObject:string]
#define IS_AN_INTEGER(string) [[NSPredicate predicateWithFormat:#"SELF MATCHES %#", REGEX_FOR_INTEGERS] evaluateWithObject:string]
For integer test it'll be:
- (BOOL) isIntegerNumber: (NSString*)input
{
return [input integerValue] != 0 || [input isEqualToString:#"0"];
}
You can use the doubleValue of your string like
NSString *string=#"1.22";
double a=[string doubleValue];
i think this will return a as 0.0 if the string is invalid (it might throw an exception, in which case you can just catch it, the docs say 0.0 tho). more info here
Hi had the exact same problem and I don't see the answer I used posted, so here it is.
I created and connected my text field via IB. When I connected it to my code via Control+Drag, I chose Action, then selected the Editing Changed event. This triggers the method on each character entry. You can use a different event to suit.
Afterwards, I used this simple code to replace the text. Note that I created my own character set to include the decimal/period character and numbers. Basically separates the string on the invalid characters, then rejoins them with empty string.
- (IBAction)myTextFieldEditingChangedMethod:(UITextField *)sender {
NSCharacterSet *validCharacterSet = [NSCharacterSet characterSetWithCharactersInString:#".0123456789"];
NSCharacterSet *invalidCharacterSet = validCharacterSet.invertedSet;
sender.text = [[sender.text componentsSeparatedByCharactersInSet:invalidCharacterSet] componentsJoinedByString:#""];
}
Credits:
Remove all but numbers from NSString
Late to the game but here a handy little category I use that accounts for decimal places and the local symbol used for it. link to its gist here
#interface NSString (Extension)
- (BOOL) isAnEmail;
- (BOOL) isNumeric;
#end
#implementation NSString (Extension)
/**
* Determines if the current string is a valid email address.
*
* #return BOOL - True if the string is a valid email address.
*/
- (BOOL) isAnEmail
{
NSString *emailRegex = #"[A-Z0-9a-z._%+-]+#[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}";
NSPredicate *emailTest = [NSPredicate predicateWithFormat:#"SELF MATCHES %#", emailRegex];
return [emailTest evaluateWithObject:self];
}
/**
* Determines if the current NSString is numeric or not. It also accounts for the localised (Germany for example use "," instead of ".") decimal point and includes these as a valid number.
*
* #return BOOL - True if the string is numeric.
*/
- (BOOL) isNumeric
{
NSString *localDecimalSymbol = [[NSLocale currentLocale] objectForKey:NSLocaleDecimalSeparator];
NSMutableCharacterSet *decimalCharacterSet = [NSMutableCharacterSet characterSetWithCharactersInString:localDecimalSymbol];
[decimalCharacterSet formUnionWithCharacterSet:[NSCharacterSet alphanumericCharacterSet]];
NSCharacterSet* nonNumbers = [decimalCharacterSet invertedSet];
NSRange r = [self rangeOfCharacterFromSet: nonNumbers];
if (r.location == NSNotFound)
{
// check to see how many times the decimal symbol appears in the string. It should only appear once for the number to be numeric.
int numberOfOccurances = [[self componentsSeparatedByString:localDecimalSymbol] count]-1;
return (numberOfOccurances > 1) ? NO : YES;
}
else return NO;
}
#end
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
if(string.length > 0)
{
NSCharacterSet *numbersOnly = [NSCharacterSet characterSetWithCharactersInString:#"0123456789"];
NSCharacterSet *characterSetFromTextField = [NSCharacterSet characterSetWithCharactersInString:string];
BOOL stringIsValid = [numbersOnly isSupersetOfSet:characterSetFromTextField];
return stringIsValid;
}
return YES;
}
IMO the best way to accomplish your goal is to display a numeric keyboard rather than the normal keyboard. This restricts which keys are available to the user. This alleviates the need to do validation, and more importantly it prevents the user from making a mistake. The number pad is also much nicer for entering numbers because the keys are substantially larger.
In interface builder select the UITextField, go to the Attributes Inspector and change the "Keyboard Type" to "Decimal Pad".
That'll make the keyboard look like this:
The only thing left to do is ensure the user doesn't enter in two decimal places. You can do this while they're editing. Add the following code to your view controller. This code removes a second decimal place as soon as it is entered. It appears to the user as if the 2nd decimal never appeared in the first place.
- (void)viewDidLoad
{
[super viewDidLoad];
[self.textField addTarget:self
action:#selector(textFieldDidChange:)
forControlEvents:UIControlEventEditingChanged];
}
- (void)textFieldDidChange:(UITextField *)textField
{
NSString *text = textField.text;
NSRange range = [text rangeOfString:#"."];
if (range.location != NSNotFound &&
[text hasSuffix:#"."] &&
range.location != (text.length - 1))
{
// There's more than one decimal
textField.text = [text substringToIndex:text.length - 1];
}
}
#property (strong) NSNumberFormatter *numberFormatter;
#property (strong) NSString *oldStringValue;
- (void)awakeFromNib
{
[super awakeFromNib];
self.numberFormatter = [[NSNumberFormatter alloc] init];
self.oldStringValue = self.stringValue;
[self setDelegate:self];
}
- (void)controlTextDidChange:(NSNotification *)obj
{
NSNumber *number = [self.numberFormatter numberFromString:self.stringValue];
if (number) {
self.oldStringValue = self.stringValue;
} else {
self.stringValue = self.oldStringValue;
}
}
Old thread, but it's worth mentioning that Apple introduced NSRegularExpression in iOS 4.0. (Taking the regular expression from Peter's response)
// Look for 0-n digits from start to finish
NSRegularExpression *noFunnyStuff = [NSRegularExpression regularExpressionWithPattern:#"^(?:|0|[1-9]\\d*)(?:\\.\\d*)?$" options:0 error:nil];
// There should be just one match
if ([noFunnyStuff numberOfMatchesInString:<#theString#> options:0 range:NSMakeRange(0, <#theString#>.length)] == 1)
{
// Yay, digits!
}
I suggest storing the NSRegularExpression instance somewhere.
I wanted a text field that only allowed integers. Here's what I ended up with (using info from here and elsewhere):
Create integer number formatter (in UIApplicationDelegate so it can be reused):
#property (nonatomic, retain) NSNumberFormatter *integerNumberFormatter;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Create and configure an NSNumberFormatter for integers
integerNumberFormatter = [[NSNumberFormatter alloc] init];
[integerNumberFormatter setMaximumFractionDigits:0];
return YES;
}
Use filter in UITextFieldDelegate:
#interface MyTableViewController : UITableViewController <UITextFieldDelegate> {
ictAppDelegate *appDelegate;
}
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
// Make sure the proposed string is a number
NSNumberFormatter *inf = [appDelegate integerNumberFormatter];
NSString* proposedString = [textField.text stringByReplacingCharactersInRange:range withString:string];
NSNumber *proposedNumber = [inf numberFromString:proposedString];
if (proposedNumber) {
// Make sure the proposed number is an integer
NSString *integerString = [inf stringFromNumber:proposedNumber];
if ([integerString isEqualToString:proposedString]) {
// proposed string is an integer
return YES;
}
}
// Warn the user we're rejecting the change
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate);
return NO;
}
Not so elegant, but simple :)
- (BOOL) isNumber: (NSString*)input
{
return [input doubleValue] != 0 || [input isEqualToString:#"0"] || [input isEqualToString:#"0.0"];
}
Accept decimal values in text fields with single (.)dot working with iPad and iPhone in Swift 3
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let inverseSet = NSCharacterSet(charactersIn:"0123456789").inverted
let components = string.components(separatedBy: inverseSet)
let filtered = components.joined(separator: "")
if filtered == string {
return true
} else {
if string == "." {
let countdots = textField.text!.components(separatedBy:".").count - 1
if countdots == 0 {
return true
}else{
if countdots > 0 && string == "." {
return false
} else {
return true
}
}
}else{
return false
}
}
}
To be more international (and not only US colored ;-) ) just replace in the code above by
-(NSNumber *) getNumber
{
NSString* localeIdentifier = [[NSLocale autoupdatingCurrentLocale] localeIdentifier];
NSLocale *l_en = [[NSLocale alloc] initWithLocaleIdentifier: localeIdentifier] ;
return [self getNumberWithLocale: [l_en autorelease] ];
}
This answer uses NSFormatter as said previously. Check it out:
#interface NSString (NSNumber)
- (BOOL) isNumberWithLocale:(NSLocale *) stringLocale;
- (BOOL) isNumber;
- (NSNumber *) getNumber;
- (NSNumber *) getNumberWithLocale:(NSLocale*) stringLocale;
#end
#implementation NSString (NSNumber)
- (BOOL) isNumberWithLocale:(NSLocale *) stringLocale
{
return [self getNumberWithLocale:stringLocale] != nil;
}
- (BOOL) isNumber
{
return [ self getNumber ] != nil;
}
- (NSNumber *) getNumber
{
NSLocale *l_en = [[NSLocale alloc] initWithLocaleIdentifier: #"en_US"] ;
return [self getNumberWithLocale: [l_en autorelease] ];
}
- (NSNumber *) getNumberWithLocale:(NSLocale*) stringLocale
{
NSNumberFormatter *formatter = [[ [ NSNumberFormatter alloc ] init ] autorelease];
[formatter setLocale: stringLocale ];
return [ formatter numberFromString:self ];
}
#end
I hope it helps someone. =)
#import "NSString+Extension.h"
//#interface NSString (Extension)
//
//- (BOOL) isAnEmail;
//- (BOOL) isNumeric;
//
//#end
#implementation NSString (Extension)
- (BOOL) isNumeric
{
NSString *emailRegex = #"[0-9]+";
NSPredicate *emailTest = [NSPredicate predicateWithFormat:#"SELF MATCHES %#", emailRegex];
return [emailTest evaluateWithObject:self];
// NSString *localDecimalSymbol = [[NSLocale currentLocale] objectForKey:NSLocaleDecimalSeparator];
// NSMutableCharacterSet *decimalCharacterSet = [NSMutableCharacterSet characterSetWithCharactersInString:localDecimalSymbol];
// [decimalCharacterSet formUnionWithCharacterSet:[NSCharacterSet alphanumericCharacterSet]];
//
// NSCharacterSet* nonNumbers = [decimalCharacterSet invertedSet];
// NSRange r = [self rangeOfCharacterFromSet: nonNumbers];
//
// if (r.location == NSNotFound)
// {
// // check to see how many times the decimal symbol appears in the string. It should only appear once for the number to be numeric.
// int numberOfOccurances = [[self componentsSeparatedByString:localDecimalSymbol] count]-1;
// return (numberOfOccurances > 1) ? NO : YES;
// }
// else return NO;
}
In Swift 4:
let formatString = "12345"
if let number = Decimal(string:formatString){
print("String contains only number")
}
else{
print("String doesn't contains only number")
}
This covers: Decimal part control (including number of decimals allowed), copy/paste control, international separators.
Steps:
Make sure your view controller inherits from UITextFieldDelegate
class MyViewController: UIViewController, UITextFieldDelegate {...
In viewDidLoad, set your control delegate to self:
override func viewDidLoad() {
super.viewDidLoad();
yourTextField.delegate = self
}
Implement the following method and update the "decsAllowed" constant with the desired amount of decimals or 0 if you want a natural number.
Swift 4
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let decsAllowed: Int = 2
let candidateText = NSString(string: textField.text!).replacingCharacters(in: range, with: string)
let decSeparator: String = NumberFormatter().decimalSeparator!;
let splitted = candidateText.components(separatedBy: decSeparator)
let decSeparatorsFound = splitted.count - 1
let decimalPart = decSeparatorsFound > 0 ? splitted.last! : ""
let decimalPartCount = decimalPart.characters.count
let characterSet = NSMutableCharacterSet.decimalDigit()
if decsAllowed > 0 {characterSet.addCharacters(in: decSeparator)}
let valid = characterSet.isSuperset(of: CharacterSet(charactersIn: candidateText)) &&
decSeparatorsFound <= 1 &&
decsAllowed >= decimalPartCount
return valid
}
If afterwards you need to safely convert that string into a number, you can just use Double(yourstring) or Int(yourstring) type cast, or the more academic way:
let formatter = NumberFormatter()
let theNumber: NSNumber = formatter.number(from: yourTextField.text)!