How to safely replace searches each with a replacement term where the former is part of the latter without running into replacing such a search again?

Viewed 124
var string = "Please click on dashboard and then open the dashboard details to verify your details on the data";
var stringArray = ["dashboard" , "dashboard" , "data"]
var replaceArray = ["https://abcd.com/login" , "https://abcd.com/home" , "https://abcd.com/data"]
for(i=0;i<stringArray.length; i++){
    string = string.replace(stringArray[i].trim(), "<a href='"+replaceArray[i].trim()+"'>"+stringArray[i].trim()+"</a>");
}

I have a string and 2 arrays like above. I need to replace my string with respective anchor link tags as mentioned in two arrays. stringArray defines the word to be linked and replaceArray defines the URL should be added. Like first occurrence of dashboard should be anchor linked with "https://abcd.com/login" and second occurance of "dashboard" should be replaced with "https://abcd.com/home" and "data" should be replaced with "https://abcd.com/data".

I tried to find out the word in string and replace it using replace/replaceAll, working fine for single occurrence word, but for multiple occurrences it is not working.

Anyone help me to resolve this.

Resulting :

"Please click on <a href='https://abcd.com/login'><a href='https://abcd.com/home'>dashboard</a></a> and then open the dashboard details to verify your details on the <a href='https://abcd.com/data'>data</a>"

Expected Output:

"Please click on <a href='https://abcd.com/login'>dashboard</a> and then open the <a href='https://abcd.com/home'>dashboard</a> details to verify your details on the <a href='https://abcd.com/data'>data</a>"
4 Answers

When using a string as the first parameter (substring) to the Javascript replace function, replace will find and replace only the first occurrence of the substring. That's why both your "login" and "home" links are nested around the first occurrence of "dashboard", and the remaining occurrences of "dashboard" remain unchanged. Using a regular expression as the first parameter is one solution, however not the only solution...

Using indexOf() to keep track of the last index where word from array strArray was matched, then slice-ing the string after the last insertion to continue the replacement search from there:

var string = "Please click on dashboard and then open the dashboard details to verify your details on the data";
var stringArray = ["dashboard", "dashboard", "data"]
var replaceArray = ["https://abcd.com/login", "https://abcd.com/home", "https://abcd.com/data"]

// keep track of last position of matched string
let ii = 0;

for (i = 0; i < stringArray.length; i++) {
  let str = stringArray[i].trim();
  let repstr = '<a href="' + replaceArray[i].trim() + '">' + str + '</a>';

  // move position to index of matched string
  ii += string.slice(ii).indexOf(str);
  string = 
    // this is the portion of string before and including last replacement
    string.slice(0, ii) 
    // this is the portion after last replacement
    + string.slice(ii).replace(str, repstr);

  // move position to past current replacement
  ii += repstr.length;
}
console.log(string);
// Please click on <a href="https://abcd.com/login">dashboard</a> and then open the <a href="https://abcd.com/home">dashboard</a> details to verify your details on the <a href="https://abcd.com/data">data</a>

And this solution benchmarks about 120 times faster than both the regular expression solution, and the reduce solutions I posted below.


Here's a solution combining the words and links into a single array, then using reduce to iterate the array replace_arr, update the string string, and maintain the match index ii:

let string = "Please click on dashboard and then open the dashboard details to verify your details on the data";
const replace_arr = [["dashboard", "https://abcd.com/login"], ["dashboard", "https://abcd.com/home"], ["data", "https://abcd.com/data"]];

replace_arr.reduce(
  (ii, [str, link]) => {
    let repstr = '<a href="' + link + '">' + str + '</a>';
    ii += string.slice(ii).indexOf(str);
    string = string.slice(0, ii) 
      + string.slice(ii).replace(str, repstr)
    return ii + repstr.length;
  }
  , 0
);

console.log(string);
// Please click on <a href="https://abcd.com/login">dashboard</a> and then open the <a href="https://abcd.com/home">dashboard</a> details to verify your details on the <a href="https://abcd.com/data">data</a>


Refactored reduction method for better performance—initially including string in the reduce() function, and processing internally, cuts execution time almost in half, compared to accessing the string externally to the reduction process with each iteration:

let string = "Please click on dashboard and then open the dashboard details to verify your details on the data";
const replace_arr = [["dashboard", "https://abcd.com/login"], ["dashboard", "https://abcd.com/home"], ["data", "https://abcd.com/data"]];

[string] = replace_arr.reduce(([ss, ii], [str, link]) => {
  let repstr = '<a href="' + link + '">' + str + '</a>';
  ii += ss.slice(ii).indexOf(str);
  return [ss.slice(0, ii) +
    ss.slice(ii).replace(str, repstr), ii + repstr.length
  ];
}, [string, 0]);

console.log(string);
// Please click on <a href="https://abcd.com/login">dashboard</a> and then open the <a href="https://abcd.com/home">dashboard</a> details to verify your details on the <a href="https://abcd.com/data">data</a>

...and this final solution benchmarks nearly twice as fast as the regex solution. :)

Here a solution using a regex with lookaround:

const text = "Please click on dashboard and then open the dashboard details to verify your details on the data or the other data";
const tokens = ["dashboard", "dashboard", "data", "data"]
const links = ["https://abcd.com/login", "https://abcd.com/home", "https://abcd.com/data", "https://abcd.com/dashboard/data"]

var result = text;

for (i = 0; i < tokens.length; i++) {
  const re = new RegExp('(?<=.*)((?<= )' + tokens[i] + '(?= |$))(?=.*)');
  result = result.replace(re, '<a href="' + links[i] + '">$&</a>'); //TODO array length validation
}

console.log(result)

This regex will only work for tokens surrounded by whitespaces to avoid replacing the texts inside URLs.

You can see more about lookahead and lookbehind here and about browser compatibility here.

How about this one guy,

var string = "Please click on dashboard and then open the dashboard details to verify your details on the data";
const stringArray = string.split(' ');
var targetTexts = ["dashboard" , "dashboard" , "data"]
var replaceTexts = ["https://abcd.com/login" , "https://abcd.com/home" , "https://abcd.com/data"]

const resultArray = []
for (let i = 0; i < stringArray.length; i++) {
  const word = stringArray[i];
  const targetTextIndex = targetTexts.indexOf(word);
  
  if (targetTextIndex > -1) {
    resultArray.push("<a href='"+replaceTexts[targetTextIndex]+"'>"+word+"</a>")
    targetTexts = targetTexts.filter((_el, idx) => idx !== targetTextIndex)
    replaceTexts = replaceTexts.filter((_el, idx) => idx !== targetTextIndex)

  } else {
    resultArray.push(word);
  }
}

console.log(resultArray.join(' '))

I hope you get a hint on this one. It works like a charm, there will be exception handling for you to handle.

The presented approach consists of

  • a mapping task which firstly merges two related arrays, the list of search terms and the list of hypertext references, into another array of replacement items.

  • a reduce tasks which processes a list of replacement items and safely (without running into the same but already replaced search again) replaces each search by its related complete hypertext expression.

The 2nd part gets achieved by making use of the offset parameter of the replace method's replacerFunction. Upon the offset value, the current matches index, one can programmatically (while reducing) split the originally provided string value into processed and unprocessed substrings/partials. The reduce task's array result gets joined back into the final entirely replaced string value.

function createReplacementItemFromBoundHrefs(search, idx) {
  const hrefList = this;
  return {
    search,
    href: hrefList[idx],
  }
}
function aggregateSafelyReplacedHrefPartials(partials, { search, href }) {
  // intentionally mutates `partials`, an array of zero to many
  // processed substrings and always an unprocessed (last item)
  // `target` substring of the originally provided string value.
  const target = partials.pop();

  let sliceAt;
  const result = target
    .replace(

      // a case insensitive search regex.
      RegExp(`${ search }`, 'i'),

      // a replacer function which helps preventing multiple
      // manipulations of always the same search/replace term.
      (match, offset) => {
        const replacement = `<a href="${ href }">${ match }</a>`;

        sliceAt = offset + replacement.length;

        return replacement;
      },
    );

  return [
    ...partials,              // - processed lately.
    result.slice(0, sliceAt), // - processed latest.
    result.slice(sliceAt),    // - to be processed.
  ];
}

function safelyReplaceSearchWithRelatedHref(str, searchList, hrefList) {
  return searchList
    .map(createReplacementItemFromBoundHrefs, hrefList)
    .reduce(aggregateSafelyReplacedHrefPartials, [str])
    .join('');
}

const string = "Please click on dashboard and then open the dashboard details to verify your details on the data";

const stringArray = [
  "dashboard",
  "dashboard",
  "data",
];
const replaceArray = [
  "https://abcd.com/login",
  "https://abcd.com/home",
  "https://abcd.com/data",
];

console.log(
  'before ... ',
  string,
  '\n\nafter ... ',
  safelyReplaceSearchWithRelatedHref(string, stringArray, replaceArray),
);
console.log(
  stringArray
    .map(createReplacementItemFromBoundHrefs, replaceArray) 
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

"I have another use case, with an occurrences array , i only want to replace that specific occurrence word from the string. From below, 1 and 3 occurrences of dashboard and 2nd occurrence of data should be replaced. Can you help me with this.. ?? [...] var string = 'On dashboard, then open dashboard details, verify your dashboard details on data and other data'; var wordsArray = ['dashboard', 'dashboard', 'data']; var occurrences = [1 , 3 , 2]; var linksArray = ['abcd.com/login', 'abcd.com/home', 'abcd.com/data'];" – Siva_K22

For this the above presented first solution could be easily refactored into a new one which keeps the two folded approach.

  • The first task would be the creation of a replacement tracker which enables a replacer function to keep track of each search's current occurrence count and replacement.

  • The second task straightforwardly does replace the provided original string via

    • a case insensitive regex which features any possible word/search ... and ...
    • a replacer function which keeps track of each search's occurrence and upon a search's current occurrence count decides whether to really replace a search by its related full hypertext reference or just with itself (its very own match).

"@PeterSeliger -- Yea, but i resolved that with below RegEx.. string= string.replace(new RegExp("(?:(?:.|\n)*?"+currentstring+"){"+occurence+"}"), function(x){return x.replace(RegExp(currentstring+"$"), replaceString)});" – Siva_K22

Again a regex only approach will never be as reliable (also not as readable) as one that truly tracks search occurrences and was implemented with both goals being generic on one hand but also specialized enough (a regex only approach is limited much earlier in the latter terms).

function aggregateReplacementTrackerFromBoundData(collector, search, idx) {
  const { occurrences = [], hrefList = [], tracker = {} } = collector;

  // create and/or access and aggregate
  // a search specific occurrence tracker.
  (tracker[search.toLowerCase()] ??= {
    occurrence: {},
    occurrenceCount: 0,
  })
  .occurrence[ occurrences[idx] ] = hrefList[idx];

  return { occurrences, hrefList, tracker };
}
function safelyReplaceSearchOccurrencesWithRelatedHref(
  str, searchList, hrefList, occurrences,
) {
  // create an overall replacement tracker for any search.
  const { tracker } = searchList
    .reduce(aggregateReplacementTrackerFromBoundData, {
      occurrences,
      hrefList,
      tracker: {},
    });

  return str
    .replace(

      // a case insensitive regex which features any possible word/search.
      RegExp(`\\b(?:${ searchList.join('|') })\\b`, 'gi'),

      // a replacer function which keeps track of each search's occurrence
      // and upon a search's current occurrence count decides whether to
      // really replace a search by its related full hypertext reference
      // or just with itself (its very own match).
      (match/*, offset, target*/) => {
        const searchTracker = tracker[match.toLowerCase()];

        const count = ++searchTracker.occurrenceCount;
        const href = searchTracker.occurrence[count] ?? null;

        return (href !== null)
          && `<a href="${ href }">${ match }</a>`
          || match;
      },
    );
}

const string = "On dashboard, then open dashboard details, verify your dashboard details on data and other data";

const wordsArray = [
  "dashboard",
  "dashboard",
  "data",
];
const occurrences = [1 , 3 , 2];

const linksArray = [
  "https://abcd.com/login",
  "https://abcd.com/home",
  "https://abcd.com/data",
];

console.log(
  'before ... ',
  string,
  '\n\nafter ... ',
  safelyReplaceSearchOccurrencesWithRelatedHref(
    string, wordsArray, linksArray, occurrences,
  ),
);
console.log({
  replacementTracker: wordsArray
    .reduce(aggregateReplacementTrackerFromBoundData, {
      occurrences,
      hrefList: linksArray,
      tracker: {},
    })
    .tracker
});
.as-console-wrapper { min-height: 100%!important; top: 0; }

Related