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}