You want to compare parts consisting entirely of digits (number) and entirely of non-digits (text) part by part.
The comparison below loops over (text, number?).
If only one string starts with a number, it has an empty text as first part, and will be considered smaller.
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
Pattern digits = Pattern.compile("\\d+");
Matcher m1 = digits.matcher(o1);
Matcher m2 = digits.matcher(o2);
int i1 = 0;
int i2 = 0;
while (i1 < o1.length() && i2 < o2.length()) {
boolean b1 = m1.find();
int j1 = b1 ? m1.start() : o1.length();
boolean b2 = m2.find();
int j2 = b2 ? m2.start() : o2.length();
String part1 = o1.substring(i1, j1);
String part2 = o2.substring(i2, j2);
int cmp = String.compareIgnoreCase(part1, part2);
if (cmp != 0) {
return;
}
if (b1 && b2) {
int num1 = Integer.parseInt(m1.group());
int num2 = Integer.parseInt(m2.group());
cmp = Integer.compare(num1, num2);
i1 = m1.end();
i2 = m2.end();
} else if (b1) {
return -1;
} else if (b2) {
return 1;
}
}
return 0;
}
});
In java 8, with a so called lambda:
Collections.sort(names, (o1, o2) -> {
Pattern digits = Pattern.compile("\\d+");
Matcher m1 = digits.matcher(o1);
Matcher m2 = digits.matcher(o2);
int i1 = 0;
int i2 = 0;
while (i1 < o1.length() && i2 < o2.length()) {
boolean b1 = m1.find();
int j1 = b1 ? m1.start() : o1.length();
boolean b2 = m2.find();
int j2 = b2 ? m2.start() : o2.length();
String part1 = o1.substring(i1, j1);
String part2 = o2.substring(i2, j2);
int cmp = String.compareIgnoreCase(part1, part2);
if (cmp != 0) {
return;
}
if (b1 && b2) {
int num1 = Integer.parseInt(m1.group());
int num2 = Integer.parseInt(m2.group());
cmp = Integer.compare(num1, num2);
i1 = m1.end();
i2 = m2.end();
} else if (b1) {
return -1;
} else if (b2) {
return 1;
}
}
return 0;
});
This is quite verbose, and there is a "simple" solution since java 9:
simply format all numbers to a fixed width, here left-padded with zeroes upto 10 positions.
Collections.sort(names, (o1, o2) ->
Strings.compareIgnoreCase(
o1.replaceAll("\\d+", mr -> String.format("%010d", Integer.parseInt(mr.group())),
o2.replaceAll("\\d+", mr -> String.format("%010d", Integer.parseInt(mr.group())))
);
Since java 9 there is an overloaded String.replaceAll
that can be passed a replacing function.
Even a bit more elegant by not repeating one-self:
Function<String, String> numFormatter = s -> s.replaceAll("\\d+",
mr -> String.format("%010d", Integer.parseInt(mr.group())));
Collections.sort(names, (o1, o2) ->
Strings.compareIgnoreCase(numFormatter.apply(o1), numFormatter.apply(o2.))
);
And finally there exists a utility function for any conversion, or passing a getter of a field: Comparator.comparing(converter)
and Comparator.comparing(converter, otherComparator)
.
To sort it by your locale/language:
Locale locale = new Locale("pl", "PL");
Collator collator = Collator.getInstance(locale); // How to sort on special letters
Function<String, String> numFormatter = s -> s /*.toUpperCase(locale)*/ .replaceAll("\\d+",
mr -> String.format("%010d", Integer.parseInt(mr.group())));
Collections.sort(names, Comparator.comparing(numFormatter, collator));
The Collator is a Comparator but with built-in sorting for the given language. It behaves better on accented letters. I dropped the case insensitive comparison here, as it might not be needed; otherwise use String.toUpperCase(Locale)
.
This is a bit much, I am not entirely sure about Android's java, or whether the code compiles (typos), but enjoy.