Set specific precision of a BigDecimal
Asked Answered
S

6

59

I have an XSD that requires me to use a BigDecimal for a lat/lon. I currently have the lat/lon as doubles, and convert them to BigDecimal, but I am only required to use about 12 places of precision. I have not been able to figure out how to set that. Can anyone help me with this?

Surfing answered 28/2, 2012 at 13:35 Comment(2)
Have you tried giving the constructor a MathContext?Owades
If I see correctly you are talking about decimal places, not floating point precision. At least the accepted answer will result in 12 decimal places, not 12 digits of precision.Pained
V
47

The title of the question asks about precision. BigDecimal distinguishes between scale and precision. Scale is the number of decimal places. You can think of precision as the number of significant figures, also known as significant digits.

Some examples in Clojure.

(.scale     0.00123M) ; 5
(.precision 0.00123M) ; 3

(In Clojure, The M designates a BigDecimal literal. You can translate the Clojure to Java if you like, but I find it to be more compact than Java!)

You can easily increase the scale:

(.setScale 0.00123M 7) ; 0.0012300M

But you can't decrease the scale in the exact same way:

(.setScale 0.00123M 3) ; ArithmeticException Rounding necessary

You'll need to pass a rounding mode too:

(.setScale 0.00123M 3 BigDecimal/ROUND_HALF_EVEN) ;
; Note: BigDecimal would prefer that you use the MathContext rounding
; constants, but I don't have them at my fingertips right now.

So, it is easy to change the scale. But what about precision? This is not as easy as you might hope!

It is easy to decrease the precision:

(.round 3.14159M (java.math.MathContext. 3)) ; 3.14M

But it is not obvious how to increase the precision:

(.round 3.14159M (java.math.MathContext. 7)) ; 3.14159M (unexpected)

For the skeptical, this is not just a matter of trailing zeros not being displayed:

(.precision (.round 3.14159M (java.math.MathContext. 7))) ; 6 
; (same as above, still unexpected)

FWIW, Clojure is careful with trailing zeros and will show them:

4.0000M ; 4.0000M
(.precision 4.0000M) ; 5

Back on track... You can try using a BigDecimal constructor, but it does not set the precision any higher than the number of digits you specify:

(BigDecimal. "3" (java.math.MathContext. 5)) ; 3M
(BigDecimal. "3.1" (java.math.MathContext. 5)) ; 3.1M

So, there is no quick way to change the precision. I've spent time fighting this while writing up this question and with a project I'm working on. I consider this, at best, A CRAZYTOWN API, and at worst a bug. People. Seriously?

So, best I can tell, if you want to change precision, you'll need to do these steps:

  1. Lookup the current precision.
  2. Lookup the current scale.
  3. Calculate the scale change.
  4. Set the new scale

These steps, as Clojure code:

(def x 0.000691M) ; the input number
(def p' 1) ; desired precision
(def s' (+ (.scale x) p' (- (.precision x)))) ; desired new scale
(.setScale x s' BigDecimal/ROUND_HALF_EVEN)
; 0.0007M

I know, this is a lot of steps just to change the precision!

Why doesn't BigDecimal already provide this? Did I overlook something?

Virescent answered 4/12, 2013 at 1:43 Comment(5)
What about BigDecimal.round(new MathContext(precision, RoundingModel.ROUND_HALF_EVEN))?Opportunism
@PaŭloEbermann Are you asking or suggesting? Have you tried it? Care sharing the detailed results for the examples I run through in my answer? If so, I think it warrants a separate answer.Virescent
For anyone who's like me and doesn't know a bit of Clojure, I think the solution shown here is x.setScale(x.scale() + p - x.precision(), RoundingMode.HALF_UP), where x is the BigDecimal you want to round, and p is the number of significant figures you want to keep. It's working for me, but correct me if I'm wrong!Elaelaborate
@Elaelaborate I like your solution but unfortunately it doesn't seem to work correctly in the corner case, e.g. for 99.99. Your statement (for 3 significant digits) turns this into 100.0 while it should be just 100. There's 1 extra significant digit because the 'overflow' of 99 to 100Invertebrate
@Invertebrate Good point. Guessing that's a limitation of this answer. Only thing I can think of would be to check afterwards and round again if the number of digits was wrong. I don't think it's possible for it to add a digit the second time.Elaelaborate
D
102

You can use setScale() e.g.

double d = ...
BigDecimal db = new BigDecimal(d).setScale(12, BigDecimal.ROUND_HALF_UP);
Dray answered 28/2, 2012 at 13:40 Comment(3)
Duh. Looking back at the error that i got, it said rounding was necessary. I had only done .setScale(12). Once i confirm it works, i'll make it as answered. Thanks.Surfing
I don't know why its doesn't just use a default rounding method like casting does, I have only seen ROUND_HALF_UP used. It is rather pedantic. ;)Dray
I'm confused. The question talks about precision but your answer talks about scale. Is the question using the wrong terminology or the answer? As far as I can tell, BigDecimal's precision and scale are mutually exclusive concepts.Metalloid
V
47

The title of the question asks about precision. BigDecimal distinguishes between scale and precision. Scale is the number of decimal places. You can think of precision as the number of significant figures, also known as significant digits.

Some examples in Clojure.

(.scale     0.00123M) ; 5
(.precision 0.00123M) ; 3

(In Clojure, The M designates a BigDecimal literal. You can translate the Clojure to Java if you like, but I find it to be more compact than Java!)

You can easily increase the scale:

(.setScale 0.00123M 7) ; 0.0012300M

But you can't decrease the scale in the exact same way:

(.setScale 0.00123M 3) ; ArithmeticException Rounding necessary

You'll need to pass a rounding mode too:

(.setScale 0.00123M 3 BigDecimal/ROUND_HALF_EVEN) ;
; Note: BigDecimal would prefer that you use the MathContext rounding
; constants, but I don't have them at my fingertips right now.

So, it is easy to change the scale. But what about precision? This is not as easy as you might hope!

It is easy to decrease the precision:

(.round 3.14159M (java.math.MathContext. 3)) ; 3.14M

But it is not obvious how to increase the precision:

(.round 3.14159M (java.math.MathContext. 7)) ; 3.14159M (unexpected)

For the skeptical, this is not just a matter of trailing zeros not being displayed:

(.precision (.round 3.14159M (java.math.MathContext. 7))) ; 6 
; (same as above, still unexpected)

FWIW, Clojure is careful with trailing zeros and will show them:

4.0000M ; 4.0000M
(.precision 4.0000M) ; 5

Back on track... You can try using a BigDecimal constructor, but it does not set the precision any higher than the number of digits you specify:

(BigDecimal. "3" (java.math.MathContext. 5)) ; 3M
(BigDecimal. "3.1" (java.math.MathContext. 5)) ; 3.1M

So, there is no quick way to change the precision. I've spent time fighting this while writing up this question and with a project I'm working on. I consider this, at best, A CRAZYTOWN API, and at worst a bug. People. Seriously?

So, best I can tell, if you want to change precision, you'll need to do these steps:

  1. Lookup the current precision.
  2. Lookup the current scale.
  3. Calculate the scale change.
  4. Set the new scale

These steps, as Clojure code:

(def x 0.000691M) ; the input number
(def p' 1) ; desired precision
(def s' (+ (.scale x) p' (- (.precision x)))) ; desired new scale
(.setScale x s' BigDecimal/ROUND_HALF_EVEN)
; 0.0007M

I know, this is a lot of steps just to change the precision!

Why doesn't BigDecimal already provide this? Did I overlook something?

Virescent answered 4/12, 2013 at 1:43 Comment(5)
What about BigDecimal.round(new MathContext(precision, RoundingModel.ROUND_HALF_EVEN))?Opportunism
@PaŭloEbermann Are you asking or suggesting? Have you tried it? Care sharing the detailed results for the examples I run through in my answer? If so, I think it warrants a separate answer.Virescent
For anyone who's like me and doesn't know a bit of Clojure, I think the solution shown here is x.setScale(x.scale() + p - x.precision(), RoundingMode.HALF_UP), where x is the BigDecimal you want to round, and p is the number of significant figures you want to keep. It's working for me, but correct me if I'm wrong!Elaelaborate
@Elaelaborate I like your solution but unfortunately it doesn't seem to work correctly in the corner case, e.g. for 99.99. Your statement (for 3 significant digits) turns this into 100.0 while it should be just 100. There's 1 extra significant digit because the 'overflow' of 99 to 100Invertebrate
@Invertebrate Good point. Guessing that's a limitation of this answer. Only thing I can think of would be to check afterwards and round again if the number of digits was wrong. I don't think it's possible for it to add a digit the second time.Elaelaborate
C
4
 BigDecimal decPrec = (BigDecimal)yo.get("Avg");
 decPrec = decPrec.setScale(5, RoundingMode.CEILING);
 String value= String.valueOf(decPrec);

This way you can set specific precision of a BigDecimal.

The value of decPrec was 1.5726903423607562595809913132345426 which is rounded off to 1.57267.

Carbonous answered 23/1, 2014 at 12:20 Comment(0)
R
2

Precision and scale are two very important terms when dealing with BigDecimals, to set them individually or setting both you can try:

1. Precision

    BigDecimal.ZERO.round(new MathContext(30, RoundingMode.FLOOR)); or 

    BigDecimal.valueOf(5878).round(new MathContext(10, RoundingMode.FLOOR));

2. Scale

    BigDecimal.valueOf(5878).setScale(7, RoundingMode.CEILING);

3. Both precision and scale

    BigDecimal.ONE.round(new MathContext(30, RoundingMode.FLOOR)).setScale(7, RoundingMode.FLOOR);

For example: Input: 321231.324 Precision: 9 Scale: 3

Remonstrate answered 16/3, 2023 at 8:19 Comment(0)
D
0

Try this code ...

    Integer perc = 5;
    BigDecimal spread = BigDecimal.ZERO; 
    BigDecimal perc = spread.setScale(perc,BigDecimal.ROUND_HALF_UP);
    System.out.println(perc);

Result: 0.00000

Defecate answered 11/7, 2016 at 12:2 Comment(0)
M
0

A BigDecimal's scale and precision are mutually-exclusive concepts. Quoting https://mcmap.net/q/188083/-bigdecimal-precision-and-scale:

Precision: Total number of significant digits

Scale: Number of digits to the right of the decimal point

To change a BigDecimal's precision, use BigDecimal.round(new MathContext(precision, roundingMode)).

To change a BigDecimal's scale, use BigDecimal.setScale(scale, roundingMode).

Metalloid answered 20/9, 2022 at 6:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.