7

I have a List of objects of the following class:

public class Foo {
    private Date date;
    private String name;
    private Long number;
}

This list is fetched from a database with order by date asc, number desc, but the part that need to be retained all the time is the ordering by date asc.

Example of the result (Dateformat = MM/dd/yyyy):

01/01/2016  Name1   928562
01/01/2016  Name2   910785
01/01/2016  Name3   811290
01/01/2016  Name4   811289
01/01/2016  Name5   5000000
02/01/2016  Name3   877702
02/01/2016  Name1   852960
02/01/2016  Name2   749640
02/01/2016  Name4   749500
02/01/2016  Name5   5000000

Now I want to order that list so that it results in:

01/01/2016  Name2   910785
01/01/2016  Name1   928562
01/01/2016  Name3   811290
01/01/2016  Name4   811289
01/01/2016  Name5   5000000
02/01/2016  Name2   749640
02/01/2016  Name1   852960
02/01/2016  Name3   877702
02/01/2016  Name4   749500
02/01/2016  Name5   5000000

As you can see, it is now sorted by date ascending and the name. The order for the names is stored in another List (NameSortingList):

Name2
Name1
Name3

Note that Name4 and Name5 are missing in the NameSortingList, can't be added to it and therefore should be added after everything that is ordered. Everything that comes after the ordered list can have any order.

If it makes it easier, everything that is not in the lsit can be merged into one Foo per unique date with name = "Other" which sums up the Numbers of all the elements in it. An example for a result like that:

01/01/2016  Name2   910785
01/01/2016  Name1   928562
01/01/2016  Name3   811290
01/01/2016  Other   5811289
02/01/2016  Name2   749640
02/01/2016  Name1   852960
02/01/2016  Name3   877702
02/01/2016  Other   5749500

My Current approach to this sorting is to extract all dates as unqiue values first, then build the NameSortingList and then iterate the data multipel times to add the data in the right ordering. The issue I have is

  1. It can miss entries if the name is not existing in the NameSortingList
  2. The performance is really really bad

data is a list of Foo as described at the very top:

List<String> sortedNames = data.stream().filter(e -> e.getDate().equals(getCurrentMonthDate()))
        .map(e -> e.getName()).collect(Collectors.toCollection(ArrayList<String>::new));

Set<Date> uniqueDates = data.stream().map(e -> e.getDate())
        .collect(Collectors.toCollection(LinkedHashSet<Date>::new));

List<Foo> sortedFoo= new ArrayList<Foo>();
for (Date d : uniqueDates) {
    for (String name : sortedNames) {
        for (Foo fr : data) {
            if (fr.Date().equals(d) && fr.getName().equals(name)) {
                sortedFoo.add(fr);
                break;
            }
        }
    }
}

How can I fix the 2 issues I described? Maybe there is even a stream solution to this which I couldn't wrap my head around?


If you have any questions, feel free to ask

XtremeBaumer
  • 6,275
  • 3
  • 19
  • 65
  • 1
    your actual data contains `02/01/2016 Name4 749500` as last record where data is 02/01/2016 and number is `749500`. I wonder on which basis you want to transform that record to `01/01/2016 Name4 749500` with date value `01/01/2016`? Note that I suggested an edit thinking it as a mistake. – vatsal mevada Nov 02 '18 at 12:55
  • There is no transformation at all. I just saw that mistake and corrected it – XtremeBaumer Nov 02 '18 at 12:57
  • My another doubt was with your `NameSortingList` which contains only `Name2,Name1,Name3` where as your example data also contains ordering `Name4`. How the name values other than `Name2,Name1,Name3` are supposed to be treated in this case? (again suggested an edit but was rejected :)) – vatsal mevada Nov 02 '18 at 13:01
  • I tried to clear it up. The ordering of elements which names are not in the list should only be sorted by date. Later on I merge them to one `Foo` per unique date with name = "Other" – XtremeBaumer Nov 02 '18 at 13:03
  • Just to sum it up to see if I understand it correctly: You have a list from the database that is sorted by `date asc, number desc` by default due to the query. You now want to sort it on `date asc, name desc` where the `name` are _not_ sorted alphabetically, but based on the order of they're in in the `nameSortingList` (where the names not in this list will be sorted at the end)? – Kevin Cruijssen Nov 02 '18 at 13:15
  • Do you have repeated names for a single date? i.e. Do you want to group by name as well, but only considering names that are in the other list, then creating a group named "Other"? – fps Nov 02 '18 at 13:16
  • @KevinCruijssen not necessarily `name desc` but you understood correctly. @Federico there are only unqiue names for each date, but everything not in the `NameSortingList` will be merged into 1 new entry with name "Other" – XtremeBaumer Nov 02 '18 at 13:21

4 Answers4

4

If I understand correctly, you have a list from the database that is sorted by date asc, number desc by default due to the query. You now want to sort it on date asc, name desc where the name are not sorted alphabetically, but based on the order they're in in the nameSortingList (where the names not in this list will be sorted at the end)?

If that is indeed the case, how about:

myList.sort(Comparator.comparing(Foo::getDate)
                      .thenComparing(foo-> {
  int index = nameSortingList.indexOf(foo.getName());
  return i == -1 ? // If not found, it should be sorted as trailing instead of leading name
    Integer.MAX_VALUE
   : // Otherwise, sort it on the index in the nameSortingList:
    i;} ));

EDIT: As correctly pointed out by @tobias_k in the comments. It might be best to first create a Map for your nameSortingList, where the names are the keys, and the index in the nameSortingList are the value. This will be better for performance, so you can change it to this instead:

myList.sort(Comparator.comparing(Foo::getDate)
                      .thenComparing(foo-> nameSortingMap.getOrDefault(foo.getName(), Integer.MAX_VALUE));

Although I doubt it will make that big of a difference for small lists.

Kevin Cruijssen
  • 9,153
  • 9
  • 61
  • 135
  • 1
    Instead of `nameSortingList.indexOf(foo.getName()` You could first create a Map` mapping names to indices so the lookup is O(1) and then use `thatMap.getOrDefault(name, Integer.MAX_VALUE)`. – tobias_k Nov 02 '18 at 13:25
  • @tobias_k : That was my approach, but it may not be any faster than indexOf() for very small lists. Thanks for the getOrDefault() suggestion, I'll augment my answer! – Wheezil Nov 02 '18 at 13:30
  • This sorts correctly, but doesn't group elements to the "Other" category. – fps Nov 02 '18 at 13:45
  • 1
    @FedericoPeraltaSchaffner "_**If it makes it easier**, everything that is not in the lsit can be merged into one `Foo` per unique date with `name = "Other"` which sums up the `Numbers` of all the elements in it._" I don't think it will make it easier, and I'm pretty sure it was an optional suggestion by OP that was added in his latest edit. – Kevin Cruijssen Nov 02 '18 at 13:46
  • 1
    Very nice solution. It solves all the problems I had. And indeed, the "Other" part was totally optional, as I alread got a solution to do that afterwards – XtremeBaumer Nov 02 '18 at 13:47
  • @Kevin Indeed :) I got confused because the same user asked a related question yesterday, and I know in advance that grouping is needed. But your answer is absolutely correct, +1 – fps Nov 02 '18 at 13:47
  • @XtremeBaumer You should try to sort and group all at once, I think that's your next challenge – fps Nov 02 '18 at 13:48
  • 1
    The crucial point is whether you can predict that the list will always be small. Then, a list is fine. You could use `myList.sort(Comparator.comparing(Foo::getDate) .thenComparingInt(foo -> nameSortingList.indexOf(foo.getName()) + Integer.MIN_VALUE))` then. Compare with [this answer](https://stackoverflow.com/a/35182824/2711488). – Holger Nov 04 '18 at 13:48
3

Create a secondary map establishing the name->ordinal order, then use that in your sorting. As specified, all missing names should be given the same order -- at the end. If this is not desired, you should add them dynamically first.

public class Sorter {
    static String input[] = {
        "01/01/2016  Name1   928562",
        "01/01/2016  Name2   910785",
        "01/01/2016  Name3   811290",
        "01/01/2016  Name4   811289",
        "02/01/2016  Name3   877702",
        "02/01/2016  Name1   852960",
        "02/01/2016  Name2   749640",
        "02/01/2016  Name4   749500",
        "02/01/2016  Name5   5000000"
    };
    static String names[] = { "Name2", "Name1", "Name3" };
    static class Foo {
        private Date date;
        private String name;
        private Long number;
        @Override
        public String toString() {
            return "Foo{" + "date=" + date + ", name=" + name + ", number=" + number + '}';
        }
    }
    static Foo parseInput(String s) throws Exception {
        Foo result = new Foo();
        String[] strs = s.split("  *");
        result.date = new SimpleDateFormat("dd/MM/yyyy").parse(strs[0]);
        result.name = strs[1];
        result.number = Long.parseLong(strs[2]);
        return result;
    }
    static class NameOrderCompare implements Comparator<Foo> {
        final Map<String,Integer> nameOrder = new HashMap<>();
        NameOrderCompare(String names[]) {
            for (String name : names) {
                nameOrder.put(name, nameOrder.size());
            }
        }
        @Override
        public int compare(Foo foo1, Foo foo2) {
            int cmp = foo1.date.compareTo(foo2.date);
            if (cmp != 0) return cmp;
            Integer order1 = nameOrder.getOrDefault(foo1.name, Integer.MAX_VALUE);
            Integer order2 = nameOrder.getOrDefault(foo2.name, Integer.MAX_VALUE);
            return order1 - order2;
        }
    }
    public static void main(String[] args) throws Exception {
        List<Foo> foos = new ArrayList<>();
        for (String s : input) {
            foos.add(parseInput(s));
        }
        Collections.sort(foos, new NameOrderCompare(names));
        for (Foo foo : foos) {
            System.out.println(foo);
        }
    }
}

When run this produces:

Foo{date=Fri Jan 01 00:00:00 MST 2016, name=Name2, number=910785}
Foo{date=Fri Jan 01 00:00:00 MST 2016, name=Name1, number=928562}
Foo{date=Fri Jan 01 00:00:00 MST 2016, name=Name3, number=811290}
Foo{date=Fri Jan 01 00:00:00 MST 2016, name=Name4, number=811289}
Foo{date=Sat Jan 02 00:00:00 MST 2016, name=Name2, number=749640}
Foo{date=Sat Jan 02 00:00:00 MST 2016, name=Name1, number=852960}
Foo{date=Sat Jan 02 00:00:00 MST 2016, name=Name3, number=877702}
Foo{date=Sat Jan 02 00:00:00 MST 2016, name=Name4, number=749500}
Foo{date=Sat Jan 02 00:00:00 MST 2016, name=Name5, number=5000000}

It should be noted that this does not take advantage of the fact that the input is already sorted by date. While it is not important for small data sets like this, it can matter especially when you have so much data that it should spill to disk. In that case you can adopt a blockwise strategy: read blocks by date, order them by name, output each sorted block. If blocks are small enough to fit in memory this can save the effort of a disk-based sort.

Sorry this answer is so verbose. There are doubtless more clever and compact ways to achieve the same end, but this should illustrate the concept clearly.

Wheezil
  • 3,157
  • 1
  • 23
  • 36
0

As long as there are no duplicate elements, I would just go with a single TreeSet, and provide a comparator for the elements sorting by date first, then name, then number.
(Even if there can be duplicate elements, I would just introduce a globally unique field, and finish sorting with that one)

public class Foo implements Comparable<Foo> {
  private Date date;
  private String name;
  private Long number;

  public int compareTo(Foo f) {
    if(!date.equals(f.date))return date.compareTo(f.date);
    if(!name.equals(f.name))return name.compareTo(f.name);
    return number-f.number;
  }
}

And add the items to a TreeSet<Foo>.


Well, if the order comes from a list, indexOf can be used and I would suggest storing the index in the object at construction time:
public class Foo implements Comparable<Foo> {
  private Date date;
  private String name;
  private Long number;
  private int index;

  public Foo(Date date, String name, Long number, List<String> NameSortingList) {
    this.date=date;
    this.name=name;
    this.number=number;
    index=NameSortingList.indexOf(name);
    if(index<0)index=Integer.MAX_VALUE;
  }

  public int compareTo(Foo f) {
    if(!date.equals(f.date))return date.compareTo(f.date);
    //if(!name.equals(f.name))return name.compareTo(f.name);
    if(index!=f.index)return index-f.index;
    return number-f.number;
  }
}
tevemadar
  • 12,389
  • 3
  • 21
  • 49
  • How exactly does that help with sorting based on `NameSortingList`? So far I only see a static ordering whereas mine needs to adapt the values of `NameSortingList` which can change every time I run the program – XtremeBaumer Nov 02 '18 at 12:59
  • @XtremeBaumer see second snippet. However now `number` has to be unique for `name`-s not appearing in `NameSortingList` (as their indices are going to be -1) – tevemadar Nov 02 '18 at 13:18
  • 1
    indexOf() will return -1 for the missing Name4 and sort it first, not last. – Wheezil Nov 02 '18 at 13:19
  • @Wheezil I am not sure if that was really specified. Yes, it appears in the example, but perhaps just in an ad-hoc manner. – tevemadar Nov 02 '18 at 13:22
  • I think it is pretty clear: "Note that Name4 and Name5 are missing in the NameSortingList, can't be added to it and therefore should be added after everything that is ordered. Everything that comes after the ordered list can have any order." To me this means "missing names come last, but there is no order amongst missing names" – Wheezil Nov 02 '18 at 13:25
  • @Wheezil ok, that appeared after an edit. Uncommented the relevant lines. – tevemadar Nov 02 '18 at 13:27
  • Ah, sorry I did not realize it is a moving target :-) – Wheezil Nov 02 '18 at 13:28
0

you can chain two comparators one for the date and the other for the name

      List<Foo> collect = data.stream().filter(e -> e.getDate().equals(LocalDate.now()))
                                     .sorted(Comparator.comparing(Foo::getDate)
                                                       .thenComparing(Foo::getName))
                              .collect(Collectors.toList());
SEY_91
  • 1,615
  • 15
  • 26