How to aggregate multiple fields using Collectors in Java - java

I have a custom Object Itemized which has two fields amount and tax. I have an array of Itemized objects and I am looking to sum the two fields in the same stream. Below is how I am calculating the sum of both the fields.
double totalAmount = Arrays.stream(getCharges()).map(Itemized::getAmount).reduce(0.0, Double::sum));
double totalTax = Arrays.stream(getCharges()).map(Itemized::getTax).reduce(0.0, Double::sum));
Is there any way I don't have to parse the stream two times and can sum the two fields in one go ? I am not looking to sum totalTax and totalAmount but want their sum separately. I was looking at Collectors but was not able to find any example which would allow aggregating of multiple fields in one go.

use a for loop ?
double taxSum = 0;
double amountSum = 0;
for (Itemized itemized : getCharges()) {
taxSum += itemized.getTax();
amountSum += itemized.getAmount();
}

You can try to use the teeing Collector, like so:
Arrays.stream(getCharges()) // Get the charges as a stream
.collect(Collectors // collect
.teeing( // both of the following:
Collectors.summingDouble(Itemized::getAmount), // first, the amounts
Collectors.summingDouble(Itemized::getTax), // second, the sums
Map::entry // and combine them as an Entry
)
);
This should give you a Map.Entry<Double, Double> with the sum of amounts as the key and the sum of tax as the value, which you can extract.
Edit:
Tested and compiled it - it works. Here we go:
ItemizedTest.java
public class ItemizedTest
{
static Itemized[] getCharges()
{
// sums should be first param = 30.6, second param = 75
return new Itemized[] { new Itemized(10, 20), new Itemized(10.4,22), new Itemized(10.2, 33) };
}
public static void main(String[] args)
{
Map.Entry<Double, Double> sums = Arrays.stream(getCharges())
.collect(Collectors
.teeing(
Collectors.summingDouble(Itemized::getAmount),
Collectors.summingDouble(Itemized::getTax),
Map::entry
)
);
System.out.println("sum of amounts: "+sums.getKey());
System.out.println("sum of tax: "+sums.getValue());
}
}
Itemized.java
public final class Itemized
{
final double amount;
final double tax;
public double getAmount()
{
return amount;
}
public double getTax()
{
return tax;
}
public Itemized(double amount, double tax)
{
super();
this.amount = amount;
this.tax = tax;
}
}
Output:
sum of amounts: 30.6
sum of tax: 75.0
P.S.: teeing Collector is only available in Java 12+.

Instead of summing by field, you define a custom object to hold both field's sum values:
ItemizedValues {
private double amount;
private double tax;
public static final ItemizedValues EMPTY = new ItemizedValues(0, 0);
// Constructor - getter - setter
public static ItemizedValues of(Itemized item) {
return new ItemizedValues(amount, tax);
}
public static ItemizedValues sum(ItemizedValues a, ItemizedValues b) {
// Sum the fields respectively
// It's your choice to reuse the existing instances, modify the values or create a brand new one
}
}
Arrays.stream(getCharges())
.map(ItemizedValues::of)
.reduce(ItemizedValues.EMPTY, ItemizedValues::sum);

With a data structure that can allow one to accumulate both sums, you can reduce the stream to a single object.
This is using double[] to hold totalAmount at index 0 and totalTax at index 1 (other options include SimpleEntry, Pair):
double[] res = Arrays.stream(getCharges())
.map(ch -> new double[] { ch.getAmount(), ch.getTax() })
.reduce(new double[] { 0, 0 },
(a1, a2) -> new double[] { a1[0] + a2[0], a1[1] + a2[1] });
double totalAmount = res[0],
totalTax = res[1];

You can do it by using Entry but still you will end up in creating lot of objects, the best solution i would suggest is for loop answered by NimChimpsky
Entry<Double, Double> entry = Arrays.stream(new Itemized[] {i1,i2})
.map(it->new AbstractMap.SimpleEntry<>(it.getAmount(), it.getTax()))
.reduce(new AbstractMap.SimpleEntry<>(0.0,0.0),
(a,b)->new AbstractMap.SimpleEntry<>(a.getKey()+b.getKey(),a.getValue()+b.getValue()));
System.out.println("Amount : "+entry.getKey());
System.out.println("Tax : "+entry.getValue());

In your specific case, it's could be done by using your Itemized class as value holder.
Itemized result = Arrays.stream(getCharges())
.reduce(new Itemized(), (acc, item) -> {
acc.setAmount(acc.getAmount() + item.getAmount());
acc.setTax(acc.getTax() + item.getTax());
return acc;
});
double totalAmount = result.getAmount();
double totalTax = result.getTax();

Related

Grouping list by variables and counting sums of object variables

I must group elements from given list using stream. I want to put them into map (do groupping by variable
'vat' <key=vat, value=list>), then count sum of every objects variable in each key.
I am being stuck in summing stream. I did groupping stream but now, after few hours of coding i still dont know what to do.
Code of function:
public static List<Product> calculatingTaxes(List<Product> products){
Map<Double, List<Product>> productTaxesMap = new HashMap<>();
productTaxesMap = products.stream()
.collect(Collectors.groupingBy(Product::getVat));
System.out.println(productTaxesMap);
return null;
}
sout function is to see what the function is doing, current output
{1.0=[Product{netPrice=100.0, vat=1.0, vatAmount=0.0, grossPrice=100.0},
Product{netPrice=100.0, vat=1.0, vatAmount=0.0, grossPrice=100.0}], 1.23=
[Product{netPrice=100.0, vat=1.23, vatAmount=23.0, grossPrice=123.0},
Product{netPrice=100.0, vat=1.23, vatAmount=23.0, grossPrice=123.0},
Product{netPrice=100.0, vat=1.23, vatAmount=23.0, grossPrice=123.0}], 1.08=
[Product{netPrice=100.0, vat=1.08, vatAmount=8.0, grossPrice=108.0},
Product{netPrice=100.0, vat=1.08, vatAmount=8.0, grossPrice=108.0}]}
code of Product class:
public class Product {
private double netPrice;
private double vat;
private double vatAmount;
private double grossPrice;
public Product(double netPrice, double vat) {
this.netPrice = netPrice;
this.vat = vat;
this.grossPrice = netPrice * vat;
this.vatAmount = grossPrice - netPrice;
}
public double getNetPrice() {
return netPrice;
}
public double getVatAmount() {
return vatAmount;
}
public double getGrossPrice() {
return grossPrice;
}
public double getVat() {
return vat;
}
#Override
public String toString() {
return "Product{" +
"netPrice=" + netPrice +
", vat=" + vat +
", vatAmount=" + vatAmount +
", grossPrice=" + grossPrice +
'}';
}
I want the upper function, to return sum of netPrice, grossPrice and vat amount to every vat key(0-8-23%).
I tried collecting and summing elements using Collectors.grouppingBy(). by filtering etc, but none of them worked.
Assuming your input is:
List<Product> products = Arrays.asList(
new Product(100.0, 1.0),
new Product(100.0, 1.0),
new Product(100.0, 1.23),
new Product(100.0, 1.23),
new Product(100.0, 1.23),
new Product(100.0, 1.08),
new Product(100.0, 1.08)
);
You can do:
Map<Double, Product> vatToReducedProduct = products.stream()
.collect(Collectors.toMap(Product::getVat,
Function.identity(),
(p1, p2) -> new Product(p1.getNetPrice() + p2.getNetPrice(), p1.getVat())));
Output:
{
1.0=Product{netPrice=200.0, vat=1.0, vatAmount=0.0, grossPrice=200.0},
1.23=Product{netPrice=300.0, vat=1.23, vatAmount=69.0, grossPrice=369.0},
1.08=Product{netPrice=200.0, vat=1.08, vatAmount=16.0, grossPrice=216.0}
}

how to create a class that contain multiple medhods that can do operations, that can receive an arraylist of int,double,float,long,short

I want to create a class that contains multiple methods that can do mathematic operations. The method should be able to receive an ArrayList of int, double, float, long, short. I made the methods so that they can receive double ArrayLists because double is the parent of all the other number types from my understanding. Why can't my methods receive an ArrayList of Integers?
Here is my code:
import java.util.*;
import java.util.ArrayList;
class Operations <U extends Number>
{
// Function for calculating mean
public static double mean(ArrayList<Double> doubles)
{
double sum = 0;
for (int i = 0; i < doubles.size(); i++)
sum += doubles.get(i);
return (double)sum / (double)doubles.size();
}
// Function for calculating maximum value
public static double max(ArrayList<Double> doubles){
Collections.sort(doubles);
double max=0;
max=doubles.get(doubles.size()-1);
return max;
}
// Driver code
}
public class Main {
public static void main(String args[])
{
ArrayList<Double> doubles = new ArrayList<>(){
{add(4.3); add(5.9); add(2.3); add(9.5);}
};
ArrayList<Integer> integers = new ArrayList<>(){
{add(4); add(5); add(2); add(9);}
};
System.out.println(new Operations<Double>().mean(doubles));
// System.out.println(new Operations<Double>().mean(integers));
}
}
Here is an example implementation of mean that should work for all Numbers:
public static <T extends Number> double mean(List<T> numbers)
{
double sum = 0;
for (Number n : numbers)
sum += n.doubleValue();
return sum / (double)numbers.size();
}
Similarly max could look like:
public static <T extends Number> T max(List<T> numbers) {
T max = null;
for (T t : numbers) {
if (max == null || t.doubleValue() > max.doubleValue()) {
max = t;
}
}
return max;
}
Test:
#Test public void testNumbers() {
List<Double> d = Arrays.asList(4.3, 5.9, 2.3, 9.5);
System.out.println("doubles: mean=" + mean(d) + " max=" + max(d));
List<Integer> i = Arrays.asList(4, 5, 2, 9);
System.out.println("integers: mean=" + mean(i) + " max=" + max(i));
}
Output:
doubles: mean=5.5 max=9.5
integers: mean=5.0 max=9
Its wrong point:
double is the parent of all the other number types
For example ArrayList<Integer> can not to be casted to ArrayList<Double>
But you can try use somthing like that:
public U mean(List<U> values) {
You can get doubles from such values.

Accumulate inside forEach Java 8

How can I accumulate inside a forEach? this is my code:
public Double accumulate() {
Double accum = 0d;
numbers.stream().forEach(p-> {
// if condition
accum = accum + numbers.getAmount();
// else
accum = accum + numbers.getAmountWithInterest();
});
return accum;
}
Maybe I should use map instead of forEach, I tried a couple of things but it didn't work. Thanks
I do not think it is a good idea to make side effect when using lambda. It is a bad way of mixing functional and imperative programming. You can do it easier by
numbers.stream().mapToInt(p-> {
// if condition
return numbers.getAmount();
// else
return numbers.getAmountWithInterest();
}).sum();
You can use mapToDouble and sum instead of forEach with Java Streams:
#Test
public void sumOfDoubleWithStreams()
{
List<Amount> numbers =
Arrays.asList(new Amount(10.0, 0.0), new Amount(20.0, 0.05), new Amount(30.0, 0.1));
double sum = numbers.stream()
.mapToDouble(
amount -> amount.hasInterest() ? amount.getAmountWithInterest() : amount.getAmount())
.sum();
Assert.assertEquals(64.0d, sum, 0.0d);
}
If you really want to use forEach, you need to have an object that can be mutated inside of the forEach call. You can use AtomicDouble here, or another class like DoubleSummaryStatistics or your own accumulating class.
#Test
public void sumOfDoubleWithForEach()
{
List<Amount> numbers =
Arrays.asList(new Amount(10.0, 0.0), new Amount(20.0, 0.05), new Amount(30.0, 0.1));
AtomicDouble sum = new AtomicDouble();
numbers.forEach(amount ->
sum.addAndGet(amount.hasInterest() ? amount.getAmountWithInterest() : amount.getAmount()));
Assert.assertEquals(64.0d, sum.get(), 0.0d);
}
If you are open to using a third-party library, you can use sumOfDouble on any container from Eclipse Collections.
#Test
public void sumOfDouble()
{
MutableList<Amount> numbers =
Lists.mutable.with(new Amount(10.0, 0.0), new Amount(20.0, 0.05), new Amount(30.0, 0.1));
double sum = numbers.sumOfDouble(
amount -> amount.hasInterest() ? amount.getAmountWithInterest() : amount.getAmount());
Assert.assertEquals(64.0d, sum, 0.0d);
}
I created an Amount class to hold the amount and interest rate:
public static final class Amount
{
private final double amount;
private final double interest;
public Amount(double amount, double interest)
{
this.amount = amount;
this.interest = interest;
}
public double getAmountWithInterest()
{
return this.amount * (1.0 + this.interest);
}
public boolean hasInterest()
{
return this.interest > 0.0d;
}
public double getAmount()
{
return amount;
}
}
Note: I am a committer for Eclipse Collections.

Passing a method reference as parameter

In a case like this:
public class Order {
List<Double> prices = List.of(1.00, 10.00, 100.00);
List<Double> pricesWithTax = List.of(1.22, 12.20, 120.00);
Double sumBy(/* method reference */) {
Double sum = 0.0;
for (Double price : /* method reference */) {
sum += price;
}
return sum;
}
public List<Double> getPrices() { return prices; }
public List<Double> getPricesWithTax() { return pricesWithTax; }
}
how can I declare the sumBy method in a way that can be called like this:
Order order = new Order();
var sum = order.sumBy(order::getPrices);
var sumWithTaxes = order.sumBy(order::getPricesWithTax);
I'm not using the Java 8 API for the sum because my goal is only to understand how pass a method reference.
Your 2 methods take no argument and return an object, so that fits the Supplier.get() method.
Don't use Double for the sum variable, since that will auto-box and auto-unbox way too much.
Method can be static since it doesn't use any fields or other methods of the class.
static double sumBy(Supplier<List<Double>> listGetter) {
double sum = 0.0;
for (double price : listGetter.get()) {
sum += price;
}
return sum;
}
Better yet:
static double sumBy(Supplier<List<Double>> listGetter) {
return listGetter.get().stream().mapToDouble(Double::doubleValue).sum();
}
You seem to want a Supplier like
Double sumBy(Supplier<List<Double>> f) {
Double sum = 0.0;
for (Double price : f.get()) {
sum += price;
}
return sum;
}
Your List.of syntax was giving me errors. So I did
List<Double> prices = Arrays.asList(1.00, 10.00, 100.00);
List<Double> pricesWithTax = Arrays.asList(1.22, 12.20, 120.00);
Then I tested like
public static void main(String[] args) throws IOException {
Order order = new Order();
double sum = order.sumBy(order::getPrices);
double sumWithTaxes = order.sumBy(order::getPricesWithTax);
System.out.printf("%.2f %.2f%n", sum, sumWithTaxes);
}
Outputs
111.00 133.42
I think the Supplier<T> functional interface is what you’re looking for:
Double sumBy(Supplier<Collection<Double>> supplier) {
Collection<Double> prices = supplier.get();
}
Use Double sumBy(Supplier<List<Double>> doubles)

Average for group no within group - stream

How to compute an average value for group using stream. Below code which I would like to transform to stream solution.
public static void main(String[] args) {
List<Item> items = Arrays.asList(
new Item("A", 1.0),
new Item("A", 1.0),
new Item("B", 1.0)
);
System.out.println(averageForGroup(items));
}
public static double averageForGroup(List<Item> items) {
Set<String> uniqueGroups = new HashSet<>();
double sum = 0;
for (Item i : items) {
String groupName = i.getGroupName();
if (!uniqueGroups.contains(groupName)) {
uniqueGroups.add(groupName);
}
sum += i.getValue();
}
return sum / uniqueGroups.size();
}
Item class:
public class Item {
private String groupName;
private Double value;
// Full-args constructor
// Getters and setters
}
I tried something like this:
public static double averageForGroup2(List<Item> items) {
return items.stream()
.collect(Collectors.groupingBy(
Item::getGroupName,
Collectors.averagingDouble(Item::getValue)) )
.entrySet().stream()
.mapToDouble(entry -> entry.getValue())
.sum();
}
But method sums up averages, so not what I expect. If it was possible to revert summing with grouping it may return excepted result.
double result = items.stream()
.collect(
Collectors.collectingAndThen(
Collectors.groupingBy(
Item::getGroupName,
Collectors.summingDouble(Item::getValue)),
map -> map.values().stream().mapToDouble(Double::doubleValue).sum() / map.size()));
To make it more readable, you can do it in two operations:
long distinct = items.stream().map(Item::getGroupName).distinct().count();
double sums = items.stream().mapToDouble(Item::getValue).sum();
System.out.println(sums / distinct);
You can do it in a single pass, but requires a custom collector...
You want something like:
Map<String, Double> map = items.stream() // Stream
.collect(Collectors.groupingBy( // Group to map
Item::getGroupName, // Key is the groupName
Collectors.averagingDouble(Item::getValue))); // Value is the average of values
To get result average of a particular group, get the value from the Map:
double averageForA = map.get("A");
Another way would be using collect(supplier, accumulator, combiner). Based on example from official tutorial (see class Averager) you can write your own class which will let you
collect current sum and unique names
handle each Item element to update sum and set of unique names
combine other instances of that class in case of parallel processing.
So such class can look like
class ItemAverager {
Set<String> set = new HashSet();
double sum = 0;
ItemAverager add(Item item) {
set.add(item.getGroupName());
sum += item.getValue();
return this;
}
ItemAverager combine(ItemAverager ia) {
set.addAll(ia.set);
sum += ia.sum;
return this;
}
double average() {
if (set.size() > 0)
return sum / set.size();
else
return 0; //or throw exception
}
}
and can be used like
List<Item> items = Arrays.asList(
new Item("A", 1.0),
new Item("A", 3.0),
new Item("B", 1.0)
);
double avrg = items
.stream()
.collect(ItemAverager::new,
ItemAverager::add,
ItemAverager::combine
).average(); // `collect` will return ItemAverager
// on which we can call average()
System.out.println(avrg); // Output: 2.5
// (since 1+3+1 = 5 and there are only two groups 5/2 = 2.5)
But to be honest in case of no parallel processing I would prefer your own solution using simple loop (maybe with little improvement since you don't need to call contains before add on sets, add internally calls it anyway)
public static double averageForGroup(List<Item> items) {
Set<String> uniqueGroups = new HashSet<>();
double sum = 0;
for (Item item : items) {
uniqueGroups.add(item.getGroupName());
sum += item.getValue();
}
return sum / uniqueGroups.size();
}
public static double getAverageByGroups(List<Item> items) {
Map<String, Double> map = Optional.ofNullable(items).orElse(Collections.emptyList()).stream()
.collect(Collectors.groupingBy(Item::getGroupName, Collectors.summingDouble(Item::getValue)));
return map.isEmpty() ? 0 : map.values().stream().mapToDouble(value -> value).sum() / map.size();
}
In this example getAverageByGroups returns 0 for empty items.

Categories

Resources