I want to restrict the number of fraction digits a user is allowed to enter into a UITextField that only accepts (localized) numeric input.
Example with 4 fraction digits allowed:
Good: 42, 10.123, 12345.2345
Bad: 0.123456, 6.54321
Right now, I'm using NSNumberFormatter's numberFromString: in the UITextField delegate's textField:shouldChangeCharactersInRange:replacementString: to determine whether it's a legal numeric value.
Unfortunately, NSNumberFormatter seems to ignore maximumFractionDigits in numberFromString:. In tests using getObjectValue:forString:range:error: I had the same problem, and range also was the full length of the string afterwards (unless I start entering letters; then range indicates only the part of the string with digits):
NSNumberFormatter* formatter = [[NSNumberFormatter alloc] init];
formatter.maximumFractionDigits = 3;
formatter.roundingMode = NSNumberFormatterRoundHalfUp;
formatter.generatesDecimalNumbers = YES;
NSDecimalNumber* n = (NSDecimalNumber*)[formatter numberFromString:#"10.12345"];
NSLog(#"Number: %#", n.description); // expected: 10.123, but is: 10.12345
How to best restrict the number of fraction digits in user input?
after you get the unrestricted number, you can use stringWithFormat on that number to create a string with a certain number of decimal places.
eg.
double number = myTextField.text.doubleValue;
NSString *restrictedString = [NSString stringWithFormat:#"%.4f", number];
There are a few ways to do this, but the easiest is probably to split the string into two parts (you will have to localize the '.') and check the length of the second part, like this:
- (BOOL)LNNumberIsValid:(NSString *)string
{
NSArray *numArray = [string componentsSeparatedByString:#"."];
if ([numArray count] == 2)
if ([[numArray objectAtIndex:1] length] > 4)
return NO;
return YES;
}
// Tests
NSLog(#"42: %i", [self LNNumberIsValid:#"42"]); // 1
NSLog(#"10.123: %i", [self LNNumberIsValid:#"10.123"]); // 1
NSLog(#"12345.2345: %i", [self LNNumberIsValid:#"12345.2345"]); // 1
NSLog(#"0.123456: %i", [self LNNumberIsValid:#"0.123456"]); // 0
NSLog(#"6.54321: %i", [self LNNumberIsValid:#"6.54321"]); // 0
EDIT:
The problem with the code that you added to your question is that you are printing the description of the NSDecimalNumber, which is not localized or limited to the number of digits. The NSDecimalNumber itself stores everything that you give it, so you need to change the original string (like my example above) if you want to change that. However, once you have your NSDecimalNumber, you can use the same number formatter to convert it back to a string in the format that you like:
NSNumberFormatter* formatter = [[NSNumberFormatter alloc] init];
formatter.maximumFractionDigits = 3;
formatter.roundingMode = NSNumberFormatterRoundHalfUp;
formatter.generatesDecimalNumbers = YES;
NSDecimalNumber* n = (NSDecimalNumber*)[formatter numberFromString:#"10.12345"];
NSString *s = [formatter stringFromNumber:n];
NSLog(#"Number: %#", s); // expected: 10.123, and is: 10.123
The way I solved this is by checking the position of the decimal separator and making sure that the insertion either is before that position or the insertion would not exceed the maximum number of fraction digits.
Also, I check that the input of a new separator does not occur at a place that would lead to more then the allowed fraction digits and that not more than one separators can be inserted
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
NSString *separator = self.numberFormatter.decimalSeparator;
if(string.length == 0) {
// Empty String means deletion, always possible
return YES;
}
// Check for valid characters (0123456789 + decimal Separator)
for (int i = 0; i < [string length]; ++i) {
unichar c = [string characterAtIndex:i];
if (![self.legalCharSet characterIsMember:c])
{
return NO;
}
}
// Checks if input is separator
if([string isEqualToString:separator]) {
// Check that separator insertion would not lead to more than 2 fraction digits and that not more than one separators are inserted
// (the MIN() makes sure that length - kMaxFractionDigits won’t be below 0 as length and location are NSUIntegers)
return range.location >= self.valueField.text.length - MIN(self.valueField.text.length,kMaxFractionDigits) && [self.valueField.text containsString:separator] == NO;
} else {
// Check if a separator is already included in the string
NSRange separatorPos = [self.valueField.text rangeOfString: separator];
if(separatorPos.location != NSNotFound) {
// Make sure that either the input is before the decimal separator or that the fraction digits would not exceed the maximum fraction digits.
NSInteger fractionDigits = self.valueField.text.length - (separatorPos.location + 1);
return fractionDigits + string.length <= kMaxFractionDigits || range.location <= separatorPos.location;
}
}
return YES;
}
The method may not be bullet proof but it should be sufficient for common text insertions.
Related
I want to read many different integer values out of an textfield and for that I have this code:
NSString *string1,*string2;
string1= [textField stringValue];
int i,c;
c=0;
NSInteger values[50];
for (i=0; i<50; ) {
string2=[NSString stringWithFormat:#"%#%i%s",#" ",i,","];
NSRange range=[string1 rangeOfString:string2];
if (range.location != NSNotFound) {
values[c]=i;
c=c+1;
}
i=i+1;
}
there is no problem with that but it can't read the first number in string like that "2, 3, 15" but I want it that it can also read a string like that, so could anybody please help me with this problem.
And if I make string2 like this #"i", "," it causes problems with values like 15 because it reads 5 and 15
How about doing this instead:
NSArray *integerStrings = [[textField stringValue] componentsSeparatedByString:#","];
for (NSString *integerString in integerStrings) {
NSDecimalNumber *number = [NSDecimalNumber decimalNumberWithString:integerString];
// do whatever you want with the number...
}
Alternatively, if the numbers are always integers, the loop could be:
for (NSString *integerString in integerStrings) {
NSInteger number = [integerString integerValue];
// do whatever you want with the number...
}
This question is unlikely to help any future visitors; it is only relevant to a small geographic area, a specific moment in time, or an extraordinarily narrow situation that is not generally applicable to the worldwide audience of the internet. For help making this question more broadly applicable, visit the help center.
Closed 10 years ago.
I am trying to find out if a 1 letter string appears in the first 2 letters of a different string. I have tried to compare the strings with == as well as the isEqualToString NSString method.
Does anyone have any idea why my code is not able to find a match. It always just returns 0.
I am wanting the methods to be returning 2 as the letter b is in the second letter location in the word (first parameter) and should match the letter (second parameter)
main method:
Finder *f = [[Finder alloc]init];
int position = [f findLetterLocation : #"abc" : #"b"];
NSLog(#"using == %d", position);
int position2 = [f findLetterLocation2 : #"abc" : #"b"];
NSLog(#"using isEqualToString %d", position);
Finder.m
-(int)findLetterLocation: (NSString*)word : (NSString*)letter{
NSRange MyOneRange = {1, 1};
NSRange MyTwoRange = {2, 1};
NSString *firstCharacter = [[NSString alloc] init];
NSString *secondCharacter = [[NSString alloc] init];
firstCharacter = [word substringWithRange:MyOneRange];
secondCharacter = [word substringWithRange:MyTwoRange];
if(firstCharacter == letter){
return 1;
}
if(firstCharacter == letter){
return 2;
}
return 0;
}
findLetterLocation2 is exactly the same as the method above but only replaces the if statements with the ones below:
if([firstCharacter isEqualToString: letter]){
return 1;
}
if([firstCharacter isEqualToString: letter]){
return 2;
}
The output is
2012-12-29 18:14:51.253 test[83101:303] using == 0
2012-12-29 18:14:51.255 test[83101:303] using isEqualToString 0
Thanks!
You're defining a method -[Finder findLetterLocation::]. You want this method to return the 1-indexed location in the first argument ("word") of the second argument ("letter") if it is the first or second letter and otherwise return 0. This implementation will do the trick:
-(int)findLetterLocation: (NSString*)word : (NSString*)letter
{
int res = 0;
if (word &&
letter &&
[letter length] == 1) {
NSRange range = [word rangeOfString:letter];
if (range.location < 2) {
res = range.location + 1;
}
}
return res;
}
This method name is unconventional. A better method name would be
- (int)locationInString:(NSString *)string ofLetter:(NSString *)letter
A still better method name would be
- (int)pfx_oneIndexedLocationInFirstTwoCharactersOfString:(NSString *)string ofCharacter:(NSString *)letter
where pfx is replaced with your project's prefix.
This method's return type should not be int. Instead it should be either NSUInteger or NSInteger, probably NSUInteger since the result will always be 0, 1 or 2.
- (NSUInteger)pfx_oneIndexedLocationInFirstTwoCharactersOfString:(NSString *)string ofCharacter:(NSString *)letter
This method should probably be in a category on NSString rather than in a stand-alone class Finder:
#interface NSString (Locating)
- (NSUInteger)pfx_oneIndexedLocationInFirstTwoCharactersOfCharacter:(NSString *)character
#end
#implementation NSString (Locating)
- (NSUInteger)pfx_oneIndexedLocationInFirstTwoCharactersOfCharacter:(NSString *)character
{
int res = 0;
if (character &&
[character length] == 1) {
NSRange range = [word rangeOfString:letter];
if (range.location < 2) {
res = range.location + 1;
}
}
return res;
}
#end
NSRange uses location as index 0. the characters you are getting are character 2 and character 3.
Also. I believe that NSString will only match with == if you use the same object.
To match strings use -[NSString isEqualToString:]
Hope that helps
You can test with
NSLog(#"First Character = %#", firstCharacter);
Is there a quick way to format numbers (e.g., when using appendFormat) so that only enough decimal places are shown? E.g., 4.23100 is shown as 4.231, while 5.0000 is shown as just 5.
Thanks for reading.
Use %g instead of %f
double n = 1234.5678;
NSString str = [NSString stringWithFormat:#"number is %g", n];
Use %g with the appropriate precision.
double number = 1234.123;
NSLog(#"Number equals: %.8g", number)
As referenced here, the %g conversion will default to 6 significant figures unless a maximum precision is specified. %.7g or larger will all give the desired float.
I couldn't find a built-in way to do this at first, so I built a quick function (below) if anyone is interested. That said, I believe %g (Thanks MSgambel, Roger Gilbrat) and the NSNumberFormatter class (Thanks Inafziger, David Nedrow) do this much more succinctly, so they're probably the better option!
+ (NSString *)trimmedFraction:(float)fraction{
int i=0;
float numDiff=0;
// Loop through possible decimal values (in this case, maximum of 6)
for (i=0;i<=6;i++){
// Calculate difference between fraction and rounded fraction
numDiff=round(fraction*pow(10,i))/pow(10,i)-fraction;
// Check if difference is less than half of the smallest possible value
if (fabsf(numDiff)<(0.5*pow(10,-6))){
break;
}
}
// Return string of truncated fraction
NSString *stringPattern=[NSString stringWithFormat:#"%%0.%df",i];
return [NSString stringWithFormat:stringPattern,fraction];
}
In the code you can specify the maximum number of decimal spaces (e.g., 6 in this case). Then you just send it a fraction and it returns a string with only the relevant decimals. For example, send it 4.231000 and it returns #"4.231", send it 5.000000 and it returns #"5". (Again, probably better to just use %g and NSNumberWithFormatter).
If you store the original values as strings instead of floating-point numbers, the question makes a lot of sense.
I handle this using a category on NSString:
+ (NSString *) stringForNotANumber {
static NSString *singleton;
if (!singleton) singleton = #"NaN";
return singleton;
}
- (NSUInteger) numberOfDecimalPlaces {
NSString *strValue = self;
// If nil, return -1
if (!strValue) return -1;
// If non-numeric, return -1
NSNumberFormatter *f = [[NSNumberFormatter alloc] init];
[f setMaximumFractionDigits:128];
NSNumber *numValue = [f numberFromString:strValue];
if (!numValue) return -1;
// Count digits after decimal point in original input
NSRange range = [strValue rangeOfString:#"."];
if (NSNotFound == range.location) return 0;
return [strValue substringFromIndex:range.location+1].length;
}
- (NSString *) plus:(NSString *) addend1 {
NSString *addend2 = self;
if (!addend1 || !addend2) return nil;
if (addend1 == [NSString stringForNotANumber] ||
addend2 == [NSString stringForNotANumber])
return [NSString stringForNotANumber];
NSNumberFormatter *f = [[NSNumberFormatter alloc] init];
NSNumber *num1 = [f numberFromString:addend1];
NSNumber *num2 = [f numberFromString:addend2];
if (!num1 || !num2) return [NSString stringForNotANumber];
double sum = num1.doubleValue + num2.doubleValue;
[f setMinimumFractionDigits:MAX(addend1.numberOfDecimalPlaces,
addend2.numberOfDecimalPlaces)];
return [f stringFromNumber:[NSNumber numberWithDouble:sum]];
}
I am a beginner Objective-C programmer and I was building a calculator as my first app. I am using an NSNumberFormatter to format the Calculator’s display. It converts NSNumber to NSString correctly, but doesn’t parse NSString to NSNumber right if they have a comma separator. Here’s the code:
- (IBAction)digitPressed:(NSButton *)sender {
if (userIsEnteringANumber) {
NSNumber *number = [formatter numberFromString:[self.calDisplay.stringValue stringByAppendingString:sender.title]];
self.calDisplay.stringValue = [formatter stringFromNumber:number];
}
else {
self.calDisplay.stringValue = sender.title;
userIsEnteringANumber = YES;
}
}
If calDisplay.stringValue = #“2,569”, then pressing another digit sets number to nil.
Pragmatically I would simply remove the comma before parsing the string into a number then it must work.
NSString *s = [self.calDisplay.stringValue
stringByReplacingOccurrencesOfString:#"," withString:#""];
Leaving you with
- (IBAction)digitPressed:(NSButton *)sender {
if (userIsEnteringANumber) {
NSString *s = [self.calDisplay.stringValue stringByReplacingOccurrencesOfString:#"," withString:#""];
NSNumber *number = [formatter numberFromString:[s stringByAppendingString:sender.title]];
self.calDisplay.stringValue = [formatter stringFromNumber:number];
} else {
self.calDisplay.stringValue = sender.title;
userIsEnteringANumber = YES;
}
}
I believe this is what happens:
The string you have is "2,569". Which is two thousand something. This works fine, because the thousands delimiter is a comma.
After another button press (e.g. 1), your string becomes "2,5691". This is not a proper number, because it has the thousands delimiter (comma) wrong, hence the nil number. "25,691" would have been correct, but you have "2,5691".
Try this:
- (IBAction)digitPressed:(NSButton *)sender {
if (userIsEnteringANumber) {
double previous = [[self.calDisplay.stringValue] doubleValue];
double pressed = [sender.title doubleValue];
NSNumber *number = [NSNumber numberWithDouble:previous*10+pressed];
self.calDisplay.stringValue = [formatter stringFromNumber:number];
}
else {
self.calDisplay.stringValue = sender.title;
userIsEnteringANumber = YES;
}
}
to expand on the bogdansrc's answer pointing out that the formatter is not at fault here, one way to solve this (homework?) problem that allows entering digits for strings that already appear as double in your display would be as follows:
- (IBAction)digitPressed:(NSButton *)sender {
if (userIsEnteringANumber)
{
double previous = self.calDisplay.stringValue.doubleValue;
NSString* pressed = sender.title;
NSString* newNumber = [NSString stringWithFormat:#"%g%s", previous, pressed];
self.calDisplay.stringValue = [formatter stringFromNumber:[newNumber doubleValue]];
}
else
{
self.calDisplay.stringValue = sender.title;
userIsEnteringANumber = YES;
}
}
of course, this is not really ideal MVC, as the code relies on the View to track its Model … and by so doing, it's hard to be able to know from only the code snippet you provided how to transition from 260 to 260.4 when 4 is pressed after getting into the state that is supposed to be a fractional decimal number. is the decimal place being shown and tracked as the last character in stringValue? if so, the above will break because the "." at the end will get lost. if not, well then the Model is basically spread out partly in the View and partly in separate state, which is even less ideal.
I've looked at NSNumberFormatter, but that hasn't worked, so is there a way of parsing written numbers and turning them in to actual numbers?
Something like this would work (for positive whole numbers anyway). This is just a starting point, you would have to check to see that the words were correct and maybe ignore capitalization to make it more robust:
[self parseNumberWords:#"five two three"];
-(NSInteger)parseNumberWords:(NSString *)input {
NSArray *wordArray = [NSArray arrayWithObjects:#"zero",#"one",#"two",#"three",#"four",#"five",#"six",#"seven",#"eight",#"nine", nil];
NSArray *words = [input componentsSeparatedByString:#" "];
NSInteger num = 0;
NSInteger j =0;
for (NSInteger i = [words count]; i>0 ;i--) {
num = num + [wordArray indexOfObject:[words objectAtIndex:i-1]] * pow(10, j);
j = j+1;
}
NSLog(#"%ld",num);
return num;
}
NSNumberFormatter will get you some of the way there, via NSNumberFormatterSpellOutStyle. The basic formatting that NSNumber does will finish it off.
NSNumberFormatter * nf = [[NSNumberFormatter alloc] init];
[nf setNumberStyle:NSNumberFormatterSpellOutStyle];
NSString * numberWordString = #"three one two";
NSMutableString * digitString = [[NSMutableString alloc] init];
// Break up the input string at spaces and iterate over the result
for(NSString * s in [numberWordString componentsSeparatedByString:#" "]){
// Let the formatter turn each string into an NSNumber, then get
// the stringValue from that, which will be a digit.
[digitString appendString:[[nf numberFromString:s] stringValue]] ;
}
NSLog(#"%#", digitString); // prints "312"
Obviously, you'll have to put some work in to handle different input formats, lowercase, bad input (this will crash if nf fails to format -- it'll return nil which is an illegal argument to appendString:), etc.