I hava an order list that contains different id and date.
Now, I need to combine order amount of same id and date.
The core of this problem maybe is how to create a Collector that counld combine Orders with same id and date, and the result is an Order list rather than a Map<Integer, List>.
Is there any other ways to simplify this flow?
public class Order {
private Integer id;
private LocalDate date;
private double amount;
public void accept(Order other) {
setId(other.getId());
setDate(other.getDate());
setAmount(getAmount() + other.getAmount());
}
public Order combine(Order other) {
setId(other.getId());
setDate(other.getDate());
setAmount(getAmount() + other.getAmount());
return this;
}
}
List<Order> result = new ArrayList<>();
List<Order> orders = mockData();
Map<Integer, List<Order>> collect = list.stream()
.collect(groupingBy(Order::getId));
collect.forEach((id, orders) -> {
Map<LocalDate, Order> resultMap = orders.stream()
.collect(groupingBy(Order::getDate, mapping(order -> order, Collector.of(Order::new, Order::accept, Order::combine))));
result.addAll(resultMap.values());
});
I would first make a new record so that you can work with id and date at once.
record IdAndDate(Integer id, LocalDate date) {}
To not get a List<Order> as the map's value type, use the toMap collector. You can then specify a "merge function". That is where you specify Order::combine.
var result = new ArrayList<>(
orders.stream().collect(
Collectors.toMap(
x -> new IdAndDate(x.getId(), x.getDate()), // key mapper
Function.identity(), // value mapper (no change)
Order::combine // merge function
)
).values()
);
Note that Order.combine changes the instance on which it is called, which means that some of the orders in the original list would be changed by this operation. This is also true for your original code, so I'll assume this fact doesn't matter in your case. Just in case you don't want that to happen, you should make combine return a new instance of Order, instead of this.
You could also nest the groupingBy calls and do it like:
List<Order> orders = mockData();
List<Order> result = orders.stream() // Stream<Order>
.collect(Collectors.groupingBy(Order::getId,
Collectors.groupingBy(Order::getDate))) //Map<Integer, Map<LocalDate,List<Order>>>
.values() //Collection<Map<LocalDate,List<Order>>>
.stream() ////Stream<Map<LocalDate,List<Order>>>
.flatMap(m -> m.values().stream()) //Stream<List<Order>>
.map(list -> list.stream().reduce(Order::combine).get()) // Stream<Order>
.collect(Collectors.toList());
Related
I'm sorting the Employee Data by grouping the employee number and employee salary. Excepted output I got, but I need to store this details in a fixedlength file. So I need to print by below format.
public class SortData {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("102", "300", "100", "200"),
new Employee("101", "200", "80", "120"),
new Employee("102", "100", "50", "50"),
new Employee("103", "300", "100", "200"),
new Employee("101", "200", "80", "120"),
new Employee("101", "100", "50", "50"));
Map<String, Map<String, List<Employee>>> map = employees.stream()
.collect(Collectors.groupingBy(Employee::getEmpNo,
Collectors.groupingBy(Employee::getEmpSal)));
System.out.println(map);
}
}
class Employee {
private String empNo;
private String empSal;
private String empHra;
private String empPf;
// getter
// setter
}
The above code's output is as below.
{101={100=[Employee(empNo=101, empSal=100, empHra=50, empPf=50)], 200=[Employee(empNo=101,
empSal=200, empHra=80, empPf=120), Employee(empNo=101, empSal=200, empHra=80, empPf=120)]},
102={100=[Employee(empNo=102, empSal=100, empHra=50, empPf=50)], 300=[Employee(empNo=102,
empSal=300, empHra=100, empPf=200)]},
103={300=[Employee(empNo=103, empSal=300, empHra=100, empPf=200)]}}
I need to print this out put in a fixedlength file. So I need the output as below.
1011005050
10120080120
10120080120
1021005050
102300100200
103300100200
Can anyone please help me on this.
First, the maps in current implementation are NOT sorted.
Second, it may be redundant to sort the maps. It appears that in order to print the input data, just the input list needs to be sorted by empNo and empSal:
Comparator<Employee> sortByNoAndSal = Comparator
.comparing(Employee::getEmpNo)
.thenComparing(Employee::getEmpSal);
employees.sort(sortByNoAndSal);
Last, to print/output an instance of Employee specific method should be implemented (by overriding Employee::toString, or creating specific method). Aside note - required output seems to be missing a delimiter between fields of Employee.
// class PrintData
public static void printEmployee(Employee emp) {
String delimiter = ""; // fix if necessary
System.out.println(String.join(delimiter, emp.getNo(), emp.getSal(), emp.getHra(), emp.getPf()));
}
// invoke this method
employees.forEach(PrintData::printEmployee);
You can use:
List<String> result = employees.stream()
.collect(Collectors.groupingBy(Employee::getEmpNo, Collectors.groupingBy(Employee::getEmpSal)))
.values()
.stream()
.flatMap(e -> buildOutput(e).stream())
.collect(Collectors.toList());
private List<String> buildOutput(Map<String, List<Employee>> entry) {
return entry.entrySet().stream()
.flatMap(e -> e.getValue().stream().map(this::buildOutputForEmployee))
.collect(Collectors.toList());
}
private String buildOutputForEmployee(Employee em) {
return String.format("%s%s%s%s", em.getEmpNo(), em.getEmpSal(), em.getEmpHra(), em.getEmpPf());
}
to print the result:
result.forEach(System.out::println);
Outputs:
1011005050
10120080120
10120080120
1021005050
102300100200
103300100200
From your comments, you would like to sort by employee number and then by the employee salary.
Currently, you aren't sorting, but relying on the default ordering of the returned map type from the groupingBy operation.
1. Use a TreeMap when grouping
You can specify the type of the map to be used for groupingBy by passing a Supplier argument. Unfortunately, you also have to pass the collector in that case (toList()).
Map<String, Map<String, List<Employee>>> map = employees.stream()
.collect(Collectors.groupingBy(Employee::getEmpNo,
() -> new TreeMap<>(Comparator.comparingInt(Integer::valueOf)),
Collectors.groupingBy(Employee::getEmpSal,
() -> new TreeMap<>(Comparator.comparingInt(Integer::valueOf)),
Collectors.toList())));
I'm using a comparator to sort by the integer value of the employee number and salary in the TreeMap1.
You can print it as (in your case, you will be writing each record into a file).
map.entrySet()
.stream()
.flatMap(entry -> entry.getValue().entrySet().stream())
.flatMap(innerEntry -> innerEntry.getValue().stream())
.forEach(emp -> System.out.println(buildRecord(emp)));
where buildRecord just concatenates the field of the Employee class.
private static String buildRecord(Employee employee) {
return new StringBuilder()
.append(employee.getEmpNo())
.append(employee.getEmpSal())
.append(employee.getEmpHra())
.append(employee.getEmpPf())
.toString();
}
2. Sort the list
But there is no need to group and operate on a complicated data structure. You can simply sort it using a comparator.
employees.sort(Comparator.<Employee>comparingInt(emp -> Integer.parseInt(emp.getEmpNo()))
.thenComparingInt(emp -> Integer.parseInt(emp.getEmpSal())));
This sorts the employees list by employee id/number and then by salary 1.
Print it as,
employees.forEach(employee -> System.out.println(buildRecord(employee)));
1 - This assumes the employee number and salary are integers. You can change it as per your need.
I have a problem with receiving data from Map nested in another Map.
private Map<Customer, Map<Item,Integer>> orders;
I'm generating this map from JSON, its add Customer if he is not on the list with Items and their number.
If Customer is already in the map then key Item in the second map is updated and if a key was there already then Integer which is the number of items is updated.
Classes Customer and Items are not connected I mean Class Customer don't have field Items and class Items don't have a field Customer.
public class Customer {
private String name;
private String surname;
private Integer age;
private BigDecimal money;
}
public class Item {
private String name;
private String category;
private BigDecimal price;
}
Using streams I want to get for example Customer who paid the most for items but I have problem with getting this data from the map, it was not so hard with List but now I can't figure it out.
Ok I did figure out something like this and it seems to be working but I'm sure it can be simplified.
Customer key = customersMap.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey,
e -> e.getValue()
.entrySet()
.stream()
.map(o -> o.getKey().getPrice().multiply(BigDecimal.valueOf(o.getValue())))
.collect(Collectors.toList())))
.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey, t -> t.getValue().stream().reduce(BigDecimal.ZERO, BigDecimal::add)))
.entrySet()
.stream()
.max(Map.Entry.comparingByValue())
.orElseThrow()
.getKey();
Your answer Naman was very helpful so maybe you can give me advice about this.
This is how I'm receiving it from JSON.
JsonConverterCustomer jsonConverterCustomer = new JsonConverterCustomer(FILENAME3);
List<Order> orders = jsonConverterCustomer.fromJson().orElseThrow();
Map<Customer, Map<Item, Integer>> customersMap = new HashMap<>();
for (Order order : orders) {
if (!customersMap.containsKey(order.getCustomer())) {
addNewCustomer(customersMap, order);
} else {
for (Product product : order.getItems()) {
if (!customersMap.get(order.getCustomer()).containsKey(items)) {
addNewCustomerItem(item, customersMap.get(order.getCustomer()));
} else {
updateCustomerItem(customersMap, order, item);
}
}
}
}
private static void updateCustomerProduct(Map<Customer, Map<Item, Integer>> customersMap, Order order, Item item) {
customersMap.get(order.getCustomer())
.replace(item,
customersMap.get(order.getCustomer()).get(item),
customersMap.get(order.getCustomer()).get(item) + 1);
}
private static void addNewCustomerItem(Item item, Map<Item, Integer> itemIntegerMap) {
itemIntegerMap.put(item, 1);
}
private static void addNewCustomer(Map<Customer, Map<Item, Integer>> customersMap, Order order) {
Map<Item, Integer> temp = new HashMap<>();
addNewCustomerItem(order.getItems().get(0), temp);
customersMap.put(order.getCustomer(), temp);
}
Order class is a class which one help me receiving data from JSON
It is a simple class with Customer as a field and List as a field.
As you can see I'm receiving List of Orders and from it, I'm creating this Map.
Can I make it more functional? Using streams? I was trying to do but not sure how;/
There are two possible ways to make it more maintainable/readable as Jason pointed out and at the same time simplify the logic performed.
One, you can get rid of one of the stages in the pipeline and merge map and reduce into a single pipeline.
Another would be to abstract out per customer computation of the total amount paid by them.
So the abstraction would look like the following and work on the inner maps for your input:
private BigDecimal totalPurchaseByCustomer(Map<Item, Integer> customerOrders) {
return customerOrders.entrySet()
.stream()
.map(o -> o.getKey().getPrice().multiply(BigDecimal.valueOf(o.getValue())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
Now to easily fit this in while you iterate for each customer entry, you can do that in a single collect itself:
private Customer maxPayingCustomer(Map<Customer, Map<Item, Integer>> customersMap) {
Map<Customer, BigDecimal> customerPayments = customersMap.entrySet()
.stream()
.collect(Collectors.toMap(Map.Entry::getKey,
e -> totalPurchaseByCustomer(e.getValue())));
return customerPayments.entrySet()
.stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElseThrow();
}
I have a list of objects as follows:
Rating {
Long id;
String aRating;
String bRating;
Date date;
//getters and Setters
}
List<Rating> list = //list with some values in it.
I have to convert the above list to a nested map:
Map<Date, Map<CustomEnum, String>>
Enum CustomEnum {
aRatingEnum("aRating", "someValue");
bRatingEnum("bRating", "someValue");
}
suppose List contains following values:
Id, Date, aRating, bRating
1 , 2/2/2019, A+, B+
2, 2/2/2018, A, B
then this needs to be converted to following map:
Map = {2/2/2019={aRatingEnum=A+, bRatingEnum=B+}, 2/2/2018 = {aRatingEnum=A, bRatingEnum=B}}
I have tried using java 8:
list.stream().collect(Collectors.groupingBy(x->x.getDate(), Collectors.toMap(
//here I am not able to proceed further how should I approach please help.
)))
The toMap collects several items into a single collection. What you have is a single object (Rating) and want to create a Map out of it.
I think what you want is collectingAndThen which allows you to group by date and then do the transfer to the Map in the finishing Function.
list.stream()
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(x->x.getDate(),
lr -> {
Map<CustomEnum, String> result = new EnumMap<>();
Rating r = lr.get(0); // take first rating for that date
result.put(CustomEnum.aRatingEnum, r.getARating());
result.put(CustomEnum.bRatingEnum, r.getBRating());
return result;
})));
Or refactor that lambda into a method, or use Map.of if you're using Java 9 or higher.
This will give you a Map<Date, Map<CustomEnum, String>>.
I have List<Entry> entries, with below code structure,
class Entry {
public Product getProduct() {
return product;
}
}
class Product {
public List<CategoriesData> getCategories() {
return categoriesData;
}
}
class CategoriesData {
public String getName() {
return name;
}
}
I'm looking at sorting by Product - CategoriesData - name (from the first element in List<CategoriesData>)
// Not sure how to refer Name within CategoriesData,
// entries.stream().sorted(
// Comparator.comparing(Entry::getProduct::getCategories::getName))
// .collect(Collectors.toList())
Using Mureinik solution you can do:
Comparator<Entry> entryComperator = Comparator
.comparing(e -> e.getProduct().getCategories().get(0).getName());
List<Entry> sorted =
entries.stream()
.sorted(entryComperator)
.collect(Collectors.toList());
You might consider accessing the name from the list better in case the list is empty. you can hide all this logic in the comparator like I did above
If you're trying to sort by the first category, you need to refer to it in the comparator:
List<Entry> sorted =
entries.stream()
.sorted(Comparator.comparing(
e -> e.getProduct().getCategories().get(0).getName())
.collect(Collectors.toList())
EDIT:
To answer the question in the comments, for a secondary sorting, you'll have to specify the type in the Comparator:
List<Entry> sorted =
entries.stream()
.sorted(Comparator.comparing(
e -> e.getProduct().getCategories().get(0).getName())
.thenComparing(
(Entry e) -> e.getProduct().getName())
.collect(Collectors.toList())
With input from #Mureinik and making some modifications, I ended up with below and it is working. My requirement slightly changed, I need result in a map. Category name is the key in the map, value will be a list of 'Entry'.
final Map<String, List<Entry>> sortedMap =
data.getEntries().stream()
.sorted(Comparator.comparing(e -> ((Entry) e).getProduct().getCategories().stream().findFirst().get().getName())
.thenComparing(Comparator.comparing(e -> ((Entry) e).getProduct().getName())) )
.collect(Collectors.groupingBy(e -> e.getProduct().getCategories().stream().findFirst().get().getName(),
LinkedHashMap::new, Collectors.toList()));
I have two classes that are structured like this:
public class Company {
private List<Person> person;
...
public List<Person> getPerson() {
return person;
}
...
}
public class Person {
private String tag;
...
public String getTag() {
return tag;
}
...
}
Basically the Company class has a List of Person objects, and each Person object can get a Tag value.
If I get the List of the Person objects, is there a way to use Stream from Java 8 to find the one Tag value that is the most common among all the Person objects (in case of a tie, maybe just a random of the most common)?
String mostCommonTag;
if(!company.getPerson().isEmpty) {
mostCommonTag = company.getPerson().stream() //How to do this in Stream?
}
String mostCommonTag = getPerson().stream()
// filter some person without a tag out
.filter(it -> Objects.nonNull(it.getTag()))
// summarize tags
.collect(Collectors.groupingBy(Person::getTag, Collectors.counting()))
// fetch the max entry
.entrySet().stream().max(Map.Entry.comparingByValue())
// map to tag
.map(Map.Entry::getKey).orElse(null);
AND the getTag method appeared twice, you can simplify the code as further:
String mostCommonTag = getPerson().stream()
// map person to tag & filter null tag out
.map(Person::getTag).filter(Objects::nonNull)
// summarize tags
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
// fetch the max entry
.entrySet().stream().max(Map.Entry.comparingByValue())
// map to tag
.map(Map.Entry::getKey).orElse(null);
You could collect the counts to a Map, then get the key with the highest value
List<String> foo = Arrays.asList("a","b","c","d","e","e","e","f","f","f","g");
Map<String, Long> f = foo
.stream()
.collect(Collectors.groupingBy(v -> v, Collectors.counting()));
String maxOccurence =
Collections.max(f.entrySet(), Comparator.comparing(Map.Entry::getValue)).getKey();
System.out.println(maxOccurence);
This should work for you:
private void run() {
List<Person> list = Arrays.asList(() -> "foo", () -> "foo", () -> "foo",
() -> "bar", () -> "bar");
Map<String, Long> commonness = list.stream()
.collect(Collectors.groupingBy(Person::getTag, Collectors.counting()));
Optional<String> mostCommon = commonness.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey);
System.out.println(mostCommon.orElse("no elements in list"));
}
public interface Person {
String getTag();
}
The commonness map contains the information which tag was found how often. The variable mostCommon contains the tag that was found most often. Also, mostCommon is empty, if the original list was empty.
If you are open to using a third-party library, you can use Collectors2 from Eclipse Collections with a Java 8 Stream to create a Bag and request the topOccurrences, which will return a MutableList of ObjectIntPair which is the tag value and the count of the number of occurrences.
MutableList<ObjectIntPair<String>> topOccurrences = company.getPerson()
.stream()
.map(Person::getTag)
.collect(Collectors2.toBag())
.topOccurrences(1);
String mostCommonTag = topOccurrences.getFirst().getOne();
In the case of a tie, the MutableList will have more than one result.
Note: I am a committer for Eclipse Collections.
This is helpful for you,
Map<String, Long> count = persons.stream().collect(
Collectors.groupingBy(Person::getTag, Collectors.counting()));
Optional<Entry<String, Long>> maxValue = count .entrySet()
.stream().max((entry1, entry2) -> entry1.getValue() > entry2.getValue() ? 1 : -1).get().getKey();
maxValue.get().getValue();
One More solution by abacus-common
// Comparing the solution by jdk stream,
// there is no "collect(Collectors.groupingBy(Person::getTag, Collectors.counting())).entrySet().stream"
Stream.of(company.getPerson()).map(Person::getTag).skipNull() //
.groupBy(Fn.identity(), Collectors.counting()) //
.max(Comparators.comparingByValue()).map(e -> e.getKey()).orNull();
// Or by multiset
Stream.of(company.getPerson()).map(Person::getTag).skipNull() //
.toMultiset().maxOccurrences().map(e -> e.getKey()).orNull();