Replacing the nth instance of a regex match in Javascript - javascript

I'm trying to write a regex function that will identify and replace a single instance of a match within a string without affecting the other instances. For example, I have this string:
12||34||56
I want to replace the second set of pipes with ampersands to get this string:
12||34&&56
The regex function needs to be able to handle x amount of pipes and allow me to replace the nth set of pipes, so I could use the same function to make these replacements:
23||45||45||56||67 -> 23&&45||45||56||67
23||34||98||87 -> 23||34||98&&87
I know that I could just split/replace/concat the string at the pipes, and I also know that I can match on /\|\|/ and iterate through the resulting array, but I'm interested to know if it's possible to write a single expression that can do this. Note that this would be for Javascript, so it's possible to generate a regex at runtime using eval(), but it's not possible to use any Perl-specific regex instructions.

A more general-purpose function
I came across this question and, although the title is very general, the accepted answer handles only the question's specific use case.
I needed a more general-purpose solution, so I wrote one and thought I'd share it here.
Usage
This function requires that you pass it the following arguments:
original: the string you're searching in
pattern: either a string to search for, or a RegExp with a capture group. Without a capture group, it will throw an error. This is because the function calls split on the original string, and only if the supplied RegExp contains a capture group will the resulting array contain the matches.
n: the ordinal occurrence to find; eg, if you want the 2nd match, pass in 2
replace: Either a string to replace the match with, or a function which will take in the match and return a replacement string.
Examples
// 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"
The 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('');
}
An Alternate Way To Define It
The least appealing part of this function is that it takes 4 arguments. It could be simplified to need only 3 arguments by adding it as a method to the String prototype, like this:
String.prototype.replaceNthMatch = function(pattern, n, replace) {
// Same code as above, replacing "original" with "this"
};
If you do that, you can call the method on any string, like this:
"foo-bar-foo".replaceNthMatch("foo", 2, "baz"); // "foo-bar-baz"
Passing Tests
The following are the Jasmine tests that this function passes.
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]");
});
});
});
May not work in IE7
This code may fail in IE7 because that browser incorrectly splits strings using a regex, as discussed here. [shakes fist at IE7]. I believe that this is the solution; if you need to support IE7, good luck. :)

here's something that works:
"23||45||45||56||67".replace(/^((?:[0-9]+\|\|){n})([0-9]+)\|\|/,"$1$2&&")
where n is the one less than the nth pipe, (of course you don't need that first subexpression if n = 0)
And if you'd like a function to do this:
function pipe_replace(str,n) {
var RE = new RegExp("^((?:[0-9]+\\|\\|){" + (n-1) + "})([0-9]+)\|\|");
return str.replace(RE,"$1$2&&");
}

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;
}
});
}

Thanks Binda, I have modified the code for generic uses:
private replaceNthMatch(original, pattern, n, replace) {
let m = -1;
return original.replaceAll(pattern, x => {
m++;
if ( n == m ) {
return replace;
} else {
return x;
}
});
}

Related

How do I access text of Select2 when there are multiple select2 instances?

I have the following code that works with the first selector and provides the result I am looking for, namely that results where the first to kth characters match rank above results where the characters merely exist in the option string.
(i.e. input string "ka" would rank kansas above alaska)
It fails when run in one of the other select2 instances on the site. Code as follows:
$(document).ready(() => {
userSettings.init()
$('.select2').select2({
sorter: function(data) {
return data.sort(function (a, b) {
// Declare levenshtein algorithm
var levenshtein = require('fast-levenshtein');
// Trim and set to lowercase on the inputs
a = a.text.trim().toLowerCase();
b = b.text.trim().toLowerCase();
/* This is the problem. I need a better way to retrieve the
input value in order to test it's fitness against my options. */
var input = $('.select2-search__field').val().trim().toLowerCase();
// Weigh by those that match the input in the beginning of the string first, and then list the rest that only contain the string next.
if (!isUndefined(input) && input.length > 0) {
if(input == a.substring(0, input.length)) {
return -1;
} else if (input == b.substring(0, input.length)) {
return 1;
} else {
// Input doesn't match the beginning of the string, still return strings with matching characters.
return levenshtein.get(a, input) - levenshtein.get(b, input);
}
}
});
// Function to ensure we are not operating on something that is undefined.
function isUndefined(value){
// Obtain `undefined` value that's guaranteed to not have been re-assigned.
var undefined = void(0);
return value === undefined;
}
}
})
});
As you can see, I need a better way to get at the input than the way I have declared var input.
**** EDIT ****
I think I have a solution.
I suspect that the sort was happening so fast, and so often, that it would just give up. We tested this by consoling out the workflow and we would erratically lose portions of it.
As a solution, I pulled the assignment out of actual sort function and then passed it in.
$(document).ready(() => {
userSettings.init()
$('.select2').select2({sorter: s2_sorter})
});
//Function to do sort on a Select2 Field.
let s2_sorter = function(data) {
/* Obtain the value out of the text field in order to test agaist the options during sort. */
var input = {d:$('.select2-container--open .select2-search__field').val().trim().toLowerCase()};
return data.sort(function (a, b) {
// Access the varible inside sort.
let i = input.d;
// Declare levenshtein algorithm
var levenshtein = require('fast-levenshtein');
// Trim and set to lowercase on the inputs
a = a.text.trim().toLowerCase();
b = b.text.trim().toLowerCase();
// Wieght by those that match character first, and then all with the character next.
if (!isUndefined(i) && i.length > 0) {
console.log('hassomethign')
if(i == a.substring(0, i.length)) {
return -1;
} else if (i == b.substring(0, i.length)) {
return 1;
} else {
// Input doesn't match the begining of the string, still return strings with matching characters.
return levenshtein.get(a, i) - levenshtein.get(b, i);
}
}
});
function isUndefined(value){
// Obtain `undefined` value that's guaranteed to not have been re-assigned.
var undefined = void(0);
return value === undefined;
}
};

Array with single element of type string "magically" turning into string

Not sure why this is happening. At this point in my code these array may have a single element.
actualAnswer = actualAnswer.split(" ");
playerAnswer = playerAnswer.split(" ");
I then pass them through this function.
function checkForPlural(playerAnswer, actualAnswer){
var answerObject = {playerAnswer: playerAnswer,
actualAnswer: actualAnswer};
for (var answerWord in actualAnswer){
if (actualAnswer[answerWord].slice(-1) == "S")
{
answerObject.actualAnswer[answerWord] = actualAnswer[answerWord].substring(0, actualAnswer[answerWord].length - 1);
}
}
for (var answerWord in playerAnswer){
if (playerAnswer[answerWord].slice(-1) == "S")
{
answerObject.playerAnswer[answerWord] = playerAnswer[answerWord].substring(0, playerAnswer[answerWord].length - 1);
}
}
}
When I return the object IF the array's passed in only had a single element javascript decides to interpret them as a string so if I were to use answerObject.actualAnswer.length it'll give me the length of the string, rather than just 1.
Here is the plunker, unfortunately/fortunately it seems to work as intended on there so i'm confused:
https://plnkr.co/edit/nIqq3VdjjMa2GDWyqkxx
EDIT: The problem was at an earlier junction in my code, sorry!
You can use Array.prototype.map() to return a new array, String.prototype.replace() to, if exists, replace the ending "S" character in a string, return an object having properties set to the function parameter identifiers
const regexp = /S$/;
const re = "";
function parseAnswer(arr, regexp_, re_) {
return arr.map(function(prop) {return prop.replace(regexp_, re_)})
}
function checkForPlural(playerAnswer, actualAnswer) {
return {
playerAnswer: parseAnswer(playerAnswer, regexp, re),
actualAnswer: parseAnswer(actualAnswer, regexp, re)
}
}
console.log(checkForPlural("abc defS ghi".split(" "), "12S 456 78S".split(" ")))
The code in question is working correctly, the "magic" conversion was because of an error in another function that was overwriting the array with a string. My bad.

Javascript Basic algorithm

Return true if the string in the first element of the array contains all of the letters of the string in the second element of the array. No case-sensitivity and order doesn't matter only the letters matter. For ex - ["Hello","hello"] returns true and so does ["Alien","lien"] and also ["Mary", "Aarmy"]. I think you get it. If not return false.
I could solve it with Array.indexOf() === -1 (in the first for loop) but can it work with this code, it's the opposite. I just can't make it return false. Ultimately, I wanna know, can you make it return false without changing the method.
function mutation(arr) {
var split = arr[1].toLowerCase().split("");
var splitSecond = arr[0].toLowerCase().split("");
for(k=0;k<=arr[0].length;k++){
for(i=0;i<=arr[1].length;i++){
if(split[i]===splitSecond[k]) {
return true
}
}
} return false
}
mutation(["hello", "hney"], "");
If using any other method, explain :)
The problem with your code is that you return true; as soon as one letter matches.
What you need to do is check if all letters match, which is easier achieved by checking if any letter doesn't match.
mainloop:
for(k=0;k<=arr[0].length;k++){
for(i=0;i<=arr[1].length;i++){
if(split[i]===splitSecond[k]) {
continue mainloop; // found the letter, move on to next search
}
}
return false; // failed to find letter, fail here
}
return true; // haven't failed yet and end of input reached. Success!
Another alternative would be:
for(k=0;k<arr[0].length;k++) {
if( arr[1].indexOf(split[k]) < 0) {
// letter not found
return false;
}
}
// not failed yet? Excellent!
return true;
function mutation(arr) {
var test = arr[0].toLowerCase(),
chars = arr[1].toLowerCase(),
len=chars.length;
for(var i=0;i<len;i++)
if(test.indexOf(chars[i])==-1) //this char not exist in test string
return false;
return true;//all chars already checked
}
mutation(["hello", "he"]);
https://jsfiddle.net/hb2rsm2x/115/
Here is an interesting way using regular expressions. escapeRegExp was taken from here.
function mutation(arr){
var matcher = new RegExp('[^'+escapeRegExp(arr[1])+']', "i");
return arr[0].match(matcher) === null;
}
function escapeRegExp(s) {
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
}

Regular expression to get class name with specific substring

I need a regular expression in javascript that will get a string with a specific substring from a list of space delimited strings.
For example, I have;
widget util cookie i18n-username
I want to be able to return only i18n-username.
How
You could use the following function, using a regex to match for your string surrounded by either a space or the beginning or end of a line. But you'll have to be careful about preparing any regular expression special characters if you plan to use them, since the search argument will be interpreted as a string instead of a RegExp literal:
var hasClass = function(s, klass) {
var r = new RegExp("(?:^| )(" + klass + ")(?: |$)")
, m = (""+s).match(r);
return (m) ? m[1] : null;
};
hasClass("a b c", "a"); // => "a"
hasClass("a b c", "b"); // => "b"
hasClass("a b c", "x"); // => null
var klasses = "widget util cookie i18n-username";
hasClass(klasses, "username"); // => null
hasClass(klasses, "i18n-username"); // => "i18n-username"
hasClass(klasses, "i18n-\\w+"); // => "i18n-username"
As others have pointed out, you could also simply use a "split" and "indexOf":
var hasClass = function(s, klass) {
return (""+s).split(" ").indexOf(klass) >= 0;
};
However, note that the "indexOf" function was introduced to JavaScript somewhat recently, so for older browsers you might have to implement it yourself.
var hasClass = function(s, klass) {
var a=(""+s).split(" "), len=a.length, i;
for (i=0; i<len; i++) {
if (a[i] == klass) return true;
}
return false;
};
[Edit]
Note that the split/indexOf solution is likely faster for most browsers (though not all). This jsPerf benchmark shows which solution is faster for various browsers - notably, Chrome must have a really good regular expression engine!
function getString(subString, string){
return (string.match(new RegExp("\S*" + subString + "\S*")) || [null])[0];
}
To Use:
var str = "widget util cookie i18n-username";
getString("user", str); //returns i18n-username
Does this need to be a regex? Would knowing if the string existed be sufficient? Regular expressions are inefficient (slower) and should be avoided if possible:
var settings = 'widget util cookie i18n-username',
// using an array in case searching the string is insufficient
features = settings.split(' ');
if (features.indexOf('i18n-username') !== -1) {
// do something based on having this feature
}
If whitespace wouldn't cause an issue in searching for a value, you could just search the string directly:
var settings = 'widget util cookie i18n-username';
if (settings.indexOf('i18n-username') !== -1) {
// do something based on having this value
}
It then becomes easy to make this into a reusable function:
(function() {
var App = {},
features = 'widget util cookie i18n-username';
App.hasFeature = function(feature) {
return features.indexOf(feature) !== -1;
// or if you prefer the array:
return features.split(' ').indexOf(feature) !== -1;
};
window.App = App;
})();
// Here's how you can use it:
App.hasFeature('i18n-username'); // returns true
EDIT
You now say you need to return all strings that start with another string, and it is possible to do this with a regular expression as well, although I am unsure about how efficient it is:
(function() {
var App = {},
features = 'widget util cookie i18n-username'.split(' ');
// This contains an array of all features starting with 'i18n'
App.i18nFeatures = features.map(function(value) {
return value.indexOf('i18n') === 0;
});
window.App = App;
})();
/i18n-\w+/ ought to work. If your string has any cases like other substrings can start with i18n- or your user names have chars that don't fit the class [a-zA-Z0-9_], you'll need to specify that.
var str = "widget util cookie i18n-username";
alert(str.match(/i18n-\w+/));
Edit:
If you need to match more than one string, you can add on the global flag (/g) and loop through the matches.
var str = "widget i18n-util cookie i18n-username";
var matches = str.match(/i18n-\w+/g);
if (matches) {
for (var i = 0; i < matches.length; i++)
alert(matches[i]);
}
else
alert("phooey, no matches");

Increment a number in a string in with regex

I get the name of an input element, which is a string with a number (url1). I want to increment the number by 1 (url2) in the easiest and quickest way possible.
My way would be to get \d / restofstring, ++ the match, then put together number with restofstring. Is there a better way?
Update:
My final (dummy)code became:
var liNew = document.createElement('li');
liNew.innerHTML = liOld.innerHTML;
var els = Y.Dom.getChildrenBy(liNew, function(el) {
return el.name.match(/\d+$/);
} // YUI method where the function is a test
for (var i = 0, el; el = els[i]; i++) {
el.name = el.name.replace(/\d+$/, function(n) { return ++n });
}
list.appendChild(liNew);
How about:
'url1'.replace(/\d+$/, function(n){ return ++n }); // "url2"
'url54'.replace(/\d+$/, function(n){ return ++n }); // "url55"
There we search for a number at the end of the string, cast it to Number, increment it by 1, and place it back in the string. I think that's the same algo you worded in your question even.
Reference:
String.prototype.replace - can take a regex
Simple. Use a substitution function with regular expressions:
s = 'abc99abc';
s = s.replace(/\d+/, function(val) { return parseInt(val)+1; });
will set variable s to: abc100abc
But it gets more complicated if you want to make sure you only change a certain parameter in the URL:
s = '?foo=10&bar=99';
s = s.replace(/[&?]bar=\d+/, function(attr) {
return attr.replace(/\d+/, function(val) { return parseInt(val)+1; });
});
will set variable s to: ?foo=10&bar=100
Looks OK. You might want to use a regex like ^(.*?)(\d+)$, making sure the number you're grabbing is at the end of the string.
You can use replace and pass it a function to use to replace the matched section:
str.replace(/\d+/, function(number) { return parseInt(number, 10) + 1; });

Categories