用多个值替换字符串,互不干扰

Replace string with multiple values, without interference

说,我有一个字符串要替换:

let searches = ['gone', 'go', 'run']
let s = 'go went gone-go'
const lookup = {
  'go': '(go)',
  'gone': '[gone]',
}

for (let key of searches) {
    s = s.replaceAll(key, lookup[key])
}

console.log(s)

我得到 (go) went [(go)ne]-(go)。 假设 s 可以是包含来自 lookup 键的某些单词的任何字符串,并且 lookup 值不一定具有一致的模式。 searches 是来自外部输入的变量。

如果我将 searches 中的订单更改为例如 ['go', 'gone', 'run'],结果将变为 (go) went (go)ne-(go)

我期待的结果是(go) went [gone]-(go),所以较长的先被替换,不会被后面匹配的替换。

我确实想出了一个解决方案,首先将查找的值替换为 uuid,从较长的键迭代到较短的键,然后将 uuid 替换回相应的值。当然,这是相当愚蠢和低效的:

let searches = ['go', 'gone', 'run']
let s = 'go went gone-go'
const lookup = {
  'go': '(go)',
  'gone': '[gone]',
}

const uuid = () => Date.now().toString(36) + Math.random().toString(36).substring(2)  // pseudo uuid for quick demo. src: 
let uuidKeys = {}
Object.keys(lookup).forEach(k => uuidKeys[k] = uuid())  // uuidKeys = {'go': 'random1', 'gone': 'random2'}
let uuids = Object.values(uuidKeys)  // uuids = ['random1', 'random2']
let uuidValues = {}
Object.keys(lookup).forEach((k, i) => uuidValues[uuids[i]] = lookup[k])  // uuidValues = {'random1': '(go)', 'random2': '[gone]'}
searches.sort((a, b) => b.length -a.length)  // searches = ['gone', 'run', 'go']
for (let key of searches) {
    s = s.replaceAll(key, uuidKeys[key])  // s = 'random1 went random2-random1'
}
for (let key of searches.map(i => uuidKeys[i])) {
    s = s.replaceAll(key, uuidValues[key])  // s = '(go) went [gone]-(go)'
}

console.log(s)

然后我想通过搜索循环拆分字符串,然后替换并记录处理过的索引,最后将列表连接回字符串。但是,如果没有 for 循环中昂贵的 Array 方法(平面、拼接等),我找不到实现它的好方法。

有没有elegant/efficient的方法来达到这个效果?

您可以通过使用带 g 标志和 replace 的正则表达式来执行此操作,并传递一个回调函数作为替换;该函数然后根据匹配的内容选择合适的替换。

例如:

let searches = ["gone", "go", "run"];
let s = "go went gone-go";
const lookup = {
    "go": "(go)",
    "gone": "[gone]",
};
let rex = new RegExp(searches.map(escapeRegex).join("|"), "g");
s = s.replace(rex, match => lookup[match]);
console.log(s);

...其中 escapeRegex 转义搜索字符串中在正则表达式中具有特殊含义的任何字符;请参阅 this question's answers 以了解可能的实施方式。

实例:

function escapeRegex(string) {
    return string.replace(/[-\/\^$*+?.()|[\]{}]/g, '\$&');
}

let searches = ["gone", "go", "run"];
let s = "go went gone-go";
const lookup = {
    "go": "(go)",
    "gone": "[gone]",
};
let rex = new RegExp(searches.map(escapeRegex).join("|"), "g");
s = s.replace(rex, match => lookup[match]);
console.log(s); // "(go) went [gone]-(go)"

注意:searches 数组中字符串的顺序很重要。 如果将 "go" 放在 "gone" 之前,它将首先匹配:

function escapeRegex(string) {
    return string.replace(/[-\/\^$*+?.()|[\]{}]/g, '\$&');
}

let searches = ["go", "gone", "run"];
// Note −−−−−−−−^
let s = "go went gone-go";
const lookup = {
    "go": "(go)",
    "gone": "[gone]",
};
let rex = new RegExp(searches.map(escapeRegex).join("|"), "g");
s = s.replace(rex, match => lookup[match]);
console.log(s); // "(go) went (go)ne-(go)"

如果您总是希望最长的优先级最高,并且您无法控制输入数组的内容,则可以在使用它之前对其进行排序:

function escapeRegex(string) {
    return string.replace(/[-\/\^$*+?.()|[\]{}]/g, '\$&');
}

let searches = ["go", "gone", "run"];
// Note −−−−−−−−^
let s = "go went gone-go";
const lookup = {
    "go": "(go)",
    "gone": "[gone]",
};
let rex = new RegExp(
    searches.sort((a, b) => b.length - a.length)
      .map(escapeRegex)
      .join("|"),
    "g"
);
s = s.replace(rex, match => lookup[match]);
console.log(s); // "(go) went [gone]-(go)"