Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Amount without Trailing Zeros #518

Merged
merged 4 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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())
}
}
Loading