Is there a way or apache utils function to split a BigDecimal number into random n smaller BigDecimals. I am looking to have this function for creating test data various combinations for a provided BigDecimal. For example it can be use for creating orderEntries for amount which can be added to the provided orderTotal so it can be used to create an order of certain amount with n number of order entries with varying order entry amount.
For example:
BigDecmial(10)
could be randomly split into 3 values as BigDecimal(1)
, BigDecimal(3.4)
and BigDecimal(5.6)
.
Or another example
BigDecmial(100)
could be randomly split into 5 values as BigDecimal(99)
, BigDecimal(0.23)
and BigDecimal(0.02)
, BigDecimal(0.03)
and BigDecimal(0.72)
.
I tried to write a function myself, but it turned out to be too long or complicated due to the nature of the BigDecimal. I thought, it sounds like a good usecase so somebody might have found the solution.
5
If you need n
random numbers that add up to a target value, you can also divide the interval of [0, targetValue]
into n-1
ranges and take the distances between the individual values as the random numbers. As an example for your 3
numbers, which add up to 10
:
randomly select two numbers from the interval and sort them in ascending order, let’s call them x
and y
you can now use this to construct your numbers, namely
- difference between 0 and x
- difference between x and y
- difference between y and 10
Example code:
import java.math.BigDecimal;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Example {
private static final Random random = new Random();
public static void main(final String[] args) {
List<BigDecimal> nums = randomSplit(BigDecimal.valueOf(100), 5);
//print the numbers
nums.forEach(System.out::println);
//verify that they add up to target value
System.out.println(nums.stream().reduce(BigDecimal.ZERO, BigDecimal::add));
}
public static List<BigDecimal> randomSplit(BigDecimal target, int count) {
AtomicReference<BigDecimal> temp = new AtomicReference<>(BigDecimal.ZERO);
return
Stream.concat(random.doubles(0, target.doubleValue())
.distinct()
.limit(count - 1)
.mapToObj(BigDecimal::valueOf),
Stream.of(target))
.sorted()
.map(bd -> bd.subtract(temp.getAndSet(bd)))
.collect(Collectors.toList());
}
}
A test run produced for example the following output
15.636848592939767
43.441254291071143
18.47865197555382
15.55883845061034
6.88440668982493
100.000000000000000
private final Random random = new Random();
public List<BigDecimal> randomSplit(BigDecimal target, int count) {
List<BigDecimal> values = new ArrayList<>();
BigDecimal remainder = target;
// randomize all values except the last
for (int i = 0; i < count - 1; ++i) {
// value will be random between 0 and the remainder
BigDecimal value = new BigDecimal(random.nextDouble()).multiply(remainder);
values.add(value);
remainder = remainder.subtract(value);
}
// last value is the remainder
values.add(remainder);
// shuffle the values to randomise ordering
Collections.shuffle(value, random);
return values;
}
3
I assume you want a positive number split into smaller positive numbers.
And additionally you want the numbers be equally distributed.
The solution is to make n independent random numbers,
and scale all terms to the correct sum afterwards.
In pseudo-code:
BigDecimal x = new BigDecimal("10.00");
BigDecimal[] randomSplit(BigDecimal x, int n) {
BigDecimal>[] terms = new BigDecimal[n];
BigDecimal sum = BigDecimal.ZERO;
for (int i = 0; i < n; ++i) {
terms[i] = newRandomUpto(x.multiply(n));
sum = sum.add(terms[i]);
}
BigDecimal factor = sum.signum() == 0? BigDecimal.ONE : x.divide(sum);
BigDecimal sum = BigDecimal.ZERO;
for (int i = 0; i < n - 1; ++i) {
terms[i] = term[i].multiply(factor);
sum = sum.add(terms[i]);
}
terms[n - 1] = x.subtract(sum); // The last term precise.
return terms;
}
BigDecimal is a pain to code (hence “pseudo-code”), you probably want to consider the precision.
Following Asmir’s solution, here’s one that uses the same concept, but done the old-fashioned way, without using streams:
public static List<BigDecimal> randomSplit(BigDecimal target, int count) {
BigDecimal[] values = new BigDecimal[count];
double targetAsDouble = target.doubleValue();
Random random = new Random();
for (int i = 0; i < count - 1; i++)
values[i] = BigDecimal.valueOf(random.nextDouble(targetAsDouble));
Arrays.sort(values, 0, count - 1);
values[count - 1] = target.subtract(values[count - 2]);
for (int i = values.length - 2; i > 0; i--)
values[i] = values[i].subtract(values[i - 1]);
return Arrays.asList(values);
}