After over a day of poking around with this problem I will see if I can get some help. This question has been more or less asked before, but it seems no one is giving a full answer so hopefully we can get it now.
Using a UILabel and a UITextView (w/ number keyboard) I want to achieve an ATM like behavior of letting the users just type the numbers and it is formatted as currency in the label. The idea is basically outlined here:
What is the best way to enter numeric values with decimal points?
The only issue is that it never explicitly says how we can go from having an integer like 123 in the textfield and displaying in the label as $1.23 or 123¥ etc. Anyone have code that does this?
I have found a solution, and as per the purpose of this question I am going to provide a complete answer for those who have this problem in the future. First I created a new Helper Class called NumberFormatting and created two methods.
//
// NumberFormatting.h
// Created by Noah Hendrix on 12/26/09.
//
#import <Foundation/Foundation.h>
#interface NumberFormatting : NSObject {
}
-(NSString *)stringToCurrency:(NSString *)aString;
-(NSString *)decimalToIntString:(NSDecimalNumber *)aDecimal;
#end
and here is the implementation file:
//
// NumberFormatting.m
// Created by Noah Hendrix on 12/26/09.
//
#import "NumberFormatting.h"
#implementation NumberFormatting
-(NSString *)stringToCurrency:(NSString *)aString {
NSNumberFormatter *currencyFormatter = [[NSNumberFormatter alloc] init];
[currencyFormatter setGeneratesDecimalNumbers:YES];
[currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
if ([aString length] == 0)
aString = #"0";
//convert the integer value of the price to a decimal number i.e. 123 = 1.23
//[currencyFormatter maximumFractionDigits] gives number of decimal places we need to have
//multiply by -1 so the decimal moves inward
//we are only dealing with positive values so the number is not negative
NSDecimalNumber *value = [NSDecimalNumber decimalNumberWithMantissa:[aString integerValue]
exponent:(-1 * [currencyFormatter maximumFractionDigits])
isNegative:NO];
return [currencyFormatter stringFromNumber:value];
}
-(NSString *)decimalToIntString:(NSDecimalNumber *)aDecimal {
NSNumberFormatter *currencyFormatter = [[NSNumberFormatter alloc] init];
[currencyFormatter setGeneratesDecimalNumbers:YES];
[currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
if (aDecimal == nil)
aDecimal = [NSDecimalNumber zero];
NSDecimalNumber *price = [NSDecimalNumber decimalNumberWithMantissa:[aDecimal integerValue]
exponent:([currencyFormatter maximumFractionDigits])
isNegative:NO];
return [price stringValue];
}
#end
The first method, stringToCurrency, will take an integer number (passed in from a textfield in this case) and convert it to a decimal value using moving the decimal point as appropriate for the users locale settings. It then returns a string representation formatted as currency using NSNumberFormatter.
The second method does the reverse it takes a value like 1.23 and converts it back to 123 using a similar method.
Here is an example of how I used it
...
self.accountBalanceCell.textField.text = [[NumberFormatting alloc] decimalToIntString:account.accountBalance];
...
[self.accountBalanceCell.textField addTarget:self
action:#selector(updateBalance:)
forControlEvents:UIControlEventEditingChanged];
Here we set the value of the text field to the decimal value from the data store and then we set a observer to watch for changes to the text field and run the method updateBalance
- (void)updateBalance:(id)sender {
UILabel *balanceLabel = (UILabel *)[accountBalanceCell.contentView viewWithTag:1000];
NSString *value = ((UITextField *)sender).text;
balanceLabel.text = [[NumberFormatting alloc] stringToCurrency:value];
}
Which simply takes the textfield value and run it through the stringToCurrency method described above.
To me this seems hackish so please take the a moment to look over and clean it up if you are interested in using it. Also I notice for large values it breaks.
Take a look at NSNumberFormatter, which will format numerical data based on the current or specified locale.
Since I still didn't see correct Answers to this question I will share my solution without using the NSScanner (the scanner doesn't seem to work for me). Is's a combination out of this " What is the best way to enter numeric values with decimal points? " and this " Remove all but numbers from NSString " answers.
First I present a NSString with the users local currency settings in a UITextField like this:
//currencyFormatter is of type NSNumberFormatter
if (currencyFormatter == nil) {
currencyFormatter = [[NSNumberFormatter alloc] init];
[currencyFormatter setLocale:[NSLocale currentLocale]];
[currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
//[currencyFormatter setGeneratesDecimalNumbers:YES];
decimalSeperator = [currencyFormatter decimalSeparator]; //NSString
currencyScale = [currencyFormatter maximumFractionDigits]; //short
//[currencyFormatter release]; don't forget to release the Formatter at one point
}
//costField is of type UITextField
NSDecimalNumber *nullValue = [NSDecimalNumber decimalNumberWithMantissa:0 exponent:currencyScale isNegative:NO];
[costField setText:[currencyFormatter stringFromNumber:nullValue]];
You might do this in the viewControllers method viewDidLoad:.
Depending on the users settings there will be displayed a string like this: $0.00 (for local settings United Stated). Depending on your situation here you might want to present a value out of your data model.
When the user touches inside the text field I will present a Keyboard with type:
costField.keyboardType = UIKeyboardTypeDecimalPad;
This prevents the user to enter anything else but digits.
In the following UITextField's delegate method I separate the string to get only the numbers (here I avoid using the NSScanner). This is possible, because I know where to set the decimal separator by using the before specified 'currencyScale' value:
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)string {
if (textField == costField) {
//if for what ever reason ther currency scale is not available set it to 2
//which is the most common scale value
if (!currencyScale) {
currencyScale = 2;
}
// separate string from all but numbers
// https://stackoverflow.com/questions/1129521/remove-all-but-numbers-from-nsstring/1163595#1163595
NSString *aString = [textField text];
NSMutableString *strippedString = [NSMutableString stringWithCapacity:10];
for (int i=0; i<[aString length]; i++) {
if (isdigit([aString characterAtIndex:i])) {
[strippedString appendFormat:#"%c",[aString characterAtIndex:i]];
}
}
//add the newly entered character as a number
// https://stackoverflow.com/questions/276382/what-is-the-best-way-to-enter-numeric-values-with-decimal-points/2636699#2636699
double cents = [strippedString doubleValue];
NSLog(#"Cents:%f ",[strippedString doubleValue]);
if ([string length]) {
for (size_t i = 0; i < [string length]; i++) {
unichar c = [string characterAtIndex:i];
if (isnumber(c)) {
cents *= 10; //multiply by 10 to add a 0 at the end
cents += c - '0'; // makes a number out of the charactor and replace the 0 (see ASCII Table)
}
}
}
else {
// back Space if the user delete a number
cents = floor(cents / 10);
}
//like this you could save the value as a NSDecimalNumber in your data model
//costPerHour is of type NSDecimalNumber
self.costPerHour = [NSDecimalNumber decimalNumberWithMantissa:cents exponent:-currencyScale isNegative:NO];
//creat the string with the currency symbol and the currency separator
[textField setText:[currencyFormatter stringFromNumber:costPerHour]];
return NO;
}
return YES;
}
In this way the currency entered by the user will always be correct and there is no need to check it. No matter which currency settings is selected, this will always result to be the correctly formatted currency.
I didn't really like the existing answers here so I combined a couple of techniques. I used a hidden UITextField with the number pad keyboard for input, and a visible UILabel for formatting.
I've got to properties that hold on to everything:
#property (weak, nonatomic) IBOutlet UILabel *amountLabel;
#property (weak, nonatomic) IBOutlet UITextField *amountText;
#property (retain, nonatomic) NSDecimalNumber *amount;
I've got the amount and a NSNumberFormatter as ivars:
NSDecimalNumber *amount_;
NSNumberFormatter *formatter;
I setup my formatter at init:
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
// Custom initialization
formatter = [NSNumberFormatter new];
formatter.numberStyle = NSNumberFormatterCurrencyStyle;
}
return self;
}
Here's the code I'm using to validate the input convert it to
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
NSString *asText = [textField.text stringByReplacingCharactersInRange:range withString:string];
if ([asText length] == 0) {
[self setAmount:[NSDecimalNumber zero]];
return YES;
}
// We just want digits so cast the string to an integer then compare it
// to itself. If it's unchanged then it's workable.
NSInteger asInteger = [asText integerValue];
NSNumber *asNumber = [NSNumber numberWithInteger:asInteger];
if ([[asNumber stringValue] isEqualToString:asText]) {
// Convert it to a decimal and shift it over by the fractional part.
NSDecimalNumber *newAmount = [NSDecimalNumber decimalNumberWithDecimal:[asNumber decimalValue]];
[self setAmount:[newAmount decimalNumberByMultiplyingByPowerOf10:-formatter.maximumFractionDigits]];
return YES;
}
return NO;
}
I've got this setter than handles formatting the label and enabling the done button:
-(void)setAmount:(NSDecimalNumber *)amount
{
amount_ = amount;
amountLabel.text = [formatter stringFromNumber:amount];
self.navigationItem.rightBarButtonItem.enabled = [self isValid];
}
Related
I want to format the textfield text when user enter a value .. It work perfectly but my problem is ,If user press 1 then 2 after that decimale point . My replacement string is "12." At that time numbar is 12 . i am not able to take decimal point ..
NSNumber* number = [numberFormatter numberFromString:string];
Here is My full code,
#pragma mark textfieldDelgate
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
NSLocale *locale=[[NSLocale alloc]initWithLocaleIdentifier:#"en_GB"];
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setLocale:locale];
[numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
NSString *text = [textField text];
NSString *replacementText = [text stringByReplacingCharactersInRange:range withString:string];
NSMutableString *newReplacement = [[ NSMutableString alloc ] initWithString:replacementText];
NSString *currencyGroupingSeparator = numberFormatter.currencyGroupingSeparator;
[newReplacement replaceOccurrencesOfString:currencyGroupingSeparator withString:#"" options:NSBackwardsSearch range:NSMakeRange(0, [newReplacement length])];
NSNumber * number = [numberFormatter numberFromString:newReplacement];
if (number == nil && [replacementText length]!=0) {
return NO;
}
NSLog(#"%#",newReplacement);
text = [numberFormatter stringFromNumber:number];
[textField setText:text];
return NO;
}
I have also attached screenshot.
How can I format the string with decimal value......
Thanks in advance
For me the line of code:
NSNumber * number = [numberFormatter numberFromString:newReplacement];
does not returns nil but it removes the decimal entered by user. This is because with numberFormatter id you pass a value like '12.' or '12.0' it will remove the decimal as a part of formatting. For other values like 12.3 it will not remove decimal.
I am not able to understand your requirement for doing all this coding but if you want decimal to be there put some logic to check if number is in format of 12. or 12.0 , then escape formatting. Decimal will remain there.
Please use this code:
NSNumber * number;
if ([newReplacement hasSuffix:#"."] || [newReplacement hasSuffix:#".0"])
{
[textField setText:newReplacement]; return NO;
}
else
{
number = [numberFormatter numberFromString:newReplacement];
}
You'll probably want to use NSNumberFormatterCurrencyStyle instead of NSNumberFormatterDecimalStyle if you're dealing with formatting money. It might Just Work™ after setting that.
But, it might not. Regardless of all that, pretty sure the reason for the behavior you're seeing is that you're not giving it a number from your string; 12 is a number, as is 12.5, but, 12. is not. I'm surprised its not returning nil outright.
If thats the case (and its still broken), than you'll probably want to special-case having the . at the end, and append instead of passing it into the number formatter.
I have tried printing your code in my log. It is working fine for me.I think there is some other type of issue.
NSLocale *locale=[[NSLocale alloc]initWithLocaleIdentifier:#"en_GB"];
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setLocale:locale];
[numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
NSNumber* number = [numberFormatter numberFromString:#"12.4"];
NSLog(#"number is %#",number);
log print-----
number is 12.4
//Add this in .h file
NSNumberFormatter *formatter;
NSInteger currencyScale;
NSString *enteredDigits;
//Add this in .m file`enter code here`
-(void)viewdidload
{
NSNumberFormatter *aFormatter = [[NSNumberFormatter alloc] init];
[aFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
currencyScale = -1 * [aFormatter maximumFractionDigits];
self.formatter = aFormatter;
}
#pragma UItextfield Delegate
- (void)textFieldDidBeginEditing:(UITextField *)textField {
// Keep a pointer to the field, so we can resign it from a toolbar
Field = textField;
if(textField.text==NULL || [textField.text isEqualToString:#""])
{
self.enteredDigits = #"";
//textField.text=#"";
}
}
- (void)textFieldDidEndEditing:(UITextField *)textField {
}
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { //give tag to text field
if (textField.tag==10) {
// self.priceInput = textField;
// Check the length of the string
int len=[self.enteredDigits length];
NSLog(#"string lenght is %d",len);
if ([string length]) {
self.enteredDigits = [self.enteredDigits stringByAppendingFormat:#"%d", [string integerValue]];
} else {
// This is a backspace
NSUInteger len = [self.enteredDigits length];
if (len > 1) {
self.enteredDigits = [self.enteredDigits substringWithRange:NSMakeRange(0, len - 1)];
} else {
self.enteredDigits = #"";
}
}
NSDecimalNumber *decimal = nil;
if ( ![self.enteredDigits isEqualToString:#""]) {
decimal = [[NSDecimalNumber decimalNumberWithString:self.enteredDigits] decimalNumberByMultiplyingByPowerOf10:currencyScale];
float deci= [decimal floatValue];
NSLog(#"decimall==%f",deci);
} else {
decimal = [NSDecimalNumber zero];
}
// Replace the text with the localized decimal number
float deci= [decimal floatValue];
NSLog(#"decimall==%f",deci);
NSString *temp = [self.formatter stringFromNumber:decimal];
textField.text=temp;
//NSLog(#"Text fielddddddddddddddf=%# %#",numberText,temp);
return NO;
}
return YES;
}
- (BOOL)textFieldShouldClear:(UITextField *)textField
{
textField.text=#"";
self.enteredDigits=#"";
NSLog(#"Clear Clicked");
return YES;
}
I think this code will help you
I am formatting floating point numbers and right now I have the %0.2f formatter, but I'd like to omit the .00 if the floating point number is an even integer.
Of course I can think of string replacing the .00, but that's crude.
I found that the description of NSNumber also does something similar:
NSNumber *number = [NSNumber numberWithFloat:_paragraphSpacing];
[retString appendFormat:#"margin-bottom:%#px;", number];
This this does hover not limit the post comma digits. if the number is 1234.56789 then the description will output that.
So my question is, is there a just as simple way - possibly without having to create an NSNumber object - to achieve this result?
Since floating-point numbers aren't exact, there's no guarantee that your number will actually be an integer. You can, however, check if it's within a reasonably small distance from an integer value. And of course you don't need an NSNumber for this. (Generally speaking, NSNumber is not used for formatting, its purpose is representing a primitive C type, either integral or floating-point types, using an Objective-C object.)
#include <math.h>
- (NSString *)stringFromFloat:(float)f
{
const float eps = 1.0e-6;
if (abs(round(f) - f) < eps) {
// assume an integer
return [NSString stringWithFormat:#"margin-bottom: %.0fpx", round(f)];
} else {
// assume a real number
return [NSString stringWithFormat:#"margin-bottom: %.2fpx", f];
}
}
Use a formatter:
NSNumberFormatter* formatter= [NSNumberFormatter new];
formatter.numberStyle= NSNumberFormatterDecimalStyle;
formatter.maximumFractionDigits=2;
NSNumber *number = [NSNumber numberWithFloat:_paragraphSpacing];
[retString appendFormat:#"margin-bottom:%#;", [formatter stringFromNumber: number]];
You can use an NSNumberFormatter for this:
static NSNumberFormatter *numberFormatter = nil;
if (numberFormatter == nil) {
numberFormatter = [[NSNumberFormatter alloc] init];
numberFormatter.minimumFractionDigits = 0;
numberFormatter.maximumFractionDigits = 2;
numberFormatter.usesGroupingSeparator = NO;
}
NSString *formattedNumberString = [numberFormatter
stringForNumber:[NSNumber numberWithDouble: _paragraphSpacing]];
You can use C function modff to get the fraction part and test it:
float fractionPart = 0.;
modff(_paragraphSpacing, &fractionPart);
if( fabsf(fractionPart) < 0.01 ) {
// format as integer
[retString appendFormat:#"margin-bottom:%d", (int)_paragraphSpacing];
} else {
// format as float
[retString appendFormat:#"margin-bottom:%0.2f", _paragraphSpacing];
}
I am taking the input in form of currency like 54 gets converted to 0.54,but When I am trying to enter 100, I get out as 0.1 only.The code is not working for 0. You cannot enter value as 100.00 .The code I am using is
(BOOL)textField:(UITextField *)transactionAmount shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)string
{
NSString *substring = transactionAmount.text;
substring = [substring stringByAppendingString:string];
NSLog(#"Text : %#",substring);
NSString *cleanCentString = [[transactionAmount.text
componentsSeparatedByCharactersInSet:
[[NSCharacterSet decimalDigitCharacterSet] invertedSet]]
componentsJoinedByString:#""];
// Parse final integer value
NSInteger centAmount = cleanCentString.integerValue;
// Check the user input
if (string.length > 0)
{
// Digit added
centAmount = centAmount * 10 + string.integerValue;
}
else
{
// Digit deleted
centAmount = centAmount / 10;
}
// Update call amount value
NSNumber *amount = [[NSNumber alloc] initWithFloat:(float)centAmount / 100.0f];
// Write amount with currency symbols to the textfield
NSNumberFormatter *_currencyFormatter = [[NSNumberFormatter alloc] init];
// [_currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
[_currencyFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
[_currencyFormatter setCurrencyCode:#"USD"];
[_currencyFormatter setNegativeFormat:#"-¤#,##0.00"];
self.transactionAmount.text = [_currencyFormatter stringFromNumber:amount];
// [self SetMainMessage:customTipsValue.text];
return NO;
}
Most of your initial "string cleaning" is wrong and your number formatter isn't correct. It should be something like this:
- (BOOL)textField:(UITextField *)transactionAmount shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
NSString *substring = transactionAmount.text;
substring = [substring stringByReplacingCharactersInRange:range withString:string];
NSLog(#"New Text : %#",substring);
NSString *cleanCentString = [[substring
componentsSeparatedByCharactersInSet:
[[NSCharacterSet decimalDigitCharacterSet] invertedSet]]
componentsJoinedByString:#""];
// Parse final integer value
NSInteger centAmount = cleanCentString.integerValue;
// Update call amount value
NSNumber *amount = [[NSNumber alloc] initWithFloat:centAmount / 100.0f];
// NOTE: make this an instance variable and set it up just once
// Write amount with currency symbols to the textfield
NSNumberFormatter *_currencyFormatter = [[NSNumberFormatter alloc] init];
[_currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
[_currencyFormatter setCurrencyCode:#"USD"];
[_currencyFormatter setNegativeFormat:#"-¤#,##0.00"];
self.transactionAmount.text = [_currencyFormatter stringFromNumber:amount];
return NO;
}
If, for some reason, you want to use Decimal format instead of Currency format, make sure you set the minimum and maximum fraction digits (decimal places) to 2.
Do you really want to hardcode USD? What about people using the app in other countries?
Your original string cleaning didn't properly support a user using cut, copy, or paste. It also used the wrong text to create cleanCentString.
What I understood is
If entered value is 0, you want 0.
If entered value is between 1 and 99, you want 0.01 to 0.99.
If entered value is 1 or more, you want 1.00, like wise.
Why don't you get straight as float requiredCurrency=inputCurrency/100.0f;
For my app's graph (line plots) it does not make sense to format the axis labels to tenths. It did not look like there was a way to change this without providing custom labels.
I was able to add custom axis labels based on the sample code in this answer, but the labels do not have tick marks.
Is this an issue (I didn't see anything here) or am I missing something?
If you want numeric labels with a format different than the default, create an NSNumberFormatter object, set it to whatever format you need, and assign it to the labelFormatter property on the axis.
Check out the CPTimeFormatter class if you need to format the labels as dates and/or times.
Derive a class from NSNumberFormatter (e.g. MyFormatter) and override stringForObjectValue:
- (NSString *)stringForObjectValue:(NSDecimalNumber *)coordinateValue {
return #"MyLabel";
}
Then set the labelFormatter property of your axis to an instance of MyFormatter, e.g.:
MyFormatter *formatter = [[MyFormatter alloc] init];
x.labelFormatter = formatter;
[formatter release];
This worked for me!
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
[formatter setMaximumFractionDigits:0];
y.labelFormatter = formatter;
You can also swizzle the method in category like this:
#import "NSNumberFormatter+BigNumber.h"
#import <objc/runtime.h>
static Method origStringFromNumberMethod = nil;
#implementation NSNumberFormatter (BigNumber)
-(NSString *)stringFromBigNumber:(NSNumber*)number{
int result = 0;
int level = 1;
NSString *format = #"";
if([number integerValue] >= 1000000000) {
level = 1000000000;
format = #"b";
}
if([number integerValue] >= 1000000) {
level = 1000000;
format = #"m";
}
if([number integerValue] >= 1000){
level = 1000;
format = #"k";
}
result = [number integerValue]/level;
NSString *kValue = [NSString stringWithFormat:#"%d%#",result,format];
return kValue;
}
+ (void)initialize {
origStringFromNumberMethod = class_getClassMethod(self, #selector(stringFromNumber:));
method_exchangeImplementations(origStringFromNumberMethod,
class_getClassMethod(self, #selector(stringFromBigNumber:)));
}
#end
iPhone/Objective-C/Cocoa newbie here. Based on a number of posts on stackoverflow, I have cobbled together an IBAction that I'm using in a basic iPhone calculator app that I'm building. The IBAction works with the numeric keypad to allow entry of decimal numbers without having to enter a decimal point.
I am trying very hard to adhere to the "use NSDecimal when dealing with currency" adage although I am finding it difficult to do so like so many others who have posted questions. I am making steady progress, but have hit a wall that I'm sure will look trivial after I get my head around NSDecimal and Format Specifications.
Here is the IBAction I'm using (it is triggered by Editing Changed UITextField Event):
// called when user touches a key or button
- (IBAction)processKeystrokes:(id)sender
{
static BOOL toggle = YES; // was this method triggered by the user?
// the user touched the keypad
if (toggle)
{
toggle = NO;
// retrieve the strings in input fields
NSString *currencyField1Text = currencyField1.text;
NSString *currencyField2Text = currencyField2.text;
if (sender == currencyField1) {
currencyField1Text = [currencyField1Text stringByReplacingOccurrencesOfString:#"." withString:#""];
float currency = [currencyFieldText floatValue]/100;
currencyField1.text = [#"" stringByAppendingFormat:#"%0.2f", currency];
}
else if (sender == currencyField2) {
currencyField2Text = [currencyField2Text stringByReplacingOccurrencesOfString:#"." withString:#""];
NSDecimalNumber *currency2 = [[NSDecimalNumber decimalNumberWithString:currencyField2Text] decimalNumberByDividingBy:[NSDecimalNumber decimalNumberWithString:#"100"]];
currencyField2.text = [#"" stringByAppendingFormat:#"%#", currency2];
}
else {
NSLog(#"Some unexpected input");
}
}
else
{
toggle = YES;
}
} // end method calculateResults
The currencyField1 code segment uses floats, the currencyField2 segment uses NSDecimal.
The currencyField1 segment works as desired: displays all numbers with two digits after the decimal point (even when the delete key is used to delete all entered digits); however it suffers from and illustrates perfectly the problem with using floats when dealing with large currency values: rounding errors show up when entered numbers exceed 8 digits.
The currencyField2 segment avoids rounding error problem by using NSDecimal instead of float; however it does not always display numbers with two digits after the decimal point -- this is shown when the delete key is used to delete all entered digits. I believe the problem is due to this line of code:
currencyField2.text = [#"" stringByAppendingFormat:#"%#", currency2];
This is the corollary to the following line that produces the desired format for floats:
currencyField1.text = [#"" stringByAppendingFormat:#"%0.2f", currency];
So, I think I need the equivalent of #"%0.2f" for formatting the display of a "0" value NSDecimalNumber. I have been at this for so many hours that I'm embarrassed, but I just can't figure it out.
Any help or pointers are appreciated.
EDIT: I incorporated the NSNumberFormatter object (similar to what Brad describes in his comment) which seems to have solved the problem. However, I would like some feedback on refactoring the code now that I have it working. Here's the revised code:
// called when user touches a key or button
- (IBAction)processKeystrokes:(id)sender
{
static BOOL toggle = YES; // was this method triggered by the user?
// the user touched the keypad
if (toggle)
{
toggle = NO;
// retrieve the strings in input fields
NSString *currencyField1Text = currencyField1.text;
NSString *currencyField2Text = currencyField2.text;
// new code elements
NSNumberFormatter * nf = [[[NSNumberFormatter alloc] init]autorelease];
[nf setNumberStyle:NSNumberFormatterCurrencyStyle];
[nf setCurrencySymbol:#""];
[nf setCurrencyGroupingSeparator:#""];
if (sender == currencyField1) {
currencyField1Text = [currencyField1Text stringByReplacingOccurrencesOfString:#"." withString:#""];
NSDecimalNumber *currency = [[NSDecimalNumber decimalNumberWithString:currencyField1Text] decimalNumberByDividingBy:[NSDecimalNumber decimalNumberWithString:#"100"]];
currencyField1.text = [nf stringFromNumber:currency];
}
else if (sender == currencyField2) {
currencyField2Text = [currencyField2Text stringByReplacingOccurrencesOfString:#"." withString:#""];
NSDecimalNumber *currency2 = [[NSDecimalNumber decimalNumberWithString:currencyField2Text] decimalNumberByDividingBy:[NSDecimalNumber decimalNumberWithString:#"100"]];
currencyField2.text = [nf stringFromNumber:currency2];
}
else {
NSLog(#"Some unexpected input");
}
}
else
{
toggle = YES;
}
} // end method calculateResults
It addresses my initial problem, but I would appreciate any advice on how to improve it. Thanks.
If you want to guarantee 2 digits after the decimal point for your text value, you could use an NSNumberFormatter like in the following code (drawn from the answer here):
NSNumberFormatter *decimalNumberFormatter = [[NSNumberFormatter alloc] init];
[decimalNumberFormatter setMinimumFractionDigits:2];
[decimalNumberFormatter setMaximumFractionDigits:2];
currencyField2.text = [decimalNumberFormatter stringFromNumber:currency2];
[decimalNumberFormatter release];
I believe this should preserve the precision of the NSDecimalNumber. Personally, I prefer to use the NSDecimal C struct for performance reasons, but that's a little harder to get values into and out of.
Can't you use the -setFormat: method (of NSNumberFormatter) instead for the NSNumberFormatter? Seems one should be able to configure it for your purposes and you wouldn't have to deal with weird "hacks" on a currency formatted string.
For more info see:
Apple's docs on -setFormat
Accepted format strings