Skip to content

Commit

Permalink
Merge pull request #518 from scireum/feature/jvo/Amount-without-Trail…
Browse files Browse the repository at this point in the history
…ing-Zeros

Amount without Trailing Zeros
  • Loading branch information
jakobvogel committed Mar 21, 2024
2 parents 1e093ee + 8ca81eb commit fbdeff1
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 47 deletions.
21 changes: 19 additions & 2 deletions src/main/java/sirius/kernel/commons/Amount.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.math.MathContext;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;

Expand All @@ -30,7 +31,7 @@
* <p>
* Adds some extended computations as well as locale aware formatting options to perform "exact" computations on
* numeric value. The internal representation is <tt>BigDecimal</tt> and uses MathContext.DECIMAL128 for
* numerical operations. Also the scale of each value is fixed to 5 decimal places after the comma, since this is
* numerical operations. Also, the scale of each value is fixed to 5 decimal places after the comma, since this is
* enough for most business applications and rounds away any rounding errors introduced by doubles.
* <p>
* A textual representation can be created by calling one of the <tt>toString</tt> methods or by supplying
Expand All @@ -39,7 +40,7 @@
* Note that {@link #toMachineString()} to be used to obtain a technical representation suitable for file formats
* like XML etc. This is also used by {@link NLS#toMachineString(Object)}. The default representation uses two
* decimal digits. However, if the amount has bed {@link #round(int, RoundingMode) rounded}, the given amount
* of decimals will be used in all subesquent call to {@link #toMachineString()}. Therefore, this can be used to
* of decimals will be used in all subsequent call to {@link #toMachineString()}. Therefore, this can be used to
* control the exact formatting (e.g. when writing XML or JSON).
* <p>
* Being able to be <i>empty</i>, this class handles <tt>null</tt> values gracefully, which simplifies many operations.
Expand Down Expand Up @@ -247,6 +248,22 @@ public BigDecimal getAmount() {
return value;
}

/**
* Unwraps the internally used <tt>BigDecimal</tt> like {@link #getAmount()}, but also strips trailing zeros from
* the decimal part.
*
* @return the amount with trailing zeros stripped of the decimal part
*/
@Nullable
public BigDecimal fetchAmountWithoutTrailingZeros() {
return Optional.ofNullable(value)
.map(BigDecimal::stripTrailingZeros)
.map(bigDecimal -> bigDecimal.scale() < 0 ?
bigDecimal.setScale(0, RoundingMode.UNNECESSARY) :
bigDecimal)
.orElse(null);
}

/**
* Unwraps the internally used <tt>BigDecimal</tt> with rounding like in {@link #toMachineString()} applied.
* This is used for Jackson Object Mapping.
Expand Down
106 changes: 61 additions & 45 deletions src/test/kotlin/sirius/kernel/commons/AmountTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class AmountTest {

@ParameterizedTest
@CsvSource(
delimiter = '|', textBlock = """
delimiter = '|', textBlock = """
4.2 | 42 | 46.2
42 | 4.2 | 46.2
0 | 42 | 42
Expand All @@ -169,17 +169,17 @@ class AmountTest {
42 | | 42 """
)
fun `add() works as expected`(
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
) {

assertEquals(result, a.add(b))
}

@ParameterizedTest
@CsvSource(
delimiter = '|', textBlock = """
delimiter = '|', textBlock = """
4.2 | 42 | -37.8
42 | 4.2 | 37.8
0 | 42 | -42
Expand All @@ -188,17 +188,17 @@ class AmountTest {
42 | | 42 """
)
fun `subtract() works as expected`(
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
) {

assertEquals(result, a.subtract(b))
}

@ParameterizedTest
@CsvSource(
delimiter = '|', textBlock = """
delimiter = '|', textBlock = """
4.2 | 42 | 176.4
42 | 4.2 | 176.4
0 | 42 | 0
Expand All @@ -207,16 +207,16 @@ class AmountTest {
42 | | """
)
fun `times() works as expected`(
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
) {
assertEquals(result, a.times(b))
}

@ParameterizedTest
@CsvSource(
delimiter = '|', textBlock = """
delimiter = '|', textBlock = """
4.2 | 42 | 0.1
42 | 4.2 | 10
0 | 42 | 0
Expand All @@ -225,16 +225,16 @@ class AmountTest {
42 | | """
)
fun `divideBy() works as expected`(
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
) {
assertEquals(result, a.divideBy(b))
}

@ParameterizedTest
@CsvSource(
delimiter = '|', textBlock = """
delimiter = '|', textBlock = """
4.2 | -4.2
-4.2 | 4.2
42 | -42
Expand All @@ -243,96 +243,96 @@ class AmountTest {
| """
)
fun `negate() works as expected`(
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
) {
assertEquals(result, a.negate())
}

@ParameterizedTest
@CsvSource(
delimiter = '|', textBlock = """
delimiter = '|', textBlock = """
4.2 | 10 | 4.62
0 | 42 | 0
42 | 0 | 42
| 42 |
42 | | 42 """
)
fun `increasePercent() works as expected`(
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
) {

assertEquals(result, a.increasePercent(b))
}

@ParameterizedTest
@CsvSource(
delimiter = '|', textBlock = """
delimiter = '|', textBlock = """
4.2 | 10 | 3.78
0 | 42 | 0
42 | 0 | 42
| 42 |
42 | | 42"""
)
fun `decreasePercent() works as expected`(
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
) {

assertEquals(result, a.decreasePercent(b))
}

@ParameterizedTest
@CsvSource(
delimiter = '|', textBlock = """
delimiter = '|', textBlock = """
4.2 | 42 | 10
0 | 42 | 0
42 | 0 |
| 42 |
42 | | """
)
fun `percentageOf() works as expected`(
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
) {

assertEquals(result, a.percentageOf(b))
}

@ParameterizedTest
@CsvSource(
delimiter = '|', textBlock = """
delimiter = '|', textBlock = """
4.62 | 4.2 | 10
0 | 42 | -100
42 | 0 |
| 42 |
42 | | """
)
fun `percentageDifferenceOf() works as expected`(
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
) {

assertEquals(result, a.percentageDifferenceOf(b))
}

@ParameterizedTest
@CsvSource(
delimiter = '|', textBlock = """
delimiter = '|', textBlock = """
0.42 | 42
1 | 100
2 | 200
0 | 0
| """
)
fun `toPercent() works as expected`(
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
) {

assertEquals(result, a.toPercent())
Expand All @@ -341,24 +341,24 @@ class AmountTest {

@ParameterizedTest
@CsvSource(
delimiter = '|', textBlock = """
delimiter = '|', textBlock = """
42 | 0.42
100 | 1
200 | 2
0 | 0
| """
)
fun `asDecimal() works as expected`(
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
) {

assertEquals(result, a.asDecimal())
}

@ParameterizedTest
@CsvSource(
delimiter = '|', textBlock = """
delimiter = '|', textBlock = """
10 | 2 | 0
10 | 3 | 1
10 | 0 |
Expand All @@ -367,11 +367,27 @@ class AmountTest {
10 | | """
)
fun `remainder() works as expected`(
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
@ConvertWith(AmountConverter::class) a: Amount,
@ConvertWith(AmountConverter::class) b: Amount,
@ConvertWith(AmountConverter::class) result: Amount,
) {

assertEquals(result, a.remainder(b))
}

@ParameterizedTest
@CsvSource(
delimiter = '|', textBlock = """
0.420 | 0.42
1.0 | 1
200 | 200
200.0000 | 200
600.010 | 600.01"""
)
fun `getAmountWithoutTrailingZeros() works as expected`(
@ConvertWith(AmountConverter::class) a: Amount,
result: String,
) {
assertEquals(result, a.fetchAmountWithoutTrailingZeros()?.toPlainString())
}
}

0 comments on commit fbdeff1

Please sign in to comment.