简体   繁体   中英

Calculating average multiple numbers in a map of objects

I have a map of the format

Map<String, List<TableDTO>>

public class TableDTO {
    private String countryName;
    private String sourceName;
    private int year;
    private Double usageValue;
    private Double powerUsers;

    //Setter & Getters

}

I want to find the average of usageValues and powerUsers and still maintain the TableDTO structure and the usageValue can be null, if it null ignore that object completely.

<Chrome, <UK, Lorem, 2013, 2.90, 5.4>>
<Chrome, <US, Lorem, 2013, 4.10, 1.5>>
<Chrome, <EU, Lorem, 2013, 1.20, 0.22>>
<Chrome, <Asia, Lorem, 2013, 3.90, -1.10>>

<IE, <UK, Lorem, 2013, 1.40, 24.4>>
<IE, <US, Lorem, 2013, 0.90, 14.4>>
<IE, <EU, Lorem, 2013, 2.10, 0>>
<IE, <Asia, Lorem, 2013, 0.90, 0.4>>

<FF, <UK, Lorem, 2013, 0.10, 2.14>>
<FF, <US, Lorem, 2013, 1.10, 4.0>>
<FF, <EU, Lorem, 2013, , 4.4>>
<FF, <Asia, Lorem, 2013, 2.90, 4.4>>

Result expected

<1, <UK, Lorem, 2013, 1.47, 10.65>>
<2, <US, Lorem, 2013, 2.03, 6.63>>
<3, <Asia, Lorem, 2013, 2.57, 1.23>>

For now in the results I have replaced the keys with index, which is fine for now. You will notice that since FF for EU has a null value the entire EU has been ignored, but for the rest I have the average calculated.

How can this be done using Lambda expressions in Java 8, or do I have to iterate through?

Update 1: This is as far as I got for now:

1. 
Map<String, List<TableDTO>> dump = mapOfAllData.values()
                .stream()
                .flatMap(list -> list.stream())
                .collect(Collectors.groupingBy(TableDTO::getCountryName));

Which give me a map with country names and the DTO orderd

2. 
  dump.values().stream().flatMap(list -> list.stream())
                .filter((o -> !o.getUsageValue().isEmpty()))
                .collect(Collectors.mapping(TableDTO::getUsageValue, Collectors.averagingDouble(Double::parseDouble)));

Basically gets the average, but does not remove the DTO in which the usageValue is empty, which I am trying at the moment to resolve.

Update 2:

I managed to remove the unwanted countries from my map.

I am trying to figure out how to find the average of two elements, I have this expression

 newMap.values().stream().flatMap(list -> list.stream())
                .collect(Collectors.mapping(TableDTO::usageValue, Collectors.averagingDouble(s -> s.isEmpty() ? Double.NaN : Double.parseDouble(s))));
                       // Collectors.mapping(TableDTO::powerUsers, Collectors.averagingDouble(c -> c.isEmpty() ? Double.NaN : Double.parseDouble(c))));

but am unable to get the average for powerUsers.

To understand, you want an average over each List<TableDTO> with a groupBy countryName , sourceName , year but average are on distinct field ?

I will expect usagePower and powerUsers to be Double , and not String like your code and your use of Double.parseDouble suggests.

This code should do it:

package stackoverflow;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

public class TableDTO {
  private final String countryName;
  private final String sourceName;
  private final int year;
  @Nullable
  private final Double usageValue;
  private final Double powerUsers;

  public TableDTO(final String countryName, final String sourceName, final int year, final Double usageValue,
      final Double powerUsers) {
    this.countryName = countryName;
    this.sourceName = sourceName;
    this.year = year;
    this.usageValue = usageValue;
    this.powerUsers = powerUsers;
  }

  public String getCountryName() {return countryName;}
  public String getSourceName() {return sourceName;}
  public int getYear() {return year;}
  @Nullable public Double getUsageValue() {return usageValue;}
  public Double getPowerUsers() {return powerUsers;}

  @Override
  public String toString() {
    return "TableDTO [countryName=" + countryName + ", sourceName=" + sourceName + ", year=" + year + ", usageValue="
        + usageValue + ", powerUsers=" + powerUsers + "]";
  }

  public static void main(final String[] args) {

    final java.util.Map<String, java.util.List<TableDTO>> data = new LinkedHashMap<>();

    final List<TableDTO> chrome = new ArrayList<>();
    chrome.add(new TableDTO("UK", "Lorem", 2013, 2.90, 5.4));
    chrome.add(new TableDTO("US", "Lorem", 2013, 4.10, 1.5));
    chrome.add(new TableDTO("EU", "Lorem", 2013, 1.20, 0.22));
    chrome.add(new TableDTO("Asia", "Lorem", 2013, 3.90, -1.10));
    data.put("Chrome", chrome);

    final List<TableDTO> ie = new ArrayList<>();
    ie.add(new TableDTO("UK", "Lorem", 2013, 1.40, 24.4));
    ie.add(new TableDTO("US", "Lorem", 2013, 0.90, 14.4));
    ie.add(new TableDTO("EU", "Lorem", 2013, 2.10, 0.));
    ie.add(new TableDTO("Asia", "Lorem", 2013, 0.90, 0.4));
    data.put("IE", ie);

    final List<TableDTO> fx = new ArrayList<>();
    fx.add(new TableDTO("UK", "Lorem", 2013, 0.10, 2.14));
    fx.add(new TableDTO("US", "Lorem", 2013, 1.10, 4.0));
    fx.add(new TableDTO("EU", "Lorem", 2013, null, 4.4));
    fx.add(new TableDTO("Asia", "Lorem", 2013, 2.90, 4.4));
    data.put("FX", fx);

    data.values()
        .stream()
        .flatMap(List::stream)
        .collect(Collectors.groupingBy(dto -> Arrays.asList(dto.getCountryName(), dto.getSourceName(), dto.getYear())))
        .values()
        .stream()
        .filter(list -> list.stream().map(TableDTO::getUsageValue).noneMatch(Objects::isNull))
        .map(
            values -> {
              final TableDTO root = values.iterator().next();

              final double usageValueAvg = values.stream().map(TableDTO::getUsageValue).filter(Objects::nonNull)
                  .collect(Collectors.averagingDouble(Double::doubleValue));
              final double powerUsersAvg = values.stream().map(TableDTO::getPowerUsers)
                  .collect(Collectors.averagingDouble(Double::doubleValue));

              return new TableDTO(root.getCountryName(), root.getSourceName(), root.getYear(), usageValueAvg,
                  powerUsersAvg);

            }).forEach(System.out::println);
    ;

  }
}

The result is:

TableDTO [countryName=UK, sourceName=Lorem, year=2013, usageValue=1.4666666666666666, powerUsers=10.646666666666667]
TableDTO [countryName=US, sourceName=Lorem, year=2013, usageValue=2.033333333333333, powerUsers=6.633333333333333]
TableDTO [countryName=Asia, sourceName=Lorem, year=2013, usageValue=2.5666666666666664, powerUsers=1.2333333333333334]

And the explanation: I've taken some of your code to do it.

  • Do a flatMap over the values of data :

     data.values() .stream() .flatMap(List::stream) 
  • Group your TableDTO by some keys: we don't care about the key, the only important thing is that it correctly implements hashCode and equals . Arrays.asList does the job. Otherwise, create a class Tuple which take an array and use Arrays.hashCode / equals .

      .collect(Collectors.groupingBy(dto -> Arrays.asList(dto.getCountryName(), dto.getSourceName(), dto.getYear()))) .values() .stream() 

    Since we don't want the list, we select the values and use a stream.

  • We filter TableDTO which contains an empty usageValue :

      .filter(list -> list.stream().map(TableDTO::getUsageValue).noneMatch(Objects::isNull)) 
  • Then we do a map, and that where you were failing at finding a solution: because of the group, all TableDTO share the same countryName , sourceName and year value. But not the usageValue and powerUsers .

    Because the list can't be empty, we get the first element.

      .map( values -> { final TableDTO root = values.iterator().next(); 
  • On the other result, we compute the two averages filtering any null values for usageValue .

      final double usageValueAvg = values.stream().map(TableDTO::getUsageValue).filter(Objects::nonNull) .collect(Collectors.averagingDouble(Double::doubleValue)); final double powerUsersAvg = values.stream().map(TableDTO::getPowerUsers) .collect(Collectors.averagingDouble(Double::doubleValue)); 
  • Then we return a new TableDTO based on the three grouping key, and the two averages.

      return new TableDTO(root.getCountryName(), root.getSourceName(), root.getYear(), usageValueAvg, powerUsersAvg); }) 
  • And we print it, and voilà! :)

      .forEach(System.out::println); 

I hope it resolve your question.

I tested it in Eclipse, it compile, but it may fails with javac since the compiler does not the same work with Lambdas.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM