49

I am building search and I am going to use javascript autocomplete with it. I am from Finland (finnish language) so I have to deal with some special characters like ä, ö and å

When user types text in to the search input field I try to match the text to data.

Here is simple example that is not working correctly if user types for example "ää". Same thing with "äl"

var title = "this is simple string with finnish word tämä on ääkköstesti älkää ihmetelkö";
// Does not work
var searchterm = "äl";

// does not work
//var searchterm = "ää";

// Works
//var searchterm = "wi";

if ( new RegExp("\\b"+searchterm, "gi").test(title) ) {
    $("#result").html("Match: ("+searchterm+"): "+title);
} else {
    $("#result").html("nothing found with term: "+searchterm);   
}

http://jsfiddle.net/7TsxB/

So how can I get those ä,ö and å characters to work with javascript regex?

I think I should use unicode codes but how should I do that? Codes for those characters are: [\u00C4,\u00E4,\u00C5,\u00E5,\u00D6,\u00F6]

=> äÄåÅöÖ

user1394520
  • 591
  • 1
  • 4
  • 3
  • 1
    @Walkerneo: `\b` means "word boundary" in a regex; the slash is escaped here because it's in a string. – apsillers May 14 '12 at 20:05
  • @apsillers, Thanks, weird that I'd not seen that before :/ – mowwwalker May 14 '12 at 20:15
  • I use the \b because I want to match at the beginning of each word. – user1394520 May 14 '12 at 20:16
  • 9
    As you see, Javascript is stuck in the idiotic 1960’s-style ASCII-only mentality. It does not meet even the most basic conformance requirements needed for Level 1’s “Basic Unicode Support” per [UTS#18 on Unicode Regular Expressions](http://unicode.org/reports/tr18/#Basic_Unicode_Support). Trying to do real Unicode text-processing work in Javascript an awful joke, and a cruel one, too: it cannot be done. The XRegexp plugin mentioned below is necessary but not sufficient for these purposes. – tchrist May 16 '12 at 16:27
  • Newcomers beware: This cannot be done in regexp. Not with `\b`, not with `\s`, not with XRegExp, not with lookaheads or lookarounds. Believe me, I've tried it all, and everything broke in some or other way. The only reliable way I've found that up until now works is encoding the unicode string back to ascii and perform an ascii only regexp search/replace with `\b` as originally intended. See here: http://stackoverflow.com/a/10590188/1329367 – Mahn Oct 12 '15 at 19:57

11 Answers11

43

There appears to be a problem with Regex and the word boundary \b matching the beginning of a string with a starting character out of the normal 256 byte range.

Instead of using \b, try using (?:^|\\s)

var title = "this is simple string with finnish word tämä on ääkköstesti älkää ihmetelkö";
// Does not work
var searchterm = "äl";

// does not work
//var searchterm = "ää";

// Works
//var searchterm = "wi";

if ( new RegExp("(?:^|\\s)"+searchterm, "gi").test(title) ) {
    $("#result").html("Match: ("+searchterm+"): "+title);
} else {
    $("#result").html("nothing found with term: "+searchterm);   
}

Breakdown:

(?: parenthesis () form a capture group in Regex. Parenthesis started with a question mark and colon ?: form a non-capturing group. They just group the terms together

^ the caret symbol matches the beginning of a string

| the bar is the "or" operator.

\s matches whitespace (appears as \\s in the string because we have to escape the backslash)

) closes the group

So instead of using \b, which matches word boundaries and doesn't work for unicode characters, we use a non-capturing group which matches the beginning of a string OR whitespace.

mowwwalker
  • 14,309
  • 23
  • 87
  • 144
  • 3
    "try this" isn't a solution. Give some information about *why* the suggested regex works. What does `(?:^|\\s)` really do? You don't explain this solution at all. – H Dog Aug 04 '14 at 20:11
  • 14
    This is NOT a correct solution. `(?:^|\\s)` is not a zero-width assertion like `\b` is, and will consume characters from the match. A positive lookahead would be a better idea (`(?=^|\\s)`) but would only work after the match, as lookbehind is still not supported. Also, word boundaries are not just spaces and string boundaries, but a ton of other characters. – Lea Verou Feb 27 '17 at 20:57
  • Is there any reason not to include $ (end of string) in the regex? I.e. (?:^|\s|$) – Ron Inbar Sep 07 '17 at 13:21
  • The proposed regexp doesn't have the same behavior when the match is at the beginning of a string or after a whitespace. When it matches at the beginning the matched text is returned, however when it matches after a whitespace it also returns the whitespace as part of the match, even though the capture is done with the colon. Test code (executed in Firefox console): let str1 = "un ejemplo"; let str2 = "ejemplo uno"; let reg = /(?:^|\s)un/gi; str1.match(reg); // ["un"] str2.match(reg); // [" un"] – Javier Mr Sep 21 '17 at 11:01
  • 1
    This also matches partial string matches. `'¿dónde está la alcaldesa?'`: `es` and `está` are matched, which is bad. Only `está` should be matched. `\\b` is supposed to be helpful with full-word boundaries. – Growler Jun 02 '18 at 06:06
  • If the pattern is like `"\\b($1)\\b";` the problem will also be with a ascii char at the beginning of the string like `'cause`. – loretoparisi Sep 23 '20 at 18:32
20

The \b character class in JavaScript RegEx is really only useful with simple ASCII encoding. \b is a shortcut code for the boundary between \w and \W sets or \w and the beginning or end of the string. These character sets only take into account ASCII "word" characters, where \w is equal to [a-zA-Z0-9_] and \W is the negation of that class.

This makes the RegEx character classes largely useless for dealing with any real language.

\s should work for what you want to do, provided that search terms are only delimited by whitespace.

Noah Freitas
  • 16,298
  • 9
  • 47
  • 65
  • 12
    +1, but `\b` is not a character class shorthand like `\w` and `\s`, it's a zero-width assertion like `\A`, `$`, and lookarounds. – Alan Moore May 14 '12 at 23:51
14

this question is old, but I think I found a better solution for boundary in regular expressions with unicode letters. Using XRegExp library you can implement a valid \b boundary expanding this

XRegExp('(?=^|$|[^\\p{L}])')

the result is a 4000+ char long, but it seems to work quite performing.

Some explanation: (?= ) is a zero-length lookahead that looks for a begin or end boundary or a non-letter unicode character. The most important think is the lookahead, because the \b doesn't capture anything: it is simply true or false.

Flimm
  • 97,949
  • 30
  • 201
  • 217
max masetti
  • 141
  • 1
  • 3
7

I would recommend you to use XRegExp when you have to work with a specific set of characters from Unicode, the author of this library mapped all kind of regional sets of characters making the work with different languages easier.

ypid
  • 1,334
  • 12
  • 10
micnic
  • 9,275
  • 5
  • 38
  • 52
5

\b is a shortcut for the transition between a letter and a non-letter character, or vice-versa.

Updating and improving on max_masseti's answer:

With the introduction of the /u modifier for RegExs in ES2018, you can now use \p{L} to represent any unicode letter, and \P{L} (notice the uppercase P) to represent anything but.

EDIT: Previous version was incomplete.

As such:

const text = 'A Fé, o Império, e as terras viciosas';

text.split(/(?<=\p{L})(?=\P{L})|(?<=\P{L})(?=\p{L})/);

// ['A', ' Fé', ',', ' o', ' Império', ',', ' e', ' as', ' terras', ' viciosas']

We're using a lookbehind (?<=...) to find a letter and a lookahead (?=...) to find a non-letter, or vice versa.

andrefs
  • 507
  • 4
  • 14
2

I noticed something really weird with \b when using Unicode:

/\bo/.test("pop"); // false (obviously)
/\bä/.test("päp"); // true (what..?)

/\Bo/.test("pop"); // true
/\Bä/.test("päp"); // false (what..?)

It appears that meaning of \b and \B are reversed, but only when used with non-ASCII Unicode? There might be something deeper going on here, but I'm not sure what it is.

In any case, it seems that the word boundary is the issue, not the Unicode characters themselves. Perhaps you should just replace \b with (^|[\s\\/-_&]), as that seems to work correctly. (Make your list of symbols more comprehensive than mine, though.)

apsillers
  • 101,930
  • 15
  • 206
  • 224
  • 15
    `\b` and `\B` aren't Unicode-aware in JavaScript, so they consider `ä` a non-alphanumeric character and therefore see a word boundary between `p` and `ä`. – Tim Pietzcker May 14 '12 at 20:54
2

Despite the fact the issue seems to be 8 years old, I run into a similar problem (I had to match Cyrillic letters) not so far ago. I spend a whole day on this and could not find any appropriate answer here on StackOverflow. So, to avoid others making lots of effort, I'd like to share my solution.

Yes, \b word boundary works only with Latin letters (Word boundary: \b):

Word boundary \b doesn’t work for non-Latin alphabets The word boundary test \b checks that there should be \w on the one side from the position and "not \w" – on the other side. But \w means a Latin letter a-z (or a digit or an underscore), so the test doesn’t work for other characters, e.g. Cyrillic letters or hieroglyphs.

Yes, JavaScript RegExp implementation hardly supports UTF-8 encoding.

So, I tried implementing own word boundary feature with the support of non-Latin characters. To make word boundary work just with Cyrillic characters I created such regular expression:

new RegExp(`(?<![\u0400-\u04ff])${cyrillicSearchValue}(?![\u0400-\u04ff])`,'gi')

Where \u0400-\u04ff is a range of Cyrillic characters provided in the table of codes. It is not an ideal solution, however, it works properly in most cases.

To make it work in your case, you just have to pick up an appropriate range of codes from the list of Unicode characters.

To try out my example run the code snippet below.

function getMatchExpression(cyrillicSearchValue) {
  return new RegExp(
    `(?<![\u0400-\u04ff])${cyrillicSearchValue}(?![\u0400-\u04ff])`,
    'gi',
  );
}

const sentence = 'Будь-який текст кирилицею, де необхідно знайти слово з контексту';

console.log(sentence.match(getMatchExpression('текст')));
// expected output: ["текст"]


console.log(sentence.match(getMatchExpression('но')));
// expected output: null
1

My idea is to search with codes representing the Finnish letters

new RegExp("\\b"+asciiOnly(searchterm), "gi").test(asciiOnly(title))

My original idea was to use plain encodeURI but the % sign seemed to interfere with the regexp.

http://jsfiddle.net/7TsxB/5/

I wrote a crude function using encodeURI to encode every character with code over 128 but removing its % and adding 'QQ' in the beginning. It is not the best marker but I couldn't get non alphanumeric to work.

Heitor Chang
  • 5,812
  • 2
  • 42
  • 58
  • This is a great idea, and the only thing that worked for me. Instead of `QQ` you can use a control string of `___` which is a bit safer and still ascii, and instead of `encodeURI` you can leverage javascript's native `escape`/`unescape` methods, but otherwise it does the job. – Mahn Oct 12 '15 at 19:53
  • this is not good solution for those who want to do something with matched substring – petriq Feb 01 '16 at 10:21
  • Does this assume any non-ASCII character to be a word character? For example, "äl" wouldn't be treated as the beginning of a word in "älkää", although it should be. – Jānis Elmeris Oct 28 '18 at 10:17
1

What you are looking for is the Unicode word boundaries standard:

http://unicode.org/reports/tr29/tr29-9.html#Word_Boundaries

There is a JavaScript implementation here (unciodejs.wordbreak.js)

https://github.com/wikimedia/unicodejs

Ed.
  • 1,793
  • 2
  • 12
  • 6
0

I have had a similar problem, but I had to replace an array of terms. All solutions, which I have found did not worked, if two terms were in the text next to each other (because their boundaries overlaped). So I had to use a little modified approach:

var text = "Ještě. že; \"už\" à. Fürs, 'anlässlich' že že že.";
var terms = ["à","anlässlich","Fürs","už","Ještě", "že"];
var replaced = [];
var order = 0;
for (i = 0; i < terms.length; i++) {
    terms[i] = "(^\|[ \n\r\t.,;'\"\+!?-])(" + terms[i] + ")([ \n\r\t.,;'\"\+!?-]+\|$)";
}
var re = new RegExp(terms.join("|"), "");
while (true) {
    var replacedString = "";
    text = text.replace(re, function replacer(match){
        var beginning = match.match("^[ \n\r\t.,;'\"\+!?-]+");
        if (beginning == null) beginning = "";
        var ending = match.match("[ \n\r\t.,;'\"\+!?-]+$");
        if (ending == null) ending = "";
        replacedString = match.replace(beginning,"");
        replacedString = replacedString.replace(ending,"");
        replaced.push(replacedString);
        return beginning+"{{"+order+"}}"+ending;
    });
if (replacedString == "") break;
order += 1;
}

See the code in a fiddle: http://jsfiddle.net/antoninslejska/bvbLpdos/1/

The regular expression is inspired by: http://breakthebit.org/post/3446894238/word-boundaries-in-javascripts-regular

I can't say, that I find the solution elegant...

Antonín Slejška
  • 1,702
  • 4
  • 22
  • 35
0

The correct answer to the question is given by andrefs. I will only rewrite it more clearly, after putting all required things together.

For ASCII text, you can use \b for matching a word boundary both at the start and the end of a pattern. When using Unicode text, you need to use 2 different patterns for doing the same:

  • Use (?<=^|\P{L}) for matching the start or a word boundary before the main pattern.
  • Use (?=\P{L}|$) for matching the end or a word boundary after the main pattern.
  • Additionally, use (?i) in the beginning of everything, to make all those matchings case-insensitive.

So the resulting answer is: (?i)(?<=^|\P{L})xxx(?=\P{L}|$), where xxx is your main pattern. This would be the equivalent of (?i)\bxxx\b for ASCII text.

For your code to work, you now need to do the following:

  • Assign to your variable "searchterm", the pattern or words you want to find.
  • Escape the variable's contents. For example, replace '\' with '\\' and also do the same for any reserved special character of regex, like '\^', '\$', '\/', etc. Check here for a question on how to do this.
  • Insert the variable's contents to the pattern above, in the place of "xxx", by simply using the string.replace() method.
Manthos
  • 31
  • 7
  • Thanks, but I get a `SyntaxError: Invalid regular expression: /(?i)(?<=^|P{L})äl(?=P{L}|$)/: Invalid group` when using `new RegExp(pattern.replace('xxx', searchterm), "g");` for `var pattern = '(?i)(?<=^|\P{L})xxx(?=\P{L}|$)'`. – loretoparisi Sep 23 '20 at 19:09
  • So the error was due to the `(?i)`. If I remove it I get `/(?<=^|P{L})äl(?=P{L}|$)/g`, but when exec I have no match. – loretoparisi Sep 23 '20 at 19:14
  • 1
    @loretoparisi My response described the answer using the PCRE (PHP) flavor for writing regular expressions. You get the error because you are applying it in an environment that uses the ECMAScript flavor. For it to properly work, you need to modify it by removing the 1st term and adding the ```i``` modifier: ```/(?<=^|\P{L})xxx(?=\P{L}|$)/gmi``` – Manthos Sep 29 '20 at 10:31