2016-12-15 7 views
4

Tout d'abord, je suis au courant des questions similaires qui ont été posées comme ici:Java Pattern.split() avec chevauchement delimiters

How to split a string, but also keep the delimiters?

Cependant, je vais avoir une scission question la mise en œuvre d'un chaîne utilisant Pattern.split() où le modèle est basé sur une liste de délimiteurs, mais où ils peuvent parfois sembler se chevaucher. Voici l'exemple:

L'objectif est de diviser une chaîne basée sur un ensemble de mots de code connus qui sont entourés par des barres obliques, où je dois garder le délimiteur (mot de code) lui-même et la valeur après (qui peut être chaîne vide).

Pour cet exemple, les mots de code sont les suivants:

/ABC/ 
/DEF/ 
/GHI/ 

Basé sur le fil mentionné ci-dessus, le modèle est construit comme suit à l'aide d'anticipation et regarder en arrière pour tokenise la chaîne en mots de code et les valeurs:

((?<=/ABC/)|(?=/ABC/))|((?<=/DEF/)|(?=/DEF/))|((?<=/GHI/)|(?=/GHI/)) 

chaîne de travail:

"123/ABC//DEF/456/GHI/789" 

En utilisant Split, ce tokenises nic ely à:

"123","/ABC/","/DEF/","456","/GHI/","789" 

chaîne de problème (notez slash unique entre "ABC" et "DEF"):

"123/ABC/DEF/456/GHI/789" 

Ici, l'attente est que "DEF/456" est la valeur après «/ABC/"codeword parce que le bit" DEF/"n'est pas réellement un mot de code, mais arrive juste à ressembler à un!

résultat souhaité est:

"123","/ABC/","DEF/456","/GHI/","789" 

résultat réel est:

"123","/ABC","/","DEF/","456","/GHI/","789" 

Comme vous pouvez le voir, la barre oblique entre "ABC" et "DEF" est isolé se comme un signe lui-même.

J'ai essayé des solutions selon l'autre thread en utilisant uniquement le look-ahead OU look-behind, mais ils semblent tous souffrir du même problème. Toute aide appréciée!

+6

À un certain point votre regex devient tellement alambiquée que vous feriez mieux d'écrire une méthode simple pour parcourir le 'String' et l'analyser ... –

+0

Avec la méthode look-ahead + look-behind, je veux le" 789 "être séparé, comme dans l'exemple de" chaîne de travail "(plus tard, je" réassemble "les mots de code et les valeurs de la liste de jetons en un tableau associatif). –

+0

@BoristheSpider - point pris, cependant l'approche de la segmentation de la chaîne en un "simple hit" semblait être un moyen sûr et efficace d'extraire les jetons/valeurs. La liste de mots de code est également configurable, donc cette regex est construite dynamiquement et je suis un peu moins intéressé par la complexité de son fonctionnement (tant qu'il fonctionne). –

Répondre

2

Si vous êtes OK avec find plutôt que split, en utilisant des allumettes non avides, essayez ceci:

public class SampleJava { 
static final String[] CODEWORDS = { 
    "ABC", 
    "DEF", 
    "GHI"}; 
static public void main(String[] args) { 
    String input = "/ABC/DEF/456/GHI/789"; 
    String codewords = Arrays.stream(CODEWORDS) 
      .collect(Collectors.joining("|", "/(", ")/")); 
    //  codewords = "/(ABC|DEF|GHI)/"; 
    Pattern p = Pattern.compile(
/* codewords */ ("(DELIM)" 
/* pre-delim */ + "|(.+?(?=DELIM))" 
/* final bit */ + "|(.+?$)").replace("DELIM", codewords)); 
    Matcher m = p.matcher(input); 
    while(m.find()) { 
     System.out.print(m.group(0)); 
     if(m.group(1) != null) { 
      System.out.print(" ← code word"); 
     } 
     System.out.println(); 
    } 
} 
} 

Sortie:

/ABC/← mot de code

DEF/456

/GHI/← code w ord

+1

Celui-ci semble intéressant, et basé sur des tests rapides sur http://regex-testdrive.com/ il semble faire face au problème. Je dois avouer que ce n'est pas mes doigts qui font le codage ici, alors je vais demander à mes développeurs d'expérimenter cela et de rapporter et d'accepter la réponse si cela fonctionne. –

+0

@JulianMclean Un avantage supplémentaire de cette approche est que vous n'avez pas besoin de faire une boucle/regex séparée pour voir si le jeton est un mot de code. Voir ma dernière édition pour plus de détails. –

1

Utilisez une combinaison de regard positif et négatif contournements:

String[] parts = s.split("(?<=/(ABC|DEF|GHI)/)(?<!/(ABC|DEF|GHI)/....)|(?=/(ABC|DEF|GHI)/)(?<!/(ABC|DEF|GHI))"); 

Il y a aussi une simplification considérable à l'intérieur en utilisant des alternances seul regard avant/arrière.

Voir live demo.

+0

Obtenez le point au sujet des alternances, et d'accord - selon mon commentaire ci-dessus, mes doigts ne font pas le codage réel ici, donc je voulais publier l'expression rationnelle qui était réellement utilisée - d'accord qu'il peut être optimisé un peu. La démo en direct semble utiliser la "chaîne de travail" de ma question originale, plutôt que celle du problème? En outre, il n'atteint pas le résultat souhaité car "/ DEF/456" sort en tant que jeton unique - voir les exemples de questions d'origine. Merci pour l'aide ici, cependant, nous sentons que nous nous rapprochons! –

+0

@julian oui, nous étions proches. Maintenant très proche. J'ai affiné le jumelage en ajoutant un regard négatif pour exclure les correspondances indésirables. Lien de démonstration mis à jour aussi. – Bohemian

+0

Un test rapide dans un testeur en ligne semble montrer que cela fonctionne. Mes développeurs avaient déjà commencé à travailler sur la solution ci-dessus de Patrick, mais voulaient faire savoir aux autres, car c'est potentiellement la réponse la plus "directe" à ma question, qui était de faire fonctionner l'expression rationnelle. Je ne sais pas si je peux accepter 2 réponses ou non ... laissez-moi voir –

0

Après quelques TDD principles (rouge-vert-Refactor), voici comment je mettre en œuvre un tel comportement:

spécifications Write (Rouge)

I défini un ensemble de tests unitaires qui expliquent comment j'ai compris votre "processus de tokenisation". Si n'importe quel test n'est pas correct selon ce que vous attendez, n'hésitez pas à me le dire et je modifierai ma réponse en conséquence.

import static org.assertj.core.api.Assertions.assertThat; 

import java.util.List; 

import org.junit.Test; 

public class TokenizerSpec { 

    Tokenizer tokenizer = new Tokenizer("/ABC/", "/DEF/", "/GHI/"); 

    @Test 
    public void itShouldTokenizeTwoConsecutiveCodewords() { 
     String input = "123/ABC//DEF/456"; 

     List<String> tokens = tokenizer.splitPreservingCodewords(input); 

     assertThat(tokens).containsExactly("123", "/ABC/", "/DEF/", "456"); 
    } 

    @Test 
    public void itShouldTokenizeMisleadingCodeword() { 
     String input = "123/ABC/DEF/456/GHI/789"; 

     List<String> tokens = tokenizer.splitPreservingCodewords(input); 

     assertThat(tokens).containsExactly("123", "/ABC/", "DEF/456", "/GHI/", "789"); 
    } 

    @Test 
    public void itShouldTokenizeWhenValueContainsSlash() { 
     String input = "1/23/ABC/456"; 

     List<String> tokens = tokenizer.splitPreservingCodewords(input); 

     assertThat(tokens).containsExactly("1/23", "/ABC/", "456"); 
    } 

    @Test 
    public void itShouldTokenizeWithoutCodewords() { 
     String input = "123/456/789"; 

     List<String> tokens = tokenizer.splitPreservingCodewords(input); 

     assertThat(tokens).containsExactly("123/456/789"); 
    } 

    @Test 
    public void itShouldTokenizeWhenEndingWithCodeword() { 
     String input = "123/ABC/"; 

     List<String> tokens = tokenizer.splitPreservingCodewords(input); 

     assertThat(tokens).containsExactly("123", "/ABC/"); 
    } 

    @Test 
    public void itShouldTokenizeWhenStartingWithCodeword() { 
     String input = "/ABC/123"; 

     List<String> tokens = tokenizer.splitPreservingCodewords(input); 

     assertThat(tokens).containsExactly("/ABC/", "123"); 
    } 

    @Test 
    public void itShouldTokenizeWhenOnlyCodeword() { 
     String input = "/ABC//DEF//GHI/"; 

     List<String> tokens = tokenizer.splitPreservingCodewords(input); 

     assertThat(tokens).containsExactly("/ABC/", "/DEF/", "/GHI/"); 
    } 
} 

Mettre en oeuvre selon les spécifications (vert)

Cette classe font tous les tests ci-dessus passe

import java.util.ArrayList; 
import java.util.Arrays; 
import java.util.List; 
import java.util.Optional; 

public final class Tokenizer { 

    private final List<String> codewords; 

    public Tokenizer(String... codewords) { 
     this.codewords = Arrays.asList(codewords); 
    } 

    public List<String> splitPreservingCodewords(String input) { 
     List<String> tokens = new ArrayList<>(); 

     int lastIndex = 0; 
     int i = 0; 
     while (i < input.length()) { 
      final int idx = i; 
      Optional<String> codeword = codewords.stream() 
               .filter(cw -> input.substring(idx).indexOf(cw) == 0) 
               .findFirst(); 
      if (codeword.isPresent()) { 
       if (i > lastIndex) { 
        tokens.add(input.substring(lastIndex, i)); 
       } 
       tokens.add(codeword.get()); 
       i += codeword.get().length(); 
       lastIndex = i; 
      } else { 
       i++; 
      } 
     } 

     if (i > lastIndex) { 
      tokens.add(input.substring(lastIndex, i)); 
     } 

     return tokens; 
    } 
} 

Améliorer la mise en œuvre (refactoring)

Pas fait à l'heure actuelle (non assez de temps que je peux consacrer à cette réponse maintenant). Je ferai un refactor sur Tokenizer avec plaisir si vous me le demandez (mais plus tard). :-) Ou vous pouvez le faire vous-même en toute sécurité puisque vous avez les tests unitaires pour éviter les régressions.