2

The following piece of code works on all phones, in my production app, except for the Galaxy Nexus running 4.1.2. The Play store sent me the error, I will first post the code, and then the error:

    // Roughly formats the currency by loping off the decimal places (amount
// comes already rounded)
public static String formatCurrencyRound(double amount) {
    String currency = formatCurrency(amount); // Returns "$8.00"

    return currency.substring(0, currency.indexOf("."));
}

The Error:

java.lang.RuntimeException: Unable to resume activity {com.ootpapps.saving.made.simple/com.ootpapps.saving.made.simple.SavingsPlanOverview}: java.lang.StringIndexOutOfBoundsException: length=9; regionStart=0; regionLength=-1
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2575)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:2603)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2089)
at android.app.ActivityThread.access$600(ActivityThread.java:130)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1195)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4745)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.StringIndexOutOfBoundsException: length=9; regionStart=0; regionLength=-1
at java.lang.String.startEndAndLength(String.java:593)
at java.lang.String.substring(String.java:1474)
at com.ootpapps.saving.made.simple.App.formatCurrencyRound(App.java:68)
at com.ootpapps.saving.made.simple.SavingsPlanOverview.updateSavingsPlanWidgets(SavingsPlanOverview.java:224)
at com.ootpapps.saving.made.simple.SavingsPlanOverview.onResume(SavingsPlanOverview.java:156)
at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1184)
at android.app.Activity.performResume(Activity.java:5082)
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2565)
... 12 more

If someone could propose a better way to round this currency string, or, a better way to round the raw number into a formatted string with no decimals, I'd hugely appreciate it. And again, this only fails on GNexus running 4.1.2 for some reason.

Thanks!

Edit Added formatCurrency code:

public static String formatCurrency(double amount) {
    NumberFormat currency = NumberFormat.getCurrencyInstance();

    return currency.format(amount);
}

Edit cont'd - Code I use which works to round currency to whole dollars:

public static String formatCurrencyWhole(double amount) {
    NumberFormat currency = NumberFormat.getCurrencyInstance();
    currency.setMaximumFractionDigits(0);
    currency.setMinimumFractionDigits(0);

    return currency.format(amount);
}
Manfred Moser
  • 29,539
  • 13
  • 92
  • 123
AutoM8R
  • 3,020
  • 3
  • 32
  • 52
  • If you still want to round down, did you consider Math.floor() ? – Konstantin Schubert Oct 19 '12 at 03:13
  • Can you post the code for formatCurrency(Double)? – Jason Hessley Oct 19 '12 at 03:54
  • Ultimately: I would have gone with NumberFormat, simply setting the FractionDigits to a Max and Min of 0. I did not know this approach existed at the time, it seems a bit unorthodox to me, perhaps the wording "Fraction" instead of "Decimal" threw me off. Anyway, the above setters force the number to have a fraction of 0, therefore, the numbers will always be whole. – AutoM8R Oct 19 '12 at 04:13
  • Yes, but none of these answers are it, it's what I posted above. – AutoM8R Oct 24 '12 at 01:01

2 Answers2

2

Firstly, currency should never, ever, ever be handled using doubles. You should use BigDecimal to keep precision. (See this question for more on that.)

That aside, you can use NumberFormat to get your String representation:

NumberFormat format = NumberFormat.getCurrencyInstance(Locale.US);
String result = format.format(amount);

.format() takes a double; if you end up using BigDecimal, use bigDecimal.doubleValue(). This answer provides some additional details in this area.

Community
  • 1
  • 1
Cat
  • 66,919
  • 24
  • 133
  • 141
  • 1
    +1 for the actual answer, but if the poster is truncating everything after the decimal separator then precision obviously isn't important. :-) – Ken White Oct 19 '12 at 03:55
  • Maybe not for displaying it, but definitely for storing it! Remember that if bad precision ends up putting him off from $8.499 to $8.500, it will show $9 instead of $8. – Cat Oct 19 '12 at 04:13
  • Um... No, it won't. If you have `$8.499` and truncate at the decimal, you have `$8`. If you have `$8.500` and truncate at the decimal point, you have `$8`. As I said, if **you truncate at the decimal point**, precision obviously isn't important. – Ken White Oct 19 '12 at 04:16
  • Then perhaps the case of $7.999 vs $8.000 is a better one. (Sorry, I misread your first comment.) – Cat Oct 19 '12 at 04:18
  • This is silly. If you have `$7.999`, and truncate at the decimal, you have `$7`. **Precision does not matter if you're truncating.** It does if you're doing math (**accumulating**) or actually **rounding**, but **not if you're truncating for display**. – Ken White Oct 19 '12 at 04:20
  • Yes, precision is not important, it's just rounding to whole dollars. This is in a simple budget app :) – AutoM8R Oct 19 '12 at 04:20
  • @KenWhite, if the value is *meant* to be $8.000, and a precision error makes it $7.999, then the outputted value will be wrong. – Cat Oct 19 '12 at 04:21
  • @AutoM8R Do what you will! It was simply a recommendation. ;) – Cat Oct 19 '12 at 04:22
  • If the **value** is supposed to be the content to the **left of the decimal**, which is what **truncation at the decimal** means, then the value is **meant to be** the value to the left of the decimal. If the value is `$7.9999999999999999`, the **intended value** after the truncation is `$7`. What part of that do you have trouble understanding? **Truncating** means **throwing away after**, meaning that the `.` and everything after it are discarded. We're not talking about **rounding**, but **truncating**. – Ken White Oct 19 '12 at 04:25
  • @KenWhite I'm talking about performing some operation to get a value, let's say `3.999999999 + 4.0` (as an example, obviously not tested canonically). If a **`double` precision error** leads those to be added to be `8.0`, then truncating to the decimal will produce `$8`. If they are **correctly added by `BigDecimal`**, they will produce `7.999999999`, which will be truncated to `$7`. – Cat Oct 19 '12 at 04:28
  • That's not what the question asked. It asks about **substring before the decimal point**. Again, the question is asking about **truncating**, not **rounding**. (I think this is 7 times I've repeated that (maybe 8)). Do I need to do it again??? If the poster correctly uses `BigDecimal` for 20 different variables, adds/multiples/divides/subtracts correctly using those values, converts to a string, and says "give me everything to the left of the decimal point", it doesn't matter if the value is `$8.00000001` or `$8.999999999` - the result is still `$8`. – Ken White Oct 19 '12 at 04:32
  • @KenWhite ... okay, clearly, you are not understanding that infinitesimally small precision errors can propagate beyond the decimal level. I *know* this is about truncating. What I'm saying is that an error can produce a value which will not be truncated properly. However, since I can tell we're both frustrated with this, if you're still not convinced of my point, I vote we agree to disagree. – Cat Oct 19 '12 at 04:35
  • I understand quite clearly. :-) Read my edited last comment (edited while you were posting yours). I think you might be the one misunderstanding the point. :-) But you're right - enough is enough. (And I won't take offense, but according to your profile I've been dealing with computer representations of floating point numbers longer than you've been alive. ) – Ken White Oct 19 '12 at 04:37
  • Yeah, I'm sure I'm still understanding your point... so something's getting lost in translation. Shame we couldn't work it out... but a good argument always exercises the ol' coconut. ;) – Cat Oct 19 '12 at 04:39
  • Agreed. :-) One last try, though - a direct quote from the question itself: `// Roughly formats the currency by loping off the decimal places (amount // comes already rounded)`. Note the part in parentheses `()`. – Ken White Oct 19 '12 at 04:44
  • @KenWhite Ahhhhh, okay, if it *already comes rounded*, then yes, my point is wrong. I was operating based on the knowledge that we could be passed *any* `double` (which may have precision errors). If it's coming as some value with no decimal points, then you're obviously correct. :) – Cat Oct 19 '12 at 04:46
  • 1
    :-) Glad to see us old guys can still read as well as you youngsters. ;-) – Ken White Oct 19 '12 at 04:49
2

I think the format of the string was already $8

If the decimal doesn't exist:

currency.substring(0, currency.indexOf("."));

Will throw an IndexOutOfBoundsException.

The solution may be as simple as testing for the decimal before hand.

public static String formatCurrencyRound(double amount) { String currency = formatCurrency(amount); // Returns "$8.00"

if(currency.indexOf(".") != -1){
   return currency.substring(0, currency.indexOf("."));
}else{
   return currency;
}

}

------- EDIT ----------

As Ken (very smart guy) pointed out in the comments the key line in the stack trace is:

java.lang.StringIndexOutOfBoundsException: length=9; regionStart=0; regionLength=-1 at java.lang.String.startEndAndLength(String.java:593)

This error shows regionLength = -1 which means that the result of indexOf(".") was -1. ((".") didn't exist)

It also shows a string length of 9 which is what makes me think that the different seperator issue (that Ken also pointed out) might be the real problem. Unless the dollar amount was in the $10000000 range.

That's why I would like to see the code for formatCurrency(Double);

-----EDIT-----

As suspected you are using the default locale:

NumberFormat currency = NumberFormat.getCurrencyInstance();

The documentation has this to say about that:

http://developer.android.com/reference/java/util/Locale.html

"The default locale is not appropriate for machine-readable output. The best choice there is usually Locale.US – this locale is guaranteed to be available on all devices, and the fact that it has no surprising special cases and is frequently used (especially for computer-computer communication) means that it tends to be the most efficient choice too.

A common mistake is to implicitly use the default locale when producing output meant to be machine-readable. This tends to work on the developer's test devices (especially because so many developers use en_US), but fails when run on a device whose user is in a more complex locale."

Unless foreign currency is required by your app, use:

NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.US);

Jason Hessley
  • 1,608
  • 10
  • 8
  • +1. I beat you (by 20 seconds), but your answer included a replacement code block and my (now deleted) answer didn't, making yours better. – Ken White Oct 19 '12 at 03:27
  • @ken You removed yours before I could +1... You pointed out that a different seperator will cause the same problem. I'm researching now to see what cases may result in a different seperator being used. Maybe foreign currency? – Jason Hessley Oct 19 '12 at 03:39
  • Yes. Many countries other than the US use a different decimal separator (such as `,`), which causes problems. I'll help your research - see [decimal_marks](http://en.wikipedia.org/wiki/Decimal_mark) for some information about which ones (and where). :-) Also, the key line in the stack trace is the 15th (with `StringIndexOutOfBounds`), which you may want to add to your answer. – Ken White Oct 19 '12 at 03:42
  • Shouldn't the comment to the poster be made to the original question instead of your own answer? ;-) – Ken White Oct 19 '12 at 03:52
  • Well foreign currency is required. That's why I didn't use Locale.US. – AutoM8R Oct 24 '12 at 01:00
  • If foreign currency is required, you will need to modify formatCurrencyRound(double amount) to detect the currency type and truncate at the seperator for that currency. Another simpler option may be to use a Regular Expression to truncate everything after the first non-number character in the string. In either case the answer to your posted question is definately covered here. – Jason Hessley Oct 24 '12 at 01:11