Pourquoi «asdf» .replace (/.*/ g, «x») == «xx»?

132

Je suis tombé sur un fait surprenant (pour moi).

console.log("asdf".replace(/.*/g, "x"));

Pourquoi deux remplacements? Il semble que toute chaîne non vide sans retour à la ligne produira exactement deux remplacements pour ce modèle. En utilisant une fonction de remplacement, je peux voir que le premier remplacement est pour la chaîne entière et le second pour une chaîne vide.

récursif
la source
9
exemple plus simple: "asdf".match(/.*/g)return ["asdf", ""]
Narro
32
À cause du drapeau global (g). Le drapeau global permet de démarrer une autre recherche à la fin de la correspondance précédente, trouvant ainsi une chaîne vide.
Celsiuss
6
et soyons honnêtes: personne ne voulait probablement exactement ce comportement. c'était probablement un détail d'implémentation de vouloir "aa".replace(/b*/, "b")aboutir babab. Et à un moment donné, nous avons normalisé tous les détails de mise en œuvre des navigateurs Web.
Lux
4
@Joshua, les anciennes versions de GNU sed (pas les autres implémentations!) Présentaient également ce bogue, qui a été corrigé quelque part entre les versions 2.05 et 3.01 (il y a plus de 20 ans). Je soupçonne que c'est là que ce comportement est originaire, avant de faire son chemin vers perl (où il est devenu une fonctionnalité) et de là vers javascript.
Mosvy
1
@recursive - Assez juste. Je les trouve tous les deux surprenants pendant une seconde, puis je réalise un "match nul" et je ne suis plus surpris. :-)
TJ Crowder

Réponses:

98

Conformément à la norme ECMA-262 , String.prototype.replace appelle RegExp.prototype [@@ replace] , qui dit:

11. Repeat, while done is false
  a. Let result be ? RegExpExec(rx, S).
  b. If result is null, set done to true.
  c. Else result is not null,
    i. Append result to the end of results.
    ii. If global is false, set done to true.
    iii. Else,
      1. Let matchStr be ? ToString(? Get(result, "0")).
      2. If matchStr is the empty String, then
        a. Let thisIndex be ? ToLength(? Get(rx, "lastIndex")).
        b. Let nextIndex be AdvanceStringIndex(S, thisIndex, fullUnicode).
        c. Perform ? Set(rx, "lastIndex", nextIndex, true).

rxest /.*/get Sest 'asdf'.

Voir 11.c.iii.2.b:

b. Soit nextIndex AdvanceStringIndex (S, thisIndex, fullUnicode).

Par conséquent, 'asdf'.replace(/.*/g, 'x')c'est en fait:

  1. result (non défini), results = [], lastIndex =0
  2. result = 'asdf', results = [ 'asdf' ], lastIndex =4
  3. résultat = '', = résultats [ 'asdf', '' ], lastIndex = 4, AdvanceStringIndex, mis à lastIndex5
  4. résultat = null, résultats = [ 'asdf', '' ], retour

Il y a donc 2 correspondances.

Alan Liang
la source
42
Cette réponse me demande de l'étudier pour la comprendre.
Felipe
Le TL; DR est qu'il correspond à 'asdf'une chaîne vide ''.
JIMH
34

Ensemble, dans une discussion hors ligne avec yawkat , nous avons trouvé un moyen intuitif de voir pourquoi "abcd".replace(/.*/g, "x")produit exactement deux correspondances. Notez que nous n'avons pas vérifié si elle est complètement égale à la sémantique imposée par la norme ECMAScript, il suffit donc de la prendre comme règle générale.

Règles de base

  • Considérez les correspondances comme une liste de tuples (matchStr, matchIndex)dans un ordre chronologique qui indique quelles parties de chaîne et indices de la chaîne d'entrée ont déjà été consommés.
  • Cette liste est continuellement constituée à partir de la gauche de la chaîne d'entrée pour l'expression régulière.
  • Les pièces déjà consommées ne peuvent plus être appariées
  • Le remplacement se fait aux indices donnés en matchIndexécrasant la sous-chaîne matchStrà cette position. Si matchStr = "", alors le "remplacement" est effectivement l'insertion.

Formellement, l'acte d'appariement et de remplacement est décrit comme une boucle comme on le voit dans l'autre réponse .

Exemples simples

  1. "abcd".replace(/.*/g, "x")sorties "xx":

    • La liste des correspondances est [("abcd", 0), ("", 4)]

      Notamment, il n'inclut pas les correspondances suivantes auxquelles on aurait pu penser pour les raisons suivantes:

      • ("a", 0), ("ab", 0): le quantificateur *est gourmand
      • ("b", 1), ("bc", 1): en raison du match précédent ("abcd", 0), les cordes "b"et "bc"sont déjà mangées
      • ("", 4), ("", 4) (ie deux fois): la position d'index 4 est déjà dévorée par la première correspondance apparente
    • Par conséquent, la chaîne de "x"remplacement remplace les chaînes de correspondance trouvées exactement à ces positions: à la position 0, elle remplace la chaîne "abcd"et à la position 4, elle remplace "".

      Ici, vous pouvez voir que le remplacement peut agir comme un véritable remplacement d'une chaîne précédente ou tout simplement comme l'insertion d'une nouvelle chaîne.

  2. "abcd".replace(/.*?/g, "x")avec un quantificateur paresseux*? sorties"xaxbxcxdx"

    • La liste des correspondances est [("", 0), ("", 1), ("", 2), ("", 3), ("", 4)]

      Contrairement à l'exemple précédent, ici ("a", 0),("ab", 0) , ("abc", 0)ou même ("abcd", 0)ne sont pas inclus en raison de la paresse qui limite strictement à trouver le plus possible de la correspondance quantificateurs.

    • Étant donné que toutes les chaînes de correspondance sont vides, aucun remplacement réel ne se produit, mais des insertions de x aux positions 0, 1, 2, 3 et 4.

  3. "abcd".replace(/.+?/g, "x") avec un quantificateur paresseux+? sorties"xxxx"

    • La liste des correspondances est [("a", 0), ("b", 1), ("c", 2), ("d", 3)]
  4. "abcd".replace(/.{2,}?/g, "x") avec un quantificateur paresseux[2,}? sorties"xx"

    • La liste des correspondances est [("ab", 0), ("cd", 2)]
  5. "abcd".replace(/.{0}/g, "x")sorties "xaxbxcxdx"par la même logique que dans l'exemple 2.

Exemples plus difficiles

Nous pouvons toujours exploiter l'idée d' insertion au lieu de remplacement si nous faisons toujours correspondre une chaîne vide et contrôlons la position où de telles correspondances se produisent à notre avantage. Par exemple, nous pouvons créer des expressions régulières correspondant à la chaîne vide à chaque position paire pour y insérer un caractère:

  1. "abcdefgh".replace(/(?<=^(..)*)/g, "_"))avec un résultat positif derrière les(?<=...) sorties "_ab_cd_ef_gh_"(uniquement pris en charge dans Chrome jusqu'à présent)

    • La liste des correspondances est [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
  2. "abcdefgh".replace(/(?=(..)*$)/g, "_"))avec une sortie d' anticipation positive(?=...)"_ab_cd_ef_gh_"

    • La liste des correspondances est [("", 0), ("", 2), ("", 4), ("", 6), ("", 8)]
ComFreek
la source
4
Je pense que c'est un peu exagéré de l'appeler intuitive (et en gras, à ce sujet). Pour moi, cela ressemble plus au syndrome de Stockholm et à la rationalisation post-hoc. Votre réponse est bonne, BTW, je ne me plains que du design JS, ou du manque de design d'ailleurs.
Eric Duminil
7
@EricDuminil Je le pensais aussi au début, mais après avoir écrit la réponse, l'algorithme global-regex-replace esquissé semble être exactement la façon dont on le trouverait si on partait de zéro. C'est comme while (!input not eaten up) { matchAndEat(); }. De plus, les commentaires ci - dessus indiquent que le comportement est né bien avant l'existence de JavaScript.
ComFreek
2
La partie qui n'a toujours pas de sens (pour toute autre raison que «c'est ce que dit la norme») est que la correspondance à quatre caractères ("abcd", 0)ne mange pas la position 4 où irait le caractère suivant, mais la correspondance à zéro caractère le ("", 4)fait manger la position 4 où irait le personnage suivant. Si je concevais cela à partir de zéro, je pense que la règle que j'utiliserais est celle qui (str2, ix2)peut suivre (str1, ix1)ssi ix2 >= ix1 + str1.length() && ix2 + str2.length() > ix1 + str1.length(), ce qui ne provoque pas cette erreur.
Anders Kaseorg
2
@AndersKaseorg ("abcd", 0)ne mange pas la position 4 car il "abcd"ne fait que 4 caractères et donc mange juste les indices 0, 1, 2, 3. Je peux voir d'où votre raisonnement pourrait provenir: pourquoi ne pouvons-nous pas avoir ("abcd" ⋅ ε, 0)une correspondance de 5 caractères où ⋅ est la concaténation et εla correspondance de largeur nulle? Officiellement parce que "abcd" ⋅ ε = "abcd". J'ai pensé à une raison intuitive pour les dernières minutes, mais je n'ai pas réussi à en trouver une. Je suppose que l'on doit toujours traiter εcomme se produisant seul "". J'adorerais jouer avec une implémentation alternative sans ce bug ou cet exploit. N'hésitez pas à partager!
ComFreek
1
Si la chaîne de quatre caractères doit manger quatre indices, la chaîne de caractères zéro ne doit pas manger d'indices. Tout raisonnement que vous pourriez faire sur l'un devrait également s'appliquer à l'autre (par exemple "" ⋅ ε = "", bien que je ne sois pas sûr de la distinction que vous avez l'intention de faire entre ""et ε, ce qui signifie la même chose). La différence ne peut donc pas être expliquée comme intuitive - elle l'est tout simplement.
Anders Kaseorg
26

Le premier match est évidemment "asdf"(Position [0,4]). Parce que le drapeau mondial (g ) est défini, il continue la recherche. À ce stade (position 4), il trouve une deuxième correspondance, une chaîne vide (position [4,4]).

N'oubliez pas que cela *correspond à zéro ou plusieurs éléments.

David SK
la source
4
Alors pourquoi pas trois matchs? Il pourrait y avoir un autre match vide à la fin. Il y en a précisément deux. Cette explication explique pourquoi il pourrait y en avoir deux, mais pas pourquoi il devrait y en avoir au lieu d'un ou de trois.
récursif le
7
Non, il n'y a pas d'autre chaîne vide. Parce que cette chaîne vide a été trouvée. une chaîne vide sur la position 4,4, elle est détectée comme un résultat unique. Une correspondance intitulée "4,4" ne peut pas être répétée. vous pouvez probablement penser qu'il y a une chaîne vide à la position [0,0] mais l'opérateur * renvoie le maximum possible d'éléments. c'est la raison pour laquelle seulement 4,4 sont possibles
David SK
16
Nous devons nous rappeler que les regex ne sont pas des expressions régulières. Dans les expressions régulières, il y a une infinité de chaînes vides entre tous les deux caractères, ainsi qu'au début et à la fin. Dans les expressions régulières, il y a exactement autant de chaînes vides que la spécification de la saveur particulière du moteur d'expression régulière en indique.
Jörg W Mittag
7
Ce n'est qu'une rationalisation post-hoc.
mosvy
9
@mosvy sauf que c'est la logique exacte qui est réellement utilisée.
Hobbs
1

simplement, le premier xest pour le remplacement de l'appariementasdf .

deuxième xpour la chaîne vide après asdf. La recherche se termine lorsqu'elle est vide.

Nilanka Manoj
la source