Java8 Stream Collectors - Splitting a list based on sum of values - java-8

I am trying partition a list into multiple sublists based on a condition that sum of a particular field should be less than 'x'. Below is sameple code:
public class TestGrouping {
public static class Transaction{
String txnId;
String comment;
Amount amount;
public Transaction(String txnId, String comment, Amount amount) {
this.txnId = txnId;
this.comment = comment;
this.amount = amount;
}
}
public static class Amount{
String amountValue;
public Amount(String amountValue) {
this.amountValue = amountValue;
}
}
public static void main(String[] args) {
List<Transaction> transactionList = new ArrayList<>();
Transaction txn1 = new Transaction("T1","comment1",new Amount("81"));
Transaction txn2 = new Transaction("T2","comment2",new Amount("5"));
Transaction txn3 = new Transaction("T3","comment3",new Amount("12"));
Transaction txn4 = new Transaction("T4","comment4",new Amount("28"));
transactionList.add(txn1);
transactionList.add(txn2);
transactionList.add(txn3);
transactionList.add(txn4);
//below is what i thought might work
// transactionList.stream().collect(groupingBy (r->Collectors.summingInt(Integer.valueOf(r.amount.amountValue)),Collectors.mapping(t -> t, toList())));
}
The goal is to split the transactionList into 2 (or more) sublists - where the sum of 'amount' is less than 100. So i could have a sublist have only txn1 - having amount as 81; and the other sublist have txn2, txn3, txn4 (as sum of these is less 100). Other possibility is - have sublist1 having txn1, txn2, txn3; and another sublist with just txn4. Not trying to create the most 'optimal' lists basically, just that sum of amounts should be less than 100.
Any clues?

The Idea is to use a custom collector to generate a list of pair(amountSum, transactions), the list should initialy be sorted. The accumulator method (here Accumulator.accept) do the grouping logic, I didn't implement combine because there is no need for a combiner in non parallel stream.
Bellow the code snippet, hope it helps.
public class TestStream {
public class Transaction {
String txnId;
String comment;
Amount amount;
public Transaction(String txnId, String comment, Amount amount) {
this.txnId = txnId;
this.comment = comment;
this.amount = amount;
}
}
public class Amount {
String amountValue;
public Amount(String amountValue) {
this.amountValue = amountValue;
}
}
#Test
public void test() {
List<Transaction> transactionList = new ArrayList<>();
Transaction txn1 = new Transaction("T1", "comment1", new Amount("81"));
Transaction txn2 = new Transaction("T2", "comment2", new Amount("5"));
Transaction txn3 = new Transaction("T3", "comment3", new Amount("12"));
Transaction txn4 = new Transaction("T4", "comment4", new Amount("28"));
transactionList.add(txn1);
transactionList.add(txn2);
transactionList.add(txn3);
transactionList.add(txn4);
transactionList.stream()
.sorted(Comparator.comparing(tr -> Integer.valueOf(tr.amount.amountValue)))
.collect(ArrayList<Pair<Integer, List<Transaction>>>::new, Accumulator::accept, (x, y) -> {
})
.forEach(t -> {
System.out.println(t.left);
});
}
static class Accumulator {
public static void accept(List<Pair<Integer, List<Transaction>>> lPair, Transaction tr) {
Pair<Integer, List<Transaction>> lastPair = lPair.isEmpty() ? null : lPair.get(lPair.size() - 1);
Integer amount = Integer.valueOf(tr.amount.amountValue);
if (Objects.isNull(lastPair) || lastPair.left + amount > 100) {
lPair.add(
new TestStream().new Pair<Integer, List<Transaction>>(amount,
Arrays.asList(tr)));
} else {
List<Transaction> newList = new ArrayList<>();
newList.addAll(lastPair.getRight());
newList.add(tr);
lastPair.setLeft(lastPair.getLeft() + amount);
lastPair.setRight(newList);
}
}
}
class Pair<T, V> {
private T left;
private V right;
/**
*
*/
public Pair(T left, V right) {
this.left = left;
this.right = right;
}
public V getRight() {
return right;
}
public T getLeft() {
return left;
}
public void setLeft(T left) {
this.left = left;
}
public void setRight(V right) {
this.right = right;
}
}
}

Related

How to group map values after Collectors.groupingBy() without using forEach

I have a list of ProductDto objects and I want to group them the similar ones using java 8 streams Collectors.groupingBy(). After grouping the records I want to combine the similar records as single productDto. To achieve this I have used map.forEach and got the expected result, but I want to avoid the forEach loop and want to know any better solution in java 8.
Below is the my Main Class code snippet.
public class GroupTest {
public static void main(String[] args) {
GroupTest t=new GroupTest();
List<ProductDto> inputDtos=t.createInputData();
List<ProductDto> resultDtos=t.getGroupedResult(inputDtos);
//writing to Excel
}
private List<ProductDto> getGroupedResult(List<ProductDto> inputDtos) {
Map<Object, List<ProductDto>> groupedMap = inputDtos.stream()
.collect(Collectors.groupingBy(ProductDto::groupSimilarProductIdentifier));
List<ProductDto> resultProductDtos=new ArrayList<>();
groupedMap.forEach((key, dtos) -> {
if (dtos.size() > 1) {
ProductDto productDto = dtos.get(0);
dtos.forEach(dto -> {
if (dto.getLap1() != null) {
productDto.setLap1(dto.getLap1());
} else if (dto.getLap2() != null) {
productDto.setLap2(dto.getLap2());
} else if (dto.getLap3() != null) {
productDto.setLap3(dto.getLap3());
}
});
resultProductDtos.add(productDto);
} else {
resultProductDtos.addAll(dtos);
}
});
return resultProductDtos;
}
private List<ProductDto> createInputData(){
List<ProductDto> dtos=new ArrayList<>();
dtos.add(new ProductDto(1L,"DELL",8,"DELL_s001",null,null));
dtos.add(new ProductDto(1L,"DELL",8,null,"DELL_s002",null));
dtos.add(new ProductDto(1L,"DELL",8,null,null,"DELL_s003"));
dtos.add(new ProductDto(1L,"HP",8,"HP_s001",null,null));
dtos.add(new ProductDto(2L,"APPLE",16,"MAC_s001",null,null));
return dtos;
}
}
This is the ProductDto class code
public class ProductDto {
private Long userId;
private String manufacter;
private int ram;
private String lap1;
private String lap2;
private String lap3;
public ProductDto(Long userId, String manufacter, int ram, String lap1, String lap2, String lap3) {
super();
this.userId = userId;
this.manufacter = manufacter;
this.ram = ram;
this.lap1 = lap1;
this.lap2 = lap2;
this.lap3 = lap3;
}
//getters and Setters
public List<Object> groupSimilarProductIdentifier() {
return Arrays.asList(userId, manufacter, ram);
}
}
Below is the screenshot image shows the input and output records. Output records is the results exactly I want it. Any alternate or better solution in java 8 which is efficient is most welcome.
After Rono comment I found the answer so posting the answer what I did in getGroupedResult method and added a new function mergetwoProduct. So this may help somebody.
Below is the code for my getGroupedResult and mergetwoProduct methods after changes.
private List<ProductDto> getGroupedResult(List<ProductDto> inputDtos) {
List<ProductDto> productdtos= new ArrayList<>(inputDtos.stream().collect(
Collectors.toMap(ProductDto::groupSimilarProductIdentifier, e -> e, (a, b) -> mergetwoProduct(a, b)))
.values());
return productdtos;
}
private ProductDto mergetwoProduct(ProductDto p1,ProductDto p2) {
if (p2.getLap1() != null) {
p1.setLap1(p2.getLap1());
} else if (p2.getLap2() != null) {
p1.setLap2(p2.getLap2());
} else if (p2.getLap3() != null) {
p1.setLap3(p2.getLap3());
}
return p1;
}

Java 8 groupingby with returning multiple field

In Java 8 group by how to groupby on a single field which returns more than one field. In the below code by I am passing name and the field to be summed which is 'total' in this scenario. however I would like to return sum of 'total' and 'balance' field for every 'name' in the Customer list (can be a map with key and value as array).
Can it be done by using a single groupingBy with the return values?
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class Sample {
public static void main(String str[]){
Customer custa = new Customer("A",1000,1500);
Customer custa1 = new Customer("A",2000,2500);
Customer custb = new Customer("B",3000,3500);
Customer custc = new Customer("C",4000,4500);
Customer custa2 = new Customer("A",1500,2500);
List<Customer> listCust = new ArrayList<>();
listCust.add(custa);
listCust.add(custa1);
listCust.add(custb);
listCust.add(custc);
listCust.add(custa2);
Map<String, Double> retObj =
listCust.stream().collect(Collectors.groupingBy(Customer::getName,Collectors.summingDouble(Customer::getTotal)));
System.out.println(retObj);
}
private static class Customer {
private String name;
private double total;
private double balance;
public Customer(String name, double total, double balance) {
super();
this.name = name;
this.total = total;
this.balance = balance;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getTotal() {
return total;
}
public void setTotal(double total) {
this.total = total;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
#Override
public String toString() {
return "Customer [name=" + name + ", total=" + total + ", balance=" + balance + "]";
}
}
}
Expected Output -
{
A = [4500,6500],
B = [3000,3500] ,
C = [4000,4500]
}
You can write your own collector to sum total and balance
Collector<Customer, List<Double>, List<Double>> collector = Collector.of(
() -> Arrays.asList(0.0, 0.0),
(a, t) -> {
a.set(0, a.get(0) + t.getTotal());
a.set(1, a.get(1) + t.getBalance());
},
(a, b) -> {
a.set(0, a.get(0) + b.get(0));
a.set(1, a.get(1) + b.get(1));
return a;
}
);
Map<String, List<Double>> retObj = listCust
.stream()
.collect(Collectors.groupingBy(Customer::getName, collector));
System.out.println(retObj);
result
{A=[4500.0, 6500.0], B=[3000.0, 3500.0], C=[4000.0, 4500.0]}
You can also use the toMap collector to accomplish the task at hand.
Map<String, List<Double>> retObj =
listCust.stream()
.collect(Collectors.toMap(Customer::getName,
c -> new ArrayList<>(Arrays.asList(c.getTotal(), c.getBalance())),
(l, l1) -> {
l.set(0, l.get(0) + l1.get(0));
l.set(1, l.get(1) + l1.get(1));
return l;
}));

Returning Page<Object> in Query DSL

I am using Query DSL and want my result set to return a Page Object. Is there a way to do that in Query DSL? If so Whats my query going to be looking like?
I am using JPAQuery and I have my QClasses
The Method structure is this
public Page<Object> searchPerson(String name,String phone){
Page<Object> results=null;
JPQLQuery query = new JPAQuery(entityManager);
QPerson person = QPerson.person;
//I am assuming my query would go here
results = query.from(person). ?????
return results;}
Help!
Here is my implementation for Paging with QueryDSL. A PageRequest defines the parameters for our query (limit and page):
public class PageRequest {
protected Long page = 1l;// 1 is the first page
protected Integer limit = 10;
public PageRequest(Long page, Integer limit) {
this.limit = limit;
this.page = page;
}
public Long getPage() {
return page;
}
public Integer getLimit() {
return limit;
}
public Long getOffset() {
return (page - 1l) * limit;
}
}
The Page Class contains the result (here the attribute objects) of the query and could implement methods to create nice paging links.
public class Page<T> extends PageRequest {
protected Collection<T> objects;
private Long totalCount;
private Long pageCount;
private Boolean hasPageLinkPrev;
private Boolean hasPageLinkNext;
private Collection<Long> pageLinks;
public Page(Long page, Integer limit, Long totalCount, Collection<T> objects) {
this.page = page;
this.limit = limit;
this.totalCount = totalCount;
this.objects = objects;
this.pageCount = totalCount / limit;
if (totalCount % limit > 0) {
this.pageCount = this.pageCount + 1;
}
this.hasPageLinkPrev = page > 1;
this.hasPageLinkNext = page < this.pageCount;
this.pageLinks = new ArrayList<>();
if (this.pageCount != 1) {
this.pageLinks.add(1l);
if (page > 3l) {
this.pageLinks.add(-1l);
}
if (page > 2l) {
if (page.equals(this.pageCount) && this.pageCount > 3l) {
this.pageLinks.add(page - 2l);
}
this.pageLinks.add(page - 1l);
}
if (page != 1l && !page.equals(this.pageCount)) {
this.pageLinks.add(page);
}
if (page < this.pageCount - 1l) {
this.pageLinks.add(page + 1l);
if (page == 1l && this.pageCount > 3l) {
this.pageLinks.add(page + 2l);
}
}
if (page < this.pageCount - 2l) {
this.pageLinks.add(-1l);
}
this.pageLinks.add(this.pageCount);
}
}
public Page(PageRequest pageRequest, Long totalCount, Collection<T> objects) {
this(pageRequest.getPage(), pageRequest.getLimit(), totalCount, objects);
}
public Long getTotalCount() {
return this.totalCount;
}
public Long getPageCount() {
return this.pageCount;
}
public Long getPage() {
return this.page;
}
public Integer getLimit() {
return this.limit;
}
public Boolean getHasPageLinkPrev() {
return this.hasPageLinkPrev;
}
public Boolean getHasPageLinkNext() {
return hasPageLinkNext;
}
public Collection<Long> getPageLinks() {
return pageLinks;
}
public Collection<T> getObjects() {
return objects;
}
}
With that stuf it is not very hard to create the query and put the results in our page object. One possibility is to write a generic method in the base class of the repository classes:
protected <T> Page<T> getPage(JPQLQuery<T> query, PageRequest pageRequest) {
List<T> resultList = query
.offset(pageRequest.getOffset())
.limit(pageRequest.getLimit())
.fetch();
Long totalCount = query.fetchCount();
return new Page<T>(pageRequest, totalCount, resultList);
}
In your repository class you create your query for the particular use case. Then you can use the method getPage to get the results in a Page.
public Page<Person> searchPerson(String name,
String phone,
PageRequest request){
Page<Person> results=null;
JPQLQuery<Person> query = new JPAQuery<>(entityManager);
QPerson person = QPerson.person;
query = query.from(person)
.where(person.name.eq(name)
.and(person.phone.eq(phone)));
return getPage(query, request);
}
The solution for the above was using BooleanBuilder implemented on the method above and changed the method name to return Person Object.
Please check BooleanBuilder
QPerson person= QPerson.person;
BooleanBuilder builder = this.getBuilder(name, phone,page, pageSize, sortFlag, sortItem);
PageRequest pg = getPRequest(page, pageSize);
Page<Person> pages personRepo.findAll(builder,pg);
return pages;
and then Implemented getBuilder Method for it which is the below one
public BooleanBuilder getBuilder(String name, String phone, Integer page, Integer pageSize, String sortFlag, String sortItem) {
QPerson person = QPerson.person;
BooleanBuilder builder = new BooleanBuilder();
builder.and(person.name.startsWith(name));
return builder;
}
and finally implemented the getPRequest Method as the following
public PageRequest getPRequest(Integer page, Integer pageSize) {
return new PageRequest(page, pageSize);
}
Oooooh Happy Days!

Recycler View with Header and Edit Text

I have a recyclerview with a header achieved by using two different element types. In my header there is an edit text which I want to use for filtering the nonheader elements of the list. Below is my current implementation, I have one concern and one problem with it.
My concern is that what I am doing in publishResults with the notifyItemRangeRemoved and notifyItemInserted is the wrong way to update the recycler view. I originally was doing notifyDatasetChanged but his would cause the header row to be refreshed too and the edit text to lose focus. What I really want is a way to refresh only the item rows and leave the header row untouched.
My current problem is that with the existing code if I scroll down too much the edit text looses focus. I want the edit text to keep focus even if I scroll to the bottom of the list.
The code used to use a ListView with setHeaderView and that worked somehow so there must be someway of achieving the goal just not sure what the trick with a recycler view is. Any help is much appreciated.
public class SideListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements Filterable {
private static final int TYPE_HEADER = 0;
private static final int TYPE_ITEM = 1;
private final List<String> data;
public List<String> filteredData;
private HeaderActionListener headerActionListener;
public SideListAdapter(Context context, ArrayList<String> data, HeaderActionListener headerActionListener) {
this.data = data;
filteredData = new ArrayList<>(data);
this.context = context;
this.headerActionListener = headerActionListener;
}
#Override
public Filter getFilter() {
return new TestFilter();
}
static class SideListItem extends RecyclerView.ViewHolder {
LinearLayout baseLayout;
public SideListItem(View itemView) {
super(itemView);
baseLayout = (LinearLayout) itemView.findViewById(R.id.settings_defaultcolor);
}
}
class SideListHeader extends SideListHeader {
EditText sort;
public SideListHeaderLoggedIn(View itemView) {
super(itemView);
sort = (EditText) itemView.findViewById(R.id.sort);
}
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_ITEM) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false);
return new SideListItem(v);
} else if (viewType == SideListHeader) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.header, parent, false);
return new SideListHeader(v);
}
throw new RuntimeException("there is no type that matches the type " + viewType + " + make sure your using types correctly");
}
public interface HeaderActionListener {
boolean onSortEditorAction(TextView arg0, int arg1, KeyEvent arg2);
}
#Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, final int position) {
if (holder instanceof SideListHeader) {
final SideListHeader sideListHeader = (SideListHeader) holder;
sideListHeader.sort.setOnEditorActionListener(new TextView.OnEditorActionListener() {
#Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
}
});
sideListHeader.sort.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
#Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
#Override
public void afterTextChanged(Editable editable) {
String result = sideListHeader.sort.getText().toString().replaceAll(" ", "");
getFilter().filter(result);
}
});
}
if (holder instanceof SideListItem) {
// Inflate normal item //
}
}
// need to override this method
#Override
public int getItemViewType(int position) {
if (isPositionHeader(position)) {
return TYPE_HEADER;
}
return TYPE_ITEM;
}
private boolean isPositionHeader(int position) {
return position == 0;
}
//increasing getItemcount to 1. This will be the row of header.
#Override
public int getItemCount() {
return filteredData.size() + 1;
}
private class TestFilter extends Filter {
#Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
String prefix = constraint.toString().toLowerCase();
if (prefix.isEmpty()) {
ArrayList<String> list = new ArrayList<>(data);
results.values = list;
results.count = list.size();
} else {
final ArrayList<String> list = new ArrayList<>(data);
final ArrayList<String> nlist = new ArrayList<>();
for (int i = 0 ; i < list.size(); i++) {
String item = list.get(i);
if (item.contains(prefix)) {
nlist.add(item);
}
}
results.values = nlist;
results.count = nlist.size();
}
return results;
}
#SuppressWarnings("unchecked")
#Override
protected void publishResults(CharSequence constraint, FilterResults results) {
notifyItemRangeRemoved(1, getItemCount()-1);
filteredData.clear();
filteredData.addAll((List<String>)results.values);
for(int i = 1; i < getItemCount() - 1; i++){
notifyItemInserted(i);
}
}
}
}
I'm not sure how correct this way is, but in my code I implemented it like that
private var headerList: List<HeaderItem> = listOf(HeaderItem("Title"))
private fun searchItem(items: List<Items>, query: String) {
items.filterIsInstance<MainItem>().filter { filteredItems ->
filteredItems.header.lowercase().contains(query.lowercase())
}.let { searchedItems ->
rvAdapter.submitList(headerList + searchedItems)
}
}
This way I was able to preserve header element when I did my search

Storm Trident 'average aggregator

I am a newbie to Trident and I'm looking to create an 'Average' aggregator similar to 'Sum(), but for 'Average'.The following does not work:
public class Average implements CombinerAggregator<Long>.......{
public Long init(TridentTuple tuple)
{
(Long)tuple.getValue(0);
}
public Long Combine(long val1,long val2){
return val1+val2/2;
}
public Long zero(){
return 0L;
}
}
It may not be exactly syntactically correct, but that's the idea. Please help if you can. Given 2 tuples with values [2,4,1] and [2,2,5] and fields 'a','b' and 'c' and doing an average on field 'b' should return '3'. I'm not entirely sure how init() and zero() work.
Thank you so much for your help in advance.
Eli
public class Average implements CombinerAggregator<Number> {
int count = 0;
double sum = 0;
#Override
public Double init(final TridentTuple tuple) {
this.count++;
if (!(tuple.getValue(0) instanceof Double)) {
double d = ((Number) tuple.getValue(0)).doubleValue();
this.sum += d;
return d;
}
this.sum += (Double) tuple.getValue(0);
return (Double) tuple.getValue(0);
}
#Override
public Double combine(final Number val1, final Number val2) {
return this.sum / this.count;
}
#Override
public Double zero() {
this.sum = 0;
this.count = 0;
return 0D;
}
}
I am a complete newbie when it comes to Trident as well, and so I'm not entirely if the following will work. But it might:
public class AvgAgg extends BaseAggregator<AvgState> {
static class AvgState {
long count = 0;
long total = 0;
double getAverage() {
return total/count;
}
}
public AvgState init(Object batchId, TridentCollector collector) {
return new AvgState();
}
public void aggregate(AvgState state, TridentTuple tuple, TridentCollector collector) {
state.count++;
state.total++;
}
public void complete(AvgState state, TridentCollector collector) {
collector.emit(new Values(state.getAverage()));
}
}

Resources