2

I'm totally new on java 8 streams and I'm trying to obtain the behavior below described:

class myVO {
    Long id;
    BigDecimal value;
    Date date;
    getter/setter
}

myVO method(Map<Long, myVO> inputMap) {
    return inputMap.stream()
                   .filter(x -> x.getValue().compareTo(BigDecimal.ZERO) > 0)
                   .sorted(); //FIXME
}

I want to obtain only one myVO obj that is the SUM of BigDecimal values of the records with the same date (the lowest one).

e.g.

xL, 10, 2015/07/07
xL, 15, 2015/07/08
xL, 20, 2015/07/07
xL, 25, 2015/07/09

result

xL, 30, 2015/07/07

N.B. id (xL) is not important field.

UPDATE -- adopted solution (although not in single pass)

if(null != map && !map.isEmpty()) {
        Date closestDate = map.values().stream()
                .filter(t -> t.getDate() != null)
                .map(MyVO::getDate)
                .min(Comparator.naturalOrder()).orElse(null);
        myVO.setDate(closestDate);

        BigDecimal totalValue = map.values().stream()
                .filter(x -> x.getValue() != null && x.getValue().signum() != 0)
                .filter(t -> t.getDate().equals(closestDate))
                .map(MyVO::getValue)
                .reduce(BigDecimal::add).orElse(null);
        myVO.setValue(totalValue != null ? totalValue.setScale(2, BigDecimal.ROUND_HALF_DOWN) : totalValue);
    }
0

3 Answers 3

1

Considering inputMap has atleast one entry, it can be done like this :

myVO method(Map<Long, myVO> inputMap) {
    Date minDate = inputMap.values().stream().map(myVO::getDate).min(Comparator.naturalOrder()).get();

    BigDecimal sum = inputMap.values().stream().filter(t -> t.getDate().equals(minDate)).map(myVO::getValue).reduce(BigDecimal::add).get();

    myVO myVOObj = new myVO();
    myVOObj.setDate(minDate);
    myVOObj.setValue(sum);
    myVOObj.setId(??);

    return myVOObj;
}
Sign up to request clarification or add additional context in comments.

Comments

1

I wrote a custom collector to solve such tasks. It's available in my StreamEx library and called MoreCollectors.maxAll(downstream). Using it you can solve the task in single pass after some preparations.

First, your compareTo method is wrong. It never returns 0 and practically it violates the contract (a.compareTo(a) == -1 which violates reflexivity and antisymmetry properties). It can be easily fixed like this:

@Override
public int compareTo(myVO o) {
    return o.getDate().compareTo(this.getDate());
}

Next, let's add a myVO.merge() method which can merge two myVO objects according to your requirements:

public myVO merge(myVO other) {
    myVO result = new myVO();
    result.setId(getId());
    result.setDate(getDate());
    result.setValue(getValue().add(other.getValue()));
    return result;
}

Now the result can be found like this:

Optional<myVO> result = input.stream().filter(x -> x.getValue().signum() > 0)
    .collect(MoreCollectors.maxAll(Collectors.reducing(myVO::merge)));

The resulting Optional will be empty if the input list is empty.

If you don't like to depend on third-party library, you can just check the source code of this collector and write something similar in your project.

Comments

1

The reduction isn’t that complicated if you think about it. You have to specify a reduction function which:

  • if the dates of two elements differ, returns the object with the lower date
  • if they match, create a result object containing the sum of the values

Compared to a two-step method, it may perform more add operations whose result will be dropped when a lower date appears in the stream, on the other hand, the number of performed date comparisons will be half.

MyVO method(Map<Long, MyVO> inputMap) {
    return inputMap.values().stream()
        .reduce((a,b)->{
            int cmp=a.getDate().compareTo(b.getDate());
            if(cmp==0)
            {
                MyVO r=new MyVO();
                r.setDate(a.date);
                r.setValue(a.value.add(b.value));
                return r;
            }
            return cmp<0? a: b;
        }).orElse(null);
}

The main reason it doesn’t look concise is that it has to create a new MyVO instance holding the sum in the case of a matching date as a reduction function must not modify the value objects. And you didn’t specify which constructors exist. If there is an appropriate constructor receiving a Date and BigDecimal, the function could be almost a one-liner.

Note that this method will return an original MyVO object if there is only a single one with a lowest date.


Alternatively you can use a mutable reduction, always creating a new MyVO instance holding the result but only creating one instance per thread and modifying that new instance during the reduction:

MyVO method(Map<Long, MyVO> inputMap) {
    BiConsumer<MyVO, MyVO> c=(a,b)->{
        Date date = a.getDate();
        int cmp=date==null? 1: date.compareTo(b.getDate());
        if(cmp==0) a.setValue(a.getValue().add(b.getValue()));
        else if(cmp>0)
        {
            a.setValue(b.getValue());
            a.setDate(b.getDate());
        }
    };
    return inputMap.values().stream().collect(()->{
        MyVO r = new MyVO();
        r.setValue(BigDecimal.ZERO);
        return r;
    }, c, c);
}

Here, the Supplier could be a one-line if an appropriate constructor exists (or if the initial value is guaranteed to be the non-null BigDecimal.ZERO)…

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.