Java BiFunction to manipulate data over an iteration

83 Views Asked by At

I'm writing a Java code that looks like this, and I have the feel it could be improved with some functional programming stuff :)

int rank = 1;

for (int i = 0; i < activities.size(); i++) {
    if (i == 0) {
        activities.get(i).setRank(rank);
    else {
        float currentAverage = activities.get(i).getQualityAverage();
        float previousAverage = activities.get(i - 1).getQualityAverage();

        activities.get(i).setRank(currentAverage == previousAverage ? rank : ++rank);
    }
}

Assuming that activities List is sorted, the sense of this code is to assign a rank (a score) to every activity according with its place in the list. But there's a rule in the else case (when the itteration is equal or higher than second element) where I'm using the rank assigned to the previous element if both elements' qualityAverage property is exactly the same.

For instance:

Activity Quality Average Rank
Read 10 1
Workout 9.5 2
Write 9.2 3
Study 9.2 3
Clean 8 4
Play guitar 8 4

As I mentioned at the beginning, how could I write a Java BiFunction (maybe something similar to reduce()) in order to achieve a cleaner and smarter code.

3

There are 3 best solutions below

1
Luis Estrada On

You can create a static function in your Utility class or any other class, then use java stream to iterate the List. Here is an example:

public static void assignRanks(List<Activity> activities) {
    // Create a stream of indices paired with activities (with an initial state)
    Stream.iterate(new Object[] {1, null},  // initial: {rank, previousActivity} 
                  entry -> {
                      int nextRank = entry[0].equals(activities.get((int)entry[1] + 1).getQualityAverage())
                                    ? (int)entry[0] : (int)entry[0] + 1;
                      return new Object[]{ nextRank, (int)entry[1] + 1 };
                  })
          .limit(activities.size()) // Limit the stream to the size of the activities list
          .forEachOrdered(entry -> activities.get((int)entry[1]).setRank((int)entry[0]));
}
2
Oleg Ushakov On

I think you can improve your code a little by writing it like this

int rank = 1;
activities.get(0).setRank(rank);
for (int i = 1; i < activities.size(); i++) {
  float previousAverage = activities.get(i - 1).getQualityAverage();
  float currentAverage = activities.get(i).getQualityAverage();
  activities.get(i).setRank(currentAverage == previousAverage ? rank : ++rank);
}

It will be the clearest way to demonstrate your logic. For BiFunction you don't have enough variables. You can use a Consumer in your logic but in my opinion in this case it would be an over-engineering

p.s. and also can't compare correctly two float values by using currentAverage == previousAverage in some cases it return false negative answer

4
shmosel On

If you're looking for a stream-based solution, you're not likely to find one that's an improvement, because your logic is stateful and streams are designed for stateless operations. I would just extract the last average to a variable to avoid the repeated lookups:

int rank = 0;
float average = -1; // or some other invalid value

for (var activity : activities) {
    activity.setRank(average == activity.getQualityAverage() ? rank : ++rank);
    average = activity.getQualityAverage();
}