web-dev-qa-db-fra.com

Trier sur une chaîne pouvant contenir un nombre

J'ai besoin d'écrire une classe Java Comparator qui compare les chaînes, mais avec une torsion. Si les deux chaînes comparées sont identiques au début et à la fin de la chaîne, la partie centrale qui diffère est un entier, puis comparez en fonction des valeurs numériques de ces entiers. Par exemple, je veux que les chaînes suivantes se retrouvent dans l'ordre indiqué:

  • aaa
  • bbb 3 ccc
  • bbb 12 ccc
  • ccc 11
  • ddd
  • eee 3 ddd jpeg2000 eee
  • eee 12 ddd jpeg2000 eee

Comme vous pouvez le constater, il peut y avoir d'autres nombres entiers dans la chaîne. Je ne peux donc pas simplement utiliser des expressions régulières pour décomposer un nombre entier. Je songe à faire marcher les cordes du début jusqu'à ce que je trouve un morceau qui ne correspond pas, puis à partir de la fin jusqu'à ce que je trouve un peu expression régulière "[0-9] +", et si elle compare, puis faire une comparaison numérique, sinon faire une comparaison lexicale.

Y a-t-il un meilleur moyen?

Mise à jour Je ne pense pas pouvoir garantir que les autres nombres de la chaîne, ceux qui peuvent correspondre, ne sont pas entourés d'espaces ou ceux qui diffèrent le sont.

71
Paul Tomblin

L'algorithme d'Alphanum

Du site

"Les utilisateurs trient les chaînes avec des nombres différemment des logiciels. La plupart des algorithmes de tri comparent les valeurs ASCII, ce qui produit un ordre incohérent avec la logique humaine. Voici comment y remédier".

Edit: Voici un lien vers/ Java Comparator Implementation à partir de ce site.

96
ScArcher2

Petit défi intéressant, j'ai adoré le résoudre.

Voici mon point de vue sur le problème:

String[] strs =
{
  "eee 5 ddd jpeg2001 eee",
  "eee 123 ddd jpeg2000 eee",
  "ddd",
  "aaa 5 yy 6",
  "ccc 555",
  "bbb 3 ccc",
  "bbb 9 a",
  "",
  "eee 4 ddd jpeg2001 eee",
  "ccc 11",
  "bbb 12 ccc",
  "aaa 5 yy 22",
  "aaa",
  "eee 3 ddd jpeg2000 eee",
  "ccc 5",
};

Pattern splitter = Pattern.compile("(\\d+|\\D+)");

public class InternalNumberComparator implements Comparator
{
  public int compare(Object o1, Object o2)
  {
    // I deliberately use the Java 1.4 syntax, 
    // all this can be improved with 1.5's generics
    String s1 = (String)o1, s2 = (String)o2;
    // We split each string as runs of number/non-number strings
    ArrayList sa1 = split(s1);
    ArrayList sa2 = split(s2);
    // Nothing or different structure
    if (sa1.size() == 0 || sa1.size() != sa2.size())
    {
      // Just compare the original strings
      return s1.compareTo(s2);
    }
    int i = 0;
    String si1 = "";
    String si2 = "";
    // Compare beginning of string
    for (; i < sa1.size(); i++)
    {
      si1 = (String)sa1.get(i);
      si2 = (String)sa2.get(i);
      if (!si1.equals(si2))
        break;  // Until we find a difference
    }
    // No difference found?
    if (i == sa1.size())
      return 0; // Same strings!

    // Try to convert the different run of characters to number
    int val1, val2;
    try
    {
      val1 = Integer.parseInt(si1);
      val2 = Integer.parseInt(si2);
    }
    catch (NumberFormatException e)
    {
      return s1.compareTo(s2);  // Strings differ on a non-number
    }

    // Compare remainder of string
    for (i++; i < sa1.size(); i++)
    {
      si1 = (String)sa1.get(i);
      si2 = (String)sa2.get(i);
      if (!si1.equals(si2))
      {
        return s1.compareTo(s2);  // Strings differ
      }
    }

    // Here, the strings differ only on a number
    return val1 < val2 ? -1 : 1;
  }

  ArrayList split(String s)
  {
    ArrayList r = new ArrayList();
    Matcher matcher = splitter.matcher(s);
    while (matcher.find())
    {
      String m = matcher.group(1);
      r.add(m);
    }
    return r;
  }
}

Arrays.sort(strs, new InternalNumberComparator());

Cet algorithme nécessite beaucoup plus de tests, mais il semble se comporter plutôt bien.

[EDIT] J'ai ajouté quelques commentaires supplémentaires pour être plus clair. Je vois qu'il y a beaucoup plus de réponses que quand j'ai commencé à coder ceci ... Mais j'espère avoir fourni une bonne base de départ et/ou quelques idées.

12
PhiLho

Ian Griffiths de Microsoft a une implémentation C # qu'il appelle Natural Sorting . Le portage sur Java devrait être assez facile, plus facile que de C de toute façon!

UPDATE: Il semble y avoir un exemple Java sur eekboom qui fait cela, voir "compareNatural" et utilisez-le comme comparateur pour le tri.

8
Ray Hayes

Je me rends compte que vous êtes en Java, mais vous pouvez jeter un coup d'œil à la manière dont StrCmpLogicalW fonctionne. C'est ce que l'explorateur utilise pour trier les noms de fichiers dans Windows. Vous pouvez regarder la mise en œuvre de WINE ici .

5
Eclipse

La mise en œuvre que je propose ici est simple et efficace. Il n'alloue pas de mémoire supplémentaire, directement ou indirectement, à l'aide d'expressions régulières ou de méthodes telles que substring (), split (), toCharArray (), etc. 

Cette mise en œuvre passe d'abord par les deux chaînes pour rechercher les premiers caractères différents, à la vitesse maximale, sans effectuer de traitement spécial pendant cette opération. La comparaison de numéros spécifiques est déclenchée uniquement lorsque ces caractères sont composés de deux chiffres. Un effet secondaire de cette implémentation est qu'un chiffre est considéré comme plus grand que les autres lettres, contrairement à l'ordre lexicographique par défaut.

public static final int compareNatural (String s1, String s2)
{
   // Skip all identical characters
   int len1 = s1.length();
   int len2 = s2.length();
   int i;
   char c1, c2;
   for (i = 0, c1 = 0, c2 = 0; (i < len1) && (i < len2) && (c1 = s1.charAt(i)) == (c2 = s2.charAt(i)); i++);

   // Check end of string
   if (c1 == c2)
      return(len1 - len2);

   // Check digit in first string
   if (Character.isDigit(c1))
   {
      // Check digit only in first string 
      if (!Character.isDigit(c2))
         return(1);

      // Scan all integer digits
      int x1, x2;
      for (x1 = i + 1; (x1 < len1) && Character.isDigit(s1.charAt(x1)); x1++);
      for (x2 = i + 1; (x2 < len2) && Character.isDigit(s2.charAt(x2)); x2++);

      // Longer integer wins, first digit otherwise
      return(x2 == x1 ? c1 - c2 : x1 - x2);
   }

   // Check digit only in second string
   if (Character.isDigit(c2))
      return(-1);

   // No digits
   return(c1 - c2);
}
5
Olivier OUDOT

Divisez la chaîne en séries de lettres et de chiffres afin que "foo 12 bar" devienne la liste ("foo", 12, "bar"), puis utilisez la liste comme clé de tri. De cette façon, les numéros seront classés par ordre numérique et non par ordre alphabétique.

4
John Millikin

Je suis arrivé à une implémentation assez simple en Java en utilisant des expressions régulières:

public static Comparator<String> naturalOrdering() {
    final Pattern compile = Pattern.compile("(\\d+)|(\\D+)");
    return (s1, s2) -> {
        final Matcher matcher1 = compile.matcher(s1);
        final Matcher matcher2 = compile.matcher(s2);
        while (true) {
            final boolean found1 = matcher1.find();
            final boolean found2 = matcher2.find();
            if (!found1 || !found2) {
                return Boolean.compare(found1, found2);
            } else if (!matcher1.group().equals(matcher2.group())) {
                if (matcher1.group(1) == null || matcher2.group(1) == null) {
                    return matcher1.group().compareTo(matcher2.group());
                } else {
                    return Integer.valueOf(matcher1.group(1)).compareTo(Integer.valueOf(matcher2.group(1)));
                }
            }
        }
    };
}

Voici comment cela fonctionne:

final List<String> strings = Arrays.asList("x15", "xa", "y16", "x2a", "y11", "z", "z5", "x2b", "z");
strings.sort(naturalOrdering());
System.out.println(strings);

[x2a, x2b, x15, xa, y11, y16, z, z, z5]

3
Helder Pereira

Le Alphanum algrothim est Nice, mais il ne correspondait pas aux exigences d’un projet sur lequel je travaille. Je dois pouvoir trier correctement les nombres négatifs et les nombres décimaux. Voici la mise en œuvre je suis venu. Tout retour serait apprécié.

public class StringAsNumberComparator implements Comparator<String> {

    public static final Pattern NUMBER_PATTERN = Pattern.compile("(\\-?\\d+\\.\\d+)|(\\-?\\.\\d+)|(\\-?\\d+)");

    /**
     * Splits strings into parts sorting each instance of a number as a number if there is
     * a matching number in the other String.
     * 
     * For example A1B, A2B, A11B, A11B1, A11B2, A11B11 will be sorted in that order instead
     * of alphabetically which will sort A1B and A11B together.
     */
    public int compare(String str1, String str2) {
        if(str1 == str2) return 0;
        else if(str1 == null) return 1;
        else if(str2 == null) return -1;

        List<String> split1 = split(str1);
        List<String> split2 = split(str2);
        int diff = 0;

        for(int i = 0; diff == 0 && i < split1.size() && i < split2.size(); i++) {
            String token1 = split1.get(i);
            String token2 = split2.get(i);

            if((NUMBER_PATTERN.matcher(token1).matches() && NUMBER_PATTERN.matcher(token2).matches()) {
                diff = (int) Math.signum(Double.parseDouble(token1) - Double.parseDouble(token2));
            } else {
                diff = token1.compareToIgnoreCase(token2);
            }
        }
        if(diff != 0) {
            return diff;
        } else {
            return split1.size() - split2.size();
        }
    }

    /**
     * Splits a string into strings and number tokens.
     */
    private List<String> split(String s) {
        List<String> list = new ArrayList<String>();
        try (Scanner scanner = new Scanner(s)) {
            int index = 0;
            String num = null;
            while ((num = scanner.findInLine(NUMBER_PATTERN)) != null) {
                int indexOfNumber = s.indexOf(num, index);
                if (indexOfNumber > index) {
                    list.add(s.substring(index, indexOfNumber));
                }
                list.add(num);
                index = indexOfNumber + num.length();
            }
            if (index < s.length()) {
                list.add(s.substring(index));
            }
        }
        return list;
    }
}

PS. Je voulais utiliser la méthode Java.lang.String.split () et utiliser "lookahead/lookbehind" pour conserver les jetons, mais je ne pouvais pas le faire fonctionner avec l'expression régulière que j'utilisais.

2
JustinKSU

Mes 2 centimes fonctionnent bien pour moi. Je l'utilise principalement pour les noms de fichiers.

    private final boolean isDigit(char ch)
        {
            return ch >= 48 && ch <= 57;
        }


        private int compareNumericalString(String s1,String s2){

            int s1Counter=0;
            int s2Counter=0;
            while(true){
                if(s1Counter>=s1.length()){
                    break;
                }
                if(s2Counter>=s2.length()){
                    break;
                }
                char currentChar1=s1.charAt(s1Counter++);
                char currentChar2=s2.charAt(s2Counter++);
                if(isDigit(currentChar1) &&isDigit(currentChar2)){
                    String digitString1=""+currentChar1;
                    String digitString2=""+currentChar2;
                    while(true){
                        if(s1Counter>=s1.length()){
                            break;
                        }
                        if(s2Counter>=s2.length()){
                            break;
                        }

                        if(isDigit(s1.charAt(s1Counter))){
                            digitString1+=s1.charAt(s1Counter);
                            s1Counter++;
                        }

                        if(isDigit(s2.charAt(s2Counter))){
                            digitString2+=s2.charAt(s2Counter);
                            s2Counter++;
                        }

                        if((!isDigit(s1.charAt(s1Counter))) && (!isDigit(s2.charAt(s2Counter)))){
                            currentChar1=s1.charAt(s1Counter);
                            currentChar2=s2.charAt(s2Counter);
                            break;
                        }
                    }
                    if(!digitString1.equals(digitString2)){
                        return Integer.parseInt(digitString1)-Integer.parseInt(digitString2);
                    }
                }

                if(currentChar1!=currentChar2){
                    return currentChar1-currentChar2;
                }

            }
            return s1.compareTo(s2);
        }
1
specialscope

problème intéressant, et voici ma solution proposée:

import Java.util.Collections;
import Java.util.Vector;

public class CompareToken implements Comparable<CompareToken>
{
    int valN;
    String valS;
    String repr;

    public String toString() {
    return repr;
    }

    public CompareToken(String s) {
    int l = 0;
    char data[] = new char[s.length()];
    repr = s;
    valN = 0;
    for (char c : s.toCharArray()) {
        if(Character.isDigit(c))
        valN = valN * 10 + (c - '0');
        else
        data[l++] = c;
    }

    valS = new String(data, 0, l);
    }

    public int compareTo(CompareToken b) {
    int r = valS.compareTo(b.valS);
    if (r != 0)
        return r;

    return valN - b.valN;
    }


    public static void main(String [] args) {
    String [] strings = {
        "aaa",
        "bbb3ccc",
        "bbb12ccc",
        "ccc 11",
        "ddd",
        "eee3dddjpeg2000eee",
        "eee12dddjpeg2000eee"
    };

    Vector<CompareToken> data = new Vector<CompareToken>();
    for(String s : strings)
        data.add(new CompareToken(s));
    Collections.shuffle(data);

    Collections.sort(data);
    for (CompareToken c : data)
        System.out.println ("" + c);
    }

}
1
Giuseppe Scrivano

Avant de découvrir ce fil, j'avais implémenté une solution similaire en javascript. Peut-être que ma stratégie vous trouvera bien malgré une syntaxe différente. Semblable à ce qui précède, j’analyse les deux chaînes comparées et les divise en tableaux, en les divisant en nombres continus. 

...
var regex = /(\d+)/g,
    str1Components = str1.split(regex),
    str2Components = str2.split(regex),
...

C'est-à-dire 'hello22goodbye 33' => ['bonjour', 22 ans, 'au revoir', 33 ans]; Ainsi, vous pouvez parcourir les éléments des tableaux en paires, entre chaîne1 et chaîne2, effectuer une contrainte de type (telle que, cet élément est-il vraiment un nombre?), Et comparer au fur et à mesure.

Exemple de travail ici: http://jsfiddle.net/F46s6/3/

Notez que je ne supporte actuellement que les types entiers, bien que la gestion des valeurs décimales ne soit pas trop difficile à modifier.

1
cdaringe

Bien que la question demande une solution Java, pour ceux qui veulent une solution scala:

object Alphanum {

   private[this] val regex = "((?<=[0-9])(?=[^0-9]))|((?<=[^0-9])(?=[0-9]))"

   private[this] val alphaNum: Ordering[String] = Ordering.fromLessThan((ss1: String, ss2: String) => (ss1, ss2) match {
     case (sss1, sss2) if sss1.matches("[0-9]+") && sss2.matches("[0-9]+") => sss1.toLong < sss2.toLong
     case (sss1, sss2) => sss1 < sss2
   })

   def ordering: Ordering[String] = Ordering.fromLessThan((s1: String, s2: String) => {
     import Ordering.Implicits.infixOrderingOps
     implicit val ord: Ordering[List[String]] = Ordering.Implicits.seqDerivedOrdering(alphaNum)

     s1.split(regex).toList < s2.split(regex).toList
   })

}
0
Bennie Krijger

Mon problème était que j'avais des listes consistant en une combinaison de chaînes alphanumériques (par exemple, C22, C3, C5, etc.), de chaînes alpha (par exemple, A, H, R, etc.) et uniquement de chiffres (par exemple, 99, 45, etc.) qui devaient être triés. l'ordre A, C3, C5, C22, H, R, 45, 99. J'ai également des doublons qui doivent être supprimés afin que je ne reçois qu'une seule entrée. 

De plus, je ne travaille pas uniquement avec Strings, je commande un objet et utilise un champ spécifique dans cet objet pour obtenir le bon ordre.

Une solution qui semble fonctionner pour moi est la suivante:

SortedSet<Code> codeSet;
codeSet = new TreeSet<Code>(new Comparator<Code>() {

private boolean isThereAnyNumber(String a, String b) {
    return isNumber(a) || isNumber(b);
}

private boolean isNumber(String s) {
    return s.matches("[-+]?\\d*\\.?\\d+");
}

private String extractChars(String s) {
    String chars = s.replaceAll("\\d", "");
    return chars;
}

private int extractInt(String s) {
    String num = s.replaceAll("\\D", "");
    return num.isEmpty() ? 0 : Integer.parseInt(num);
}

private int compareStrings(String o1, String o2) {

    if (!extractChars(o1).equals(extractChars(o2))) {
        return o1.compareTo(o2);
    } else
        return extractInt(o1) - extractInt(o2);
}

@Override
public int compare(Code a, Code b) {

    return isThereAnyNumber(a.getPrimaryCode(), b.getPrimaryCode()) 
            ? isNumber(a.getPrimaryCode()) ? 1 : -1 
                : compareStrings(a.getPrimaryCode(), b.getPrimaryCode());
                }
            });

Il "emprunte" du code que j'ai trouvé ici sur Stackoverflow, plus quelques ajustements personnels pour le faire fonctionner exactement comme j'en avais besoin.

En essayant de commander des objets, nécessitant un comparateur ainsi que la suppression des doublons, un fudge négatif que je devais employer était que je devais d'abord écrire mes objets sur une TreeMap avant de les écrire sur un arbre. Cela peut avoir un léger impact sur les performances, mais étant donné que les listes contiendront un maximum de 80 codes, cela ne devrait pas poser de problème.

0
mavisto

J'ai eu un problème similaire où mes chaînes contenaient des segments séparés par des espaces. Je l'ai résolu de cette façon:

public class StringWithNumberComparator implements Comparator<MyClass> {

@Override
public int compare(MyClass o1, MyClass o2) {
    if (o1.getStringToCompare().equals(o2.getStringToCompare())) {
        return 0;
    }
    String[] first = o1.getStringToCompare().split(" ");
    String[] second = o2.getStringToCompare().split(" ");
    if (first.length == second.length) {
        for (int i = 0; i < first.length; i++) {

            int segmentCompare = StringUtils.compare(first[i], second[i]);
            if (StringUtils.isNumeric(first[i]) && StringUtils.isNumeric(second[i])) {

                segmentCompare = NumberUtils.compare(Integer.valueOf(first[i]), Integer.valueOf(second[i]));
                if (0 != segmentCompare) {
                    // return only if uneven numbers in case there are more segments to be checked
                    return segmentCompare;
                }
            }
            if (0 != segmentCompare) {
                return segmentCompare;
            }
        }
    } else {
        return StringUtils.compare(o1.getDenominazione(), o2.getDenominazione());
    }

    return 0;
}

Comme vous pouvez le constater, j'ai utilisé Apaches StringUtils.compare () et NumberUtils.compere () comme aide standard.

0
Sasa

Réponse courte: en fonction du contexte, je ne peux pas dire s'il s'agit simplement d'un code simple à utiliser, ou d'un élément clé du dernier logiciel de comptabilité interne de Goldman Sachs. Je vais donc commencer par: eww . C'est un algorithme de tri assez funky; essayez d'utiliser quelque chose d'un peu moins "tordu" si vous le pouvez.

Longue réponse:

Les deux problèmes qui vous viennent immédiatement à l’esprit dans votre cas sont la performance et l’exactitude. De manière informelle, assurez-vous que tout est rapide et que votre algorithme est un total ordering .

(Bien sûr, si vous ne triez pas plus de 100 éléments, vous pouvez probablement ignorer ce paragraphe.) La performance compte, car la vitesse du comparateur sera le facteur le plus important de la vitesse de votre tri (en supposant que l'algorithme de tri est "idéal" à la liste typique). Dans votre cas, la vitesse du comparateur dépendra principalement de la taille de la chaîne. Les chaînes semblent assez courtes, elles ne domineront donc probablement pas autant que la taille de votre liste.

Transformer chaque chaîne en un tuple chaîne-nombre-chaîne puis trier cette liste de n-uplets, comme suggéré dans une autre réponse, échouera dans certains cas, car vous aurez apparemment des chaînes avec plusieurs numéros.

L'autre problème est la justesse. Plus précisément, si l'algorithme que vous avez décrit autorisera jamais A> B> ...> A, votre tri sera non déterministe. Dans votre cas, je le crains, bien que je ne puisse pas le prouver. Considérez certains cas d'analyse tels que:

  aa 0 aa
  aa 23aa
  aa 2a3aa
  aa 113aa
  aa 113 aa
  a 1-2 a
  a 13 a
  a 12 a
  a 2-3 a
  a 21 a
  a 2.3 a
0
Paul Brinkley

Je pense que vous devrez faire la comparaison sur chaque mode. Saisissez un caractère, s’il s’agit d’un chiffre, continuez à le saisir, puis réassemblez les caractères en une chaîne numérique unique et convertissez-la en int. Répétez l'opération sur l'autre chaîne, puis effectuez la comparaison. 

0
sblundy