I am trying to sort a list based on sort key and sort order I receive from an API.
For example,
I have a list with sortkey and sortorder and based on that I need to sort.
List<SortList> sortlist;
I have a list of an object :
List<Employee> employee;
I am able to sort using
Collections.sort(sourceList, Comparator
.comparing(Employee::getAge).reversed()
.thenComparing(Employee::getCount));
But i need to check the sortfeild on a condition and based on that only the field is considered for sorting.
ex:
if(sortkey = "name")
sortbythatkey from sortlist by the sort order
if (sortkey = "place")
sortbythat key from sortlist by the sort order
So here if sortlist has both name and place then it should sort by both key and order
Any idea how could i achieve this?
Sort List contains:
{
"sortKey":"name",
"sortOrder":"ASC"
},
{
"sortKey":"place",
"sortOrder":"DESC"
}
Requirement is to chain them together like ORDER BY in SQL
Assuming that sortlist is a list of SortCriteria, which is a class like this:
class SortCritera {
private String key;
private String order;
public String getKey() {
return key;
}
public String getOrder() {
return order;
}
// constructors, setters...
}
You first need a HashMap<String, Comparator<Employee>> to store all the corresponding comparators for each possible key:
HashMap<String, Comparator<Employee>> comparators = new HashMap<>();
comparators.put("name", Comparator.comparing(Employee::getName));
comparators.put("age", Comparator.comparing(Employee::getAge));
// ...
Then you can loop through the sortlist and keep calling thenComparing:
Comparator<Employee> comparator = comparators.get(sortlist.get(0).getKey());
if (sortlist.get(0).getOrder().equals("DESC")) {
comparator = comparator.reversed();
}
for(int i = 1 ; i < sortlist.size() ; i++) {
if (sortlist.get(i).getOrder().equals("DESC")) {
comparator = comparator.thenComparing(comparators.get(sortlist.get(i).getKey()).reversed());
} else {
comparator = comparator.thenComparing(comparators.get(sortlist.get(i).getKey()));
}
}
// now you can sort with "comparator".
As Holger has suggested, you can use the Stream API to do this as well:
sortlist.stream().map(sc -> {
Comparator<Employee> c = comparators.get(sc.getKey());
return sc.getOrder().equals("DESC")? c.reversed(): c;
}).reduce(Comparator::thenComparing)
.ifPresent(x -> Collections.sort(originalList, x));
You can create a method which when passed the sort key, you provide the proper Comparator:
public Comparator<Employee> getComparator(String sortKey) {
if("name".equals(sortKey)) {
return Comparator.comparing(Employee::getName);
} else if ("place".equals(sortKey) {
return Comparator.comparing(Employee::getPlace);
} else {
throw new IllegalArgumentException();
}
}
To call it it would simply be:
Collections.sort(sourceList, getComparator(sortKey).reversed()
.thenComparing(Employee::getCount));
While you could also write your own, I find it is better to delegate the "standard" parts and simply write the part that differs from this.
If you find yourself having many such sort keys, then a more suitable means to do this would be to use a map:
private static final Map<String, Comparator<Employee>> COMPARE_MAP = new HashMap<>() {{
put.("name", Comparator.comparing(Employee::getName));
put.("place", Comparator.comparing(Employee::getPlace));
}});
public Comparator<Employee> getComparator(String sortKey) {
if(COMPARE_MAP.containsKey(sortKey)) {
return COMPARE_MAP.get(sortKey);
} else {
throw new IllegalArgumentException();
}
}
Reflection is also an option, but I would be cautious to use reflection unless it becomes impractical to do otherwise. In that case, you could create your own annotation to determine which fields of class Employee can be used for sorting.
Related
I would like to build a Sort object based on Map<Column, Direction>. I have a problem with the fact that the Sort class only has a private constructor, it just has to be created by the static method by() or and(), therefore I have a problem with initialising the sort object with the first element from the map.
private Sort buildSort(Map<WorklistColumn, Direction> columnsDirectionsmap){
Sort sort = by("wartość inicjalna której nie chcemy", Direction.Ascending);
for (Map.Entry<WorklistColumn, Direction> columnWithDirection : columnsDirectionsmap.entrySet()) {
sort.and(columnWithDirection.getKey().toString(), columnWithDirection.getValue());
}
return sort;
}
public class Sort {
private List<Column> columns = new ArrayList();
private Sort() {
}
public static Sort by(String column) {
return (new Sort()).and(column);
}
public static Sort by(String column, Direction direction) {
return (new Sort()).and(column, direction);
}
public Sort and(String name) {
this.columns.add(new Column(name));
return this;
}
public Sort and(String name, Direction direction) {
this.columns.add(new Column(name, direction));
return this;
}
Build a Sort object from a map
I think the question is about the fact that to fully configure a Sort object from your map, you need to use the first map entry in conjunction with Sort.by(), and then use all the other entries in conjunction with Sort.and(). That is, the first entry requires different handling than the rest.
There are lots of ways of dealing with that, but the one I'm going to suggest is to work directly with the iterator of the map's entry set. Something like this:
private Sort buildSort(Map<WorklistColumn, Direction> columnsDirectionsMap) {
if (columnsDirectionsMap.isEmpty()) {
throw new NoCriteriaException(); // or whatever
}
Iterator<Map.Entry<WorklistColumn, Direction>> criterionIterator =
columnsDirectionsMap.entrySet().iterator();
Map.Entry<WorklistColumn, Direction> criterion = criterionIterator.next();
Sort sort = Sort.by(criterion.key().toString(), criterion.value());
while (criterionIterator.hasNext()) {
criterion = criterionIterator.next();
sort.and(criterion.key().toString(), criterion.value());
}
return sort;
}
Do note that depending on the Map implementation involved, the order of the entries may not be easily predictable. I assume that you need control of that order for this approach to work as desired, so it's on you to choose a Map implementation that provides that. A LinkedHashMap might be suitable, for example, but probably not a HashMap.
What I need is to order a list in a custom way, I'm looking into the correct way and found guava's Ordering api but the thing is that the list I'm ordering is not always going to be the same, and I just need 2 fields to be at the top of the list, for example I have this:
List<AccountType> accountTypes = new ArrayList<>();
AccountType accountType = new AccountType();
accountType.type = "tfsa";
AccountType accountType2 = new AccountType();
accountType2.type = "rrsp";
AccountType accountType3 = new AccountType();
accountType3.type = "personal";
accountTypes.add(accountType3);
accountTypes.add(accountType2);
accountTypes.add(accountType);
//The order I might have is : ["personal", "rrsp", "tfsa"]
//The order I need is first "rrsp" then "tfsa" then anything else
I tried with a custom comparator and using Ordering in Guava library, something like this:
public static class SupportedAccountsComparator implements Comparator<AccountType> {
Ordering<String> ordering = Ordering.explicit(ImmutableList.of("rrsp", "tfsa"));
#Override
public int compare(AccountType o1, AccountType o2) {
return ordering.compare(o1.type, o2.type);
}
}
but it throws an exception because explicit ordering doesnt support other items that are not in the list you provided, is there a way to do a partial explicit ordering? something like:
Ordering.explicit(ImmutableList.of("rrsp", "tfsa")).anythingElseWhatever();
You don't need Guava for this, everything you need is in the Collections API.
Assuming AccountType implements Comparable, you can just provide a Comparator that returns minimum values for "tfsa" and "rrsp", but leaves the rest of the sorting to AccountType's default comparator:
Comparator<AccountType> comparator = (o1, o2) -> {
if(Objects.equals(o1.type, "rrsp")) return -1;
else if(Objects.equals(o2.type, "rrsp")) return 1;
else if(Objects.equals(o1.type, "tfsa")) return -1;
else if(Objects.equals(o2.type, "tfsa")) return 1;
else return o1.compareTo(o2);
};
accountTypes.sort(comparator);
If you don't want your other items sorted, just provide a default comparator that always returns 0.
Here's a Comparator solution that uses a List of strings to represent your sorting order. Change your sorting order by merely changing the order of the strings in your sortOrder list.
Comparator<AccountType> accountTypeComparator = (at1, at2) -> {
List<String> sortOrder = Arrays.asList(
"rrsp",
"tfsa",
"third"
);
int i1 = sortOrder.contains(at1.type) ? sortOrder.indexOf(at1.type) : sortOrder.size();
int i2 = sortOrder.contains(at2.type) ? sortOrder.indexOf(at2.type) : sortOrder.size();
return i1 - i2;
};
accountTypes.sort(accountTypeComparator);
Given a list of objects (List<MyClass> objects).
class MyClass {
int id;
String name;
}
And a list with names:
name1
name2
name3
Whats a nice way to write a comparator to use the list of names as a priority list and if a priority doesnt exist for a name use alphabetic ordering?
I would suggest, that you use the java.util.Collections.sort method, and provide a custom comparator.
// Define a new static comparator attribute for your class
public static Comparator<MyClass> MY_COMPARATOR = new Comparator<>() {
#Override
public int compare(MyClass o1, MyClass o2) {
return o1.name.compareTo(o2.name); // or whatever logic
}
};
//Then just call this to sort when you need it
List<MyClass> myList; // initialised somewhere
Collections.sort(myList, MY_COMPARATOR);
If you're using java 8+, then the code to create the comparator is even shorter:
public static Comparator<MyClass> MY_COMPARATOR = (o1, o2) -> o1.name.compareTo(o2.name);
Put the strings into an array and loop through it to see which one you encounter first.
public class NameComparator implements Comparator {
static private [] String strNames = {"Ken", "Alisia", "Ben"};
public int compare(MyClass objX, MyClass objY) {
String x = objX.Name;
String y = objY.Name;
String strCurrentName;
if(x.equals(y)) {
return 0;
}
for(strCurrentName: strNames) {
if(strCurrentName.equals(x)) {
return 1;
}
if(strCurrentName.equals(y)) {
return -1;
}
}
return x.compareTo(y);
}
}
Sorting with this comparator would give you, for instance, "Ken", "Alicia", "Michelle" and "Nancy".
If speed is an issue you could put the names in a HashMap instead of an array. The code would then be quite different, I can give you an example if you are interested.
I have bunch of log files and I want to process them in java, but I want to sort them first so I can have more human readable results.
My Log Class :
public class Log{
//only relevant fields here
private String countryCode;
private AccessType accessType;
...etc..
}
AccessType is Enum, which has values WEB, API, OTHER.
I'd like to group Log objects by both countryCode and accessType, so that end product would be log list.
I got this working for grouping Logs into log list by countryCode like this :
public List<Log> groupByCountryCode(String countryCode) {
Map<String, List<Log>> map = new HashMap<String, List<Log>>();
for (Log log : logList) {
String key = log.getCountryCode();
if (map.get(key) == null) {
map.put(key, new ArrayList<Log>());
}
map.get(key).add(log);
}
List<Log> sortedByCountryCodeLogList = map.get(countryCode);
return sortedByCountryCodeLogList;
}
from this #Kaleb Brasee example :
Group by field name in Java
Here is what I've been trying for some time now, and really stuck now ..
public List<Log> groupByCountryCode(String countryCode) {
Map<String, Map<AccessType, List<Log>>> map = new HashMap<String, Map<AccessType, List<Log>>>();
AccessType mapKey = null;
List<Log> innerList = null;
Map<AccessType, List<Log>> innerMap = null;
// inner sort
for (Log log : logList) {
String key = log.getCountryCode();
if (map.get(key) == null) {
map.put(key, new HashMap<AccessType, List<Log>>());
innerMap = new HashMap<AccessType, List<Log>>();
}
AccessType innerMapKey = log.getAccessType();
mapKey = innerMapKey;
if (innerMap.get(innerMapKey) == null) {
innerMap.put(innerMapKey, new ArrayList<Log>());
innerList = new ArrayList<Log>();
}
innerList.add(log);
innerMap.put(innerMapKey, innerList);
map.put(key, innerMap);
map.get(key).get(log.getAccessType()).add(log);
}
List<Log> sortedByCountryCodeLogList = map.get(countryCode).get(mapKey);
return sortedByCountryCodeLogList;
}
I'm not sure I know what I'm doing anymore
Your question is confusing. You want to sort the list, but you are creating many new lists, then discarding all but one of them?
Here is a method to sort the list. Note that Collections.sort() uses a stable sort. (This means that the original order of items within a group of country code and access type is preserved.)
class MyComparator implements Comparator<Log> {
public int compare(Log a, Log b) {
if (a.getCountryCode().equals(b.getCountryCode()) {
/* Country code is the same; compare by access type. */
return a.getAccessType().ordinal() - b.getAccessType().ordinal();
} else
return a.getCountryCode().compareTo(b.getCountryCode());
}
}
Collections.sort(logList, new MyComparator());
If you really want to do what your code is currently doing, at least skip the creation of unnecessary lists:
public List<Log> getCountryAndAccess(String cc, AccessType access) {
List<Log> sublist = new ArrayList<Log>();
for (Log log : logList)
if (cc.equals(log.getCountryCode()) && (log.getAccessType() == access))
sublist.add(log);
return sublist;
}
If you're able to use it, Google's Guava library has an Ordering class that might be able to help simplify things. Something like this might work:
Ordering<Log> byCountryCode = new Ordering<Log>() {
#Override
public int compare(Log left, Log right) {
return left.getCountryCode().compareTo(right.getCountryCode());
}
};
Ordering<Log> byAccessType = new Ordering<Log>() {
#Override
public int compare(Log left, Log right) {
return left.getAccessType().compareTo(right.getAccessType());
}
};
Collections.sort(logList, byCountryCode.compound(byAccessType));
You should create the new inner map first, then add it to the outer map:
if (map.get(key) == null) {
innerMap = new HashMap<AccessType, List<Log>>();
map.put(key, innerMap);
}
and similarly for the list element. This avoids creating unnecessary map elements which will then be overwritten later.
Overall, the simplest is to use the same logic as in your first method, i.e. if the element is not present in the map, insert it, then just get it from the map:
for (Log log : logList) {
String key = log.getCountryCode();
if (map.get(key) == null) {
map.put(key, new HashMap<AccessType, List<Log>>());
}
innerMap = map.get(key);
AccessType innerMapKey = log.getAccessType();
if (innerMap.get(innerMapKey) == null) {
innerMap.put(innerMapKey, new ArrayList<Log>());
}
innerMap.get(innerMapKey).add(log);
}
Firstly, it looks like you're adding each log entry twice with the final line map.get(key).get(log.getAccessType()).add(log); inside your for loop. I think you can do without that, given the code above it.
After fixing that, to return your List<Log> you can do:
List<Log> sortedByCountryCodeLogList = new ArrayList<Log>();
for (List<Log> nextLogs : map.get(countryCode).values()) {
sortedByCountryCodeLogList.addAll(nextLogs);
}
I think that code above should flatten it down into one list, still grouped by country code and access type (not in insertion order though, since you used HashMap and not LinkedHashMap), which I think is what you want.
Assuming I have
final Iterable<String> unsorted = asList("FOO", "BAR", "PREFA", "ZOO", "PREFZ", "PREFOO");
What can I do to transform this unsorted list into this:
[PREFZ, PREFA, BAR, FOO, PREFOO, ZOO]
(a list which begin with known values that must appears first (here "PREFA" and "PREFZ") and the rest is alphabetically sorted)
I think there are some usefull classes in guava that can make the job (Ordering, Predicates...), but I have not yet found a solution...
I would keep separate lists.
One for known values and unknown values. And sort them separately, when you need them in a one list you can just concatenate them.
knownUnsorted.addAll(unsorted.size - 1, unknonwUnsorted);
I suggest filling List with your values and using Collections.sort(...).
Something like
Collections.sort(myList, new FunkyComparator());
using this:
class FunkyComparator implements Comparator {
private static Map<String,Integer> orderedExceptions =
new HashMap<String,Integer>(){{
put("PREFZ", Integer.valueOf(1));
put("PREFA", Integer.valueOf(2));
}};
public int compare(Object o1, Object o2) {
String s1 = (String) o1;
String s2 = (String) o2;
Integer i1 = orderedExceptions.get(s1);
Integer i2 = orderedExceptions.get(s2);
if (i1 != null && i2 != null) {
return i1 - i2;
}
if (i1 != null) {
return -1;
}
if (i2 != null) {
return +1;
}
return s1.compareTo(s2);
}
}
Note: This is not the most efficient solution. It is just a simple, straightforward solution that gets the job done.
I would first use Collections.sort(list) to sort the list.
Then, I would remove the known items, and add them to the front.
String special = "PREFA";
if (list.remove(special)
list.add(0, special);
Or, if you have a list of array of these values you need in the front you could do:
String[] knownValues = {};
for (String s: knownValues) {
if (list.remove(s))
list.add(0, s);
}
Since I'm a fan of the guava lib, I wanted to find a solution using it. I don't know if it's efficient, neither if you find it as simple as others solution, but it's here:
final Iterable<String> all = asList("FOO", "BAR", "PREFA", "ZOO", "PREFOO", "PREFZ");
final List<String> mustAppearFirst = asList("PREFZ", "PREFA");
final Iterable<String> sorted =
concat(
Ordering.explicit(mustAppearFirst).sortedCopy(filter(all, in(mustAppearFirst))),
Ordering.<String>natural().sortedCopy(filter(all, not(in(mustAppearFirst)))));
You specifically mentioned guava; along with Sylvain M's answer, here's another way (more as an academic exercise and demonstration of guava's flexibility than anything else)
// List is not efficient here; for large problems, something like SkipList
// is more suitable
private static final List<String> KNOWN_INDEXES = asList("PREFZ", "PREFA");
private static final Function<Object, Integer> POSITION_IN_KNOWN_INDEXES
= new Function<Object, Integer>() {
public Integer apply(Object in) {
int index = KNOWN_INDEXES.indexOf(in);
return index == -1 ? null : index;
}
};
...
List<String> values = asList("FOO", "BAR", "PREFA", "ZOO", "PREFZ", "PREFOO");
Collections.sort(values,
Ordering.natural().nullsLast().onResultOf(POSITION_IN_KNOWN_INDEXES).compound(Ordering.natural())
);
So, in other words, sort on natural order of the Integer returned by List.indexOf(), then break ties with natural order of the object itself.
Messy, perhaps, but fun.
I would also use Collections.sort(list) but I think I would use a Comparator and within the comparator you could define your own rules, e.g.
class MyComparator implements Comparator<String> {
public int compare(String o1, String o2) {
// Now you can define the behaviour for your sorting.
// For example your special cases should always come first,
// but if it is not a special case then just use the normal string comparison.
if (o1.equals(SPECIAL_CASE)) {
// Do something special
}
// etc.
return o1.compareTo(o2);
}
}
Then sort by doing:
Collections.sort(list, new MyComparator());