Javascript 如何匹配和突出显示字符串数组中任意顺序的所有术语?

Javascript 如何匹配和突出显示字符串数组中任意顺序的所有术语?,javascript,regex,algorithm,search,highlighting,Javascript,Regex,Algorithm,Search,Highlighting,这些要求是: 从数组(从这里开始调用选项)中查找包含ALL任何顺序中的术语的字符串 正确突出显示匹配项-即在每个匹配项前后插入一个字符串-我在这里使用和 搜索查询和选项都可以是“任意” 为了简单起见,答案可以集中于通过仅包含ASCII字符的列表进行不区分大小写的搜索,并假设术语分隔符是一个纯空格-即,输入为“Foo-bar-baz”的查询意味着搜索术语是['Foo','bar','baz'] 澄清: 所有术语必须在匹配选项中彼此独立存在,即较短术语不应仅作为较长术语的子字符串存在,且两个术

这些要求是:

  • 从数组(从这里开始调用选项)中查找包含ALL任何顺序中的术语的字符串
  • 正确突出显示匹配项-即在每个匹配项前后插入一个字符串-我在这里使用
  • 搜索查询和选项都可以是“任意”
为了简单起见,答案可以集中于通过仅包含ASCII字符的列表进行不区分大小写的搜索,并假设术语分隔符是一个纯空格-即,输入为“Foo-bar-baz”的查询意味着搜索术语是
['Foo','bar','baz']

澄清:

  • 所有术语必须在匹配选项中彼此独立存在,即较短术语不应仅作为较长术语的子字符串存在,且两个术语不应重叠
  • 选项中重复的搜索词必须至少存在于查询中的次数
最后一个应用程序是(也许并不奇怪)自动完成的排序

TL;博士 最新的小提琴手将提出的算法并排比较:

(如果添加新算法,请随时更新此链接)

jsPerf以一种更真实/更具代表性的方式比较算法-基本上在每个rep上一次“输入”一个字符的几个字符串:

在这一点上,很明显(多亏了trincot出色的基本比较),原始实现使用的大部分时间都花在DOM输出上。它的重要性在小提琴中被尽可能地降低了

在每次搜索中,算法之间的性能仍然存在明显差异,但没有一种算法在每次击键时都始终保持最快的速度。在重新审视和清理了我自己的“分而治之”之后,在我尝试的任何现实场景中,它的表现都始终优于其他人


Tigregalis引入了预运行优化的思想,考虑到按键之间的选项不太可能改变,这似乎是合理的。我已经在这里的所有方法中添加了(一个函数)。唯一的算法,我从它的一个明显的好处是在Srutle的排列,但我会鼓励每个回答者考虑它可能对他们自己的算法有用。 有些算法比其他算法更容易适应。我仍然认为这比实际实现中的微小性能差异更重要

请注意,Tigregalis的收缩集的当前版本有一个bug——在修复之前,我已经将其从fiddle和jsperf中排除


病毒排列 理论上,这可以通过“手动”构造一个RegExp来解决,该RegExp包含由捕获组分隔的搜索词的所有可能排列,以捕获词与词之间的任何内容-搜索“foo-bar”会导致
(foo)(.*)(bar)|(bar)(.*)(foo)

然后使用
string.replace()
一次性完成高亮显示。如果字符串中有任何变化,我们就有一个匹配项

演示:

var选项=[“美国”、“联合王国”、“阿富汗”、“阿兰群岛”、“阿尔巴尼亚”、“阿尔及利亚”、“美属萨摩亚”、“安道尔”、“安哥拉”、“安圭拉”、“南极洲”、“安提瓜和巴布达”、“阿根廷”、“亚美尼亚”、“阿鲁巴”、“澳大利亚”、“奥地利”、“阿塞拜疆”、“巴哈马”、“巴林”、“孟加拉国”、“巴巴多斯”、“白俄罗斯”、“比利时”、“伯利兹”、“贝宁”、“百慕大”,“不丹”、“玻利维亚、多民族国家”、“博内尔、圣尤斯特修斯和萨巴”、“波斯尼亚和黑塞哥维那”、“博茨瓦纳”、“布韦特岛”、“巴西”、“英属印度洋领土”、“文莱达鲁萨兰国”、“保加利亚”、“布基纳法索”、“布隆迪”、“柬埔寨”、“喀麦隆”、“加拿大”、“佛得角”、“开曼群岛”、“中非共和国”、“乍得”、“智利”‘中国’、‘圣诞岛’、‘可可(基陵)’等群岛、哥伦比亚、科摩罗、刚果、刚果民主共和国、库克群岛、哥斯达黎加、科特迪瓦、克罗地亚、古巴、库拉索岛、塞浦路斯、捷克共和国、丹麦、吉布提、多米尼加、多米尼加共和国、厄瓜多尔、埃及、萨尔瓦多、赤道几内亚、厄立特里亚、爱沙尼亚、埃塞俄比亚、福克兰群岛土地(马尔维纳斯)“,”法罗群岛“,”斐济“,”芬兰“,”法国“,”法属圭亚那“,”法属波利尼西亚“,”法国南部领土“,”加蓬“,”冈比亚“,”格鲁吉亚“,”德国“,”加纳“,”直布罗陀“,”希腊“,”格陵兰“,”格林纳达“,”瓜德罗普“,”关岛“,”危地马拉“,”根西岛“,”几内亚“,”几内亚比绍“,”圭亚那“,”海地“,”赫德岛和麦克唐纳见(梵蒂冈城邦)“洪都拉斯”、“香港”、“匈牙利”、“冰岛”、“印度”、“印度尼西亚”、“伊朗、伊斯兰共和国”、“伊拉克”、“爱尔兰”、“马恩岛”、“以色列”、“意大利”、“牙买加”、“日本”、“泽西岛”、“约旦”、“哈萨克斯坦”、“肯尼亚”、“基里巴斯”、“韩国、朝鲜、大韩民国”、“科威特”、“吉尔吉斯斯坦”、“老挝人民民主共和国”拉脱维亚、黎巴嫩、莱索托、利比里亚、利比亚、列支敦士登、立陶宛、卢森堡、澳门、前南斯拉夫的马其顿共和国、马达加斯加、马拉维、马来西亚、马尔代夫、马里、马耳他、马绍尔群岛、马提尼克、毛里塔尼亚、毛里求斯、马约特、墨西哥、密克罗尼西亚联邦、摩尔多瓦、,共和国、摩纳哥、蒙古、黑山、蒙特塞拉特、摩洛哥、莫桑比克、缅甸、纳米比亚、瑙鲁、尼泊尔、荷兰、新喀里多尼亚、新西兰、尼加拉瓜、尼日尔、尼日利亚、纽埃、诺福克岛、北马里亚纳群岛、挪威、阿曼、巴基斯坦、帕劳、被占领巴勒斯坦领土、巴拿马、Papua新几内亚、巴拉圭、秘鲁、菲律宾、皮特凯恩、波兰、葡萄牙、波多黎各、卡塔尔、留尼汪、罗马尼亚、俄罗斯联邦、卢旺达、圣巴塞勒米、圣赫勒拿、阿森松岛和特里斯坦达库尼亚、圣基茨和尼维斯、圣卢西亚
var separator = /\s|\*|,/;

// this function enhances the raw options array 
function enhanceOptions(options) {
  return options.map(option => ({
    working: option.toLowerCase(), // for use in filtering the set and matching
    display: option // for displaying
  }))
}

// this function changes the input to lower case, splits the input into terms, removes empty strings from the array, and enhances the terms with the size and wiping string
function processInput(input) {
  return input.trim().toLowerCase().split(separator).filter(term => term.length).map(term => ({
    value: term.toLowerCase(),
    size: term.length,
    wipe: " ".repeat(term.length)
  })).sort((a, b) => b.size - a.size);
}

// this function filters the data set, then finds the match ranges, and finally returns an array with HTML tags inserted
function filterAndHighlight(terms, enhancedOptions) {
  let options = enhancedOptions,
    l = terms.length;

  // filter the options - consider recursion instead
  options = options.filter(option => {
    let i = 0,
      working = option.working,
      term;
    while (i < l) {
      if (!~working.indexOf((term = terms[i]).value)) return false;
      working = working.replace(term.value, term.wipe);
      i++;
    }
    return true;
  })

  // generate the display string array
  let displayOptions = options.map(option => {
    let rangeSet = [],
      working = option.working,
      display = option.display;

    // find the match ranges
    terms.forEach(term => {
      working = working.replace(term.value, (match, offset) => { // duplicate the wipe string replacement from the filter, but grab the offsets
        rangeSet.push({
          start: offset,
          end: offset + term.size
        });
        return term.wipe;
      })
    })

    // sort the match ranges, last to first
    rangeSet.sort((a, b) => b.start - a.start);

    // insert the html tags within the string around each match range
    rangeSet.forEach(range => {
      display = display.slice(0, range.start) + '<u>' + display.slice(range.start, range.end) + '</u>' + display.slice(range.end)
    })

    return display;

  })

  return displayOptions;
}