Created
June 8, 2013 15:16
-
-
Save bkromhout/5735475 to your computer and use it in GitHub Desktop.
Currency Utils: A class used on android to parse Locale-formatted currency strings to long values for the purpose of storing in a database, and parsing long currency values to locale-formatted strings. Note that this class does not care about exchange rates and does not attempt to do currency conversions.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.math.BigDecimal; | |
import java.math.RoundingMode; | |
import java.text.DecimalFormat; | |
import java.text.DecimalFormatSymbols; | |
import java.text.NumberFormat; | |
import java.text.ParseException; | |
import java.util.Currency; | |
import java.util.Locale; | |
/** | |
* Android/Java class that allows for i18n and formatting of currency, both user-facing and storage-wise. | |
* Written such that values to be stored in the database are returned as long, and user-facing strings are properly | |
* formatted based upon the user's default locale. | |
*/ | |
class CurrencyUtils { | |
/** | |
* Assume that the input is an integer that has been multiplied by a power of 10 sufficient to not lose data. E.g. for | |
* the US, this would be dollars * 100. | |
* | |
* This function properly divides the integer, and then formats it as a currency. | |
* | |
* <b>IMPORTANT</b>: Make sure you convert the <i>amount</i> of the currency, as needed. E.g. You stored dollars, but | |
* are showing a value in Euros. This function assumes the amount is in the correct, current, locale money unit. | |
* | |
* @param amount The amount of money, as stored in an long (e.g. as cents) | |
* @return A string formatted in the current locale that represents the monetary amount. | |
*/ | |
public static String longToCurrencyString(long amount) { | |
return longToCurrencyString(amount, true); | |
} | |
/** | |
* Assume that the input is an integer that has been multiplied by a power of 10 sufficient to not lose data. E.g. for | |
* the US, this would be dollars * 100. | |
* | |
* This function properly divides the integer, and then formats it as a currency. | |
* | |
* <b>IMPORTANT</b>: Make sure you convert the <i>amount</i> of the currency, as needed. E.g. You stored dollars, but | |
* are showing a value in Euros. This function assumes the amount is in the correct, current, locale money unit. | |
* | |
* @param amount The amount of money, as stored in an long (e.g. as cents) | |
* @return A number that has be correctly divided to have the right number of fractional digits. | |
*/ | |
public static String longToCurrencyString(long amount, boolean bCurrencySign) { | |
int scale = getFractionDigits(); | |
int divisor = scale > 0 ? (int) Math.pow(10, scale) : 1; | |
BigDecimal b = new BigDecimal(amount); | |
b = b.divide(new BigDecimal(divisor), scale, RoundingMode.HALF_UP); | |
return numberToCurrencyString(b, bCurrencySign); | |
} | |
/** | |
* Returns default fraction digits according to the locale. | |
* | |
* @return The number of floating point digits. | |
*/ | |
public static int getFractionDigits() { | |
Currency c = Currency.getInstance(Locale.getDefault()); | |
return c.getDefaultFractionDigits(); | |
} | |
/** | |
* Accepts a floating-point number (usually double or Double) that represents a currency amount. This function | |
* properly rounds the amount, and returns a string formatted for the current locale (including a currency symbol). | |
* | |
* @param damount The floating-point amount to format. | |
* @return A locale-specific string rounded and formatted to look like a currency. | |
*/ | |
public static String numberToCurrencyString(Number damount) { | |
return numberToCurrencyString(damount, true); | |
} | |
/** | |
* Get a locale-specific string representing the amount of currency provided. This is identical to | |
* numberToCurrencyString(Number), but allows you to turn off the currency symbol. | |
* | |
* @param damount The amount | |
* @param bCurrencySign whether to include the currency symbol in the string. | |
* @return The currency string. | |
*/ | |
public static String numberToCurrencyString(Number damount, boolean bCurrencySign) { | |
String rv = ""; | |
DecimalFormat d = (DecimalFormat) NumberFormat.getCurrencyInstance(Locale.getDefault()); | |
if (damount.doubleValue() < 0) { | |
if (d.getNegativePrefix().contains("(")) | |
d.setNegativePrefix(d.getNegativePrefix().replace("(", "-")); | |
if (d.getNegativeSuffix().contains(")")) | |
d.setNegativeSuffix(d.getNegativeSuffix().replace(")", "")); | |
} | |
if (!bCurrencySign) { | |
d.setPositivePrefix(""); | |
d.setPositiveSuffix(""); | |
d.setNegativePrefix("-"); | |
d.setNegativeSuffix(""); | |
} | |
rv = d.format(damount.doubleValue()).replace(String.valueOf((char) 160), ""); | |
return rv; | |
} | |
/** | |
* Take a string that represents a currency amount (with or without the currency symbol), multiplies it by the correct | |
* power of 10 to push the fractional digits into an integer form, and returns the result as a long. | |
* <p> | |
* E.g. In US: 100.34 -> 10034<br/> | |
* In France/Germany: 100,34 -> 10034<br/> | |
* etc.<br/> | |
* <p> | |
* <b>This function is quite tolerant of user input</b>, and will accept anything that is "normal" when writing a | |
* currency in that locale. | |
* | |
* <pre> | |
* // locale is en | |
* I.currencyStringToLong("$1,345.66", 0L); // returns 134566 | |
* I.currencyStringToLong("1345.66", 0L); // returns 134566 | |
* // locale is fr | |
* I.currencyStringToLong("1 345,66", 0L); // returns 134566 | |
* I.currencyStringToLong("1345.66 &euro", 0L); // returns 134566 | |
* </pre> | |
* | |
* @param amount The string representing a user-input amount of currency. | |
* @param defaultValue The value to return if the parsing fails. | |
* @return A long, multiplied by the correct power of 10 for the current fractional storage for the currency. | |
*/ | |
public static long currencyStringToLong(String amount, long defaultValue) { | |
Number n = currencyStringToNumber(amount, defaultValue); | |
int scale = getFractionDigits(); | |
int multiple = scale > 0 ? (int) Math.pow(10, scale) : 1; | |
BigDecimal b = new BigDecimal(n.toString()); | |
return b.multiply(new BigDecimal(multiple)).longValue(); | |
} | |
/** | |
* Parse the given locale-specific currency string, and return a Number that represents the amount. The Number object | |
* then easily allows conversion to primitives or even BigDecimal. | |
* <p> | |
* Use preferredCurrencyFormat() to get a help string that indicates the preferred input format for the currency. | |
* <p> | |
* <b>This function is quite tolerant of user input</b>, and will accept anything that is "normal" when writing a | |
* currency in that locale. | |
* | |
* <pre> | |
* // locale is en | |
* I.currencyStringToNumber("$1,345.66", 0); // returns 1345.66 | |
* I.currencyStringToNumber("1345.66", 0); // returns 1345.66 | |
* // locale is fr | |
* I.currencyStringToNumber("1 345,66", 0); // returns 1345.66 | |
* I.currencyStringToNumber("1345.66 &euro", 0); // returns 1345.66 | |
* </pre> | |
* | |
* | |
* @param amount The string (e.g. 100.34) to be parsed | |
* @param defaultValue The Number to return if the parsing fails. | |
* @param locale The locale to use when parsing | |
* @return A Number (e.g. rv.toDouble() == 100.34), or defaultValue if the string isn't understandable. | |
*/ | |
public static Number currencyStringToNumber(String amount, Number defaultValue, Locale locale) { | |
if (amount == null || amount.length() == 0) | |
return defaultValue; | |
NumberFormat fmt = NumberFormat.getCurrencyInstance(locale); | |
try { | |
DecimalFormat d = (DecimalFormat) fmt; | |
DecimalFormatSymbols symbols = d.getDecimalFormatSymbols(); | |
if (d.getNegativePrefix().length() > 0) | |
amount = amount.replace(d.getNegativePrefix(), "-").trim(); | |
if (d.getNegativeSuffix().length() > 0) | |
amount = amount.replace(d.getNegativeSuffix(), "").trim(); | |
if (d.getPositivePrefix().length() > 0) | |
amount = amount.replace(d.getPositivePrefix(), "").trim(); | |
if (d.getPositiveSuffix().length() > 0) | |
amount = amount.replace(d.getPositiveSuffix(), "").trim(); | |
d.setPositivePrefix(""); | |
d.setPositiveSuffix(""); | |
d.setNegativePrefix("-"); | |
d.setNegativeSuffix(""); | |
// In french, the official grouping separator is a Unicode thin space...convert ASCII spaces to thin | |
// spaces keeps input conversion from failing.... | |
if (symbols.getGroupingSeparator() == '\u00a0') | |
amount = amount.replace(" ", "\u00a0"); | |
return fmt.parse(amount); | |
} catch (ParseException e) { | |
Log.d("Failed to parse currency: " + amount, e.toString()); | |
} | |
return defaultValue; | |
} | |
/** | |
* Convenience call to the above method, automatically uses the default locale | |
* @see {@link #currencyStringToNumber(String, Number, java.util.Locale) currencyStringToNumber()} | |
* | |
* @param amount The string (e.g. 100.34) to be parsed | |
* @param defaultValue The Number to return if the parsing fails. | |
* @return A Number (e.g. rv.toDouble() == 100.34), or defaultValue if the string isn't understandable. | |
*/ | |
public static Number currencyStringToNumber(String amount, Number defaultValue) { | |
return currencyStringToNumber(amount, defaultValue, Locale.getDefault()); | |
} | |
/** | |
* Get a help string that indicates the desired currency input format. This is useful in UI forms: | |
* | |
* <pre> | |
* <input type="text" name="amount"> <%= I.preferredCurrencyFormat() %> | |
* </pre> | |
* <p> | |
* would show something like this:<br> | |
* <input type="text"> #,###.## | |
* | |
* @return A String of the form #,###.## | |
*/ | |
public static String preferredCurrencyFormat() { | |
NumberFormat fmt = NumberFormat.getCurrencyInstance(Locale.getDefault()); | |
StringBuilder rv = new StringBuilder(); | |
if (fmt instanceof DecimalFormat) { | |
DecimalFormat cfmt = ((DecimalFormat) fmt); | |
int fractional = cfmt.getMaximumFractionDigits(); | |
int groupSize = cfmt.getGroupingSize(); | |
DecimalFormatSymbols symbols = cfmt.getDecimalFormatSymbols(); | |
rv.append("#"); | |
rv.append(symbols.getGroupingSeparator()); | |
for (int i = 0; i < groupSize; i++) | |
rv.append("#"); | |
rv.append(symbols.getDecimalSeparator()); | |
for (int i = 0; i < fractional; i++) | |
rv.append("#"); | |
} | |
return rv.toString(); | |
} | |
/** | |
* Parse the given locale-specific number, and return a Number object that represents the value. | |
* <p> | |
* Use preferredNumberFormat() to get a help string that indicates the preferred input format for numbers. | |
* <p> | |
* In general, this function is very tolerant of user input. Digit groupings are optional, but the fractional | |
* separator must be correct. | |
* | |
* <pre> | |
* // in en locale | |
* I.stringToNumber("1,534,100.34", 0); // returns 1534100.34 | |
* I.stringToNumber("1534100.34", 0); // returns 1534100.34 | |
* // in fr locale | |
* I.stringToNumber("1 534 100,34", 0); // returns 1534100.34 | |
* I.stringToNumber("1534100,34", 0); // returns 1534100.34 | |
* </pre> | |
* | |
* @param value The string (e.g. 100.34) to be parsed | |
* @param defaultValue The Number to return if the parsing fails. | |
* @return A Number (e.g. rv.toDouble() == 100.34), or defaultValue if the string isn't understandable. | |
*/ | |
public static Number stringToNumber(String value, Number defaultValue) { | |
if (value == null || value.length() == 0) | |
return defaultValue; | |
NumberFormat fmt = NumberFormat.getInstance(Locale.getDefault()); | |
try { | |
value = value.replace(" ", ""); | |
return fmt.parse(value); | |
} catch (ParseException e) { | |
Log.d("Failed to parse number: " + value, e.toString()); | |
} | |
return defaultValue; | |
} | |
/** | |
* Used when we need to get a string with symbols and separators from a string without them. | |
* | |
* @param value String WITHOUT symbols and separators | |
* @return String WITH symbols and separators | |
*/ | |
public static String stringToString(String value) { | |
return numberToCurrencyString(currencyStringToNumber(value, 0)); | |
} | |
/** | |
* Takes a string value formatted using the en_US locale and returns it properly formatted in the default locale. | |
* This is used to set defaults should the user fail to provide them, so that the default is always the correct string. | |
* NOTE!! "Formatted" in this case doesn't include symbols! | |
* | |
* @param enUS_FormattedString Amount string formatted using en_US | |
* @return Amount string formatted using default locale WITHOUT symbols | |
*/ | |
public static String getStringFromString(String enUS_FormattedString) { | |
final Locale en_US_Locale = new Locale("en_US"); | |
return numberToCurrencyString(currencyStringToNumber(enUS_FormattedString, 0, en_US_Locale), false); | |
} | |
/** | |
* Returns a help string that indicates the recommended number input format for the current locale. This will be the | |
* locale-specific format. The input functions all tolerate plain math numbers (without groupings), though the | |
* locale-specific fraction separator is required. | |
* | |
* @param nFractional Indicate the number of fractional digits wanted. 0 means you want an integer. | |
* @return A string representing the recommended number input for the locale. | |
* @see CurrencyUtils#preferredCurrencyFormat() | |
*/ | |
public static String preferredNumberFormat(int nFractional) { | |
NumberFormat fmt = NumberFormat.getInstance(Locale.getDefault()); | |
StringBuilder rv = new StringBuilder(); | |
if (fmt instanceof DecimalFormat) { | |
DecimalFormat cfmt = ((DecimalFormat) fmt); | |
int groupSize = cfmt.getGroupingSize(); | |
DecimalFormatSymbols symbols = cfmt.getDecimalFormatSymbols(); | |
rv.append("#"); | |
rv.append(symbols.getGroupingSeparator()); | |
for (int i = 0; i < groupSize; i++) | |
rv.append("#"); | |
if (nFractional > 0) { | |
rv.append(symbols.getDecimalSeparator()); | |
for (int i = 0; i < nFractional; i++) | |
rv.append("#"); | |
} | |
} | |
return rv.toString(); | |
} | |
/** | |
* Convert a number to a string. The returned string is formatted according to the locale to include digit groupings | |
* for easy reading. | |
* | |
* <pre> | |
* // in en locale | |
* I.numberToString(1294855.234); // returns "1,294,855.234" | |
* // in fr locale | |
* I.numberToString(1294855.234); // returns "1 294 855,234" | |
* // in de locale | |
* I.numberToString(1294855.234); // returns "1.294.855,234" | |
* </pre> | |
* | |
* @param d The number to format. | |
* @return The number as a string. Tolerates null input (returns 0) | |
*/ | |
public static String numberToString(Number d) { | |
if (d == null) | |
return "0"; | |
NumberFormat fmt = NumberFormat.getInstance(Locale.getDefault()); | |
return fmt.format(d).replace("\u00a0", " "); | |
} | |
/** | |
* Get the currency symbol for the current locale. | |
* | |
* @return A string containing the currency symbol in the current locale. | |
*/ | |
public static String currencySign() { | |
NumberFormat fmt = NumberFormat.getCurrencyInstance(Locale.getDefault()); | |
DecimalFormat d = (DecimalFormat) fmt; | |
return d.getDecimalFormatSymbols().getCurrencySymbol(); | |
} | |
/** | |
* Use the current locale's understanding of currency to round a double to the correct number of fractional digits. | |
* | |
* <p> | |
* <b>CAUTION:</b> Currency calculations must be done in a way that is consistent with accounting practices. Usually, | |
* this means rounding at the end of a sequence of operations (so that rounding errors don't accumulate). The basic | |
* rule is to round any currency amount that becomes visible to the user. So, for example: | |
* | |
* <pre> | |
* double discount = 0.0123 * amount; // 1.23% discount | |
* String userMessage = I.trf("You get a {0,currency} discount!", discount); // currency formatting will show it rounded | |
* double roundedDiscount = I.roundCurrency(discount); | |
* // now use the rounded number, since that is what they expect. | |
* double price = price - roundedDiscount; | |
* </pre> | |
* | |
* @param unroundedNum The number to round | |
* @return The same value, but rounded to the correct number of significant fractional digits for the locale's currency. | |
*/ | |
public static String roundCurrency(String unroundedNum) { | |
DecimalFormat fmt = (DecimalFormat) NumberFormat.getCurrencyInstance(); | |
// We don't wanna deal with this junk, strip it out | |
String unroundedNumber = unroundedNum.replace(fmt.getCurrency().getSymbol(), "") | |
.replace(String.valueOf(fmt.getDecimalFormatSymbols().getGroupingSeparator()), "") | |
.replace(String.valueOf((char)160), "") // Be sure we get rid of NBSP's | |
.trim(); | |
if(unroundedNumber.equals("-")){ | |
// In case the user types a '-' then decides to do something that triggers this method, otherwise we crash | |
return unroundedNumber; | |
} | |
// Make sure we only have one decimal separator, if there are more than one then strip all but the first one out | |
char decimalSeparator = fmt.getDecimalFormatSymbols().getDecimalSeparator(); | |
int firstOccurance = unroundedNumber.indexOf(decimalSeparator); | |
while(unroundedNumber.lastIndexOf(decimalSeparator) != firstOccurance){ | |
int lastOccurance = unroundedNumber.lastIndexOf(decimalSeparator); | |
unroundedNumber = new StringBuilder(unroundedNumber).deleteCharAt(lastOccurance).toString(); | |
} | |
if(unroundedNumber.equals(String.valueOf(decimalSeparator))) { | |
return ""; | |
} | |
if(unroundedNumber.equals("")){ | |
return unroundedNumber; | |
} | |
unroundedNumber = unroundedNumber.replace(String.valueOf(fmt.getDecimalFormatSymbols().getDecimalSeparator()), "."); | |
fmt.setNegativePrefix("-"); | |
fmt.setNegativeSuffix(""); | |
BigDecimal v_bigDec = new BigDecimal(unroundedNumber.trim()); | |
v_bigDec = v_bigDec.setScale(getFractionDigits(), BigDecimal.ROUND_HALF_UP); | |
return fmt.format(v_bigDec.doubleValue()) | |
.replace(fmt.getCurrency().getSymbol(), "") | |
.replace(String.valueOf(fmt.getDecimalFormatSymbols().getGroupingSeparator()), "") | |
.replace(String.valueOf((char)160), "") // Once again, get rid of these NBSP's | |
.trim(); | |
} | |
/** | |
* Checks if a currency string has a value of zero | |
* @param n Currency string | |
* @return Boolean has value of zero | |
*/ | |
public static boolean checkZero(String n) { | |
String num = roundCurrency(n); | |
if(new BigDecimal(num).compareTo(BigDecimal.ZERO) == 0){ | |
return true; | |
} else { | |
return false; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment