Bogue dans la double négation des classes de caractères regex?
TL;DR
Pourquoi [^\\D2]
, [^[^0-9]2]
, [^2[^0-9]]
obtenir des résultats différents en Java?
Code utilisé pour les tests. Vous pouvez l'ignorer pour l'instant.
String[] regexes = { "[[^0-9]2]", "[\\D2]", "[013-9]", "[^\\D2]", "[^[^0-9]2]", "[^2[^0-9]]" };
String[] tests = { "x", "1", "2", "3", "^", "[", "]" };
System.out.printf("match | %9s , %6s | %6s , %6s , %6s , %10s%n", (Object[]) regexes);
System.out.println("-----------------------------------------------------------------------");
for (String test : tests)
System.out.printf("%5s | %9b , %6b | %7b , %6b , %10b , %10b %n", test,
test.matches(regexes[0]), test.matches(regexes[1]),
test.matches(regexes[2]), test.matches(regexes[3]),
test.matches(regexes[4]), test.matches(regexes[5]));
Disons que j'ai besoin de regex qui acceptera les caractères qui sont
- pas de chiffres,
- , à l'exception de
2
.
Une telle expression régulière devrait donc représenter tous les caractères sauf 0
, 1
, 3
,4
, ... , 9
. Je peux l'écrire au moins de deux manières qui seront somme de tout ce qui n'est pas de chiffres avec 2:
[[^0-9]2]
[\\D2]
Ces deux expressions rationnelles fonctionnent comme prévu
match , [[^0-9]2] , [\D2]
--------------------------
x , true , true
1 , false , false
2 , true , true
3 , false , false
^ , true , true
[ , true , true
] , true , true
Maintenant, disons que je veux inverser les caractères acceptés. (je veux donc accepter tous les chiffres sauf 2) Je pourrais créer regex qui contient explicitement tous les caractères acceptés comme
-
[013-9]
Ou essayez de nier deux expressions rationnelles décrites précédemment en les enveloppant dans un autre [^...]
comme
[^\\D2]
-
[^[^0-9]2]
ou même [^2[^0-9]]
Mais à ma grande surprise, seules les deux premières versions fonctionnent comme prévu
match | [[^0-9]2] , [\D2] | [013-9] , [^\D2] , [^[^0-9]2] , [^2[^0-9]]
------+--------------------+-------------------------------------------
x | true , true | false , false , true , true
1 | false , false | true , true , false , true
2 | true , true | false , false , false , false
3 | false , false | true , true , false , true
^ | true , true | false , false , true , true
[ | true , true | false , false , true , true
] | true , true | false , false , true , true
, Donc ma question est pourquoi [^[^0-9]2]
ou [^2[^0-9]]
ne pas se comporter comme [^\D2]
? Puis-je corriger en quelque sorte ces expressions rationnelles afin de pouvoir utiliser [^0-9]
à l'intérieur?
2 answers
Il y a un vaudou étrange qui se passe dans le code d'analyse de classe de caractères de l'implémentation d'Oracle de la classe Pattern
, qui vient avec votre JRE/JDK si vous l'avez téléchargé depuis le site Web d'Oracle ou si vous utilisez OpenJDK. Je n'ai pas vérifié comment d'autres implémentations JVM (notamment GNU Classpath) analysent l'expression régulière dans la question.
À partir de ce point, toute référence à la classe Pattern
et à son fonctionnement interne est strictement limitée à l'implémentation d'Oracle (la référence application).
Il faudrait un certain temps pour lire et comprendre comment la classe Pattern
analyse la négation imbriquée comme indiqué dans la question. Cependant, j'ai écrit un programme1 extraire des informations d'un objet Pattern
(avec Reflection API) pour regarder le résultat de la compilation. La sortie ci-dessous provient de l'exécution de mon programme sur Java HotSpot Client VM version 1.7.0_51.
1: Actuellement, le programme est un gâchis embarrassant. Je vais mettre à jour ce post avec un lien quand je l'ai terminé et refactorisé.
[^0-9]
Start. Start unanchored match (minLength=1)
CharProperty.complement (character class negation). Match any character NOT matched by the following character class:
Pattern.rangeFor (character range). Match any character within the range from code point U+0030 to code point U+0039 (both ends inclusive)
LastNode
Node. Accept match
Rien de surprenant ici.
[^[^0-9]]
Start. Start unanchored match (minLength=1)
CharProperty.complement (character class negation). Match any character NOT matched by the following character class:
Pattern.rangeFor (character range). Match any character within the range from code point U+0030 to code point U+0039 (both ends inclusive)
LastNode
Node. Accept match
[^[^[^0-9]]]
Start. Start unanchored match (minLength=1)
CharProperty.complement (character class negation). Match any character NOT matched by the following character class:
Pattern.rangeFor (character range). Match any character within the range from code point U+0030 to code point U+0039 (both ends inclusive)
LastNode
Node. Accept match
Les 2 cas ci-dessus sont compilés dans le même programme que [^0-9]
, qui est contre-intuitif.
[[^0-9]2]
Start. Start unanchored match (minLength=1)
Pattern.union (character class union). Match any character matched by either character classes below:
CharProperty.complement (character class negation). Match any character NOT matched by the following character class:
Pattern.rangeFor (character range). Match any character within the range from code point U+0030 to code point U+0039 (both ends inclusive)
BitClass. Optimized character class with boolean[] to match characters in Latin-1 (code point <= 255). Match the following 1 character(s):
[U+0032]
2
LastNode
Node. Accept match
[\D2]
Start. Start unanchored match (minLength=1)
Pattern.union (character class union). Match any character matched by either character classes below:
CharProperty.complement (character class negation). Match any character NOT matched by the following character class:
Ctype. Match POSIX character class DIGIT (US-ASCII)
BitClass. Optimized character class with boolean[] to match characters in Latin-1 (code point <= 255). Match the following 1 character(s):
[U+0032]
2
LastNode
Node. Accept match
Rien d'étrange dans les 2 cas ci-dessus, comme indiqué dans la question.
[013-9]
Start. Start unanchored match (minLength=1)
Pattern.union (character class union). Match any character matched by either character classes below:
BitClass. Optimized character class with boolean[] to match characters in Latin-1 (code point <= 255). Match the following 2 character(s):
[U+0030][U+0031]
01
Pattern.rangeFor (character range). Match any character within the range from code point U+0033 to code point U+0039 (both ends inclusive)
LastNode
Node. Accept match
[^\D2]
Start. Start unanchored match (minLength=1)
Pattern.setDifference (character class subtraction). Match any character matched by the 1st character class, but NOT the 2nd character class:
CharProperty.complement (character class negation). Match any character NOT matched by the following character class:
CharProperty.complement (character class negation). Match any character NOT matched by the following character class:
Ctype. Match POSIX character class DIGIT (US-ASCII)
BitClass. Optimized character class with boolean[] to match characters in Latin-1 (code point <= 255). Match the following 1 character(s):
[U+0032]
2
LastNode
Node. Accept match
Ces 2 cas fonctionnent comme prévu, comme indiqué dans la question. Toutefois, prenez note de la façon dont le moteur prend complément de la première classe de caractères (\D
) et appliquer une différence à la classe de caractères composée des restes.
[^[^0-9]2]
Start. Start unanchored match (minLength=1)
Pattern.setDifference (character class subtraction). Match any character matched by the 1st character class, but NOT the 2nd character class:
CharProperty.complement (character class negation). Match any character NOT matched by the following character class:
Pattern.rangeFor (character range). Match any character within the range from code point U+0030 to code point U+0039 (both ends inclusive)
BitClass. Optimized character class with boolean[] to match characters in Latin-1 (code point <= 255). Match the following 1 character(s):
[U+0032]
2
LastNode
Node. Accept match
[^[^[^0-9]]2]
Start. Start unanchored match (minLength=1)
Pattern.setDifference (character class subtraction). Match any character matched by the 1st character class, but NOT the 2nd character class:
CharProperty.complement (character class negation). Match any character NOT matched by the following character class:
Pattern.rangeFor (character range). Match any character within the range from code point U+0030 to code point U+0039 (both ends inclusive)
BitClass. Optimized character class with boolean[] to match characters in Latin-1 (code point <= 255). Match the following 1 character(s):
[U+0032]
2
LastNode
Node. Accept match
[^[^[^[^0-9]]]2]
Start. Start unanchored match (minLength=1)
Pattern.setDifference (character class subtraction). Match any character matched by the 1st character class, but NOT the 2nd character class:
CharProperty.complement (character class negation). Match any character NOT matched by the following character class:
Pattern.rangeFor (character range). Match any character within the range from code point U+0030 to code point U+0039 (both ends inclusive)
BitClass. Optimized character class with boolean[] to match characters in Latin-1 (code point <= 255). Match the following 1 character(s):
[U+0032]
2
LastNode
Node. Accept match
Comme confirmé par les tests de Keppil dans le commentaire, la sortie ci-dessus montre que les 3 expressions régulières ci-dessus sont compilées dans le même programme!
[^2[^0-9]]
Start. Start unanchored match (minLength=1)
Pattern.union (character class union). Match any character matched by either character classes below:
CharProperty.complement (character class negation). Match any character NOT matched by the following character class:
BitClass. Optimized character class with boolean[] to match characters in Latin-1 (code point <= 255). Match the following 1 character(s):
[U+0032]
2
CharProperty.complement (character class negation). Match any character NOT matched by the following character class:
Pattern.rangeFor (character range). Match any character within the range from code point U+0030 to code point U+0039 (both ends inclusive)
LastNode
Node. Accept match
Au Lieu de NOT(UNION(2, NOT(0-9))
, qui est 0-13-9
, nous obtenons UNION(NOT(2), NOT(0-9))
, ce qui est équivalent à NOT(2)
.
[^2[^[^0-9]]]
Start. Start unanchored match (minLength=1)
Pattern.union (character class union). Match any character matched by either character classes below:
CharProperty.complement (character class negation). Match any character NOT matched by the following character class:
BitClass. Optimized character class with boolean[] to match characters in Latin-1 (code point <= 255). Match the following 1 character(s):
[U+0032]
2
CharProperty.complement (character class negation). Match any character NOT matched by the following character class:
Pattern.rangeFor (character range). Match any character within the range from code point U+0030 to code point U+0039 (both ends inclusive)
LastNode
Node. Accept match
L'expression régulière [^2[^[^0-9]]]
compile dans le même programme que [^2[^0-9]]
en raison du même bogue.
Il y a un bug non résolu cela semble être de même nature: JDK-6609854 .
Explication
Préliminaire
Voici les détails d'implémentation de la classe Pattern
que l'on devrait connaître avant de lire plus loin:
-
La classe
Pattern
compile unString
dans une chaîne de nœuds, chaque nœud est responsable d'une petite responsabilité bien définie et délègue le travail au nœud suivant de la chaîne.Node
classe est la classe de base de tous les nœud. -
CharProperty
class est la classe de base de tous lesNode
s liés à la classe de caractères. -
La classe
BitClass
est une sous-classe de la classeCharProperty
qui utilise un tableauboolean[]
pour accélérer la correspondance des caractères latin-1 (point de code add, qui permet d'ajouter des caractères lors de la compilation. -
CharProperty.complement
,Pattern.union
,Pattern.intersection
sont des méthodes correspondant aux opérations sur les ensembles. Ce qu'ils font est auto-explicatif. -
Pattern.setDifference
est asymétrique ensemble différence.
Analyser la classe de caractères à première vue
Avant de regarder le code complet de la méthode CharProperty clazz(boolean consume)
, qui est la méthode responsable de l'analyse d'une classe de caractères, examinons une version extrêmement simplifiée du code pour comprendre le flux du code:
private CharProperty clazz(boolean consume) {
// [Declaration and initialization of local variables - OMITTED]
BitClass bits = new BitClass();
int ch = next();
for (;;) {
switch (ch) {
case '^':
// Negates if first char in a class, otherwise literal
if (firstInClass) {
// [CODE OMITTED]
ch = next();
continue;
} else {
// ^ not first in class, treat as literal
break;
}
case '[':
// [CODE OMITTED]
ch = peek();
continue;
case '&':
// [CODE OMITTED]
continue;
case 0:
// [CODE OMITTED]
// Unclosed character class is checked here
break;
case ']':
// [CODE OMITTED]
// The only return statement in this method
// is in this case
break;
default:
// [CODE OMITTED]
break;
}
node = range(bits);
// [CODE OMITTED]
ch = peek();
}
}
Le code lit essentiellement l'entrée (l'entrée String
convertie en terminée par un caractère nul int[]
de points de code) jusqu'à ce qu'il atteigne ]
ou la fin de la chaîne (non fermée classe de caractères).
Le code est un peu déroutant avec continue
et break
se mélangeant à l'intérieur du bloc switch
. Cependant, tant que vous réalisez que continue
appartient à la boucle externe for
et que break
appartient au bloc switch
, le code est facile à comprendre:
- Les cas se terminant par
continue
n'exécuteront jamais le code après l'instructionswitch
. - Les cas se terminant par
break
peuvent exécuter le code après l'instructionswitch
(si ce n'est pas le casreturn
déjà).
, Avec l'observation ci-dessus, nous pouvons voir qu'à chaque fois , un caractère est jugé non spécial et doit être inclus dans la classe de caractères, nous allons exécuter le code après le switch
déclaration, dans laquelle node = range(bits);
est la première instruction.
Si vous vérifiez le code source, la méthode CharProperty range(BitClass bits)
analyse "un seul caractère ou une plage de caractères dans une classe de caractères". La méthode renvoie soit le même BitClass
objet passé (avec un nouveau caractère ajouté) ou renvoie une nouvelle instance de la classe CharProperty
.
Les détails sanglants
Ensuite, regardons la version complète du code (avec la partie analysant l'intersection de la classe de caractères &&
omise):
private CharProperty clazz(boolean consume) {
CharProperty prev = null;
CharProperty node = null;
BitClass bits = new BitClass();
boolean include = true;
boolean firstInClass = true;
int ch = next();
for (;;) {
switch (ch) {
case '^':
// Negates if first char in a class, otherwise literal
if (firstInClass) {
if (temp[cursor-1] != '[')
break;
ch = next();
include = !include;
continue;
} else {
// ^ not first in class, treat as literal
break;
}
case '[':
firstInClass = false;
node = clazz(true);
if (prev == null)
prev = node;
else
prev = union(prev, node);
ch = peek();
continue;
case '&':
// [CODE OMITTED]
// There are interesting things (bugs) here,
// but it is not relevant to the discussion.
continue;
case 0:
firstInClass = false;
if (cursor >= patternLength)
throw error("Unclosed character class");
break;
case ']':
firstInClass = false;
if (prev != null) {
if (consume)
next();
return prev;
}
break;
default:
firstInClass = false;
break;
}
node = range(bits);
if (include) {
if (prev == null) {
prev = node;
} else {
if (prev != node)
prev = union(prev, node);
}
} else {
if (prev == null) {
prev = node.complement();
} else {
if (prev != node)
prev = setDifference(prev, node);
}
}
ch = peek();
}
}
Regardant le code de case '[':
de la switch
déclaration et le code après le switch
instruction:
- La variable
node
stocke le résultat de l'analyse d'une unité (un caractère autonome, une plage de caractères, une classe de caractères Classe de caractères POSIX/Unicode ou une classe de caractères imbriqués) - La variable
prev
stocke le résultat de la compilation jusqu'à présent, et est toujours mise à jour juste après la compilation d'une unité dansnode
.
Étant donné que la variable locale boolean include
, qui enregistre si la classe de caractères est annulée, n'est jamais passée à un appel de méthode, elle ne peut être utilisée que dans cette méthode seule. Et le seul endroit où include
est lu et traité est après l'instruction switch
.
Poste en cours de construction
Selon la page JavaDoc les classes d'imbrication produisent l'union des deux classes, ce qui rend impossible la création d'une intersection en utilisant cette notation:
Pour créer une union, il suffit d'imbriquer une classe dans l'autre, telle que [0-4[6-8]]. Cette union particulière crée une classe de caractères unique qui correspond aux nombres 0, 1, 2, 3, 4, 6, 7, et 8.
Pour créer une intersection, vous devrez utiliser &&
:
Pour créer une classe de caractères unique correspondant uniquement aux caractères communs à toutes ses classes imbriquées, utilisez &&, comme dans [0-9&&[345]]. Cette intersection particulière crée une classe de caractères unique correspondant uniquement aux nombres communs aux deux classes de caractères: 3, 4 et 5.
La dernière partie de votre problème est encore un mystère pour moi aussi. L'union de [^2]
et [^0-9]
devrait en effet être [^2]
, donc [^2[^0-9]]
se comporte comme prévu. [^[^0-9]2]
se comporter comme [^0-9]
est en effet étrange bien.