Performing multiple reduction operations using Streams - java

I'm attempting to Stream a list of Objects to aggregate the int min and int max based on the grouping of several Field attributes.
public class OccupancyEntry {
private final int entryID;
private final String attribute1;
private final String attribute2;
private final String attribute3;
private final int min;
private final int max;
public OccupancyEntry(int entryID, String attribute1, String attribute2, String attribute3, int min, int max) {
this.entryID = entryID;
this.attribute1 = attribute1;
this.attribute2 = attribute2;
this.attribute3 = attribute3;
this.min = min;
this.max = max;
}
}
I'd like to map the above Objects to a a list of the following:
public class OccupancyAggregated {
private final LocalDate date;
private final String attribute1;
private final String attribute2;
private final String attribute3;
private final int min;
private final int max;
public OccupancyAggregated(LocalDate date, String attribute1, String attribute2, String attribute3, int min, int max) {
this.date = date;
this.attribute1 = attribute1;
this.attribute2 = attribute2;
this.attribute3 = attribute3;
this.min = min; //these are summed values
this.max = max; //these are summed values
}
}
My attempt so far. Through the help of this answer I've been able to group the entries by a set of fields, but using this method I can no longer get the mapping and reducing functions to work.
OccupancyEntry entry1 = new OccupancyEntry(1, "1", "2", "3", 1, 10);
OccupancyEntry entry2 = new OccupancyEntry(1, "1", "2", "3", 1, 10);
OccupancyEntry entry3 = new OccupancyEntry(1, "A", "B", "C", 1, 10);
ArrayList<OccupancyEntry> occupancyEntries = new ArrayList<>();
occupancyEntries.add(entry1);
occupancyEntries.add(entry2);
occupancyEntries.add(entry3);
LocalDate date = LocalDate.now();
ArrayList<OccupancyAggregated> aggregated = new ArrayList<>();
Map<List<String>, List<OccupancyEntry>> collect = occupancyEntries.stream()
.collect(groupingBy(x -> Arrays.asList(x.getAttribute1(), x.getAttribute2(), x.getAttribute3())));
The sought after output. Where the min and max fields are reduced from from the entirety of the grouped OccupancyEntry list.
ArrayList<OccupancyAggregated> aggregated = new ArrayList<>();
My previous attempts have consisted out of creating multiple streams which reduce either the min field or the max field based on String concatenated attribute fields.
final Map<String, Integer> minOccupancy = occupancyEntries.stream()
.collect((groupingBy(OccupancyEntry::getGroupedAttributes,
mapping(OccupancyEntry::getMin,
reducing(0, Integer::sum)))));
final Map<String, Integer> maxOccupancy = occupancyEntries.stream()
.collect((groupingBy(OccupancyEntry::getGroupedAttributes,
mapping(OccupancyEntry::getMax,
reducing(0, Integer::sum)))));
Afterwards I'd for loop through one of the maps and then start creating a new list with OccupancyAggregated objects and look up the values in both maps to construct it. This seemed much too convoluted.
I've also been trying to perform the reduce operation in one pass using this answer but I'm not sure how to get it to map to the new Class type properly.
occupancyEntries.stream()
.reduce((x, y) -> new OccupancyAggregated(
date,
x.getAttribute1(),
x.getAttribute2(),
x.getAttribute3(),
x.getMin() + y.getMin(),
x.getMax() + y.getMax()
))
.orElse(new OccupancyAggregated(date, "no data", "no data", "no data", 0, 0));

This is a very common request that's messy to solve with streams, because there's no concept of an "empty" or "identity" instance of the aggregate. I prefer the semi-imperative approach using a loop and Map.merge():
Map<List<String>, OccupancyAggregated> grouped = new HashMap<>();
for (OccupancyEntry e : occupancyEntries) {
grouped.merge(Arrays.asList(e.attribute1, e.attribute2, e.attribute3),
OccupancyAggregated.from(e, date),
OccupancyAggregated::merge);
}
List<OccupancyAggregated> aggregated = new ArrayList<>(grouped.values());
Where the merge() method looks like this:
public OccupancyAggregated merge(OccupancyAggregated o) {
return new OccupancyAggregated(
date, attribute1, attribute2, attribute3, min + o.min, max + o.max);
}
Alternatively, if you can make the aggregate mutable, you can update min/max in place:
OccupancyAggregated agg = grouped.computeIfAbsent(
Arrays.asList(e.attribute1, e.attribute2, e.attribute3),
attributes -> OccupancyAggregated.from(attributes, date));
agg.add(e.min, e.max);
I left out some of the new method implementations for the sake of brevity. Let me know if they need more elaboration.

You could do this:
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class AggregateToGetMinMax {
public static void main(String[] args) {
List<OccupancyEntry> list = setup();
List<OccupancyAggregated> result = aggregate(list, LocalDate.now());
System.out.println("\nresult: " + result);
}
public static List<OccupancyEntry> setup() {
OccupancyEntry[] entries = {
new OccupancyEntry(1, "1", "2", "3", 1, 10),
new OccupancyEntry(1, "1", "2", "3", 1, 10),
new OccupancyEntry(1, "A", "B", "C", 1, 10)
};
return List.of(entries);
}
public static List<OccupancyAggregated> aggregate(List<OccupancyEntry> occupancyEntries, LocalDate date) {
class MinMax {
int max = 0;
int min = 0;
public MinMax() {
}
public MinMax(OccupancyEntry oe) {
this.max = oe.max;
this.min = oe.min;
}
public MinMax add(MinMax mm) {
this.max += mm.max;
this.min += mm.min;
return this;
}
public String toString() {
return "min: " + min + "; max: " + max;
}
}
Map<String, List<OccupancyEntry>> groupedEntries = occupancyEntries.stream()
.collect(Collectors.groupingBy(OccupancyEntry::getGroupedAttributes));
System.out.println("grouped entries: ");
groupedEntries.entrySet().stream()
.forEach(entry -> System.out.println(entry));
List<OccupancyAggregated> result = groupedEntries.values().stream()
.map(list -> {
MinMax mm = list.stream()
.map(MinMax::new)
.reduce(
new MinMax(),
(MinMax acc, MinMax item) -> acc.add(item));
// It's okay to get(0) because all entries with have values with at least one item
// in the list.
OccupancyAggregated aggregated = new OccupancyAggregated(date, list.get(0), mm.min, mm.max);
return List.of(aggregated);
})
.collect(Collectors.toList()).stream()
.flatMap(List::stream)
.collect(Collectors.toList());
return result;
}
public static class OccupancyEntry {
private final int entryID;
private final String attribute1, attribute2, attribute3;
public final int min, max;
public OccupancyEntry(int entryID, String attribute1, String attribute2, String attribute3, int min, int max) {
this.entryID = entryID;
this.attribute1 = attribute1;
this.attribute2 = attribute2;
this.attribute3 = attribute3;
this.min = min;
this.max = max;
}
public String getGroupedAttributes() {
return List.of(attribute1, attribute2, attribute3).stream()
.collect(Collectors.joining("|"));
}
#Override
public String toString() {
return "OccupancyEntry [" + entryID + ", " + attribute1 + ", " + attribute2
+ ", " + attribute3 + ", " + min + ", " + max + "]";
}
}
public static class OccupancyAggregated {
final LocalDate date;
String attribute1, attribute2, attribute3;
private int min, max;
public OccupancyAggregated(LocalDate date, OccupancyEntry oe, int min, int max) {
this.date = date;
this.attribute1 = oe.attribute1;
this.attribute2 = oe.attribute2;
this.attribute3 = oe.attribute3;
this.min = min; // these are summed values
this.max = max; // these are summed values
}
#Override
public String toString() {
return "OccupancyAggregated: ( " +
attribute1 + ", " +
attribute2 + ", " +
attribute3 + ", " +
min + ", " +
max +
" )";
}
}
}
Note that I have separated the grouping and reducing so that one can more clearly focus on the reducing part of the work. Otherwise, building the entire operation straight through might become a bit tedious to mentally parse the important operation.
Output:
grouped entries:
A|B|C=[OccupancyEntry [1, A, B, C, 1, 10]]
1|2|3=[OccupancyEntry [1, 1, 2, 3, 1, 10], OccupancyEntry [1, 1, 2, 3, 1, 10]]
result: [OccupancyAggregated: ( A, B, C, 1, 10 ), OccupancyAggregated: ( 1, 2, 3, 2, 20 )]
The more succinct version of accumulate() looks like this:
public static List<OccupancyAggregated> aggregate(List<OccupancyEntry> occupancyEntries, LocalDate date) {
class MinMax {
int max = 0;
int min = 0;
public MinMax() {
}
public MinMax(OccupancyEntry oe) {
this.max = oe.max;
this.min = oe.min;
}
public MinMax add(MinMax mm) {
this.max += mm.max;
this.min += mm.min;
return this;
}
public String toString() {
return "min: " + min + "; max: " + max;
}
}
List<OccupancyAggregated> result = occupancyEntries.stream()
.collect(Collectors.groupingBy(OccupancyEntry::getGroupedAttributes))
.values().stream()
.map(list -> {
MinMax mm = list.stream()
.map(MinMax::new)
.reduce(
new MinMax(),
(MinMax acc, MinMax item) -> acc.add(item));
// It's okay to get(0) because all entries with have values with at least one item
// in the list.
OccupancyAggregated aggregated = new OccupancyAggregated(date, list.get(0), mm.min, mm.max);
return List.of(aggregated);
})
.collect(Collectors.toList()).stream()
.flatMap(List::stream)
.collect(Collectors.toList());
return result;
}

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}
}

Method to count property of an Object linked to another property

Lets say I have a file containing multiple planes and information about the planes structured as such :
type,max speed,name,color
commercial plane,750,boeing_777,white
private jet,800,cynthia,blue
propeller plane,200,venus,yellow
commercial plane,640,boeing_737,white
commercial plane,550,airbus,blue
propeller planes,150,tatoo,yellow
private jet,670,VLJ, white
From this I created the Planes class
public Planes{
static int counter = 1;
String type;
int maxSpeed;
String name;
String color;
int id;
public Planes(List<String> element) {
this.type = element.get(0)
this.maxSpeed = element.get(1)
this.name = element.get(2)
this.color = element.get(3)
this.id = counter;
counter++;
}
I made several methods and now my objects of type Planes are in an ArrayList
My goal is to output a string which prints the type and the number of different colors the plane type has. So the output would be :
commercial plane,2 //2 because it has both white and blue NOT 3
private jet,2
propeller plane,1
I tried the following :
for (int i = 0; i < arrayListPlanes.size(); ++i) {
int it = 0;
newList.add(arrayListPlanes.get(i).getColor());
if(newList.contains(arrayListPlanes.get(i).getColor())) {
it = it + 1;
}
String line += arrayListPlanes.get(i).type() + ", " +
Collections.frequency(newList, arrayListPlanes.get(i).getColor()) +
"\n";
}
The actual list is way longer and contains multiple other types of planes so I can't make an object for each types of plane.
I apologize for the badly written title
Grouping planes by type with consequent collecting set of colors and re-mapping to the size of color set does the trick:
static void countPlaneColorsByType(List<Plane> planes) {
planes.stream()
.collect(Collectors.groupingBy(p -> p.type, LinkedHashMap::new, Collectors.mapping(p -> p.color, Collectors.toSet())))
.entrySet().stream()
.map(e -> new StringBuilder(e.getKey()).append(": ").append(e.getValue().size()))
.forEach(System.out::println);
}
However, there several points to be fixed in Planes class:
rename to Plane as it describes a single plane
fix compilation issues
public static class Plane{
static int counter = 1;
String type;
int maxSpeed;
String name;
String color;
int id;
public Plane(List<String> element) {
this.type = element.get(0);
this.maxSpeed = Integer.parseInt(element.get(1));
this.name = element.get(2);
this.color = element.get(3);
this.id = counter;
counter++;
}
}
Test
public static void main(String...args) {
List<Plane> planes = Arrays.asList(
new Plane(Arrays.asList("commercial plane", "750", "boeing_777", "white")),
new Plane(Arrays.asList("private jet", "800", "cynthia", "blue")),
new Plane(Arrays.asList("propeller plane", "200", "venus", "yellow")),
new Plane(Arrays.asList("commercial plane", "640", "boeing_737", "white")),
new Plane(Arrays.asList("commercial plane", "550", "airbus", "blue")),
new Plane(Arrays.asList("propeller plane", "150", "tatoo", "yellow")),
new Plane(Arrays.asList("private jet", "670", "VLJ", " white"))
);
countPlaneColorsByType(planes);
}
Output
commercial plane: 2
private jet: 2
propeller plane: 1
Here is simple solution based on the Stream API:
// Group all planes by type
// key is plane type, value is list of planes
Map<String, Set<Planes>> plainsByType = planes.stream()
.collect(Collectors.groupingBy(p -> p.type));
plainsByType.entrySet().stream()
.map(e -> String.format("%s, %d", e.getKey(), countOfUniqueColors(e.getValue())))
.forEach(System.out::println);
long countOfUniqueColors(Set<Planes> planes) {
return planes.stream().map(p -> p.color).distinct().count();
}
You can use HashMap here for newList like below:
Map<Color,Interger> newList = new HashMap();
int it = 0;
if( newList.containsKey(color) )
{
it = newList.get(color).value();
}
it++;
newList.put( color, it );

How to add/append new rows from Array1[][] that do not exist in Array2[][] to Array2[][] in Java?

I came across a problem where one needs to check for rows in Array1 that are not in Array2 and append it at the end of Array2 in Java. The rows that are common with regard to the first column i.e. name can be skipped. In the below example, the rows in firstarray with "Nick" and "Bruce" should be appended at the end of secondarray.
I have edited the arrays again slightly to get more clarity.
String firstarray[][] = {
{"John","04-Feb-1982","Economics","Leeds"},
{"Mathias","08-Jan-1985","Arts","London"},
{"Nick","09-06-1974","History","Johanesburg"},
{"Bruce","13-08-1975","Philosophy","Seattle"}};
String secondarray[][] = {
{"Adam","01-Dec-1980","Commerce","New York"},
{"John","04-Feb-1982","Economics","Leeds"},
{"Mathias","08-Jan-1985","Arts","London"}};
The solution should be like:
secondarray[][]:
{"Adam","01-Dec-1980","Commerce","New York"},
{"John","04-Feb-1982","Economics","Leeds"},
{"Mathias","08-Jan-1985","Arts","London"},
{"Nick","09-06-1974","History","Johanesburg"},
{"Bruce","13-08-1975","Philosophy","Seattle"}}
Collect the names of the second array to a set, iterate over your first array and filter out those elements which are in the set and collect the result in a third array (or any other collection). Append this collection to your second array.
public static void main(String[] args){
String firstarray[][] = {
{"Adam","01-Dec-1980","Commerce","Kansas"},
{"John","04-Feb-1982","Economics","Leeds"},
{"Mathias","08-Jan-1985","Arts","London"},
{"Nick","09-06-1974","History","Johanesburg"},
{"Bruce","13-08-1975","Philosophy","Seattle"}};
String secondarray[][] = {
{"Adam","01-Dec-1980","Commerce","Kansas"},
{"John","04-Feb-1982","Economics","Leeds"},
{"Mathias","08-Jan-1985","Arts","London"},
{"Sujay Muramalla","08-Jan-1985","Arts","London"}};
//collect names of second array to set
Set<String> secondSet = Arrays.stream(secondarray).map(e -> e[0]).collect(Collectors.toSet());
//stream over your first array and keep only those which are not in the above set
String[][] third = Arrays.stream(firstarray)
.filter(e -> !secondSet.contains(e[0]))
.toArray(String[][]::new);
//stream over second and delta (third array) and collect to a result array
String[][] result = Stream.concat(Arrays.stream(secondarray), Arrays.stream(third))
.toArray(String[][]::new);
//output
Arrays.stream(result).forEach(e ->{
System.out.println(Arrays.toString(e));
});
}
You should not be using arrays for this. Much better to use libraries already doing the job.
Even if the data you receive is already that way, you can converted first to a Map<String, Person> it will be more efficient, and readable code.
With arrays or many solutions not using some hashing system, you end up with exponential complexity O(n2), so not efficient.
Convert at least secondarray
Map<String, Person> secondMap = new HashMap();
for(String [] row : secondarray){
secondMap.put(row[0], new Person(row[0], row[1], row[2], row[3]));
}
Then put in the map if not already there
for(String[] row : firstarray){
if(!secondMap.containsKey(row[0])){
secondMap.put(row[0], new Person(row[0], row[1], row[2], row[3]));
}
}
Where Person class could be simply defined as
private static class Person{
public Person(String name, String birth, String area, String city){
this.name = name;
this.birth = birth;
this.area = area;
this.city = city;
}
String name;
String birth;
String area;
String city;
}
if you want check more than the first element of the array you can use nopens solution and replace e[0] with Arrays.toString(e).
A cleaner way if this is possible for you, is to use a list with a object and use a id for checking equals or override the hashcode function of the customer object.
You can also check for name and birth like that:
class Customer {
private String name;
private String birth;
private String type;
private String location;
public Customer(String name, String birth, String type, String location) {
this.name = name;
this.birth = birth;
this.type = type;
this.location = location;
}
#Override
public String toString() {
return "Customer [name=" + name + ", birth=" + birth + ", type=" + type + ", location=" + location + "]";
}
}
List<Customer> firstList = new ArrayList<Customer>();
firstList.add(new Customer("Adam", "01-Dec-1980", "Commerce", "Kansas"));
firstList.add(new Customer("John", "04-Feb-1982", "Economics", "Leeds"));
firstList.add(new Customer("Mathias", "08-Jan-1985", "Arts", "London"));
firstList.add(new Customer("Nick", "09-06-1974", "History", "Johanesburg"));
firstList.add(new Customer("Bruce", "13-08-1975", "Philosophy", "Seattle"));
List<Customer> secondList = new ArrayList<Customer>();
secondList.add(new Customer("Adam", "01-Dec-1980", "Commerce", "Kansas"));
secondList.add(new Customer("John", "04-Feb-1982", "Economics", "Leeds"));
secondList.add(new Customer("Mathias", "08-Jan-1985", "Arts", "London"));
for (Customer customer : firstList) {
if (containsNameAndBirth(secondList, customer) == false) {
secondList.add(customer);
}
}
for (Customer customer : secondList) {
System.out.println(customer);
}
}
public static boolean containsNameAndBirth(final List<Customer> list, final Customer customer) {
return list.stream().filter(o -> o.name.equals(customer.name) && o.birth.equals(customer.birth)).findFirst()
.isPresent();
}
EDIT 1 - Using Custom Class
I suggest you to always use List over Array.
import java.time.*;
import java.util.*;
import java.util.stream.Collectors;
public class Main {
static class Person {
public String name;
public String birthDate;
public String field;
public String city;
public static Person fromArray(String[] data) {
Person p = new Person();
if (data.length == 4) {
p.name = data[0];
p.birthDate = data[1];
p.field = data[2];
p.city = data[3];
} else {
// Handle me
}
return p;
}
#Override
public String toString() {
return new StringBuilder("[").append(name)
.append(",").append(birthDate)
.append("] learns ").append(field)
.append(" at ").append(city).toString();
}
}
public static void main(String[] args) {
String firstArray[][] = {
{"Adam","01-Dec-1980","Commerce","Kansas"},
{"John","04-Feb-1982","Economics","Leeds"},
{"Mathias","08-Jan-1985","Arts","London"},
{"Nick","09-06-1974","History","Johanesburg"},
{"Bruce","13-08-1975","Philosophy","Seattle"}};
String secondArray[][] = {
{"Adam","01-Dec-1980","Commerce","Kansas"},
{"John","04-Feb-1982","Economics","Leeds"},
{"Mathias","08-Jan-1985","Arts","London"}};
List<Person> finalList = getFinalList(firstArray, secondArray);
// Display
System.out.println(finalList);
}
public static List<Person> getFinalList(String[][] arr1, String[][] arr2) {
// First cast to Lists of persons
List<Person> firstList = Arrays.asList(arr1).stream().map(Person::fromArray).collect(Collectors.toList());
List<Person> secondList = Arrays.asList(arr2).stream().map(Person::fromArray).collect(Collectors.toList());
// Get names in secondList
Set<String> existingNames = secondList.stream().map(p -> p.name).collect(Collectors.toSet());
System.out.println("Names: "+ existingNames);
firstList.forEach(person -> {
if (! existingNames.contains(person.name)) {
secondList.add(person);
}
});
return secondList;
}
}
I upvoted nopens solutions cause it is nice one
Here another that uses maps and makes use of a logic of skipping common keys using removeAll on the keySet of the map, which was a functional method existing befor Java turned "more" functional
static public <T> Map<T,T[]> arrayToMap(T[][] array, int i) {
return Arrays.stream(array).collect(Collectors.toMap(e -> e[i], e -> e));
}
public static void main(String[] args){
String firstarray[][] = {
{"Adam","01-Dec-1980","Commerce","Kansas"},
{"John","04-Feb-1982","Economics","Leeds"},
{"Mathias","08-Jan-1985","Arts","London"},
{"Nick","09-06-1974","History","Johanesburg"},
{"Bruce","13-08-1975","Philosophy","Seattle"}};
String secondarray[][] = {
{"Adam","01-Dec-1980","Commerce","Kansas"},
{"John","04-Feb-1982","Economics","Leeds"},
{"Mathias","08-Jan-1985","Arts","London"},
{"Sujay Muramalla","08-Jan-1985","Arts","London"}};
Map<String,String[]> firstMap = arrayToMap(firstarray, 0);
Map<String,String[]> secondMap = arrayToMap(secondarray, 0);
secondMap.keySet().removeAll(firstMap.keySet());
firstMap.putAll(secondMap);
String[][] result = firstMap.values().stream().toArray(String[][]::new);
//output
Arrays.stream(result).forEach(e ->{
System.out.println(Arrays.toString(e));
});
}
sidenote: in arrayToMap you can choose which column you use as key.
And the logic could be even reduced to this 3 lines:
Map<String,String[]> firstMap = arrayToMap(firstarray, 0);
firstMap.putAll(arrayToMap(secondarray, 0));
String[][] result = firstMap.values().stream().toArray(String[][]::new);
since inserting a value with the same key overwrites the existing one and you get the same if the values are the same in case of equal keys.
A simple an efficient way to do it (if you don't care about ordering) is the following:
Time complexity: O(nlog(n))
Space complexity: O(n+m)
import java.util.Arrays;
public class Main {
public static void main(String ... args) {
String firstarray[][] = {
{"John","04-Feb-1982","Economics","Leeds"},
{"Mathias","08-Jan-1985","Arts","London"},
{"Nick","09-06-1974","History","Johanesburg"},
{"Bruce","13-08-1975","Philosophy","Seattle"}};
String secondarray[][] = {
{"Adam","01-Dec-1980","Commerce","Kansas"},
{"John","04-Feb-1982","Economics","Leeds"},
{"Mathias","08-Jan-1985","Arts","London"}};
String result [][] = new String[firstarray.length
+ secondarray.length][firstarray[0].length];
// sort firstarray
java.util.Arrays.sort(firstarray, new java.util.Comparator<String[]>() {
public int compare(String [] a, String[] b) {
return a[0].compareTo(b[0]);
}
});
//sort secondarray
java.util.Arrays.sort(secondarray, new java.util.Comparator<String[]>() {
public int compare(String [] a, String[] b) {
return a[0].compareTo(b[0]);
}
});
int i = 0, j=0, k=0, cmp ;
for ( ;i < secondarray.length && j< firstarray.length;) {
cmp = firstarray[i][0].compareTo(secondarray[j][0]);
if(cmp ==0) {
System.arraycopy(firstarray[i], 0, result[k++], 0, 4);
i++; j++;
}else if( cmp <0){
System.arraycopy(firstarray[i], 0, result[k++], 0, 4);
i++;
} else {
System.arraycopy(secondarray[j], 0, result[k++], 0, 4);
j++;
}
}
// copy the remaining if any from firstarray to the result
for (; i < firstarray.length; i++) {
System.arraycopy(firstarray[i], 0, result[k++], 0, 4);
}
// copy the remaining if any from secondarray to the result
for (; j < secondarray.length; j++) {
System.arraycopy(secondarray[j], 0, result[k++], 0, 4);
}
//resize it
secondarray = Arrays.copyOf(result, k);
// just print the secondarray
for (int x = 0; x < secondarray.length; x++) {
for (int y = 0; y < 4; y++) {
System.out.print(secondarray[x][y] + ",");
}
System.out.println("");
}
}
}

Java Lambda - Trying to sum by 2 groups

I have a object list, which I need to group by 2 different atributes and then sum the values of an attribute, the structure of my object is something like this:
private Long id1;
private Long id2;
private Double amountReserved;
private Double amountRequired;
//... more atributes and getters and setters
So, I then have a list, for example:
List<MyList> list = Arrays.asList(
list(1, A, 50, 200)
list(1, A, 50, 200)
list(1, B, 0, 100)
list(2, A, 10, 15)
list(2, A, 5, 15)
list(3, A, 0, 25));
What I am trying to achieve is a new list with the below structure:
list(1, A, 100, 100)
list(1, B, 0, 100)
list(2, A, 15, 0)
list(3, A, 0, 25)
Elucidating what is the requisite I am trying to achieve:
Group objects by id1 and id2
sum the amountReservedof the grouped object
subtract amountRequired from the summed amountReserved
What I have so far:
This one got me the groupings as I wanted
Map<Long, Map<String, List<MyList>>> map = null;
map = lista.stream().collect(Collectors.groupingBy(PreSeparacaoDto::getCodigoPedido,
Collectors.groupingBy(PreSeparacaoDto::getCodigoProduto)));
This one sums by group id1, but I am struggling to add the second groupingby on it, as I get syntax errors:
lista.stream()
.collect(Collectors.groupingBy(PreSeparacaoDto::getCodigoPedido,
Collectors.summingDouble(PreSeparacaoDto::getProdutoQuantidadeSeparada)))
.forEach((codigoPedido, ProdutoQuantidadeSeparada) -> System.out.println( codigoPedido + ": " + ProdutoQuantidadeSeparada ));
My problem is that I failed to get those together ( as per requisite 2) and was not even close to achieve my requisite 3.
I tried to use reduction, as explained here , but honestly, I was not able to replicate it with a single grouping, the reducing is returning an error informing that my parameters don't meet the reducing parameters. I looked for some other options here on stackoverflow and other websites, but without success.
Can someone help me out and poiting where I am failing to combine the reduction with my group, or if that is the correct path I should be following.
I think an easy way is to use Collectors.grouping : you tell it how to group and what to collect.
Here's an example, computing only the sum of AmountReserved :
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class GroupedSums {
static class MyList {
Long id1;
char id2;
Double amountReserved;
Double amountRequired;
public Long getId1() {
return id1;
}
public char getId2() {
return id2;
}
public Double getAmountReserved() {
return amountReserved;
}
public Double getAmountRequired() {
return amountRequired;
}
public MyList(Long id1, char id2, Double amountReserved, Double amountRequired) {
super();
this.id1 = id1;
this.id2 = id2;
this.amountReserved = amountReserved;
this.amountRequired = amountRequired;
}
Key key() {
return new Key(id1, id2);
}
}
private static MyList list(Long id1, char id2, Double amountReserved, Double amountRequired) {
return new MyList(id1, id2, amountReserved, amountRequired);
}
public GroupedSums() {
}
private static class Key {
Long id1;
char id2;
public Long getId1() {
return id1;
}
public char getId2() {
return id2;
}
public Key(Long id1, char id2) {
super();
this.id1 = id1;
this.id2 = id2;
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id1 == null) ? 0 : id1.hashCode());
result = prime * result + id2;
return result;
}
#Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Key other = (Key) obj;
if (id1 == null) {
if (other.id1 != null)
return false;
} else if (!id1.equals(other.id1))
return false;
if (id2 != other.id2)
return false;
return true;
}
#Override
public String toString() {
return "[id1=" + id1 + ", id2=" + id2 + "]";
}
}
public static void main(String[] args) {
List<MyList> list = Arrays.asList(
list(1L, 'A', 50d, 200d),
list(1L, 'A', 50d, 200d),
list(1L, 'B', 0d, 100d),
list(2L, 'A', 10d, 15d),
list(2L, 'A', 5d, 15d),
list(3L, 'A', 0d, 25d));
list.stream().collect(Collectors.groupingBy(MyList::key, Collectors.summingDouble(MyList::getAmountReserved)))
.forEach((k,v)->System.out.println("" + k + " :" + v));
}
}
HTH!
You might just be looking for simply Collectors.toMap as :
List<MyList> output = new ArrayList<>(lista.stream()
.collect(Collectors.toMap(a -> a.getId1() + "-" + a.getId2(), a -> a, (myList1, myList2) -> {
myList1.amountReserved = myList1.amountReserved + myList2.amountReserved;
myList1.amountRequired = myList1.amountRequired - myList1.amountReserved;
return myList1;
})).values());
You can stream over the input list twice.
First time, you group by id1, id2 and compute the sum of amount reserved. Second time, you can stream the list again, group it (by id1 and id2) by making use of the above result to find the difference.
Map<Long, Map<Long, Double>> amountReservedGroup = list.stream()
.collect(Collectors.groupingBy(MyList::getId1, Collectors.groupingBy(MyList::getId2,
Collectors.summingDouble(MyList::getAmountReserved))));
Map<Long, Map<Long, List<MyList>>> finalResult = list.stream()
.collect(Collectors.groupingBy(MyList::getId1, Collectors.groupingBy(MyList::getId2,
Collectors.mapping(o -> new MyList(o.getId1(), o.getId2(),
amountReservedGroup.get(o.getId1()).get(o.getId2()),
o.getAmountRequired() - amountReservedGroup.get(o.getId1()).get(o.getId2())),
Collectors.toList()))));
Note:
This does not handle the case when the result of the subtraction is negative!!
As pointed out by nullpointer# in the comments, will the value of amountRequired be the same for a given id1 and id2?
you can do order by id1 and then order id2 (to make sure elements of the same list and sublist are after each other) and then you do nested foreach (before you iterate the sublist, you init result_reserved_amount to 0 and result_required_amount to the initial value)
then you do if same ids (if id1= previous_id1 and id2 = previous_id2) do result_reserved_amount+= current_reserved_amount and result_required_amount -= current_reserved_amount, otherwise update previous_id1, previous_id2, result_reserved_amount, result_required_amount

Converting the same String value into an Object

I have an object share:
Share tea = new Share("TEA", "Common", 0, 100);
ArrayList<Share> shares = new ArrayList<Share>();
shares.add(tea);
What I'd like to do is, reading parameters from the keybord, convert directly the "tea" string into a share tea object :
Trade trade = new Trade( tea, Boolean.parseBoolean(buyOrSell), Integer.parseInt(quantity), Double.parseDouble(tradePrice));
What should I put instead of tea because my constructor is waiting a Share and not a String. The user is entering a string and I don't need ton create a new instance, I have to use the Share object "tea" that already exists.
The Share.java class :
public class Share {
private String shareSymbol = "";
private String type = "Common";
private double lastDividend = 0;
private double fixedDividend = 0;
private int parValue = 0;
// Calucul values
public String getShareSymbol() {
return shareSymbol;
}
public String getType() {
return type;
}
public double getLastDividend() {
return lastDividend;
}
public double getFixedDividend() {
return fixedDividend;
}
public int getParValue() {
return parValue;
}
// Constructor without Fixed Dividend
public Share(String shareSymbol, String type, int lastDividend, int parValue) {
this.shareSymbol = shareSymbol;
this.type = type;
this.lastDividend = lastDividend;
this.parValue = parValue;
}
// Constructor with Fixed Dividend
public Share(String shareSymbol, String type, int lastDividend,
int fixedDividend, int parValue) {
this(shareSymbol, type, lastDividend, parValue);
this.fixedDividend = fixedDividend / 100.0;
}
public String toString(){
String result="";
result += shareSymbol + " "+type + " " + lastDividend + " " + fixedDividend + " " + parValue + "\n";
return result;
}
}
The Trade.java class :
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
// The class trade allows the stock, the quantity and the price to be intialised
public class Trade {
Share share;
private int quantity;
double price;
private double dividendYield;
private double pERatio;
private boolean buyOrSell;
private Date tradeDate;
public Trade(Share share, boolean buyOrSell, int quantity, double price) {
this.share = share;
this.buyOrSell = buyOrSell;
this.quantity = quantity;
this.price = price;
tradeDate = Calendar.getInstance().getTime();
}
public String toString() {
String result = "";
result += "stock symbol : " + share.getShareSymbol() + " \n";
result += "Buy or Sell : " + buyOrSell + " \n";
result += "quantity :" + quantity + " \n";
result += "price : " + price + " \n";
result += "Dividend Yield : " + dividendYield + " \n";
result += "P/E Ratio : " + pERatio + " \n";
result += "tradeDate : " + tradeDate + " \n\n";
return result;
}
public Share getShare() {
return share;
}
public int getQuantity() {
return quantity;
}
public double getPrice() {
return price;
}
public double getDividendYield() {
return dividendYield;
}
public double getpERatio() {
return pERatio;
}
public Date getTradeDate() {
return tradeDate;
}
public double calcDividendYield() {
if ("Common".equalsIgnoreCase(share.getType())) {
dividendYield = share.getLastDividend() / price;
} else if ("Preferred".equalsIgnoreCase(share.getType())) {
dividendYield = share.getFixedDividend() * share.getParValue()
/ price;
}
return dividendYield;
}
public double calcPERatio() {
if (dividendYield > 0)
pERatio = price / dividendYield;
return pERatio;
}
}
Your constructor is waiting a share. So give it:
/* Read parameters from keyboard, stock in s1, s2, i1 and i2 variables */
Trade trade = new Trade( new Share(s1,s2,i1,i2), Boolean.parseBoolean(buyOrSell), Integer.parseInt(quantity), Double.parseDouble(tradePrice));
Otherwise you can implement a method (in Share class) that do it for you
public static Share stringToShare(String s1, String s2, int t1, int t2){
return new Share(s1, s2, t1, t2);
}
And then:
Trade trade = new Trade( Share.stringToShare(...), Integer.parseInt(quantity), Double.parseDouble(tradePrice));
EDIT: better use real variable names that means something, not s1, s2, etc.
I've got around of this, by creating a new constructor :
Trade trade = new Trade(shares, stockSymbol, Boolean.parseBoolean(buyOrSell),
Integer.parseInt(quantity), Double.parseDouble(tradePrice));
This is then the new constructor :
public Trade(ArrayList<Share> shares, String stockSymbol,
boolean buyOrSell, int quantity, double price) {
Iterator<Share> iter = shares.iterator();
int index = -1;
while (iter.hasNext()) {
Share share = iter.next();
index++;
if (stockSymbol.equalsIgnoreCase((share.getShareSymbol()))) {
this.share = shares.get(index);
this.buyOrSell = buyOrSell;
this.quantity = quantity;
this.price = price;
tradeDate = Calendar.getInstance().getTime();
break;
}
}
}
It's a bit complacated, because I had also to create a new method to get the Share for the given String(having the same name, for instance). But that does What i needed.
It would be cleaner though to make it with Java Reflection, without having to create a new constructor. So if any one have an idea with a code more clever, I would be happy!

Categories

Resources