Sort Array List lexicographically ignoring integers

72 Views Asked by At

I have this code. I want to order a list of strings. Every item in the list consists of a three word sentence. I want to ignore the first word and sort the sentence lexicographically with the 2nd and 3rd words. If the 2nd or 3rd words contain an integer, I want to ignore sorting them but add them to the end of the list. For example: (19th apple orange, 17th admin 7th, 19th apple table) should be sorted in the list as (19th apple orange, 19th apple table, 17th admin 7th) So far my code only ignores the first word and sort lexicographically the rest of the lists

public static List<String> sortOrders(List<String> orderList) {
     // Write your code here
    Collections.sort( orderList,
    (a, b) -> a.split(" *", 2)[1].compareTo( b.split(" *", 2)[1] )
    );
    
    return orderList;
}
2

There are 2 best solutions below

0
OscarRyz On

In your compare method check for numbers first and then strings. You just have to add code to the steps you described:

Here's a pseudo code of what you described

...
(a,b) -> {
    // Every item in the list consists of a three word sentence.
    var awords = a.split(" ")
    var bwords = a.split(" ")
   
    // I want to ignore the first word
    var as = awords[1] + " " awords[2]
    var bs ...  
    //  and sort the sentence lexicographically with the 2nd and 3rd words.
    var r = as.compareTo(bs) 
    
    // If the 2nd or 3rd words contain an integer, I want to ignore sorting them but add them to the end of the list
    if ( as.matches(".*\\d.*) ) {
        return -1
     } else { 
        return r
     }

}
...

It's not clear what to do if both have numbers, e.g. a 1 a vs a 1 b, but that's something you have to clarify.

So basically you just have to go, divide each of the statements in your problem and add some code that solves it (like the example below )

You might notice there are some gaps (like what to do if two of them have strings). Once you have a working solution you can clean it up.

Another alternative with a similar idea

var as = a.substring(a.indexOf(" ")) // "a b c" -> "b c"
var bs = b.substring(b.indexOf(" ")) // "a b c" -> "b c"
return as.matches("\\d+") ? -1 : as.compareTo(bs);

Remember the compare(T,T) method returns < 0 if a is "lower" than b, so if a has numbers, it will always be "higher" thus should return 1, if b has numbers then a will be "lower", thus it should return -1, otherwise just compare the strings

Here's the full program:

import java.util.*;

public class Sl {

  public static void main(String ... args ) {

    var list = Arrays.asList("19th apple orange", "17th admin 7th", "19th apple table");

    Collections.sort(list, (a, b) -> {
      // just use the last two words
      var as = a.substring(a.indexOf(" "));
      var bs = b.substring(b.indexOf(" "));

      // if a has a number, will always be higher
      return as.matches(".*\\d+.*") ? 1 
         // if b has a number, a will always be lower
         : bs.matches(".*\\d+.*") ? -1 
         // if none of the above, compare lexicographically the strings
         : as.compareTo(bs);
    });

    System.out.println(list);
  }
}
0
WJS On

If you aren't careful, you will get an error such as Exception in thread "main" java.lang.IllegalArgumentException: Comparison method violates its general contract!

In order to prevent that you can do it as follows by creating a comparator that parses the string and checks the second and third elements for integers. If the first element has not integers but the second one does, it will be sent to the bottom since the second one is considered greater by returning a 1. But the next condition must only check on the second element and return a -1 indicating that it is smaller than the one so gain, it goes to the bottom of the list.

public static List<String> sortOrders(List<String> orderList) {
    Comparator<String> comp = (a, b) -> {
        String[] aa = a.split("\\s+", 2);
        String[] bb = b.split("\\s+", 2);
        boolean aam = aa[1].matches(".*[0-9]+.*");
        boolean bbm = bb[1].matches(".*[0-9]+.*");
        
        return aam && !bbm ? 1 : bbm ? -1 :
           aa[1].compareTo(bb[1]);
    };
    
    return orderList.stream().sorted(comp).toList();    
}

If you want to preserve your original data, use the above. If you want to sort in place, then apply the Comparator defined above and use Collections.sort(data, comp).

I have tested this extensively using the following data generation code which generated random strings meeting your requirements. I suggest you test any answers you get (including this one) to ensure it satisfies your requirements.

String letters = "abcdefghijklmnopqrstuvwxyz";
Random r = new Random(123);
List<String> data = r.ints(200000, 1, 100).mapToObj(i -> {
    StringBuilder sb = new StringBuilder();
    boolean first = r.nextBoolean();
    boolean second = r.nextBoolean();
    int ltr = r.nextInt(letters.length());
    String fstr = letters.substring(ltr,ltr+1);
    ltr = r.nextInt(letters.length());
    String sstr = letters.substring(ltr,ltr+1);
    sb.append(fstr).append(first ? ltr : "").append(" ");
    sb.append(fstr);
    if (first) {
        sb.append(r.nextInt(100));
    }
    sb.append(" ").append(sstr);
    
    if (!first && second) {
        sb.append(r.nextInt(100));
    }
     
    return sb.toString();
}).collect(Collectors.toCollection(ArrayList::new));