Logic on a Complicated Regex on My Napkin Math Parser - javascript

So I'm writing my own napkin math parser[1] for fun. Napkin math is a part of a custom text editor I am building - when you press space after an equals sign it will detect the equation prior to the equals sign and does the math for you. E.g., you type 1+1= and it kicks out the 2 for you magically so you have 1+1=2 on the screen.
I'm really struggling with getting a regex to match equations prior to the equals sign. My brain is shot and I am in desparate need of help from a lord of regex. Below are my test strings, with the highlighted sections being what I want the regex to match.
Particularly, I'm having trouble getting the regex to match the true start of an equation. My current regex gets thrown off if there are numbers or words before the equation starts. I feel like I need to somehow work backwards from the equals sign. I started reversing my string characters around and that's when I threw my hands up and came to you for help.
My test strings with desired matches (the true equation part of the string):
test text (1) pi=
test text (1 pi=
test text 1) pi=
test text (2) pi + 10 / 20=
test text (3) test pi ^ 10 / 20=
test text (30) 10 + 5=
test text (500) abs(10 + 5)=
test text (1) pi + 10 / 20=
test text 10*5=
test text pi / phi=
test text 10 mod phi=
test text 50 10 mod phi=
test text pi mod abs(phi)=
apple banana cherry apple 10 apple cherry banana hi 10 99+1=
Here are all of my special key words allowed and useful in napkin math:
var napkinMathKeyWords = [
'pi',
'\\u03C0',
'\\u03A0',
'phi',
'\\u03C6',
'\\u03A6',
'abs',
'acos',
'cos',
'atan',
'tan',
'asin',
'sin',
'ceil',
'floor',
'sqrt',
'log',
'ln',
'mod',
];
EDIT
Here's the regex I've got so far:
(\d|pi|\\u03C0|\\u03A0|phi|\\u03C6|\\u03A6|abs|acos|cos|atan|tan|asin|sin|ceil|floor|sqrt|log|ln|mod).*?=
It's hitting most of my cases, there are just a couple of cases throwing it off:
http://i.imgur.com/QbueeFC.png
[1] http://blogs.msdn.com/b/onenotetips/archive/2008/05/09/napkin-math-in-onenote.aspx

As you implied, this regex gets a little messy. But I have one that works (see regex101). On regex101, I used the PCRE mode, but only so I could use the "extended" option to make the regex more readable. In JavaScript, just collapse all the line-breaks and spaces and it will work.
I realized that your list of keywords contained two distinct groups:
keywords that are functions, to be followed by operations in parens
var regex_function = "(abs|acos|cos|atan|tan|asin|sin|ceil|floor|sqrt|log|ln)\([^)]+)";
keywords that are constants (stand-alone)
var regex_constant = "(\d+|pi|\\u03C0|\\u03A0|phi|\\u03C6|\\u03A6)";
Then the combination (alternation) of the above parts represents an operand:
var regex_operand = "(" + regex_constant + "|" + regex_function + ")";
Each operand must have an operator in-between:
var regex_operator = "([+\-*\/^]|mod)";
The entire regex can be put together like this:
var regex = new RegExp(regex_operand + "(\s*" + regex_operator + "\s*" + regex_operand + ")*\s*=");
Basically, you have an operand followed by any number of operator/operand pairs, then the equal sign.

Looks to me like your best bet is asserting that spaces must always be followed by some sort of operator . For instance, "5 5 =" is invalid while "5 + 5 =" and "5 =" are valid. Thus, I'd say your regex should look something like this:
([numbers and special names like pi][ ]?[operators][ ]?)*[numbers and special names like pi][ ]?\=[ ]?
I may have written the spaces wrong, what I mean to say is that they can appear at most one time. The other things in brackets would just be big or statements. Parentheses might get tricky, if I get the chance I'll edit my answer to properly handle them.

Related

Works in Chrome, but breaks in Safari: Invalid regular expression: invalid group specifier name /\b((?<!\$)(?:\b\d+(?:\.\d+)?|\B\.\d+)(?:e-?\d+)?)\b/ [duplicate]

Is there a way to achieve the equivalent of a negative lookbehind in JavaScript regular expressions? I need to match a string that does not start with a specific set of characters.
It seems I am unable to find a regex that does this without failing if the matched part is found at the beginning of the string. Negative lookbehinds seem to be the only answer, but JavaScript doesn't has one.
This is the regex that I would like to work, but it doesn't:
(?<!([abcdefg]))m
So it would match the 'm' in 'jim' or 'm', but not 'jam'
Since 2018, Lookbehind Assertions are part of the ECMAScript language specification.
// positive lookbehind
(?<=...)
// negative lookbehind
(?<!...)
Answer pre-2018
As Javascript supports negative lookahead, one way to do it is:
reverse the input string
match with a reversed regex
reverse and reformat the matches
const reverse = s => s.split('').reverse().join('');
const test = (stringToTests, reversedRegexp) => stringToTests
.map(reverse)
.forEach((s,i) => {
const match = reversedRegexp.test(s);
console.log(stringToTests[i], match, 'token:', match ? reverse(reversedRegexp.exec(s)[0]) : 'Ø');
});
Example 1:
Following #andrew-ensley's question:
test(['jim', 'm', 'jam'], /m(?!([abcdefg]))/)
Outputs:
jim true token: m
m true token: m
jam false token: Ø
Example 2:
Following #neaumusic comment (match max-height but not line-height, the token being height):
test(['max-height', 'line-height'], /thgieh(?!(-enil))/)
Outputs:
max-height true token: height
line-height false token: Ø
Lookbehind Assertions got accepted into the ECMAScript specification in 2018.
Positive lookbehind usage:
console.log(
"$9.99 €8.47".match(/(?<=\$)\d+\.\d*/) // Matches "9.99"
);
Negative lookbehind usage:
console.log(
"$9.99 €8.47".match(/(?<!\$)\d+\.\d*/) // Matches "8.47"
);
Platform support:
✔️ V8
✔️ Google Chrome 62.0
✔️ Microsoft Edge 79.0
✔️ Node.js 6.0 behind a flag and 9.0 without a flag
✔️ Deno (all versions)
✔️ SpiderMonkey
✔️ Mozilla Firefox 78.0
🛠️ JavaScriptCore: support in beta as of 2023-02-16: the feature has been merged
✔️ Apple Safari 16.4 (in beta as of 2023-02-16)
✔️ iOS 16.4 beta WebView (all browsers on iOS + iPadOS)
✔️ Bun 0.2.2
❌ Chakra: Microsoft was working on it but Chakra is now abandoned in favor of V8
❌ Internet Explorer
❌ Edge versions prior to 79 (the ones based on EdgeHTML+Chakra)
Let's suppose you want to find all int not preceded by unsigned :
With support for negative look-behind:
(?<!unsigned )int
Without support for negative look-behind:
((?!unsigned ).{9}|^.{0,8})int
Basically idea is to grab n preceding characters and exclude match with negative look-ahead, but also match the cases where there's no preceeding n characters. (where n is length of look-behind).
So the regex in question:
(?<!([abcdefg]))m
would translate to:
((?!([abcdefg])).|^)m
You might need to play with capturing groups to find exact spot of the string that interests you or you want to replace specific part with something else.
Mijoja's strategy works for your specific case but not in general:
js>newString = "Fall ball bill balll llama".replace(/(ba)?ll/g,
function($0,$1){ return $1?$0:"[match]";});
Fa[match] ball bi[match] balll [match]ama
Here's an example where the goal is to match a double-l but not if it is preceded by "ba". Note the word "balll" -- true lookbehind should have suppressed the first 2 l's but matched the 2nd pair. But by matching the first 2 l's and then ignoring that match as a false positive, the regexp engine proceeds from the end of that match, and ignores any characters within the false positive.
Use
newString = string.replace(/([abcdefg])?m/, function($0,$1){ return $1?$0:'m';});
You could define a non-capturing group by negating your character set:
(?:[^a-g])m
...which would match every m NOT preceded by any of those letters.
This is how I achieved str.split(/(?<!^)#/) for Node.js 8 (which doesn't support lookbehind):
str.split('').reverse().join('').split(/#(?!$)/).map(s => s.split('').reverse().join('')).reverse()
Works? Yes (unicode untested). Unpleasant? Yes.
following the idea of Mijoja, and drawing from the problems exposed by JasonS, i had this idea; i checked a bit but am not sure of myself, so a verification by someone more expert than me in js regex would be great :)
var re = /(?=(..|^.?)(ll))/g
// matches empty string position
// whenever this position is followed by
// a string of length equal or inferior (in case of "^")
// to "lookbehind" value
// + actual value we would want to match
, str = "Fall ball bill balll llama"
, str_done = str
, len_difference = 0
, doer = function (where_in_str, to_replace)
{
str_done = str_done.slice(0, where_in_str + len_difference)
+ "[match]"
+ str_done.slice(where_in_str + len_difference + to_replace.length)
len_difference = str_done.length - str.length
/* if str smaller:
len_difference will be positive
else will be negative
*/
} /* the actual function that would do whatever we want to do
with the matches;
this above is only an example from Jason's */
/* function input of .replace(),
only there to test the value of $behind
and if negative, call doer() with interesting parameters */
, checker = function ($match, $behind, $after, $where, $str)
{
if ($behind !== "ba")
doer
(
$where + $behind.length
, $after
/* one will choose the interesting arguments
to give to the doer, it's only an example */
)
return $match // empty string anyhow, but well
}
str.replace(re, checker)
console.log(str_done)
my personal output:
Fa[match] ball bi[match] bal[match] [match]ama
the principle is to call checker at each point in the string between any two characters, whenever that position is the starting point of:
--- any substring of the size of what is not wanted (here 'ba', thus ..) (if that size is known; otherwise it must be harder to do perhaps)
--- --- or smaller than that if it's the beginning of the string: ^.?
and, following this,
--- what is to be actually sought (here 'll').
At each call of checker, there will be a test to check if the value before ll is not what we don't want (!== 'ba'); if that's the case, we call another function, and it will have to be this one (doer) that will make the changes on str, if the purpose is this one, or more generically, that will get in input the necessary data to manually process the results of the scanning of str.
here we change the string so we needed to keep a trace of the difference of length in order to offset the locations given by replace, all calculated on str, which itself never changes.
since primitive strings are immutable, we could have used the variable str to store the result of the whole operation, but i thought the example, already complicated by the replacings, would be clearer with another variable (str_done).
i guess that in terms of performances it must be pretty harsh: all those pointless replacements of '' into '', this str.length-1 times, plus here manual replacement by doer, which means a lot of slicing...
probably in this specific above case that could be grouped, by cutting the string only once into pieces around where we want to insert [match] and .join()ing it with [match] itself.
the other thing is that i don't know how it would handle more complex cases, that is, complex values for the fake lookbehind... the length being perhaps the most problematic data to get.
and, in checker, in case of multiple possibilities of nonwanted values for $behind, we'll have to make a test on it with yet another regex (to be cached (created) outside checker is best, to avoid the same regex object to be created at each call for checker) to know whether or not it is what we seek to avoid.
hope i've been clear; if not don't hesitate, i'll try better. :)
Using your case, if you want to replace m with something, e.g. convert it to uppercase M, you can negate set in capturing group.
match ([^a-g])m, replace with $1M
"jim jam".replace(/([^a-g])m/g, "$1M")
\\jiM jam
([^a-g]) will match any char not(^) in a-g range, and store it in first capturing group, so you can access it with $1.
So we find im in jim and replace it with iM which results in jiM.
As mentioned before, JavaScript allows lookbehinds now. In older browsers you still need a workaround.
I bet my head there is no way to find a regex without lookbehind that delivers the result exactly. All you can do is working with groups. Suppose you have a regex (?<!Before)Wanted, where Wanted is the regex you want to match and Before is the regex that counts out what should not precede the match. The best you can do is negate the regex Before and use the regex NotBefore(Wanted). The desired result is the first group $1.
In your case Before=[abcdefg] which is easy to negate NotBefore=[^abcdefg]. So the regex would be [^abcdefg](m). If you need the position of Wanted, you must group NotBefore too, so that the desired result is the second group.
If matches of the Before pattern have a fixed length n, that is, if the pattern contains no repetitive tokens, you can avoid negating the Before pattern and use the regular expression (?!Before).{n}(Wanted), but still have to use the first group or use the regular expression (?!Before)(.{n})(Wanted) and use the second group. In this example, the pattern Before actually has a fixed length, namely 1, so use the regex (?![abcdefg]).(m) or (?![abcdefg])(.)(m). If you are interested in all matches, add the g flag, see my code snippet:
function TestSORegEx() {
var s = "Donald Trump doesn't like jam, but Homer Simpson does.";
var reg = /(?![abcdefg])(.{1})(m)/gm;
var out = "Matches and groups of the regex " +
"/(?![abcdefg])(.{1})(m)/gm in \ns = \"" + s + "\"";
var match = reg.exec(s);
while(match) {
var start = match.index + match[1].length;
out += "\nWhole match: " + match[0] + ", starts at: " + match.index
+ ". Desired match: " + match[2] + ", starts at: " + start + ".";
match = reg.exec(s);
}
out += "\nResulting string after statement s.replace(reg, \"$1*$2*\")\n"
+ s.replace(reg, "$1*$2*");
alert(out);
}
This effectively does it
"jim".match(/[^a-g]m/)
> ["im"]
"jam".match(/[^a-g]m/)
> null
Search and replace example
"jim jam".replace(/([^a-g])m/g, "$1M")
> "jiM jam"
Note that the negative look-behind string must be 1 character long for this to work.

Lookbehind regex not working in firefox/safari [duplicate]

Is there a way to achieve the equivalent of a negative lookbehind in JavaScript regular expressions? I need to match a string that does not start with a specific set of characters.
It seems I am unable to find a regex that does this without failing if the matched part is found at the beginning of the string. Negative lookbehinds seem to be the only answer, but JavaScript doesn't has one.
This is the regex that I would like to work, but it doesn't:
(?<!([abcdefg]))m
So it would match the 'm' in 'jim' or 'm', but not 'jam'
Since 2018, Lookbehind Assertions are part of the ECMAScript language specification.
// positive lookbehind
(?<=...)
// negative lookbehind
(?<!...)
Answer pre-2018
As Javascript supports negative lookahead, one way to do it is:
reverse the input string
match with a reversed regex
reverse and reformat the matches
const reverse = s => s.split('').reverse().join('');
const test = (stringToTests, reversedRegexp) => stringToTests
.map(reverse)
.forEach((s,i) => {
const match = reversedRegexp.test(s);
console.log(stringToTests[i], match, 'token:', match ? reverse(reversedRegexp.exec(s)[0]) : 'Ø');
});
Example 1:
Following #andrew-ensley's question:
test(['jim', 'm', 'jam'], /m(?!([abcdefg]))/)
Outputs:
jim true token: m
m true token: m
jam false token: Ø
Example 2:
Following #neaumusic comment (match max-height but not line-height, the token being height):
test(['max-height', 'line-height'], /thgieh(?!(-enil))/)
Outputs:
max-height true token: height
line-height false token: Ø
Lookbehind Assertions got accepted into the ECMAScript specification in 2018.
Positive lookbehind usage:
console.log(
"$9.99 €8.47".match(/(?<=\$)\d+\.\d*/) // Matches "9.99"
);
Negative lookbehind usage:
console.log(
"$9.99 €8.47".match(/(?<!\$)\d+\.\d*/) // Matches "8.47"
);
Platform support:
✔️ V8
✔️ Google Chrome 62.0
✔️ Microsoft Edge 79.0
✔️ Node.js 6.0 behind a flag and 9.0 without a flag
✔️ Deno (all versions)
✔️ SpiderMonkey
✔️ Mozilla Firefox 78.0
🛠️ JavaScriptCore: support in beta as of 2023-02-16: the feature has been merged
✔️ Apple Safari 16.4 (in beta as of 2023-02-16)
✔️ iOS 16.4 beta WebView (all browsers on iOS + iPadOS)
✔️ Bun 0.2.2
❌ Chakra: Microsoft was working on it but Chakra is now abandoned in favor of V8
❌ Internet Explorer
❌ Edge versions prior to 79 (the ones based on EdgeHTML+Chakra)
Let's suppose you want to find all int not preceded by unsigned :
With support for negative look-behind:
(?<!unsigned )int
Without support for negative look-behind:
((?!unsigned ).{9}|^.{0,8})int
Basically idea is to grab n preceding characters and exclude match with negative look-ahead, but also match the cases where there's no preceeding n characters. (where n is length of look-behind).
So the regex in question:
(?<!([abcdefg]))m
would translate to:
((?!([abcdefg])).|^)m
You might need to play with capturing groups to find exact spot of the string that interests you or you want to replace specific part with something else.
Mijoja's strategy works for your specific case but not in general:
js>newString = "Fall ball bill balll llama".replace(/(ba)?ll/g,
function($0,$1){ return $1?$0:"[match]";});
Fa[match] ball bi[match] balll [match]ama
Here's an example where the goal is to match a double-l but not if it is preceded by "ba". Note the word "balll" -- true lookbehind should have suppressed the first 2 l's but matched the 2nd pair. But by matching the first 2 l's and then ignoring that match as a false positive, the regexp engine proceeds from the end of that match, and ignores any characters within the false positive.
Use
newString = string.replace(/([abcdefg])?m/, function($0,$1){ return $1?$0:'m';});
You could define a non-capturing group by negating your character set:
(?:[^a-g])m
...which would match every m NOT preceded by any of those letters.
This is how I achieved str.split(/(?<!^)#/) for Node.js 8 (which doesn't support lookbehind):
str.split('').reverse().join('').split(/#(?!$)/).map(s => s.split('').reverse().join('')).reverse()
Works? Yes (unicode untested). Unpleasant? Yes.
following the idea of Mijoja, and drawing from the problems exposed by JasonS, i had this idea; i checked a bit but am not sure of myself, so a verification by someone more expert than me in js regex would be great :)
var re = /(?=(..|^.?)(ll))/g
// matches empty string position
// whenever this position is followed by
// a string of length equal or inferior (in case of "^")
// to "lookbehind" value
// + actual value we would want to match
, str = "Fall ball bill balll llama"
, str_done = str
, len_difference = 0
, doer = function (where_in_str, to_replace)
{
str_done = str_done.slice(0, where_in_str + len_difference)
+ "[match]"
+ str_done.slice(where_in_str + len_difference + to_replace.length)
len_difference = str_done.length - str.length
/* if str smaller:
len_difference will be positive
else will be negative
*/
} /* the actual function that would do whatever we want to do
with the matches;
this above is only an example from Jason's */
/* function input of .replace(),
only there to test the value of $behind
and if negative, call doer() with interesting parameters */
, checker = function ($match, $behind, $after, $where, $str)
{
if ($behind !== "ba")
doer
(
$where + $behind.length
, $after
/* one will choose the interesting arguments
to give to the doer, it's only an example */
)
return $match // empty string anyhow, but well
}
str.replace(re, checker)
console.log(str_done)
my personal output:
Fa[match] ball bi[match] bal[match] [match]ama
the principle is to call checker at each point in the string between any two characters, whenever that position is the starting point of:
--- any substring of the size of what is not wanted (here 'ba', thus ..) (if that size is known; otherwise it must be harder to do perhaps)
--- --- or smaller than that if it's the beginning of the string: ^.?
and, following this,
--- what is to be actually sought (here 'll').
At each call of checker, there will be a test to check if the value before ll is not what we don't want (!== 'ba'); if that's the case, we call another function, and it will have to be this one (doer) that will make the changes on str, if the purpose is this one, or more generically, that will get in input the necessary data to manually process the results of the scanning of str.
here we change the string so we needed to keep a trace of the difference of length in order to offset the locations given by replace, all calculated on str, which itself never changes.
since primitive strings are immutable, we could have used the variable str to store the result of the whole operation, but i thought the example, already complicated by the replacings, would be clearer with another variable (str_done).
i guess that in terms of performances it must be pretty harsh: all those pointless replacements of '' into '', this str.length-1 times, plus here manual replacement by doer, which means a lot of slicing...
probably in this specific above case that could be grouped, by cutting the string only once into pieces around where we want to insert [match] and .join()ing it with [match] itself.
the other thing is that i don't know how it would handle more complex cases, that is, complex values for the fake lookbehind... the length being perhaps the most problematic data to get.
and, in checker, in case of multiple possibilities of nonwanted values for $behind, we'll have to make a test on it with yet another regex (to be cached (created) outside checker is best, to avoid the same regex object to be created at each call for checker) to know whether or not it is what we seek to avoid.
hope i've been clear; if not don't hesitate, i'll try better. :)
Using your case, if you want to replace m with something, e.g. convert it to uppercase M, you can negate set in capturing group.
match ([^a-g])m, replace with $1M
"jim jam".replace(/([^a-g])m/g, "$1M")
\\jiM jam
([^a-g]) will match any char not(^) in a-g range, and store it in first capturing group, so you can access it with $1.
So we find im in jim and replace it with iM which results in jiM.
As mentioned before, JavaScript allows lookbehinds now. In older browsers you still need a workaround.
I bet my head there is no way to find a regex without lookbehind that delivers the result exactly. All you can do is working with groups. Suppose you have a regex (?<!Before)Wanted, where Wanted is the regex you want to match and Before is the regex that counts out what should not precede the match. The best you can do is negate the regex Before and use the regex NotBefore(Wanted). The desired result is the first group $1.
In your case Before=[abcdefg] which is easy to negate NotBefore=[^abcdefg]. So the regex would be [^abcdefg](m). If you need the position of Wanted, you must group NotBefore too, so that the desired result is the second group.
If matches of the Before pattern have a fixed length n, that is, if the pattern contains no repetitive tokens, you can avoid negating the Before pattern and use the regular expression (?!Before).{n}(Wanted), but still have to use the first group or use the regular expression (?!Before)(.{n})(Wanted) and use the second group. In this example, the pattern Before actually has a fixed length, namely 1, so use the regex (?![abcdefg]).(m) or (?![abcdefg])(.)(m). If you are interested in all matches, add the g flag, see my code snippet:
function TestSORegEx() {
var s = "Donald Trump doesn't like jam, but Homer Simpson does.";
var reg = /(?![abcdefg])(.{1})(m)/gm;
var out = "Matches and groups of the regex " +
"/(?![abcdefg])(.{1})(m)/gm in \ns = \"" + s + "\"";
var match = reg.exec(s);
while(match) {
var start = match.index + match[1].length;
out += "\nWhole match: " + match[0] + ", starts at: " + match.index
+ ". Desired match: " + match[2] + ", starts at: " + start + ".";
match = reg.exec(s);
}
out += "\nResulting string after statement s.replace(reg, \"$1*$2*\")\n"
+ s.replace(reg, "$1*$2*");
alert(out);
}
This effectively does it
"jim".match(/[^a-g]m/)
> ["im"]
"jam".match(/[^a-g]m/)
> null
Search and replace example
"jim jam".replace(/([^a-g])m/g, "$1M")
> "jiM jam"
Note that the negative look-behind string must be 1 character long for this to work.

Invalid regex results in 500 with safari/firefox while using sapper/ssr/svelte [duplicate]

Is there a way to achieve the equivalent of a negative lookbehind in JavaScript regular expressions? I need to match a string that does not start with a specific set of characters.
It seems I am unable to find a regex that does this without failing if the matched part is found at the beginning of the string. Negative lookbehinds seem to be the only answer, but JavaScript doesn't has one.
This is the regex that I would like to work, but it doesn't:
(?<!([abcdefg]))m
So it would match the 'm' in 'jim' or 'm', but not 'jam'
Since 2018, Lookbehind Assertions are part of the ECMAScript language specification.
// positive lookbehind
(?<=...)
// negative lookbehind
(?<!...)
Answer pre-2018
As Javascript supports negative lookahead, one way to do it is:
reverse the input string
match with a reversed regex
reverse and reformat the matches
const reverse = s => s.split('').reverse().join('');
const test = (stringToTests, reversedRegexp) => stringToTests
.map(reverse)
.forEach((s,i) => {
const match = reversedRegexp.test(s);
console.log(stringToTests[i], match, 'token:', match ? reverse(reversedRegexp.exec(s)[0]) : 'Ø');
});
Example 1:
Following #andrew-ensley's question:
test(['jim', 'm', 'jam'], /m(?!([abcdefg]))/)
Outputs:
jim true token: m
m true token: m
jam false token: Ø
Example 2:
Following #neaumusic comment (match max-height but not line-height, the token being height):
test(['max-height', 'line-height'], /thgieh(?!(-enil))/)
Outputs:
max-height true token: height
line-height false token: Ø
Lookbehind Assertions got accepted into the ECMAScript specification in 2018.
Positive lookbehind usage:
console.log(
"$9.99 €8.47".match(/(?<=\$)\d+\.\d*/) // Matches "9.99"
);
Negative lookbehind usage:
console.log(
"$9.99 €8.47".match(/(?<!\$)\d+\.\d*/) // Matches "8.47"
);
Platform support:
✔️ V8
✔️ Google Chrome 62.0
✔️ Microsoft Edge 79.0
✔️ Node.js 6.0 behind a flag and 9.0 without a flag
✔️ Deno (all versions)
✔️ SpiderMonkey
✔️ Mozilla Firefox 78.0
🛠️ JavaScriptCore: support in beta as of 2023-02-16: the feature has been merged
✔️ Apple Safari 16.4 (in beta as of 2023-02-16)
✔️ iOS 16.4 beta WebView (all browsers on iOS + iPadOS)
✔️ Bun 0.2.2
❌ Chakra: Microsoft was working on it but Chakra is now abandoned in favor of V8
❌ Internet Explorer
❌ Edge versions prior to 79 (the ones based on EdgeHTML+Chakra)
Let's suppose you want to find all int not preceded by unsigned :
With support for negative look-behind:
(?<!unsigned )int
Without support for negative look-behind:
((?!unsigned ).{9}|^.{0,8})int
Basically idea is to grab n preceding characters and exclude match with negative look-ahead, but also match the cases where there's no preceeding n characters. (where n is length of look-behind).
So the regex in question:
(?<!([abcdefg]))m
would translate to:
((?!([abcdefg])).|^)m
You might need to play with capturing groups to find exact spot of the string that interests you or you want to replace specific part with something else.
Mijoja's strategy works for your specific case but not in general:
js>newString = "Fall ball bill balll llama".replace(/(ba)?ll/g,
function($0,$1){ return $1?$0:"[match]";});
Fa[match] ball bi[match] balll [match]ama
Here's an example where the goal is to match a double-l but not if it is preceded by "ba". Note the word "balll" -- true lookbehind should have suppressed the first 2 l's but matched the 2nd pair. But by matching the first 2 l's and then ignoring that match as a false positive, the regexp engine proceeds from the end of that match, and ignores any characters within the false positive.
Use
newString = string.replace(/([abcdefg])?m/, function($0,$1){ return $1?$0:'m';});
You could define a non-capturing group by negating your character set:
(?:[^a-g])m
...which would match every m NOT preceded by any of those letters.
This is how I achieved str.split(/(?<!^)#/) for Node.js 8 (which doesn't support lookbehind):
str.split('').reverse().join('').split(/#(?!$)/).map(s => s.split('').reverse().join('')).reverse()
Works? Yes (unicode untested). Unpleasant? Yes.
following the idea of Mijoja, and drawing from the problems exposed by JasonS, i had this idea; i checked a bit but am not sure of myself, so a verification by someone more expert than me in js regex would be great :)
var re = /(?=(..|^.?)(ll))/g
// matches empty string position
// whenever this position is followed by
// a string of length equal or inferior (in case of "^")
// to "lookbehind" value
// + actual value we would want to match
, str = "Fall ball bill balll llama"
, str_done = str
, len_difference = 0
, doer = function (where_in_str, to_replace)
{
str_done = str_done.slice(0, where_in_str + len_difference)
+ "[match]"
+ str_done.slice(where_in_str + len_difference + to_replace.length)
len_difference = str_done.length - str.length
/* if str smaller:
len_difference will be positive
else will be negative
*/
} /* the actual function that would do whatever we want to do
with the matches;
this above is only an example from Jason's */
/* function input of .replace(),
only there to test the value of $behind
and if negative, call doer() with interesting parameters */
, checker = function ($match, $behind, $after, $where, $str)
{
if ($behind !== "ba")
doer
(
$where + $behind.length
, $after
/* one will choose the interesting arguments
to give to the doer, it's only an example */
)
return $match // empty string anyhow, but well
}
str.replace(re, checker)
console.log(str_done)
my personal output:
Fa[match] ball bi[match] bal[match] [match]ama
the principle is to call checker at each point in the string between any two characters, whenever that position is the starting point of:
--- any substring of the size of what is not wanted (here 'ba', thus ..) (if that size is known; otherwise it must be harder to do perhaps)
--- --- or smaller than that if it's the beginning of the string: ^.?
and, following this,
--- what is to be actually sought (here 'll').
At each call of checker, there will be a test to check if the value before ll is not what we don't want (!== 'ba'); if that's the case, we call another function, and it will have to be this one (doer) that will make the changes on str, if the purpose is this one, or more generically, that will get in input the necessary data to manually process the results of the scanning of str.
here we change the string so we needed to keep a trace of the difference of length in order to offset the locations given by replace, all calculated on str, which itself never changes.
since primitive strings are immutable, we could have used the variable str to store the result of the whole operation, but i thought the example, already complicated by the replacings, would be clearer with another variable (str_done).
i guess that in terms of performances it must be pretty harsh: all those pointless replacements of '' into '', this str.length-1 times, plus here manual replacement by doer, which means a lot of slicing...
probably in this specific above case that could be grouped, by cutting the string only once into pieces around where we want to insert [match] and .join()ing it with [match] itself.
the other thing is that i don't know how it would handle more complex cases, that is, complex values for the fake lookbehind... the length being perhaps the most problematic data to get.
and, in checker, in case of multiple possibilities of nonwanted values for $behind, we'll have to make a test on it with yet another regex (to be cached (created) outside checker is best, to avoid the same regex object to be created at each call for checker) to know whether or not it is what we seek to avoid.
hope i've been clear; if not don't hesitate, i'll try better. :)
Using your case, if you want to replace m with something, e.g. convert it to uppercase M, you can negate set in capturing group.
match ([^a-g])m, replace with $1M
"jim jam".replace(/([^a-g])m/g, "$1M")
\\jiM jam
([^a-g]) will match any char not(^) in a-g range, and store it in first capturing group, so you can access it with $1.
So we find im in jim and replace it with iM which results in jiM.
As mentioned before, JavaScript allows lookbehinds now. In older browsers you still need a workaround.
I bet my head there is no way to find a regex without lookbehind that delivers the result exactly. All you can do is working with groups. Suppose you have a regex (?<!Before)Wanted, where Wanted is the regex you want to match and Before is the regex that counts out what should not precede the match. The best you can do is negate the regex Before and use the regex NotBefore(Wanted). The desired result is the first group $1.
In your case Before=[abcdefg] which is easy to negate NotBefore=[^abcdefg]. So the regex would be [^abcdefg](m). If you need the position of Wanted, you must group NotBefore too, so that the desired result is the second group.
If matches of the Before pattern have a fixed length n, that is, if the pattern contains no repetitive tokens, you can avoid negating the Before pattern and use the regular expression (?!Before).{n}(Wanted), but still have to use the first group or use the regular expression (?!Before)(.{n})(Wanted) and use the second group. In this example, the pattern Before actually has a fixed length, namely 1, so use the regex (?![abcdefg]).(m) or (?![abcdefg])(.)(m). If you are interested in all matches, add the g flag, see my code snippet:
function TestSORegEx() {
var s = "Donald Trump doesn't like jam, but Homer Simpson does.";
var reg = /(?![abcdefg])(.{1})(m)/gm;
var out = "Matches and groups of the regex " +
"/(?![abcdefg])(.{1})(m)/gm in \ns = \"" + s + "\"";
var match = reg.exec(s);
while(match) {
var start = match.index + match[1].length;
out += "\nWhole match: " + match[0] + ", starts at: " + match.index
+ ". Desired match: " + match[2] + ", starts at: " + start + ".";
match = reg.exec(s);
}
out += "\nResulting string after statement s.replace(reg, \"$1*$2*\")\n"
+ s.replace(reg, "$1*$2*");
alert(out);
}
This effectively does it
"jim".match(/[^a-g]m/)
> ["im"]
"jam".match(/[^a-g]m/)
> null
Search and replace example
"jim jam".replace(/([^a-g])m/g, "$1M")
> "jiM jam"
Note that the negative look-behind string must be 1 character long for this to work.

Filtering existing keywords using regular expression [duplicate]

Is there a way to achieve the equivalent of a negative lookbehind in JavaScript regular expressions? I need to match a string that does not start with a specific set of characters.
It seems I am unable to find a regex that does this without failing if the matched part is found at the beginning of the string. Negative lookbehinds seem to be the only answer, but JavaScript doesn't has one.
This is the regex that I would like to work, but it doesn't:
(?<!([abcdefg]))m
So it would match the 'm' in 'jim' or 'm', but not 'jam'
Since 2018, Lookbehind Assertions are part of the ECMAScript language specification.
// positive lookbehind
(?<=...)
// negative lookbehind
(?<!...)
Answer pre-2018
As Javascript supports negative lookahead, one way to do it is:
reverse the input string
match with a reversed regex
reverse and reformat the matches
const reverse = s => s.split('').reverse().join('');
const test = (stringToTests, reversedRegexp) => stringToTests
.map(reverse)
.forEach((s,i) => {
const match = reversedRegexp.test(s);
console.log(stringToTests[i], match, 'token:', match ? reverse(reversedRegexp.exec(s)[0]) : 'Ø');
});
Example 1:
Following #andrew-ensley's question:
test(['jim', 'm', 'jam'], /m(?!([abcdefg]))/)
Outputs:
jim true token: m
m true token: m
jam false token: Ø
Example 2:
Following #neaumusic comment (match max-height but not line-height, the token being height):
test(['max-height', 'line-height'], /thgieh(?!(-enil))/)
Outputs:
max-height true token: height
line-height false token: Ø
Lookbehind Assertions got accepted into the ECMAScript specification in 2018.
Positive lookbehind usage:
console.log(
"$9.99 €8.47".match(/(?<=\$)\d+\.\d*/) // Matches "9.99"
);
Negative lookbehind usage:
console.log(
"$9.99 €8.47".match(/(?<!\$)\d+\.\d*/) // Matches "8.47"
);
Platform support:
✔️ V8
✔️ Google Chrome 62.0
✔️ Microsoft Edge 79.0
✔️ Node.js 6.0 behind a flag and 9.0 without a flag
✔️ Deno (all versions)
✔️ SpiderMonkey
✔️ Mozilla Firefox 78.0
🛠️ JavaScriptCore: support in beta as of 2023-02-16: the feature has been merged
✔️ Apple Safari 16.4 (in beta as of 2023-02-16)
✔️ iOS 16.4 beta WebView (all browsers on iOS + iPadOS)
✔️ Bun 0.2.2
❌ Chakra: Microsoft was working on it but Chakra is now abandoned in favor of V8
❌ Internet Explorer
❌ Edge versions prior to 79 (the ones based on EdgeHTML+Chakra)
Let's suppose you want to find all int not preceded by unsigned :
With support for negative look-behind:
(?<!unsigned )int
Without support for negative look-behind:
((?!unsigned ).{9}|^.{0,8})int
Basically idea is to grab n preceding characters and exclude match with negative look-ahead, but also match the cases where there's no preceeding n characters. (where n is length of look-behind).
So the regex in question:
(?<!([abcdefg]))m
would translate to:
((?!([abcdefg])).|^)m
You might need to play with capturing groups to find exact spot of the string that interests you or you want to replace specific part with something else.
Mijoja's strategy works for your specific case but not in general:
js>newString = "Fall ball bill balll llama".replace(/(ba)?ll/g,
function($0,$1){ return $1?$0:"[match]";});
Fa[match] ball bi[match] balll [match]ama
Here's an example where the goal is to match a double-l but not if it is preceded by "ba". Note the word "balll" -- true lookbehind should have suppressed the first 2 l's but matched the 2nd pair. But by matching the first 2 l's and then ignoring that match as a false positive, the regexp engine proceeds from the end of that match, and ignores any characters within the false positive.
Use
newString = string.replace(/([abcdefg])?m/, function($0,$1){ return $1?$0:'m';});
You could define a non-capturing group by negating your character set:
(?:[^a-g])m
...which would match every m NOT preceded by any of those letters.
This is how I achieved str.split(/(?<!^)#/) for Node.js 8 (which doesn't support lookbehind):
str.split('').reverse().join('').split(/#(?!$)/).map(s => s.split('').reverse().join('')).reverse()
Works? Yes (unicode untested). Unpleasant? Yes.
following the idea of Mijoja, and drawing from the problems exposed by JasonS, i had this idea; i checked a bit but am not sure of myself, so a verification by someone more expert than me in js regex would be great :)
var re = /(?=(..|^.?)(ll))/g
// matches empty string position
// whenever this position is followed by
// a string of length equal or inferior (in case of "^")
// to "lookbehind" value
// + actual value we would want to match
, str = "Fall ball bill balll llama"
, str_done = str
, len_difference = 0
, doer = function (where_in_str, to_replace)
{
str_done = str_done.slice(0, where_in_str + len_difference)
+ "[match]"
+ str_done.slice(where_in_str + len_difference + to_replace.length)
len_difference = str_done.length - str.length
/* if str smaller:
len_difference will be positive
else will be negative
*/
} /* the actual function that would do whatever we want to do
with the matches;
this above is only an example from Jason's */
/* function input of .replace(),
only there to test the value of $behind
and if negative, call doer() with interesting parameters */
, checker = function ($match, $behind, $after, $where, $str)
{
if ($behind !== "ba")
doer
(
$where + $behind.length
, $after
/* one will choose the interesting arguments
to give to the doer, it's only an example */
)
return $match // empty string anyhow, but well
}
str.replace(re, checker)
console.log(str_done)
my personal output:
Fa[match] ball bi[match] bal[match] [match]ama
the principle is to call checker at each point in the string between any two characters, whenever that position is the starting point of:
--- any substring of the size of what is not wanted (here 'ba', thus ..) (if that size is known; otherwise it must be harder to do perhaps)
--- --- or smaller than that if it's the beginning of the string: ^.?
and, following this,
--- what is to be actually sought (here 'll').
At each call of checker, there will be a test to check if the value before ll is not what we don't want (!== 'ba'); if that's the case, we call another function, and it will have to be this one (doer) that will make the changes on str, if the purpose is this one, or more generically, that will get in input the necessary data to manually process the results of the scanning of str.
here we change the string so we needed to keep a trace of the difference of length in order to offset the locations given by replace, all calculated on str, which itself never changes.
since primitive strings are immutable, we could have used the variable str to store the result of the whole operation, but i thought the example, already complicated by the replacings, would be clearer with another variable (str_done).
i guess that in terms of performances it must be pretty harsh: all those pointless replacements of '' into '', this str.length-1 times, plus here manual replacement by doer, which means a lot of slicing...
probably in this specific above case that could be grouped, by cutting the string only once into pieces around where we want to insert [match] and .join()ing it with [match] itself.
the other thing is that i don't know how it would handle more complex cases, that is, complex values for the fake lookbehind... the length being perhaps the most problematic data to get.
and, in checker, in case of multiple possibilities of nonwanted values for $behind, we'll have to make a test on it with yet another regex (to be cached (created) outside checker is best, to avoid the same regex object to be created at each call for checker) to know whether or not it is what we seek to avoid.
hope i've been clear; if not don't hesitate, i'll try better. :)
Using your case, if you want to replace m with something, e.g. convert it to uppercase M, you can negate set in capturing group.
match ([^a-g])m, replace with $1M
"jim jam".replace(/([^a-g])m/g, "$1M")
\\jiM jam
([^a-g]) will match any char not(^) in a-g range, and store it in first capturing group, so you can access it with $1.
So we find im in jim and replace it with iM which results in jiM.
As mentioned before, JavaScript allows lookbehinds now. In older browsers you still need a workaround.
I bet my head there is no way to find a regex without lookbehind that delivers the result exactly. All you can do is working with groups. Suppose you have a regex (?<!Before)Wanted, where Wanted is the regex you want to match and Before is the regex that counts out what should not precede the match. The best you can do is negate the regex Before and use the regex NotBefore(Wanted). The desired result is the first group $1.
In your case Before=[abcdefg] which is easy to negate NotBefore=[^abcdefg]. So the regex would be [^abcdefg](m). If you need the position of Wanted, you must group NotBefore too, so that the desired result is the second group.
If matches of the Before pattern have a fixed length n, that is, if the pattern contains no repetitive tokens, you can avoid negating the Before pattern and use the regular expression (?!Before).{n}(Wanted), but still have to use the first group or use the regular expression (?!Before)(.{n})(Wanted) and use the second group. In this example, the pattern Before actually has a fixed length, namely 1, so use the regex (?![abcdefg]).(m) or (?![abcdefg])(.)(m). If you are interested in all matches, add the g flag, see my code snippet:
function TestSORegEx() {
var s = "Donald Trump doesn't like jam, but Homer Simpson does.";
var reg = /(?![abcdefg])(.{1})(m)/gm;
var out = "Matches and groups of the regex " +
"/(?![abcdefg])(.{1})(m)/gm in \ns = \"" + s + "\"";
var match = reg.exec(s);
while(match) {
var start = match.index + match[1].length;
out += "\nWhole match: " + match[0] + ", starts at: " + match.index
+ ". Desired match: " + match[2] + ", starts at: " + start + ".";
match = reg.exec(s);
}
out += "\nResulting string after statement s.replace(reg, \"$1*$2*\")\n"
+ s.replace(reg, "$1*$2*");
alert(out);
}
This effectively does it
"jim".match(/[^a-g]m/)
> ["im"]
"jam".match(/[^a-g]m/)
> null
Search and replace example
"jim jam".replace(/([^a-g])m/g, "$1M")
> "jiM jam"
Note that the negative look-behind string must be 1 character long for this to work.

Line continuation characters in JavaScript

What is the best practice for line continuation in JavaScript? I know that you can use \ for strings. But how would you split the following code?
var statement = con.createStatement("select * from t where
(t.a1 = 0 and t.a2 >=-1)
order by a3 desc limit 1");
If I properly understood your question:
var statement = con.createStatement('select * from t where '
+ '(t.a1 = 0 and t.a2 >=-1) '
+ 'order by a3 desc limit 1');
For readability, it is fine to align + operator on each row:
Anyway, unless you're using Ecmascript 2015, avoid to split a multiline string with \, because:
It's not standard JavaScript
A whitespace after that character could generate a parsing error
I like using backslashes for JavaScript line continuation, like so:
// validation
$(".adjustment, .info input, .includesAndTiming input, \
.independentAdj, .generalAdj, .executiveAdj \
#officeExpense, #longDistanceExpense, #digitalImages, #milesReimbursment, #driveTime, #statementTranscription").keypress(function (event) {
My personal preference is similar to your first response there, but for my eyes its readability is easier:
var statement = con.createStatement
(
'select * from t where ' +
'(t.a1 = 0 and t.a2 >=-1) ' +
'order by a3 desc limit 1'
);
It strikes a close similarity to the SQL syntax format I've been using for nearly 20 years:
SELECT *
FROM t
WHERE
t.a1 = 0 AND
t.a2 >=-1
ORDER BY a3 DESC
LIMIT 1
Keeping the continuation (+ in JavaScript or AND in SQL) on the far right permits the eye to slide evenly down the left edge, checking lvalues & syntax. That's slightly more difficult to do with the continuation on the left - not important unless you do a LOT of this stuff, at which point every calorie you spend is a calorie that might've been saved by a slight improvement in format.
Since this query is so simple, breaking it all out to SQL format is wasteful of space and bandwidth, which is why the suggested JavaScript is on six lines instead of ten. Collapsing the curlies up one line each brings it to four lines, saving whitespace. Not quite as clear or as simple to edit, though.
The "+" is for concatenation of strings and most of the examples are dealing with strings. What if you have a command you need to string across multiple lines, such as a compound "if" statement? You need a backslash at the end of each line that is to be continued. This escapes the invisible next line character so that it will not delimit the command in mid statement.

Categories

Resources