grouping values with diferent values in columns using java lambda - java

I have this Object:
QuoteProductDTO with three columns ( name, value1, value2)
List<QuoteProductDTO> lstQuoteProductDTO = new ArrayList<>();
lstQuoteProductDTO.add( new QuoteProductDTO("product", 10, 15.5) );
lstQuoteProductDTO.add( new QuoteProductDTO("product", 05, 2.5) );
lstQuoteProductDTO.add( new QuoteProductDTO("product", 13, 1.0) );
lstQuoteProductDTO.add( new QuoteProductDTO("product", 02, 2.0) );
I need to get a consolidate ( a new object QuoteProductDTO ):
the firts column name,I have to get the first value "product".
the second one (value1) I have to get the biggest value 13.
and third column I heve to get the sum of all values 20.

This takes the current data provided and generates a new object with the required data. It uses the Collectors.teeing() method of Java 12+
Given the following data:
ArrayList<QuoteProductDTO> lstQuoteProductDTO = new ArrayList<>();
ArrayList<QuoteProductDTO> nextQuoteProductDTO = new ArrayList<>();
// empty Quote for Optional handling below.
QuoteProductDTO emptyQuote = new QuoteProductDTO("EMPTY", -1, -1);
lstQuoteProductDTO.add(
new QuoteProductDTO("Product", 10, 15.5));
lstQuoteProductDTO.add(
new QuoteProductDTO("Product", 05, 2.5));
lstQuoteProductDTO.add(
new QuoteProductDTO("Product", 13, 1.0));
lstQuoteProductDTO.add(
new QuoteProductDTO("Product", 02, 2.0));
You can consolidate like you want into a new instance of QuoteProductDTO.
QuoteProductDTO prod = lstQuoteProductDTO.stream()
.collect(Collectors.teeing(
Collectors.maxBy(Comparator
.comparing(p -> p.value1)),
Collectors.summingDouble(
p -> p.value2),
(a, b) -> new QuoteProductDTO(
a.orElse(emptyQuote).name,
a.orElse(emptyQuote).value1,
b.doubleValue())));
System.out.println(prod);
Prints
Product, 13, 21.0
You can also take a list of lists of different products and put them in a list of consolidated products. Add the following to a new list and then add those to a main list.
nextQuoteProductDTO.add(
new QuoteProductDTO("Product2", 10, 15.5));
nextQuoteProductDTO.add(
new QuoteProductDTO("Product2", 25, 20.5));
nextQuoteProductDTO.add(
new QuoteProductDTO("Product2", 13, 1.0));
nextQuoteProductDTO.add(
new QuoteProductDTO("Product2", 02, 2.0));
List<List<QuoteProductDTO>> list = List.of(
lstQuoteProductDTO, nextQuoteProductDTO);
Now consolidate those into a list of objects.
List<QuoteProductDTO> prods = list.stream().map(lst -> lst.stream()
.collect(Collectors.teeing(
Collectors.maxBy(Comparator
.comparing(p -> p.value1)),
Collectors.summingDouble(
p -> p.value2),
(a, b) -> new QuoteProductDTO(
a.orElse(emptyQuote).name,
a.orElse(emptyQuote).value1,
b.doubleValue()))))
.collect(Collectors.toList());
prods.forEach(System.out::println);
Prints
Product, 13, 21.0
Product2, 25, 39.0
I created a class to help demonstrate this.
class QuoteProductDTO {
public String name;
public int value1;
public double value2;
public QuoteProductDTO(String name, int value1,
double value2) {
this.name = name;
this.value1 = value1;
this.value2 = value2;
}
public String toString() {
return name + ", " + value1 + ", " + value2;
}
}

Related

Java Stream - groupingBy() and counting() when a curtain Condition is met

Given the following class Test
class Test {
String testName;
String studName;
String status;
}
and a list of tests
List<Test> tests = List.of(
new Test("English", "John", "passed"),
new Test("English", "Dave", "passed"),
new Test("Science", "Alex", "failed"),
new Test("Science", "Jane", "failed"),
new Test("History", "Dave", "passed"),
new Test("Mathematics", "Anna", "passed"),
new Test("Mathematics", "Lisa", "passed"),
new Test("Mathematics", "Paul", "failed"),
new Test("Geography", "Mark", "passed"),
new Test("Physics", "John", "failed"));
I need to group by testName and count only where status equals "passed". I need to do the equivalent of below code with streams :
Map<String, Long> result2 = new HashMap<>();
for (Test t : tests) {
result2.putIfAbsent(t.getTestName(), 0L);
if (t.getStatus().equals("passed")) {
result2.computeIfPresent(t.getTestName(), (k, v) -> v + 1);
}
}
The correct and desired output:
{Geography=1, English=2, Science=0, Mathematics=2, History=1, Physics=0}
I'm looking for a stream approach, but couldn't find a solution yet. A simple Collectors.counting will count all, regardless of status "failed/passed":
Map<String, Long> resultCounting = tests.stream()
.collect(Collectors.groupingBy(
Test::getTestName,
Collectors.counting()
));
Output:
{Geography=1, English=2, Science=2, Mathematics=3, History=1, Physics=1}
I thought about filtering beforehand, but then I will loose those subjects where all statuses are "failed".
Map<String, Long> resultFilter = tests.stream()
.filter(t -> t.getStatus().equals("passed"))
.collect(Collectors.groupingBy(
Test::getTestName,
Collectors.counting()
));
Output:
{Geography=1, English=2, Mathematics=2, History=1}
How can I group all tests by testName, but count only those where status is "passed" ?
Is it possible to wrap Collectors.counting() in some kind of condition?
You can achieve the desired result by using collector toMap(keyMapper,valueMapper,mergeFunction).
valueMapper function would either produce 1 or 0 depending on on the status.
Map<String, Integer> passCountByTestName = tests.stream()
.collect(Collectors.toMap(
Test::getTestName,
test -> test.getStatus().equals("passed") ? 1 : 0,
Integer::sum
));
passCountByTestName.forEach((k, v) -> System.out.println(k + " -> " + v));
Output:
Geography -> 1
English -> 2
Science -> 0
Mathematics -> 2
History -> 1
Physics -> 0
Sidenote: it would be better to use boolean or enum as type for the status property instead of relying on string values.

Java8 Sorting Custom Objects having Custom Object in it

I have an Employee Object, with in it Department Object. I need to sort by Employee Object Fields and then by Department Fields too. Data looks like below.
public static List getEmployeeData() {
Department account = new Department("Account", 75);
Department hr = new Department("HR", 50);
Department ops = new Department("OP", 25);
Department tech = new Department("Tech", 150);
List<Employee> employeeList = Arrays.asList(new Employee("David", 32, "Matara", account),
new Employee("Brayan", 25, "Galle", hr), new Employee("JoAnne", 45, "Negombo", ops),
new Employee("Jake", 65, "Galle", hr), new Employee("Brent", 55, "Matara", hr),
new Employee("Allice", 23, "Matara", ops), new Employee("Austin", 30, "Negombo", tech),
new Employee("Gerry", 29, "Matara", tech), new Employee("Scote", 20, "Negombo", ops),
new Employee("Branden", 32, "Matara", account), new Employee("Iflias", 31, "Galle", hr));
return employeeList;
}
I want to sort by Employee::name, Employee::Age, Department::DepartmentName how it can be sorted?
This should led to the desired result:
List<Employee> employees = getEmployeeData()
.stream()
.sorted(Comparator
.comparing(Employee::getName)
.thenComparing(Employee::getAge)
.thenComparing(e -> e.getDepartment().getName()))
.collect(Collectors.toList());

Mapping List objects using lambdas and streams

To start with, I have the following list of invoices. Each list object has a part number, a description, quantity and a price.
Invoice[] invoices = new Invoice[8];
invoices[0] = new Invoice("83","Electrische schuurmachine",7,57.98);
invoices[1] = new Invoice("24","Power zaag", 18, 99.99);
invoices[2] = new Invoice("7","Voor Hamer", 11, 21.50);
invoices[3] = new Invoice("77","Hamer", 76, 11.99);
invoices[4] = new Invoice("39","Gras maaier", 3, 79.50);
invoices[5] = new Invoice("68","Schroevendraaier", 16, 6.99);
invoices[6] = new Invoice("56","Decoupeer zaal", 21, 11.00);
invoices[7] = new Invoice("3","Moersleutel", 34, 7.50);
List<Invoice> list = Arrays.asList(invoices);
What's asked: Use lambdas and streams to map every Invoice on PartDescription and Quantity, sort by Quantity and show the results.
So what I do have now:
list.stream()
.map(Invoice::getQuantity)
.sorted()
.forEach(System.out::println);
I mapped it on quantity and sorted it on quantity as well and I get below results:
3
7
11
16
18
21
34
76
But how do I map on PartDescription as well, so that's showed in my results in front of the shown quantities too? I can't do this:
list.stream()
.map(Invoice::getPartDescription)
.map(Invoice::getQuantity)
.sorted()
.forEach(System.out::println);
You don't use map. You sort the original Stream of Invoices, and then print whatever properties you wish.
list.stream()
.sorted(Comparator.comparing(Invoice::getQuantity))
.forEach(i -> System.out.println(i.getgetQuantity() + " " + i.getPartDescription()));
EDIT: If you want to sort by quantity * price:
list.stream()
.sorted(Comparator.comparing(i -> i.getQuantity() * i.getPrice()))
.forEach(i -> System.out.println(i.getgetQuantity() * i.getPrice() + " " + i.getPartDescription()));

Converting a list of object values to group

I have the following piece of code
OrderCriteria o1 = new OrderCriteria(1, 1, 101, 201);
OrderCriteria o2 = new OrderCriteria(1, 1, 102, 202);
OrderCriteria o4 = new OrderCriteria(1, 1, 102, 201);
OrderCriteria o5 = new OrderCriteria(2, 2, 501, 601);
OrderCriteria o6 = new OrderCriteria(2, 2, 501, 602);
OrderCriteria o7 = new OrderCriteria(2, 2, 502, 601);
OrderCriteria o8 = new OrderCriteria(2, 2, 502, 602);
OrderCriteria o9 = new OrderCriteria(2, 2, 503, 603);
Where OrderCriteria looks like below:
public class OrderCriteria {
private final long orderId;
private final long orderCatalogId;
private final long procedureId;
private final long diagnosisId;
public OrderCriteria(long orderId, long orderCatalogId, long procedureId, long diagnosisId) {
this.orderId = orderId;
this.orderCatalogId = orderCatalogId;
this.procedureId = procedureId;
this.diagnosisId = diagnosisId;
}
// Getters
}
What I want is to get a list of procedures and list of diagnosis grouped by order id. So it should return:
{1, {101, 102}, {201, 202}}
{2, {501, 502, 503}, {601, 602, 603}}
which means Order with id 1 is having procedure ids 101, 102 and diagnosis ids 201, 202 etc. I tried using google guava table but could not come up with any valid solution.
First you'll need a new structure to hold the grouped data:
class OrderCriteriaGroup {
final Set<Long> procedures = new HashSet<>();
final Set<Long> diagnoses = new HashSet<>();
void add(OrderCriteria o) {
procedures.add(o.getProcedureId());
diagnoses.add(o.getDiagnosisId());
}
OrderCriteriaGroup merge(OrderCriteriaGroup g) {
procedures.addAll(g.procedures);
diagnoses.addAll(g.diagnoses);
return this;
}
}
add() and merge() are convenience methods that will help us stream and collect the data, like so:
Map<Long, OrderCriteriaGroup> grouped = criteriaList.stream()
.collect(Collectors.groupingBy(OrderCriteria::getOrderId,
Collector.of(
OrderCriteriaGroup::new,
OrderCriteriaGroup::add,
OrderCriteriaGroup::merge)));
I highly recommend you to change the output structure. The current, according to your example is probably Map<List<Set<Long>>>. I suggest you distinguish between "procedure: and "diagnosis" set of data using the following structure:
Map<Long, Map<String, Set<Long>>> map = new HashMap<>();
Now filling the data is quite easy:
for (OrderCriteria oc: list) {
if (map.containsKey(oc.getOrderId())) {
map.get(oc.getOrderId()).get("procedure").add(oc.getProcedureId());
map.get(oc.getOrderId()).get("diagnosis").add(oc.getDiagnosisId());
} else {
Map<String, Set<Long>> innerMap = new HashMap<>();
innerMap.put("procedure", new HashSet<>());
innerMap.put("diagnosis", new HashSet<>());
map.put(oc.getOrderId(), innerMap);
}
}
Output: {1={diagnosis=[201, 202], procedure=[102]}, 2={diagnosis=[601, 602, 603], procedure=[501, 502, 503]}}
If you insist on the structure you have drafted, you would have to remember that the first Set contains procedures and the second one contains the diagnosis and the maintenaince would be impractical.
Map<Long, List<Set<Long>>> map = new HashMap<>();
for (OrderCriteria oc: list) {
if (map.containsKey(oc.getOrderId())) {
map.get(oc.getOrderId()).get(0).add(oc.getProcedureId());
map.get(oc.getOrderId()).get(1).add(oc.getDiagnosisId());
} else {
List<Set<Long>> listOfSet = new ArrayList<>();
listOfSet.add(new HashSet<>());
listOfSet.add(new HashSet<>());
map.put(oc.getOrderId(), listOfSet);
}
}
Output: {1=[[102], [201, 202]], 2=[[501, 502, 503], [601, 602, 603]]}
Alternatively you might want to create a new object with 2 Set<Long> to store the data instead (another answer shows the way).

Extract Aggregator values in Batch Execution

Is there any way to programatically extract the final value of the aggregators after a Dataflow batch execution ?
Based on the DirectePipelineRunner class, I wrote the following method. It seems to work, but for dinamically created counters, it gives different values than the values shown in the console output.
PS. If it helps, I'm assuming that aggregators are based on Long values, with a sum combining function.
public static Map<String, Object> extractAllCounters(Pipeline p, PipelineResult pr)
{
AggregatorPipelineExtractor aggregatorExtractor = new AggregatorPipelineExtractor(p);
Map<String, Object> results = new HashMap<>();
for (Map.Entry<Aggregator<?, ?>, Collection<PTransform<?, ?>>> e :
aggregatorExtractor.getAggregatorSteps().entrySet()) {
Aggregator agg = e.getKey();
try {
results.put(agg.getName(), pr.getAggregatorValues(agg).getTotalValue(agg.getCombineFn()));
} catch(AggregatorRetrievalException|IllegalArgumentException aggEx) {
//System.err.println("Can't extract " + agg.getName() + ": " + aggEx.getMessage());
}
}
return results;
}
The values of aggregators should be available in the PipelineResult. For example:
CountOddsFn countOdds = new CountOddsFn();
pipeline
.apply(Create.of(1, 3, 5, 7, 2, 4, 6, 8, 10, 12, 14, 20, 42, 68, 100))
.apply(ParDo.of(countOdds));
PipelineResult result = pipeline.run();
// Here you may need to use the BlockingDataflowPipelineRunner
AggregatorValues<Integer> values =
result.getAggregatorValues(countOdds.aggregator);
Map<String, Integer> valuesAtSteps = values.getValuesAtSteps();
// Now read the values from the step...
Example DoFn that reports the aggregator:
private static class CountOddsFn extends DoFn<Integer, Void> {
Aggregator<Integer, Integer> aggregator =
createAggregator("odds", new SumIntegerFn());
#Override
public void processElement(ProcessContext c) throws Exception {
if (c.element() % 2 == 1) {
aggregator.addValue(1);
}
}
}

Categories

Resources