Remplacement de la nième instance d'une correspondance d'expression régulière en Javascript
-
09-06-2019 - |
Question
J'essaie d'écrire une fonction regex qui identifiera et remplacera une seule instance d'une correspondance dans une chaîne sans affecter les autres instances.Par exemple, j'ai cette chaîne :
12||34||56
Je souhaite remplacer le deuxième jeu de tuyaux par des esperluettes pour obtenir cette chaîne :
12||34&&56
La fonction regex doit être capable de gérer un nombre x de tuyaux et me permettre de remplacer le nième ensemble de tuyaux, afin que je puisse utiliser la même fonction pour effectuer ces remplacements :
23||45||45||56||67 -> 23&&45||45||56||67
23||34||98||87 -> 23||34||98&&87
Je sais que je pourrais simplement diviser/remplacer/concaténer la chaîne au niveau des tuyaux, et je sais aussi que je peux faire correspondre /\|\|/
et parcourir le tableau résultant, mais je souhaite savoir s'il est possible d'écrire une seule expression capable de faire cela.Notez que ce serait pour Javascript, il est donc possible de générer une expression régulière au moment de l'exécution en utilisant eval()
, mais il n'est pas possible d'utiliser des instructions regex spécifiques à Perl.
La solution
voici quelque chose qui fonctionne :
"23||45||45||56||67".replace(/^((?:[0-9]+\|\|){n})([0-9]+)\|\|/,"$1$2&&")
où n est celui de moins que le nième tube (bien sûr, vous n'avez pas besoin de cette première sous-expression si n = 0)
Et si vous souhaitez qu'une fonction fasse cela :
function pipe_replace(str,n) {
var RE = new RegExp("^((?:[0-9]+\\|\\|){" + (n-1) + "})([0-9]+)\|\|");
return str.replace(RE,"$1$2&&");
}
Autres conseils
Une fonction plus généraliste
Je suis tombé sur cette question et, bien que le titre soit très général, la réponse acceptée ne traite que du cas d'utilisation spécifique de la question.
J'avais besoin d'une solution plus générale, j'en ai donc écrit une et j'ai pensé la partager ici.
Usage
Cette fonction nécessite que vous lui passiez les arguments suivants :
original
:la chaîne dans laquelle vous recherchezpattern
:soit une chaîne à rechercher, soit une RegExp avec un groupe de capture.Sans groupe de capture, une erreur sera générée.C'est parce que la fonction appellesplit
sur la chaîne d'origine, et ce n'est que si la RegExp fournie contient un groupe de capture que le tableau résultant contiendra les correspondances.n
:l'occurrence ordinale à trouver ;par exemple, si vous voulez le 2ème match, passez2
replace
:Soit une chaîne avec laquelle remplacer la correspondance, soit une fonction qui prendra en compte la correspondance et renverra une chaîne de remplacement.
Exemples
// Pipe examples like the OP's
replaceNthMatch("12||34||56", /(\|\|)/, 2, '&&') // "12||34&&56"
replaceNthMatch("23||45||45||56||67", /(\|\|)/, 1, '&&') // "23&&45||45||56||67"
// Replace groups of digits
replaceNthMatch("foo-1-bar-23-stuff-45", /(\d+)/, 3, 'NEW') // "foo-1-bar-23-stuff-NEW"
// Search value can be a string
replaceNthMatch("foo-stuff-foo-stuff-foo", "foo", 2, 'bar') // "foo-stuff-bar-stuff-foo"
// No change if there is no match for the search
replaceNthMatch("hello-world", "goodbye", 2, "adios") // "hello-world"
// No change if there is no Nth match for the search
replaceNthMatch("foo-1-bar-23-stuff-45", /(\d+)/, 6, 'NEW') // "foo-1-bar-23-stuff-45"
// Passing in a function to make the replacement
replaceNthMatch("foo-1-bar-23-stuff-45", /(\d+)/, 2, function(val){
//increment the given value
return parseInt(val, 10) + 1;
}); // "foo-1-bar-24-stuff-45"
Le code
var replaceNthMatch = function (original, pattern, n, replace) {
var parts, tempParts;
if (pattern.constructor === RegExp) {
// If there's no match, bail
if (original.search(pattern) === -1) {
return original;
}
// Every other item should be a matched capture group;
// between will be non-matching portions of the substring
parts = original.split(pattern);
// If there was a capture group, index 1 will be
// an item that matches the RegExp
if (parts[1].search(pattern) !== 0) {
throw {name: "ArgumentError", message: "RegExp must have a capture group"};
}
} else if (pattern.constructor === String) {
parts = original.split(pattern);
// Need every other item to be the matched string
tempParts = [];
for (var i=0; i < parts.length; i++) {
tempParts.push(parts[i]);
// Insert between, but don't tack one onto the end
if (i < parts.length - 1) {
tempParts.push(pattern);
}
}
parts = tempParts;
} else {
throw {name: "ArgumentError", message: "Must provide either a RegExp or String"};
}
// Parens are unnecessary, but explicit. :)
indexOfNthMatch = (n * 2) - 1;
if (parts[indexOfNthMatch] === undefined) {
// There IS no Nth match
return original;
}
if (typeof(replace) === "function") {
// Call it. After this, we don't need it anymore.
replace = replace(parts[indexOfNthMatch]);
}
// Update our parts array with the new value
parts[indexOfNthMatch] = replace;
// Put it back together and return
return parts.join('');
}
Une autre façon de le définir
La partie la moins attrayante de cette fonction est qu’elle prend 4 arguments.Il pourrait être simplifié de n'avoir besoin que de 3 arguments en l'ajoutant comme méthode au prototype String, comme ceci :
String.prototype.replaceNthMatch = function(pattern, n, replace) {
// Same code as above, replacing "original" with "this"
};
Si vous faites cela, vous pouvez appeler la méthode sur n'importe quelle chaîne, comme ceci :
"foo-bar-foo".replaceNthMatch("foo", 2, "baz"); // "foo-bar-baz"
Réussir les tests
Voici les tests Jasmine que cette fonction réussit.
describe("replaceNthMatch", function() {
describe("when there is no match", function() {
it("should return the unmodified original string", function() {
var str = replaceNthMatch("hello-there", /(\d+)/, 3, 'NEW');
expect(str).toEqual("hello-there");
});
});
describe("when there is no Nth match", function() {
it("should return the unmodified original string", function() {
var str = replaceNthMatch("blah45stuff68hey", /(\d+)/, 3, 'NEW');
expect(str).toEqual("blah45stuff68hey");
});
});
describe("when the search argument is a RegExp", function() {
describe("when it has a capture group", function () {
it("should replace correctly when the match is in the middle", function(){
var str = replaceNthMatch("this_937_thing_38_has_21_numbers", /(\d+)/, 2, 'NEW');
expect(str).toEqual("this_937_thing_NEW_has_21_numbers");
});
it("should replace correctly when the match is at the beginning", function(){
var str = replaceNthMatch("123_this_937_thing_38_has_21_numbers", /(\d+)/, 2, 'NEW');
expect(str).toEqual("123_this_NEW_thing_38_has_21_numbers");
});
});
describe("when it has no capture group", function() {
it("should throw an error", function(){
expect(function(){
replaceNthMatch("one_1_two_2", /\d+/, 2, 'NEW');
}).toThrow('RegExp must have a capture group');
});
});
});
describe("when the search argument is a string", function() {
it("should should match and replace correctly", function(){
var str = replaceNthMatch("blah45stuff68hey", 'stuff', 1, 'NEW');
expect(str).toEqual("blah45NEW68hey");
});
});
describe("when the replacement argument is a function", function() {
it("should call it on the Nth match and replace with the return value", function(){
// Look for the second number surrounded by brackets
var str = replaceNthMatch("foo[1][2]", /(\[\d+\])/, 2, function(val) {
// Get the number without the [ and ]
var number = val.slice(1,-1);
// Add 1
number = parseInt(number,10) + 1;
// Re-format and return
return '[' + number + ']';
});
expect(str).toEqual("foo[1][3]");
});
});
});
Peut ne pas fonctionner dans IE7
Ce code peut échouer dans IE7 car ce navigateur divise incorrectement les chaînes à l'aide d'une expression régulière, comme indiqué ici.[serre le poing à IE7].Je crois que ce est la solution ;si vous avez besoin de prendre en charge IE7, bonne chance.:)
function pipe_replace(str,n) {
m = 0;
return str.replace(/\|\|/g, function (x) {
//was n++ should have been m++
m++;
if (n==m) {
return "&&";
} else {
return x;
}
});
}