iPhone Currency input using NSDecimal instead of float - iphone

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

Related

Elegant method to omit fraction formatting number if number is an integer

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];
}

swipe directions and loop text string

Help me... I'm stuck. I'm trying to make simple game something like angry bird swipe. I don't know how to code this. I have two labels and swipe move code (up, down,left or right, any directions).
I want to transfer label1.text ("333333777777555555888888") on label2.text with shorter number ("3758"). So I can make Artificial Intelligence game better.
How do I do this?
Here's the code.
for (NSValue *point_value in TouchRecord) {
CGPoint current_point = [point_value CGPointValue];
NSUInteger idx = [self subregionIndexContainingPoint:current_point];
//collect string on label2
NSString *numValue = [[NSString alloc] initWithFormat:#"%d", idx];
label1.text = [label1.text stringByAppendingString:numValue];
}
Thanks.
I'm not entirely sure this is what you want but I think it is.
To shorten the string so that there are only one of each number use this function.
+(NSString*) removeDuplicateNumbersFromString:(NSString*)first{
//if first is "1112222334445" the return value of this function would be "12345"
NSString *end = [NSString stringWithString:#""]; //the return string
char last; //will track changes in numbers
for (int i = 0; i < [first length]; i++) {
char charAtIndex = [first characterAtIndex:i];
if (last != charAtIndex) {
//if the last character is different than the current character
//set the current character as last, and add that character to the return string
last = charAtIndex;
end = [end stringByAppendingFormat:#"%c",last];
}
}
NSLog(#"First:%#, End:%#",first,end); //prints out the start/end strings
return end;
}

Spotted a leak in UITextView delegate method. Confused about solution

I've got a problem with an UITextView and one of its delegate methods in my navigation based app:
- (BOOL)textView:aView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
I managed to limit the max length of text the user can input using the above method. But I'm using a leaky array for that matter I think.
The problem is:
I want to save the amount of typed characters right in the very moment the user enters the last line of my textview. I then use that value to calculate the string length - which I compare to the textview's content size to set a limit. The code works fine - but since the method it's inside of is updating with every text input, I'm having trouble releasing the array in the right moment.
Here's some code:
if (numLines == 9)
{
if (!numCharsArray)
{
numCharsArray = [[NSMutableArray alloc] initWithCapacity:1]; // Stack trace gives this line 3,3% of the leak.
}
numChars = tView.text.length;
NSNumber *number = [[NSNumber alloc] initWithInteger:numChars]; // This line gets 77,3%.
[numCharsArray addObject:number]; // This line gets the rest, 24,3%.
[number release];
startChars = [[numCharsArray objectAtIndex:0] integerValue];
NSString *lastLine = [[NSString alloc]initWithString:[[tView text] substringFromIndex:startChars]];
CGSize lineSize = [lastLine sizeWithFont:tView.font forWidth:tView.contentSize.width lineBreakMode:UILineBreakModeWordWrap];
[lastLine release];
if (range.length > text.length)
{
return YES;
}
else if (numLines == 9 && lineSize.width >= tView.contentSize.width - 45)
{
return NO;
}
}
else
{
numCharsArray = nil;
/*
if(numCharsArray)
{
[numCharsArray release];
}
*/
}
I tried the out-commented statement above, but that gives me an app crash once I leave the last line of the textview. And as you can see in the code comments - without releasing the array I get a leak.
So how and where do I release that array correctly - keeping it safe while the user is on the last line?
Just replace with
first one
numCharsArray = [NSMutableArray array]; // you do not need to release
//explicitly as its autorelease numberWithInt
second one
NSNumber *number = [NSNumber numberWithInt:numChars]; //autorelease
NSString *lastLine = [[tView text] substringFromIndex:startChars];

Is it possible that NSNumberFormatter leaks memory?

This code leaks when I send a non-numeric string, but doesn't when I send a numeric string. Is it possible that numberFromString: leaks memory when failing and returning nil?
- (BOOL)isNum:(NSString*)str
{
BOOL ans = YES;
NSNumberFormatter* nf = [[NSNumberFormatter alloc] init];
if ([nf numberFromString:str] == nil)
ans = NO;
[nf release];
return ans;
}
Yes, it is possible. It is fine when the parameter only containing letters, such as #"asdf" or only containing numbers, such as #"1234". It will leak, as the Instruments shown, when the parameter is the combination of letters and numbers, such as #"123asdf".

Converting NSString to Currency - The Complete Story

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];
}