14

How can I emulate the SQL keyword LIKE in JavaScript?

For those of you who don't know what LIKE is, it's a very simple regex which only supports the wildcards %, which matches 0 or more characters, and _ which matches exactly one character.

However, it's not just possible to do something like:

var match = new RegEx(likeExpr.replace("%", ".*").replace("_", ".")).exec(str) != null;

...because the pattern might contain dots, stars and any other special regex characters.

Syscall
  • 19,327
  • 10
  • 37
  • 52
erikkallen
  • 33,800
  • 13
  • 85
  • 120
  • fyi- in your sample code you're missing a close parenthesis before "`.exec`" – Kip Aug 21 '09 at 20:54
  • @C.barlow: don't hesitate to remove older 'edit: .. blah' markings when editing. Aim for "the best standalone question"; the edit history is there for all to see, so no need to keep commentary around. :) – sarnold Feb 10 '12 at 22:57
  • Doesn't directly get what you're seeking; however, if you're merely doing for a match of any text, simplest way is to use [includes](https://www.w3schools.com/jsref/jsref_includes.asp) syntax: `if(evalText.includes(searchText){ ... }` – Dave Skender Jun 06 '19 at 17:18

10 Answers10

12

What you have will work as long as you first escape the regex characters in your pattern. Below is one example from Simon Willison’s blog:

RegExp.escape = function(text) {
  if (!arguments.callee.sRE) {
    var specials = [
      '/', '.', '*', '+', '?', '|',
      '(', ')', '[', ']', '{', '}', '\\'
    ];
    arguments.callee.sRE = new RegExp(
      '(\\' + specials.join('|\\') + ')', 'g'
    );
  }
  return text.replace(arguments.callee.sRE, '\\$1');
}

You could then implement your code as:

likeExpr = RegExp.escape(likeExpr);
var match = new RegEx(likeExpr.replace("%", ".*").replace("_", ".")).exec(str) != null;
Kip
  • 107,154
  • 87
  • 232
  • 265
Chris Van Opstal
  • 36,423
  • 9
  • 73
  • 90
  • 2
    Assuming what you have already - `RegExp.like = function (text) { return new RegExp("^"+(RegExp.escape(text).replace(/%/g, ".*").replace(/_/g, "."))+"$"); }` might be a bit more reusable for him. – gnarf Aug 21 '09 at 21:04
  • 1
    That `Regex.escape` implementation is quite of an overkill. First of all, `arguments.callee` occurrence prevents some of the optimizations in modern browsers (and is deprecated in ES5-strict), so it's better to avoid it when possible. Second, there's an unnecessary escaping of characters, when instead they could be placed in a character class. Here's a smaller (and most likely faster) version that we use in Prototype.js - `RegExp.escape = function(str) { return str.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); };` – kangax Aug 23 '09 at 06:45
  • In SQL, `LIKE 'examp%'` matches "example" but does not match "an example" (see https://www.w3schools.com/SQL/sql_like.asp), however the answer above matches both. An improvement for the second line could be: `var match = new RegEx("^"+likeExpr.replace("%", ".*").replace("_", ".")+"$").exec(str) != null;` – Nicolas Form Aug 04 '22 at 10:09
6

I was looking for an answer the same question and came up with this after reading Kip's reply:

String.prototype.like = function(search) {
    if (typeof search !== 'string' || this === null) {return false; }
    // Remove special chars
    search = search.replace(new RegExp("([\\.\\\\\\+\\*\\?\\[\\^\\]\\$\\(\\)\\{\\}\\=\\!\\<\\>\\|\\:\\-])", "g"), "\\$1");
    // Replace % and _ with equivalent regex
    search = search.replace(/%/g, '.*').replace(/_/g, '.');
    // Check matches
    return RegExp('^' + search + '$', 'gi').test(this);
}

You can then use it as follows (note that it ignores UPPER/lower case):

var url = 'http://www.mydomain.com/page1.aspx';
console.log(url.like('%mydomain.com/page_.asp%')); // true

NOTE 29/11/2013: Updated with RegExp.test() performance improvement as per Lucios comment below.

Steven de Salas
  • 20,944
  • 9
  • 74
  • 82
  • instead of !!this.match(..., shouldn't you use this.test(... ? – Lucio M. Tato Nov 17 '13 at 00:59
  • 1
    hi @LucioM.Tato, x.test() method is for RegExp instances, not for String object.. [http://www.w3schools.com/jsref/jsref_regexp_test.asp](http://www.w3schools.com/jsref/jsref_regexp_test.asp) – Steven de Salas Nov 18 '13 at 11:35
  • I'm sorry I wasn't clear. You're getting a match array (expensive regexp) instead of using .test (better performance). Should be: return new RegExp(...).test(this); instead of converting an array or null to boolean by double negation.. http://stackoverflow.com/questions/10940137/regex-test-v-s-string-match-to-know-if-a-string-matches-a-regular-expression – Lucio M. Tato Nov 23 '13 at 12:36
  • 1
    +1 upvoted. I've used your solution with my change ( return RegExp('^' + search + '$', 'gi').test(this); – Lucio M. Tato Nov 23 '13 at 12:42
  • Good solution. Code is cleaner as well as faster. I've updated with your suggestion. – Steven de Salas Nov 28 '13 at 23:40
3

An old question but there are actually no good answers here. TSQL LIKE expressions can contain square-bracket escaped sections that are already almost valid regular expressions and allow for matching % and _. E.g.:

'75%' LIKE '75[%]'
'[foo]' LIKE '[[]foo]' -- ugh

Here's my function to convert a LIKE expression into a RegExp. The input is split into square-bracket and non-square-bracket sections. The square-bracket sections just need backslash escaping and the non-square-bracket sections are fully escaped while the % and _ directives are converted to regular expressions.

const likeRegExp = (expression, caseSensitive = false) =>
    new RegExp(`^${
        expression.split(/(\[.+?\])/g)
        .map((s, i) => i % 2 ?
            s.replace(/\\/g, '\\\\') :
            s.replace(/[-\/\\^$*+?.()|[\]{}%_]/g, m => {
                switch(m) {
                    case '%': return '.*';
                    case '_': return '.';
                    default: return `\\${m}`;
                }
            })
        ).join('')
    }$`, caseSensitive ? '' : 'i');
JohnLock
  • 381
  • 3
  • 5
  • 2
    Anyone looking for a solution to convert from sql LIKE syntax to javascript RegExp this is the way to go. It will spit out a RegExp that you can .test() on etc. – user1819575 Dec 07 '18 at 15:10
2

Here's a function I use, based on PHP's preg_quote function:

function regex_quote(str) {
  return str.replace(new RegExp("([\\.\\\\\\+\\*\\?\\[\\^\\]\\$\\(\\)\\{\\}\\=\\!\\<\\>\\|\\:\\-])", "g"), "\\$1");
}

So your line would now be:

var match = new RegEx(regex_quote(likeExpr).replace("%", ".*").replace("_", ".")).exec(str) != null;
Kip
  • 107,154
  • 87
  • 232
  • 265
1

If you want to use a regex, you can wrap each character of the string in square-brackets. Then you only have a few characters to escape.

But a better option might be to truncate the target strings so the length matches your search string and check for equality.

Joel Coehoorn
  • 399,467
  • 113
  • 570
  • 794
1

I wanted something that also handles escaping the wildcards % and _ using \% and \_.

Here is my solution using negative lookbehind:

// escapes RegExp special characters
const escapePattern = s => s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');

// converts ILIKE pattern to a RegExp object
const ilikeToRegExp = pattern =>
  new RegExp(
    `^${escapePattern(pattern)}$`
      // convert ILIKE wildcards, don't match escaped
      .replace(/(?<![\\])%/g, '.*')
      .replace(/(?<![\\])_/g, '.')
      // replace ILIKE escapes
      .replace(/\\%/g, '%')
      .replace(/\\_/g, '_'),
    'i'
  );

Usage:

ilikeToRegExp('%eLlo WoR%').test('hello world')  
// true

ilikeToRegExp('ello wor').test('hello world')  
// false

ilikeToRegExp('%90\%%').test('...90%...') 
// true
Rafi
  • 816
  • 9
  • 21
0

In Chris Van Opstal's answer you should use replaceAll instead of replace to replace all occurrances of '%' and '_'. Reference to how to do replaceAll - here

Community
  • 1
  • 1
0

Johnny come lately here but this works for me I use it for my spa pages to avoid certain pages showing results after the default page:

function like(haystack,needle){
    needle = needle.split(','); 
    var str = haystack.toLowerCase();
    var n = -1;
    for(var i=0;i<needle.length;i++){
        n = str.search(needle[i]);
        if(n > -1){
            return n;
        }
    }
return n;
}

usage is - here I want to not show any results on the tools,contact or home pages - results() is a function I do not show here:

var n = like($data,'tools,contact,home');
//~ alert(n);
if(n < 0){// does not match anything in the above string
  results($data);
}
richardwhitney
  • 506
  • 1
  • 6
  • 21
0

I needed this, with escaping and working in Safari (no negative lookbehinds). Here is what I came up with:

/**
 * Quotes a string following the same rules as https://www.php.net/manual/function.preg-quote.php
 *
 * Sourced from https://locutus.io/php/preg_quote/
 *
 * @param {string} str String to quote.
 * @param {?string} [delimiter] Delimiter to also quote.
 * @returns {string} The quoted string.
 */
function regexQuote(str, delimiter) {
    return (str + '').replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + (delimiter || '') + '-]', 'g'), '\\$&');
}

/**
 * Removes the diacritical marks from a string.
 *
 * Diacritical marks: {@link https://unicode-table.com/blocks/combining-diacritical-marks/}
 *
 * @param {string} str The string from which to strip the diacritical marks.
 * @returns {string} Stripped string.
 */
function stripDiacriticalMarks(str) {
    return unorm.nfkd(str).replaceAll(/[\u0300-\u036f]+/g, '');
}

/**
 * Checks if the string `haystack` is like `needle`, `needle` can contain '%' and '_'
 * characters which will behave as if used in a SQL LIKE condition. Character escaping
 * is supported with '\'.
 *
 * @param {string} haystack The string to check if it is like `needle`.
 * @param {string} needle The string used to check if `haystack` is like it.
 * @param {boolean} [ai] Whether to check likeness in an accent-insensitive manner.
 * @param {boolean} [ci] Whether to check likeness in a case-insensitive manner.
 * @returns {boolean} True if `haystack` is like `needle`, otherwise, false.
 */
function strLike(haystack, needle, ai = true, ci = true) {
    if (ai) {
        haystack = stripDiacriticalMarks(haystack);
        needle = stripDiacriticalMarks(needle);
    }

    needle = regexQuote(needle, '/');

    let tokens = [];

    for (let i = 0; i < needle.length; ) {
        if (needle[i] === '\\') {
            i += 2;
            if (i < needle.length) {
                if (needle[i] === '\\') {
                    tokens.push('\\\\');
                    i += 2;
                } else {
                    tokens.push(needle[i]);
                    ++i;
                }
            } else {
                tokens.push('\\\\');
            }
        } else {
            switch (needle[i]) {
                case '_':
                    tokens.push('.')
                    break;
                case '%':
                    tokens.push('.*')
                    break;
                default:
                    tokens.push(needle[i]);
                    break;
            }
            ++i;
        }
    }

    return new RegExp(`^${tokens.join('')}$`, `u${ci ? 'i' : ''}`).test(haystack);
}

/**
 * Escapes a string in a way that `strLike` will match it as-is, thus '%' and '_'
 * would match a literal '%' and '_' respectively (and not behave as in a SQL LIKE
 * condition).
 *
 * @param {string} str The string to escape.
 * @returns {string} The escaped string.
 */
function escapeStrLike(str) {
    let tokens = [];

    for (let i = 0; i < str.length; i++) {
        switch (str[i]) {
            case '\\':
                tokens.push('\\\\');
                break;
            case '%':
                tokens.push('\\%')
                break;
            case '_':
                tokens.push('\\_')
                break;
            default:
                tokens.push(str[i]);
        }
    }

    return tokens.join('');
}

The code above is dependant on unorm, and is unicode aware to be able to catch cases like:

strLike('Hello ', 'Hello _'); // true
strLike('Hello ', '_e%o__');  // true
strLike('asdfas \\H\\\\%É\\l\\_\\l\\o asdfasf', '%' . escapeStrLike('\\h\\\\%e\\l\\_\\l\\o') . '%'); // true
mishamosher
  • 1,003
  • 1
  • 13
  • 28
0

I ended up writing a function based on a few answers here that worked pretty well for me. I needed something that would preserve the "startswith%" and "%endswith" syntax, and returns no matches for an empty search string.

function sqlLIKE(target, likeExp) {
  let regex = likeExp
    .replaceAll(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1')
    .replaceAll("%", ".*")
    .replaceAll("_", ".");

  if (likeExp.charAt(0) !== '%' || !likeExp.includes('%')) regex = `^${regex}`;

  if (likeExp.charAt(likeExp.length - 1) !== '%' || !likeExp.includes('%')) regex = `${regex}$`;

  return new RegExp(regex).exec(target) !== null;
}
pigeontoe
  • 446
  • 3
  • 8