Home > Blockchain >  Sort list of map items based on unknown list of fields
Sort list of map items based on unknown list of fields

Time:02-01

I have a big list of HashMap<String, Object> items that I need to sort dynamically based on a list of fields.

If the list of fields were static, I know I could easily use Comparator.compare(..) follow by thenComparing(..).

But if I do not know how many fields and which ones, I do not understand how I can chain the thenComparion dynamically based on the number of fields.

Currently my data is only Strings, but I expect it will change to other field types later so here is a future proof example:

[
  {
     "destination": "tokyo",
     "origin": "paris",
     "model": "speedflight 3000",
     "id": 5000047632459593,
     "speed": 502.5,
     "altitude": 5001,
     "time": "2023-01-30T13:35:23Z"
  },
  ....
]

The request could be for example to filter on the fields in following order id, speed, time, or also just simply id. There is no sorting direction given (asc,desc) so the default is always used.

CodePudding user response:

I would change the approach. Instead of chaining thenComparing(), write a normal Comparator implementation, depending on the list of fields.

public class MapComparator implements Comparator<Map<String, Object>> {

  private final List<String> fields;

  public MapComparator(List<String> fields) {
    this.fields = fields;
  }

  @Override
  public int compare(Map<String, Object> map1, Map<String, Object> map2) {
    for (String field : this.fields) {
      Object value1 = map1.get(field);
      Object value2 = map2.get(field);
      //don't forget to handle nulls, if applicable
      int cmp;
      if (value1 instanceof Comparable comparable) {
        cmp = comparable.compareTo(value2);
      } else {
        cmp = // deal with non-Comparable types, if there are such
        // may make them implement Comparable, if they are custom
        // or depend on Comparators
      }
      if (cmp != 0) {
        return cmp;
      }
    }
    return 0;
  }
}

Or even better, make a custom class to represent the elements. Then map fields names (String) to Comparator functions using the particular field.

public class MyClassComparator implements Comparator<MyClassA> {

  private final List<String> fields;
  private final Map<String, Comparator<MyClassA>> map;

  public MyClassComparator(List<String> fields, Map<String, Comparator<MyClassA>> map) {
    this.fields = fields;
    this.map = map;
  }

  @Override
  public int compare(MyClassA o1, MyClassA o2) {
    for (String field : this.fields) {
      int cmp = this.map.get(field).compare(o1, o2);
      if (cmp != 0) {
        return cmp;
      }
    }
    return 0;
  }
}

You can also make the map static, since most probably it will never change once intialized.

CodePudding user response:

You could create all possible comparators and store them in a map using your field names as keys and then depending on the input you could combine the comparators needed. An example method:

static void sortBy(List<Map<String, Object>> maps, List<String> sortOrder) {
    Comparator<Map<String, Object>> byDest   = Comparator.comparing(m -> (String) m.get("destination"));
    Comparator<Map<String, Object>> byOrigin = Comparator.comparing(m -> (String) m.get("origin"));
    Comparator<Map<String, Object>> byModel  = Comparator.comparing(m -> (String) m.get("model"));
    Comparator<Map<String, Object>> byId     = Comparator.comparing(m -> (long)   m.get("id"));
    Comparator<Map<String, Object>> bySpeed  = Comparator.comparing(m -> (double) m.get("speed"));
    Comparator<Map<String, Object>> byAlt    = Comparator.comparing(m -> (int)    m.get("altitude"));
    Comparator<Map<String, Object>> byTime   = Comparator.comparing(m -> (String) m.get("time"));

    Map<String, Comparator<Map<String, Object>>> compMap = Map.of(
            "destination", byDest,
            "origin", byOrigin,
            "model", byModel,
            "id", byId,
            "speed", bySpeed,
            "altitude", byAlt,
            "time", byTime);

    Comparator<Map<String, Object>> combined = sortOrder.stream().map(compMap::get).reduce(Comparator::thenComparing).get();

    maps.sort(combined);
}

assuming you have your map and the sort order in a list of strings:

List<Map<String, Object>> yourListOfMaps = ...
List<String> sortByFields = List.of("destination", "origin", "speed");

use it as:

sortBy(yourListOfMaps, sortByFields);

If the list of sort orders is empty you'll get a NoSuchElementException. To avoid this you could define a default comparator instead of calling get blindly on the optional returned from reduce. For example byId

Comparator<Map<String, Object>> combined = sortOrder.stream()
                                                    .map(compMap::get)
                                                    .reduce(Comparator::thenComparing)
                                                    .orElse(byId);

CodePudding user response:

This may help demo what you could do. I made up some data to facilitate the demo.

List<Map<String, Object>> maps = new ArrayList<>(List.of(
        Map.of("integer", 2, "string", "process", "double", 6.8),
        Map.of("integer", 5, "string", "test", "double", 7.8),
        Map.of("integer", 10, "string", "think", "double", 2.8),
        Map.of("integer", 7, "string", "think", "double", 2.8),
        Map.of("integer", 7, "string", "data", "double", 9.9)));

Define some functions as keyExtractors to access the map info.

Comparator<Map<String, Object>> icomp = Comparator
                .comparing(m -> (Integer) m.get("integer"));
Comparator<Map<String, Object>> dcomp = Comparator
                .comparing(m -> (Double) m.get("double"));
Comparator<Map<String, Object>> scomp = Comparator
                .comparing(m -> (String) m.get("string"));

Now build the comparator. This will sort the integers in ascending order followed by the doubles sorted in reverse order if the ints compare equally.

Comparator<Map<String, Object>> comp = 
        icomp.thenComparing(dcomp.reversed());
              

maps.sort(comp);
maps.forEach(System.out::println);

prints

{integer=2, double=6.8, string=process}
{integer=5, double=7.8, string=test}
{integer=7, double=9.9, string=data}
{integer=7, double=2.8, string=think}
{integer=10, double=3.8, string=think}
  • Related