fluent-bigdecimals is a library that lets Java programmers forget about BigDecimal rounding/scaling.
It does this by wrapping Javas BigDecimal into a class with fancy fluent API.
Only one runtime dependency: checker-qual for @Nullable/@NonNull annotations.
Java 9 modules supported.
This project requires Java >= 9.
Maven dependency:
<dependency>
<groupId>com.github.honoluluhenk.fluent-bigdecimals</groupId>
<artifactId>fluent-bigdecimals</artifactId>
<version>x.y.z</version>
</dependency>
Current version: see GitHub releases or Maven Central
fluent-bigdecimals requires you to only setup precision, rounding and scaling (a.k.a.: the Configuration
) once. You can then re-use this configuration on all BigDecimal operations.
public class MyMathUtil {
private static final MathContext DEFAULT_MATH_CONTEXT = new MathContext(7, HALF_UP);
// some custom configuration to your liking (here: just rounding when calling the operation but no scaling)
public static final Configuration<FluentBigDecimal> DEFAULT = ConfigurationFactory
.create(DEFAULT_MATH_CONTEXT, new NopScaler());
// predefined: round/scale in a database compatible way.
public static final BigDecimalFactory DATABASE = ConfigurationFactory.jpaBigDecimal();
}
public class MyBusinessTest {
@Test
public void showcase() {
// after each step: automatic rounding/scaling according to current configuration
BigDecimal result = DEFAULT.of("12.3456789")
.add("54.555555")
// currently supports BigDecimal, String and other FluentBigDecimals on all implemented operations
.multiply("42.23")
.divide(new BigDecimal("555.5"))
.subtract(DEFAULT.of("99"))
// continue with other configuration
.roundInto(DATABASE)
.multiply("123.99999")
.getValue();
// return result;
assertThat(result)
.isEqualTo("-11644.84");
}
@Test
public void usingFancyOperators() {
// call your own operators (or BigDecimal operations not yet directly implemented by fluent-bigdecimals)
FluentBigDecimal result = DEFAULT.of("12.3456789")
.apply(this::myFancyOperation, 42);
// operators returning other values thant BigDecimal
int signum = DEFAULT.of("12.3456789")
.map(BigDecimal::signum);
}
}
In short: no breaking changes on minor or patch version upgrades.
All method arguments and return values are @NonNull if not explicitly stated otherwise (see param/method annotations).
Passing an illegal null parameter will immediately result in a NullPointerException
.
All classes are immutable and - as such - thread-safe
Class: Configuration.java
A Configuration is a combination of a MathContext (i.e.: precision and rounding) and some Scaler.
The Configuration class is as factory and creates the main workhorse: FluentBigDecimal. As such, Configuration is the main starting-point of all that is to come.
To help setup your configurations, see ConfigurationFactory.
Basic setups may be modified by various with()
methods.
Classs: ConfigurationFactory.java.
Helps building a Configuration by providing some useful defaults.
The most basic factory method is ConfigurationFactory::create(MathContext, Scaler)
(all the others build on that).
Some more convenient factory-methods in ConfigurationFactory
:
method | description |
---|---|
::monetary | The most common usecase: custom precision with a maximum scale of 2 and HALF_UP rounding. |
::jpaBigDecimal | compatible with JPA/Hibernate defaults: @Column(precision = 16, scale = 2). See JPA/Database |
::databaseJavaNotation | Scale in a database compatible way and use Java notation for precision/scale. See JPA/Database |
::databaseDBNotation | Scale in a database compatible way and use database notation for precision/scale. See JPA/Database |
::cashRounding | User Cash Rouding/Scaling. See Cash Rounding |
Class: FluentBigDecimal.java and AbstractFluentBigDecimal.java
The main workhorse: apply operations on BigDecimals and keep track of precision, RoundingMode and scale (i.e.: the Configuration).
Provides the operations (add/subtract/multiply/divide/...) and a multitude of helper methods.
Basic flow for each operation:
- apply operation using the configured MathContext (i.e. apply precision/rounding).
- scale using the Scaler
- return a new instance of FluentBigDecimal (that's where the "Fluent" comes from :) )
Interface: Scaler.java.
A Scaler takes care of scaling the rounded value after applying an operation.
There exist some predefined Scalers:
scaler | description |
---|---|
CashRoundingScaler | See Cash Rounding |
FixedScaleScaler | reduces/expands to the desired scale |
IntegerScaler | Scales to scale 0 (i.e.: effectively convert to integer) |
MaxScaleScaler | Reduces scale to a given maximum |
NopScaler | Does not scale at all. Use this if you just care about precision |
See methods in ConfigurationFactory for some predefined configurations.
Some examples:
ConfigurationFactory::monetary
(configurable precision/scale, HALF_UP rounding and stick to the given scale)ConfigurationFactory::cashRounding
(same as monetary plus apply Cash Rounding on each step, See also enumCashRoundingUnits
for some predefined values, see also: Advanced Usage)ConfigurationFactory::jpaBigDecimal
(precision/scale taken from JPA/Hibernate defaults for BigDecimal, see also: Advanced Usage)
See chapter Scaler for more.
All relevant classes (Configuration, Scaler, FluentBigDecimal, ...) support various with
methods. This means you can always start with some defaults from ConfigurationFactory
and then adjust to your liking.
Example:
class Foo {
private final MonetaryConfiguration<FluentBigDecimal> SWISS_CASH = ConfigurationFactory
.monetary(20)
.withScale(10);
}
class Foo {
private final Configuration<FluentBigDecimal> SWISS_CASH = ConfigurationFactory
.cashRounding(20, CashRoundingUnits.ROUND_DOT05);
private final Configuration<FluentBigDecimal> HIGH_PRECISION = ConfigurationFactory
.create(20, HALF_UP, MaxScaleScaler.of(10));
@Test
void roundIntoCash() {
// start off with some high precision calculations
FluentBigDecimal cash = HIGH_PRECISION.of("12345.67890")
.multiply("3")
// intermediate result: 37037.03670
.roundInto(SWISS_CASH);
assertThat(cash.getValue())
.isEqualTo("37037.05");
}
}
You might also give a custom rounding unit using the CashRoundingScaler
.
Rouding units represent the smallest amount representable in the cash system.
Example: Switzerland. The Swiss Rappen is avaiable only 5/100 units (0.05, 0.10, 0.15, ...). As such, the unit parameter would be "0.05"
(but for most units you should find constants in CashRoundingUnits
)
In contrast to Java BigDecimals, databases usually only allow a definable maximum scale for numeric values.
Also, Java usually treat precision and scale differently:
Java: precision
is total number of relevant digits (integer + decimal part together), allowing a maximum of scale
digits after the decimal point. Database: precision
is the total number of digits including scale
. There are precision - scale
digits available for the integer part.
A configuration matching the above behavior can be obtained by Using database notation: ConfigurationFactory.database(precision, scale) .
Also there is a shortcut to obtain a database configuration using JPA/Hibernate default values for precision/scale: Using JPA/Hibernate notation: ConfigurationFactory.jpaBigDecimal(precision, scale) .
Just the scaling part is implemented by the MaxScaleScaler.
someFluentBigDecimal.isZero()
is shorthand for someFluentBigDecimal.compareTo(BigDecimal.ZERO) == 0
someFluentBigDecimal.comparesTo(other)
is shorthand for someFluentBigDecimal.compareTo(other) == 0
See BigDecimal#compareTo(BigDecimal) for semantic details.
YourConfiguration.ZERO()
YourConfiguration.ONE()
YourConfiguration.TEN()
Also allows for your own predefined constants in Configuration subclasses, see AbstractFluentBigDecimal#memoizedConstant().
compareTo
, equals
and hashCode
are implemented by directly delegating to their BigDecimal equivalents and thus could be treated as a drop-in replacement for mose usecases.
See Wikipedia
Some countries round to some custom fraction (e.g.: Switzerland rounds to 0.05 Rappen).
Maybe you do some high-precision calculations. Then, at some point, you need to continue calculating using the (e.g.) database precision.
This can be achieved using the roundInto
method:
class Foo {
public void roundIntoDemo() {
FluentBigDecimal result = DEFAULT.of("12345678.90")
.add("999.999999")
.roundInto(DATABASE) // exact result: 124456789.899999, gets rounded to (124456789.90).
// now we continue with DATABASE precision/scaling
.multiply("12.3456");
assertThat(result.getValue())
.isEqualTo("152427172.61");
}
}
Rare usecase: If immediate rounding is not wanted: use the withConfiguration
method instead:
class Foo {
public void withConfiguration() {
FluentBigDecimal result = DEFAULT.of("12345678.90")
.add("999.999999")
.withConfiguration(DATABASE) // exact result: 124456789.899999, does *not* get rounded here
// ... but on the next step:
.multiply("12.3456");
assertThat(result.getValue())
.isEqualTo("152427172.61");
}
}
If you need this rounded, you might call the round()
method afterwards.
There is the map
method:
class Foo {
public void mapping() {
double result = DATABASE.of("12345678.90")
.map(BigDecimal::doubleValue);
}
}
If you need re-usable operators, consider extension (see below) instead!
Custom operators can be applied using the various apply()
methods.
Operation with one argument:
class Foo {
public void customOperationsWithOneArgument() {
FluentBigDecimal result = DATABASE.of("12345678.90")
.apply(BigDecimal::divideToIntegralValue, new BigDecimal("42"))
.apply(this::myFancyOperation, 42);
}
public BigDecimal myFancyOperation(BigDecimal value, int argument, MathContext mc) {
// just a simple simulation
BigDecimal result = value.add(BigDecimal.valueOf(argument));
return result;
}
}
Operation with arguments of your choice:
class Foo {
public void customOperationsWithAnyArgument(Object someParam) {
FluentBigDecimal result = DATABASE.of("12345678.90")
.apply((value, mathContext) -> doStuffWith(value, someParam));
}
private BigDecimal doStuffWith(BigDecimal value, Object whatever) {
return value.add(new BigDecimal(whatever.hashCode()));
}
}
If you regularly need some custom operators, you do have the option to extend your custom FluentBigDecimal from AbstractFluentBigDecimal
.
Configuration
supports the factory pattern for instantiating your subclass on each operation:
public class MyConfig {
public static final Configuration<MyMath> MY_MATH = ConfigurationFactory.monetary(20)
.withFactory(MyMath::new);
public static final Configuration<MyMath> SWISS_CASH = ConfigurationFactory
.cashRounding(20, CashRoundingUnits.ROUND_DOT05)
.withFactory(MyMath::new);
}
public class MyMath extends AbstractFluentBigDecimal<MyMath> {
private static final long serialVersionUID = -1828369497254888980L;
protected MyMath(@NonNull BigDecimal value, @NonNull Configuration<MyMath> configuration) {
super(value, configuration);
}
public String toJson() {
return "{ value: \"" + getValue().toPlainString() + "\" }";
}
public MyMath roundIntoSwissRappen() {
return roundInto(SWISS_CASH);
}
}
class MyBusiness {
void doStuff() {
var json = MY_MATH.of("42.04") // of() creates an instance of MyMath
.roundIntoSwissRappen()
.add(new BigDecimal("23"))
.toJson();
assertThat(json)
.isEqualTo("{ value: \"65.05\" }");
}
}
Just create an issue.
This project uses the following license: LGPL-3.0
See releases