diff --git a/9-regular-expressions/01-regexp-introduction/article.md b/9-regular-expressions/01-regexp-introduction/article.md index 59775d3c0..a35d19a7b 100644 --- a/9-regular-expressions/01-regexp-introduction/article.md +++ b/9-regular-expressions/01-regexp-introduction/article.md @@ -1,59 +1,65 @@ # Patterns and flags -Regular expressions is a powerful way of searching and replacing inside a string. +Regular expressions are patterns that provide a powerful way to search and replace in text. -In JavaScript regular expressions are implemented using objects of a built-in `RegExp` class and integrated with strings. +In JavaScript, they are available via the [RegExp](mdn:js/RegExp) object, as well as being integrated in methods of strings. -Please note that regular expressions vary between programming languages. In this tutorial we concentrate on JavaScript. Of course there's a lot in common, but they are a somewhat different in Perl, Ruby, PHP etc. - -## Regular expressions +## Regular Expressions A regular expression (also "regexp", or just "reg") consists of a *pattern* and optional *flags*. -There are two syntaxes to create a regular expression object. +There are two syntaxes that can be used to create a regular expression object. -The long syntax: +The "long" syntax: ```js regexp = new RegExp("pattern", "flags"); ``` -...And the short one, using slashes `"/"`: +And the "short" one, using slashes `"/"`: ```js regexp = /pattern/; // no flags regexp = /pattern/gmi; // with flags g,m and i (to be covered soon) ``` -Slashes `"/"` tell JavaScript that we are creating a regular expression. They play the same role as quotes for strings. +Slashes `pattern:/.../` tell JavaScript that we are creating a regular expression. They play the same role as quotes for strings. -## Usage +In both cases `regexp` becomes an instance of the built-in `RegExp` class. -To search inside a string, we can use method [search](mdn:js/String/search). +The main difference between these two syntaxes is that pattern using slashes `/.../` does not allow for expressions to be inserted (like string template literals with `${...}`). They are fully static. -Here's an example: +Slashes are used when we know the regular expression at the code writing time -- and that's the most common situation. While `new RegExp`, is more often used when we need to create a regexp "on the fly" from a dynamically generated string. For instance: -```js run -let str = "I love JavaScript!"; // will search here +```js +let tag = prompt("What tag do you want to find?", "h2"); -let regexp = /love/; -alert( str.search(regexp) ); // 2 +let regexp = new RegExp(`<${tag}>`); // same as /

/ if answered "h2" in the prompt above ``` -The `str.search` method looks for the pattern `pattern:/love/` and returns the position inside the string. As we might guess, `pattern:/love/` is the simplest possible pattern. What it does is a simple substring search. +## Flags -The code above is the same as: +Regular expressions may have flags that affect the search. -```js run -let str = "I love JavaScript!"; // will search here +There are only 6 of them in JavaScript: -let substr = 'love'; -alert( str.search(substr) ); // 2 -``` +`pattern:i` +: With this flag the search is case-insensitive: no difference between `A` and `a` (see the example below). -So searching for `pattern:/love/` is the same as searching for `"love"`. +`pattern:g` +: With this flag the search looks for all matches, without it -- only the first match is returned. -But that's only for now. Soon we'll create more complex regular expressions with much more searching power. +`pattern:m` +: Multiline mode (covered in the chapter ). + +`pattern:s` +: Enables "dotall" mode, that allows a dot `pattern:.` to match newline character `\n` (covered in the chapter ). + +`pattern:u` +: Enables full unicode support. The flag enables correct processing of surrogate pairs. More about that in the chapter . + +`pattern:y` +: "Sticky" mode: searching at the exact position in the text (covered in the chapter ) ```smart header="Colors" From here on the color scheme is: @@ -63,65 +69,109 @@ From here on the color scheme is: - result -- `match:green` ``` +## Searching: str.match -````smart header="When to use `new RegExp`?" -Normally we use the short syntax `/.../`. But it does not support variable insertions `${...}`. +As mentioned previously, regular expressions are integrated with string methods. -On the other hand, `new RegExp` allows to construct a pattern dynamically from a string, so it's more flexible. +The method `str.match(regexp)` finds all matches of `regexp` in the string `str`. -Here's an example of a dynamically generated regexp: +It has 3 working modes: -```js run -let tag = prompt("Which tag you want to search?", "h2"); -let regexp = new RegExp(`<${tag}>`); +1. If the regular expression has flag `pattern:g`, it returns an array of all matches: + ```js run + let str = "We will, we will rock you"; -// finds

by default -alert( "

".search(regexp)); -``` -```` + alert( str.match(/we/gi) ); // We,we (an array of 2 substrings that match) + ``` + Please note that both `match:We` and `match:we` are found, because flag `pattern:i` makes the regular expression case-insensitive. +2. If there's no such flag it returns only the first match in the form of an array, with the full match at index `0` and some additional details in properties: + ```js run + let str = "We will, we will rock you"; -## Flags + let result = str.match(/we/i); // without flag g -Regular expressions may have flags that affect the search. + alert( result[0] ); // We (1st match) + alert( result.length ); // 1 -There are only 5 of them in JavaScript: + // Details: + alert( result.index ); // 0 (position of the match) + alert( result.input ); // We will, we will rock you (source string) + ``` + The array may have other indexes, besides `0` if a part of the regular expression is enclosed in parentheses. We'll cover that in the chapter . -`i` -: With this flag the search is case-insensitive: no difference between `A` and `a` (see the example below). +3. And, finally, if there are no matches, `null` is returned (doesn't matter if there's flag `pattern:g` or not). -`g` -: With this flag the search looks for all matches, without it -- only the first one (we'll see uses in the next chapter). + This a very important nuance. If there are no matches, we don't receive an empty array, but instead receive `null`. Forgetting about that may lead to errors, e.g.: -`m` -: Multiline mode (covered in the chapter ). + ```js run + let matches = "JavaScript".match(/HTML/); // = null -`s` -: "Dotall" mode, allows `.` to match newlines (covered in the chapter ). + if (!matches.length) { // Error: Cannot read property 'length' of null + alert("Error in the line above"); + } + ``` -`u` -: Enables full unicode support. The flag enables correct processing of surrogate pairs. More about that in the chapter . + If we'd like the result to always be an array, we can write it this way: + + ```js run + let matches = "JavaScript".match(/HTML/)*!* || []*/!*; + + if (!matches.length) { + alert("No matches"); // now it works + } + ``` + +## Replacing: str.replace + +The method `str.replace(regexp, replacement)` replaces matches found using `regexp` in string `str` with `replacement` (all matches if there's flag `pattern:g`, otherwise, only the first one). + +For instance: -`y` -: Sticky mode (covered in the chapter ) +```js run +// no flag g +alert( "We will, we will".replace(/we/i, "I") ); // I will, we will + +// with flag g +alert( "We will, we will".replace(/we/ig, "I") ); // I will, I will +``` + +The second argument is the `replacement` string. We can use special character combinations in it to insert fragments of the match: -We'll cover all these flags further in the tutorial. +| Symbols | Action in the replacement string | +|--------|--------| +|`$&`|inserts the whole match| +|$`|inserts a part of the string before the match| +|`$'`|inserts a part of the string after the match| +|`$n`|if `n` is a 1-2 digit number, then it inserts the contents of n-th parentheses, more about it in the chapter | +|`$`|inserts the contents of the parentheses with the given `name`, more about it in the chapter | +|`$$`|inserts character `$` | -For now, the simplest flag is `i`, here's an example: +An example with `pattern:$&`: ```js run -let str = "I love JavaScript!"; +alert( "I love HTML".replace(/HTML/, "$& and JavaScript") ); // I love HTML and JavaScript +``` + +## Testing: regexp.test -alert( str.search(/LOVE/i) ); // 2 (found lowercased) +The method `regexp.test(str)` looks for at least one match, if found, returns `true`, otherwise `false`. + +```js run +let str = "I love JavaScript"; +let regexp = /LOVE/i; -alert( str.search(/LOVE/) ); // -1 (nothing found without 'i' flag) +alert( regexp.test(str) ); // true ``` -So the `i` flag already makes regular expressions more powerful than a simple substring search. But there's so much more. We'll cover other flags and features in the next chapters. +Later in this chapter we'll study more regular expressions, walk through more examples, and also meet other methods. +Full information about the methods is given in the article . ## Summary -- A regular expression consists of a pattern and optional flags: `g`, `i`, `m`, `u`, `s`, `y`. -- Without flags and special symbols that we'll study later, the search by a regexp is the same as a substring search. -- The method `str.search(regexp)` returns the index where the match is found or `-1` if there's no match. In the next chapter we'll see other methods. +- A regular expression consists of a pattern and optional flags: `pattern:g`, `pattern:i`, `pattern:m`, `pattern:u`, `pattern:s`, `pattern:y`. +- Without flags and special symbols (that we'll study later), the search by a regexp is the same as a substring search. +- The method `str.match(regexp)` looks for matches: all of them if there's `pattern:g` flag, otherwise, only the first one. +- The method `str.replace(regexp, replacement)` replaces matches found using `regexp` with `replacement`: all of them if there's `pattern:g` flag, otherwise only the first one. +- The method `regexp.test(str)` returns `true` if there's at least one match, otherwise, it returns `false`. diff --git a/9-regular-expressions/02-regexp-character-classes/article.md b/9-regular-expressions/02-regexp-character-classes/article.md new file mode 100644 index 000000000..7baa6984b --- /dev/null +++ b/9-regular-expressions/02-regexp-character-classes/article.md @@ -0,0 +1,203 @@ +# Character classes + +Consider a practical task -- we have a phone number like `"+7(903)-123-45-67"`, and we need to turn it into pure numbers: `79031234567`. + +To do so, we can find and remove anything that's not a number. Character classes can help with that. + +A *character class* is a special notation that matches any symbol from a certain set. + +For the start, let's explore the "digit" class. It's written as `pattern:\d` and corresponds to "any single digit". + +For instance, the let's find the first digit in the phone number: + +```js run +let str = "+7(903)-123-45-67"; + +let regexp = /\d/; + +alert( str.match(regexp) ); // 7 +``` + +Without the flag `pattern:g`, the regular expression only looks for the first match, that is the first digit `pattern:\d`. + +Let's add the `pattern:g` flag to find all digits: + +```js run +let str = "+7(903)-123-45-67"; + +let regexp = /\d/g; + +alert( str.match(regexp) ); // array of matches: 7,9,0,3,1,2,3,4,5,6,7 + +// let's make the digits-only phone number of them: +alert( str.match(regexp).join('') ); // 79031234567 +``` + +That was a character class for digits. There are other character classes as well. + +Most used are: + +`pattern:\d` ("d" is from "digit") +: A digit: a character from `0` to `9`. + +`pattern:\s` ("s" is from "space") +: A space symbol: includes spaces, tabs `\t`, newlines `\n` and few other rare characters, such as `\v`, `\f` and `\r`. + +`pattern:\w` ("w" is from "word") +: A "wordly" character: either a letter of Latin alphabet or a digit or an underscore `_`. Non-Latin letters (like cyrillic or hindi) do not belong to `pattern:\w`. + +For instance, `pattern:\d\s\w` means a "digit" followed by a "space character" followed by a "wordly character", such as `match:1 a`. + +**A regexp may contain both regular symbols and character classes.** + +For instance, `pattern:CSS\d` matches a string `match:CSS` with a digit after it: + +```js run +let str = "Is there CSS4?"; +let regexp = /CSS\d/ + +alert( str.match(regexp) ); // CSS4 +``` + +Also we can use many character classes: + +```js run +alert( "I love HTML5!".match(/\s\w\w\w\w\d/) ); // ' HTML5' +``` + +The match (each regexp character class has the corresponding result character): + +![](love-html5-classes.svg) + +## Inverse classes + +For every character class there exists an "inverse class", denoted with the same letter, but uppercased. + +The "inverse" means that it matches all other characters, for instance: + +`pattern:\D` +: Non-digit: any character except `pattern:\d`, for instance a letter. + +`pattern:\S` +: Non-space: any character except `pattern:\s`, for instance a letter. + +`pattern:\W` +: Non-wordly character: anything but `pattern:\w`, e.g a non-latin letter or a space. + +In the beginning of the chapter we saw how to make a number-only phone number from a string like `subject:+7(903)-123-45-67`: find all digits and join them. + +```js run +let str = "+7(903)-123-45-67"; + +alert( str.match(/\d/g).join('') ); // 79031234567 +``` + +An alternative, shorter way is to find non-digits `pattern:\D` and remove them from the string: + +```js run +let str = "+7(903)-123-45-67"; + +alert( str.replace(/\D/g, "") ); // 79031234567 +``` + +## A dot is "any character" + +A dot `pattern:.` is a special character class that matches "any character except a newline". + +For instance: + +```js run +alert( "Z".match(/./) ); // Z +``` + +Or in the middle of a regexp: + +```js run +let regexp = /CS.4/; + +alert( "CSS4".match(regexp) ); // CSS4 +alert( "CS-4".match(regexp) ); // CS-4 +alert( "CS 4".match(regexp) ); // CS 4 (space is also a character) +``` + +Please note that a dot means "any character", but not the "absense of a character". There must be a character to match it: + +```js run +alert( "CS4".match(/CS.4/) ); // null, no match because there's no character for the dot +``` + +### Dot as literally any character with "s" flag + +By default, a dot doesn't match the newline character `\n`. + +For instance, the regexp `pattern:A.B` matches `match:A`, and then `match:B` with any character between them, except a newline `\n`: + +```js run +alert( "A\nB".match(/A.B/) ); // null (no match) +``` + +There are many situations when we'd like a dot to mean literally "any character", newline included. + +That's what flag `pattern:s` does. If a regexp has it, then a dot `pattern:.` matches literally any character: + +```js run +alert( "A\nB".match(/A.B/s) ); // A\nB (match!) +``` + +````warn header="Not supported in Firefox, IE, Edge" +Check for the most recent state of support. At the time of writing it doesn't include Firefox, IE, Edge. + +Luckily, there's an alternative, that works everywhere. We can use a regexp like `pattern:[\s\S]` to match "any character". + +```js run +alert( "A\nB".match(/A[\s\S]B/) ); // A\nB (match!) +``` + +The pattern `pattern:[\s\S]` literally says: "a space character OR not a space character". In other words, "anything". We could use another pair of complementary classes, such as `pattern:[\d\D]`, that doesn't matter. Or even the `pattern:[^]` -- as it means match any character except nothing. + +Also we can use this trick if we want both kind of "dots" in the same pattern: the actual dot `pattern:.` behaving the regular way ("not including a newline"), and also a way to match "any character" with `pattern:[\s\S]` or alike. +```` + +````warn header="Pay attention to spaces" +Usually we pay little attention to spaces. For us strings `subject:1-5` and `subject:1 - 5` are nearly identical. + +But if a regexp doesn't take spaces into account, it may fail to work. + +Let's try to find digits separated by a hyphen: + +```js run +alert( "1 - 5".match(/\d-\d/) ); // null, no match! +``` + +Let's fix it adding spaces into the regexp `pattern:\d - \d`: + +```js run +alert( "1 - 5".match(/\d - \d/) ); // 1 - 5, now it works +// or we can use \s class: +alert( "1 - 5".match(/\d\s-\s\d/) ); // 1 - 5, also works +``` + +**A space is a character. Equal in importance with any other character.** + +We can't add or remove spaces from a regular expression and expect to work the same. + +In other words, in a regular expression all characters matter, spaces too. +```` + +## Summary + +There exist following character classes: + +- `pattern:\d` -- digits. +- `pattern:\D` -- non-digits. +- `pattern:\s` -- space symbols, tabs, newlines. +- `pattern:\S` -- all but `pattern:\s`. +- `pattern:\w` -- Latin letters, digits, underscore `'_'`. +- `pattern:\W` -- all but `pattern:\w`. +- `pattern:.` -- any character if with the regexp `'s'` flag, otherwise any except a newline `\n`. + +...But that's not all! + +Unicode encoding, used by JavaScript for strings, provides many properties for characters, like: which language the letter belongs to (if it's a letter) it is it a punctuation sign, etc. + +We can search by these properties as well. That requires flag `pattern:u`, covered in the next article. diff --git a/9-regular-expressions/02-regexp-character-classes/love-html5-classes.svg b/9-regular-expressions/02-regexp-character-classes/love-html5-classes.svg new file mode 100644 index 000000000..9c88cc088 --- /dev/null +++ b/9-regular-expressions/02-regexp-character-classes/love-html5-classes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/9-regular-expressions/03-regexp-unicode/article.md b/9-regular-expressions/03-regexp-unicode/article.md new file mode 100644 index 000000000..fb8fed470 --- /dev/null +++ b/9-regular-expressions/03-regexp-unicode/article.md @@ -0,0 +1,167 @@ +# Unicode: flag "u" and class \p{...} + +JavaScript uses [Unicode encoding](https://en.wikipedia.org/wiki/Unicode) for strings. Most characters are encoded with 2 bytes, but that allows to represent at most 65536 characters. + +That range is not big enough to encode all possible characters, that's why some rare characters are encoded with 4 bytes, for instance like `𝒳` (mathematical X) or `πŸ˜„` (a smile), some hieroglyphs and so on. + +Here are the unicode values of some characters: + +| Character | Unicode | Bytes count in unicode | +|------------|---------|--------| +| a | `0x0061` | 2 | +| β‰ˆ | `0x2248` | 2 | +|𝒳| `0x1d4b3` | 4 | +|𝒴| `0x1d4b4` | 4 | +|πŸ˜„| `0x1f604` | 4 | + +So characters like `a` and `β‰ˆ` occupy 2 bytes, while codes for `𝒳`, `𝒴` and `πŸ˜„` are longer, they have 4 bytes. + +Long time ago, when JavaScript language was created, Unicode encoding was simpler: there were no 4-byte characters. So, some language features still handle them incorrectly. + +For instance, `length` thinks that here are two characters: + +```js run +alert('πŸ˜„'.length); // 2 +alert('𝒳'.length); // 2 +``` + +...But we can see that there's only one, right? The point is that `length` treats 4 bytes as two 2-byte characters. That's incorrect, because they must be considered only together (so-called "surrogate pair", you can read about them in the article ). + +By default, regular expressions also treat 4-byte "long characters" as a pair of 2-byte ones. And, as it happens with strings, that may lead to odd results. We'll see that a bit later, in the article . + +Unlike strings, regular expressions have flag `pattern:u` that fixes such problems. With such flag, a regexp handles 4-byte characters correctly. And also Unicode property search becomes available, we'll get to it next. + +## Unicode properties \p{...} + +```warn header="Not supported in Firefox and Edge" +Despite being a part of the standard since 2018, unicode properties are not supported in Firefox ([bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1361876)) and Edge ([bug](https://github.com/Microsoft/ChakraCore/issues/2969)). + +There's [XRegExp](http://xregexp.com) library that provides "extended" regular expressions with cross-browser support for unicode properties. +``` + +Every character in Unicode has a lot of properties. They describe what "category" the character belongs to, contain miscellaneous information about it. + +For instance, if a character has `Letter` property, it means that the character belongs to an alphabet (of any language). And `Number` property means that it's a digit: maybe Arabic or Chinese, and so on. + +We can search for characters with a property, written as `pattern:\p{…}`. To use `pattern:\p{…}`, a regular expression must have flag `pattern:u`. + +For instance, `\p{Letter}` denotes a letter in any of language. We can also use `\p{L}`, as `L` is an alias of `Letter`. There are shorter aliases for almost every property. + +In the example below three kinds of letters will be found: English, Georgean and Korean. + +```js run +let str = "A ბ γ„±"; + +alert( str.match(/\p{L}/gu) ); // A,ბ,γ„± +alert( str.match(/\p{L}/g) ); // null (no matches, as there's no flag "u") +``` + +Here's the main character categories and their subcategories: + +- Letter `L`: + - lowercase `Ll` + - modifier `Lm`, + - titlecase `Lt`, + - uppercase `Lu`, + - other `Lo`. +- Number `N`: + - decimal digit `Nd`, + - letter number `Nl`, + - other `No`. +- Punctuation `P`: + - connector `Pc`, + - dash `Pd`, + - initial quote `Pi`, + - final quote `Pf`, + - open `Ps`, + - close `Pe`, + - other `Po`. +- Mark `M` (accents etc): + - spacing combining `Mc`, + - enclosing `Me`, + - non-spacing `Mn`. +- Symbol `S`: + - currency `Sc`, + - modifier `Sk`, + - math `Sm`, + - other `So`. +- Separator `Z`: + - line `Zl`, + - paragraph `Zp`, + - space `Zs`. +- Other `C`: + - control `Cc`, + - format `Cf`, + - not assigned `Cn`, + -- private use `Co`, + - surrogate `Cs`. + + +So, e.g. if we need letters in lower case, we can write `pattern:\p{Ll}`, punctuation signs: `pattern:\p{P}` and so on. + +There are also other derived categories, like: +- `Alphabetic` (`Alpha`), includes Letters `L`, plus letter numbers `Nl` (e.g. β…« - a character for the roman number 12), plus some other symbols `Other_Alphabetic` (`OAlpha`). +- `Hex_Digit` includes hexadecimal digits: `0-9`, `a-f`. +- ...And so on. + +Unicode supports many different properties, their full list would require a lot of space, so here are the references: + +- List all properties by a character: . +- List all characters by a property: . +- Short aliases for properties: . +- A full base of Unicode characters in text format, with all properties, is here: . + +### Example: hexadecimal numbers + +For instance, let's look for hexadecimal numbers, written as `xFF`, where `F` is a hex digit (0..1 or A..F). + +A hex digit can be denoted as `pattern:\p{Hex_Digit}`: + +```js run +let regexp = /x\p{Hex_Digit}\p{Hex_Digit}/u; + +alert("number: xAF".match(regexp)); // xAF +``` + +### Example: Chinese hieroglyphs + +Let's look for Chinese hieroglyphs. + +There's a unicode property `Script` (a writing system), that may have a value: `Cyrillic`, `Greek`, `Arabic`, `Han` (Chinese) and so on, [here's the full list]("https://en.wikipedia.org/wiki/Script_(Unicode)"). + +To look for characters in a given writing system we should use `pattern:Script=`, e.g. for Cyrillic letters: `pattern:\p{sc=Cyrillic}`, for Chinese hieroglyphs: `pattern:\p{sc=Han}`, and so on: + +```js run +let regexp = /\p{sc=Han}/gu; // returns Chinese hieroglyphs + +let str = `Hello ΠŸΡ€ΠΈΠ²Π΅Ρ‚ δ½ ε₯½ 123_456`; + +alert( str.match(regexp) ); // δ½ ,ε₯½ +``` + +### Example: currency + +Characters that denote a currency, such as `$`, `€`, `Β₯`, have unicode property `pattern:\p{Currency_Symbol}`, the short alias: `pattern:\p{Sc}`. + +Let's use it to look for prices in the format "currency, followed by a digit": + +```js run +let regexp = /\p{Sc}\d/gu; + +let str = `Prices: $2, €1, Β₯9`; + +alert( str.match(regexp) ); // $2,€1,Β₯9 +``` + +Later, in the article we'll see how to look for numbers that contain many digits. + +## Summary + +Flag `pattern:u` enables the support of Unicode in regular expressions. + +That means two things: + +1. Characters of 4 bytes are handled correctly: as a single character, not two 2-byte characters. +2. Unicode properties can be used in the search: `\p{…}`. + +With Unicode properties we can look for words in given languages, special characters (quotes, currencies) and so on. diff --git a/9-regular-expressions/04-regexp-anchors/1-start-end/solution.md b/9-regular-expressions/04-regexp-anchors/1-start-end/solution.md new file mode 100644 index 000000000..702f992d7 --- /dev/null +++ b/9-regular-expressions/04-regexp-anchors/1-start-end/solution.md @@ -0,0 +1,5 @@ +An empty string is the only match: it starts and immediately finishes. + +The task once again demonstrates that anchors are not characters, but tests. + +The string is empty `""`. The engine first matches the `pattern:^` (input start), yes it's there, and then immediately the end `pattern:$`, it's here too. So there's a match. diff --git a/9-regular-expressions/04-regexp-anchors/1-start-end/task.md b/9-regular-expressions/04-regexp-anchors/1-start-end/task.md new file mode 100644 index 000000000..abdfec938 --- /dev/null +++ b/9-regular-expressions/04-regexp-anchors/1-start-end/task.md @@ -0,0 +1,3 @@ +# Regexp ^$ + +Which string matches the pattern `pattern:^$`? diff --git a/9-regular-expressions/04-regexp-anchors/article.md b/9-regular-expressions/04-regexp-anchors/article.md new file mode 100644 index 000000000..c34999ee5 --- /dev/null +++ b/9-regular-expressions/04-regexp-anchors/article.md @@ -0,0 +1,52 @@ +# Anchors: string start ^ and end $ + +The caret `pattern:^` and dollar `pattern:$` characters have special meaning in a regexp. They are called "anchors". + +The caret `pattern:^` matches at the beginning of the text, and the dollar `pattern:$` -- at the end. + +For instance, let's test if the text starts with `Mary`: + +```js run +let str1 = "Mary had a little lamb"; +alert( /^Mary/.test(str1) ); // true +``` + +The pattern `pattern:^Mary` means: "string start and then Mary". + +Similar to this, we can test if the string ends with `snow` using `pattern:snow$`: + +```js run +let str1 = "it's fleece was white as snow"; +alert( /snow$/.test(str1) ); // true +``` + +In these particular cases we could use string methods `startsWith/endsWith` instead. Regular expressions should be used for more complex tests. + +## Testing for a full match + +Both anchors together `pattern:^...$` are often used to test whether or not a string fully matches the pattern. For instance, to check if the user input is in the right format. + +Let's check whether or not a string is a time in `12:34` format. That is: two digits, then a colon, and then another two digits. + +In regular expressions language that's `pattern:\d\d:\d\d`: + +```js run +let goodInput = "12:34"; +let badInput = "12:345"; + +let regexp = /^\d\d:\d\d$/; +alert( regexp.test(goodInput) ); // true +alert( regexp.test(badInput) ); // false +``` + +Here the match for `pattern:\d\d:\d\d` must start exactly after the beginning of the text `pattern:^`, and the end `pattern:$` must immediately follow. + +The whole string must be exactly in this format. If there's any deviation or an extra character, the result is `false`. + +Anchors behave differently if flag `pattern:m` is present. We'll see that in the next article. + +```smart header="Anchors have \"zero width\"" +Anchors `pattern:^` and `pattern:$` are tests. They have zero width. + +In other words, they do not match a character, but rather force the regexp engine to check the condition (text start/end). +``` diff --git a/9-regular-expressions/05-regexp-multiline-mode/article.md b/9-regular-expressions/05-regexp-multiline-mode/article.md new file mode 100644 index 000000000..f8ac08ec7 --- /dev/null +++ b/9-regular-expressions/05-regexp-multiline-mode/article.md @@ -0,0 +1,87 @@ +# Multiline mode of anchors ^ $, flag "m" + +The multiline mode is enabled by the flag `pattern:m`. + +It only affects the behavior of `pattern:^` and `pattern:$`. + +In the multiline mode they match not only at the beginning and the end of the string, but also at start/end of line. + +## Searching at line start ^ + +In the example below the text has multiple lines. The pattern `pattern:/^\d/gm` takes a digit from the beginning of each line: + +```js run +let str = `1st place: Winnie +2nd place: Piglet +3rd place: Eeyore`; + +*!* +alert( str.match(/^\d/gm) ); // 1, 2, 3 +*/!* +``` + +Without the flag `pattern:m` only the first digit is matched: + +```js run +let str = `1st place: Winnie +2nd place: Piglet +3rd place: Eeyore`; + +*!* +alert( str.match(/^\d/g) ); // 1 +*/!* +``` + +That's because by default a caret `pattern:^` only matches at the beginning of the text, and in the multiline mode -- at the start of any line. + +```smart +"Start of a line" formally means "immediately after a line break": the test `pattern:^` in multiline mode matches at all positions preceeded by a newline character `\n`. + +And at the text start. +``` + +## Searching at line end $ + +The dollar sign `pattern:$` behaves similarly. + +The regular expression `pattern:\d$` finds the last digit in every line + +```js run +let str = `Winnie: 1 +Piglet: 2 +Eeyore: 3`; + +alert( str.match(/\d$/gm) ); // 1,2,3 +``` + +Without the flag `pattern:m`, the dollar `pattern:$` would only match the end of the whole text, so only the very last digit would be found. + +```smart +"End of a line" formally means "immediately before a line break": the test `pattern:$` in multiline mode matches at all positions succeeded by a newline character `\n`. + +And at the text end. +``` + +## Searching for \n instead of ^ $ + +To find a newline, we can use not only anchors `pattern:^` and `pattern:$`, but also the newline character `\n`. + +What's the difference? Let's see an example. + +Here we search for `pattern:\d\n` instead of `pattern:\d$`: + +```js run +let str = `Winnie: 1 +Piglet: 2 +Eeyore: 3`; + +alert( str.match(/\d\n/gm) ); // 1\n,2\n +``` + +As we can see, there are 2 matches instead of 3. + +That's because there's no newline after `subject:3` (there's text end though, so it matches `pattern:$`). + +Another difference: now every match includes a newline character `match:\n`. Unlike the anchors `pattern:^` `pattern:$`, that only test the condition (start/end of a line), `\n` is a character, so it becomes a part of the result. + +So, a `\n` in the pattern is used when we need newline characters in the result, while anchors are used to find something at the beginning/end of a line. diff --git a/9-regular-expressions/06-regexp-boundary/1-find-time-hh-mm/solution.md b/9-regular-expressions/06-regexp-boundary/1-find-time-hh-mm/solution.md new file mode 100644 index 000000000..829eda13e --- /dev/null +++ b/9-regular-expressions/06-regexp-boundary/1-find-time-hh-mm/solution.md @@ -0,0 +1,6 @@ + +The answer: `pattern:\b\d\d:\d\d\b`. + +```js run +alert( "Breakfast at 09:00 in the room 123:456.".match( /\b\d\d:\d\d\b/ ) ); // 09:00 +``` diff --git a/9-regular-expressions/06-regexp-boundary/1-find-time-hh-mm/task.md b/9-regular-expressions/06-regexp-boundary/1-find-time-hh-mm/task.md new file mode 100644 index 000000000..95ab5777d --- /dev/null +++ b/9-regular-expressions/06-regexp-boundary/1-find-time-hh-mm/task.md @@ -0,0 +1,9 @@ +# Find the time + +The time has a format: `hours:minutes`. Both hours and minutes has two digits, like `09:00`. + +Make a regexp to find time in the string: `subject:Breakfast at 09:00 in the room 123:456.` + +P.S. In this task there's no need to check time correctness yet, so `25:99` can also be a valid result. + +P.P.S. The regexp shouldn't match `123:456`. diff --git a/9-regular-expressions/06-regexp-boundary/article.md b/9-regular-expressions/06-regexp-boundary/article.md new file mode 100644 index 000000000..aad65877f --- /dev/null +++ b/9-regular-expressions/06-regexp-boundary/article.md @@ -0,0 +1,52 @@ +# Word boundary: \b + +A word boundary `pattern:\b` is a test, just like `pattern:^` and `pattern:$`. + +When the regexp engine (program module that implements searching for regexps) comes across `pattern:\b`, it checks that the position in the string is a word boundary. + +There are three different positions that qualify as word boundaries: + +- At string start, if the first string character is a word character `pattern:\w`. +- Between two characters in the string, where one is a word character `pattern:\w` and the other is not. +- At string end, if the last string character is a word character `pattern:\w`. + +For instance, regexp `pattern:\bJava\b` will be found in `subject:Hello, Java!`, where `subject:Java` is a standalone word, but not in `subject:Hello, JavaScript!`. + +```js run +alert( "Hello, Java!".match(/\bJava\b/) ); // Java +alert( "Hello, JavaScript!".match(/\bJava\b/) ); // null +``` + +In the string `subject:Hello, Java!` following positions correspond to `pattern:\b`: + +![](hello-java-boundaries.svg) + +So, it matches the pattern `pattern:\bHello\b`, because: + +1. At the beginning of the string matches the first test `pattern:\b`. +2. Then matches the word `pattern:Hello`. +3. Then the test `pattern:\b` matches again, as we're between `subject:o` and a space. + +The pattern `pattern:\bHello\b` would also match. But not `pattern:\bHell\b` (because there's no word boundary after `l`) and not `Java!\b` (because the exclamation sign is not a wordly character `pattern:\w`, so there's no word boundary after it). + +```js run +alert( "Hello, Java!".match(/\bHello\b/) ); // Hello +alert( "Hello, Java!".match(/\bJava\b/) ); // Java +alert( "Hello, Java!".match(/\bHell\b/) ); // null (no match) +alert( "Hello, Java!".match(/\bJava!\b/) ); // null (no match) +``` + +We can use `pattern:\b` not only with words, but with digits as well. + +For example, the pattern `pattern:\b\d\d\b` looks for standalone 2-digit numbers. In other words, it looks for 2-digit numbers that are surrounded by characters different from `pattern:\w`, such as spaces or punctuation (or text start/end). + +```js run +alert( "1 23 456 78".match(/\b\d\d\b/g) ); // 23,78 +alert( "12,34,56".match(/\b\d\d\b/g) ); // 12,34,56 +``` + +```warn header="Word boundary `pattern:\b` doesn't work for non-latin alphabets" +The word boundary test `pattern:\b` checks that there should be `pattern:\w` on the one side from the position and "not `pattern:\w`" - on the other side. + +But `pattern:\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. +``` diff --git a/9-regular-expressions/06-regexp-boundary/hello-java-boundaries.svg b/9-regular-expressions/06-regexp-boundary/hello-java-boundaries.svg new file mode 100644 index 000000000..3d421a323 --- /dev/null +++ b/9-regular-expressions/06-regexp-boundary/hello-java-boundaries.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/9-regular-expressions/07-regexp-escaping/article.md b/9-regular-expressions/07-regexp-escaping/article.md new file mode 100644 index 000000000..7bf989471 --- /dev/null +++ b/9-regular-expressions/07-regexp-escaping/article.md @@ -0,0 +1,99 @@ + +# Escaping, special characters + +As we've seen, a backslash `pattern:\` is used to denote character classes, e.g. `pattern:\d`. So it's a special character in regexps (just like in regular strings). + +There are other special characters as well, that have special meaning in a regexp. They are used to do more powerful searches. Here's a full list of them: `pattern:[ \ ^ $ . | ? * + ( )`. + +Don't try to remember the list -- soon we'll deal with each of them separately and you'll know them by heart automatically. + +## Escaping + +Let's say we want to find literally a dot. Not "any character", but just a dot. + +To use a special character as a regular one, prepend it with a backslash: `pattern:\.`. + +That's also called "escaping a character". + +For example: +```js run +alert( "Chapter 5.1".match(/\d\.\d/) ); // 5.1 (match!) +alert( "Chapter 511".match(/\d\.\d/) ); // null (looking for a real dot \.) +``` + +Parentheses are also special characters, so if we want them, we should use `pattern:\(`. The example below looks for a string `"g()"`: + +```js run +alert( "function g()".match(/g\(\)/) ); // "g()" +``` + +If we're looking for a backslash `\`, it's a special character in both regular strings and regexps, so we should double it. + +```js run +alert( "1\\2".match(/\\/) ); // '\' +``` + +## A slash + +A slash symbol `'/'` is not a special character, but in JavaScript it is used to open and close the regexp: `pattern:/...pattern.../`, so we should escape it too. + +Here's what a search for a slash `'/'` looks like: + +```js run +alert( "/".match(/\//) ); // '/' +``` + +On the other hand, if we're not using `pattern:/.../`, but create a regexp using `new RegExp`, then we don't need to escape it: + +```js run +alert( "/".match(new RegExp("/")) ); // finds / +``` + +## new RegExp + +If we are creating a regular expression with `new RegExp`, then we don't have to escape `/`, but need to do some other escaping. + +For instance, consider this: + +```js run +let regexp = new RegExp("\d\.\d"); + +alert( "Chapter 5.1".match(regexp) ); // null +``` + +The similar search in one of previous examples worked with `pattern:/\d\.\d/`, but `new RegExp("\d\.\d")` doesn't work, why? + +The reason is that backslashes are "consumed" by a string. As we may recall, regular strings have their own special characters, such as `\n`, and a backslash is used for escaping. + +Here's how "\d\.\d" is preceived: + +```js run +alert("\d\.\d"); // d.d +``` + +String quotes "consume" backslashes and interpret them on their own, for instance: + +- `\n` -- becomes a newline character, +- `\u1234` -- becomes the Unicode character with such code, +- ...And when there's no special meaning: like `pattern:\d` or `\z`, then the backslash is simply removed. + +So `new RegExp` gets a string without backslashes. That's why the search doesn't work! + +To fix it, we need to double backslashes, because string quotes turn `\\` into `\`: + +```js run +*!* +let regStr = "\\d\\.\\d"; +*/!* +alert(regStr); // \d\.\d (correct now) + +let regexp = new RegExp(regStr); + +alert( "Chapter 5.1".match(regexp) ); // 5.1 +``` + +## Summary + +- To search for special characters `pattern:[ \ ^ $ . | ? * + ( )` literally, we need to prepend them with a backslash `\` ("escape them"). +- We also need to escape `/` if we're inside `pattern:/.../` (but not inside `new RegExp`). +- When passing a string `new RegExp`, we need to double backslashes `\\`, cause string quotes consume one of them. diff --git a/9-regular-expressions/08-regexp-character-sets-and-ranges/1-find-range-1/solution.md b/9-regular-expressions/08-regexp-character-sets-and-ranges/1-find-range-1/solution.md new file mode 100644 index 000000000..378471611 --- /dev/null +++ b/9-regular-expressions/08-regexp-character-sets-and-ranges/1-find-range-1/solution.md @@ -0,0 +1,12 @@ +Answers: **no, yes**. + +- In the script `subject:Java` it doesn't match anything, because `pattern:[^script]` means "any character except given ones". So the regexp looks for `"Java"` followed by one such symbol, but there's a string end, no symbols after it. + + ```js run + alert( "Java".match(/Java[^script]/) ); // null + ``` +- Yes, because the part `pattern:[^script]` part matches the character `"S"`. It's not one of `pattern:script`. As the regexp is case-sensitive (no `pattern:i` flag), it treats `"S"` as a different character from `"s"`. + + ```js run + alert( "JavaScript".match(/Java[^script]/) ); // "JavaS" + ``` diff --git a/9-regular-expressions/08-regexp-character-sets-and-ranges/1-find-range-1/task.md b/9-regular-expressions/08-regexp-character-sets-and-ranges/1-find-range-1/task.md new file mode 100644 index 000000000..5a48e01e7 --- /dev/null +++ b/9-regular-expressions/08-regexp-character-sets-and-ranges/1-find-range-1/task.md @@ -0,0 +1,5 @@ +# Java[^script] + +We have a regexp `pattern:/Java[^script]/`. + +Does it match anything in the string `subject:Java`? In the string `subject:JavaScript`? diff --git a/9-regular-expressions/08-regexp-character-sets-and-ranges/2-find-time-2-formats/solution.md b/9-regular-expressions/08-regexp-character-sets-and-ranges/2-find-time-2-formats/solution.md new file mode 100644 index 000000000..69ade1b19 --- /dev/null +++ b/9-regular-expressions/08-regexp-character-sets-and-ranges/2-find-time-2-formats/solution.md @@ -0,0 +1,8 @@ +Answer: `pattern:\d\d[-:]\d\d`. + +```js run +let regexp = /\d\d[-:]\d\d/g; +alert( "Breakfast at 09:00. Dinner at 21-30".match(regexp) ); // 09:00, 21-30 +``` + +Please note that the dash `pattern:'-'` has a special meaning in square brackets, but only between other characters, not when it's in the beginning or at the end, so we don't need to escape it. diff --git a/9-regular-expressions/08-regexp-character-sets-and-ranges/2-find-time-2-formats/task.md b/9-regular-expressions/08-regexp-character-sets-and-ranges/2-find-time-2-formats/task.md new file mode 100644 index 000000000..c8441caf4 --- /dev/null +++ b/9-regular-expressions/08-regexp-character-sets-and-ranges/2-find-time-2-formats/task.md @@ -0,0 +1,12 @@ +# Find the time as hh:mm or hh-mm + +The time can be in the format `hours:minutes` or `hours-minutes`. Both hours and minutes have 2 digits: `09:00` or `21-30`. + +Write a regexp to find time: + +```js +let regexp = /your regexp/g; +alert( "Breakfast at 09:00. Dinner at 21-30".match(regexp) ); // 09:00, 21-30 +``` + +P.S. In this task we assume that the time is always correct, there's no need to filter out bad strings like "45:67". Later we'll deal with that too. diff --git a/9-regular-expressions/08-regexp-character-sets-and-ranges/article.md b/9-regular-expressions/08-regexp-character-sets-and-ranges/article.md new file mode 100644 index 000000000..cb6a27e9d --- /dev/null +++ b/9-regular-expressions/08-regexp-character-sets-and-ranges/article.md @@ -0,0 +1,197 @@ +# Sets and ranges [...] + +Several characters or character classes inside square brackets `[…]` mean to "search for any character among given". + +## Sets + +For instance, `pattern:[eao]` means any of the 3 characters: `'a'`, `'e'`, or `'o'`. + +That's called a *set*. Sets can be used in a regexp along with regular characters: + +```js run +// find [t or m], and then "op" +alert( "Mop top".match(/[tm]op/gi) ); // "Mop", "top" +``` + +Please note that although there are multiple characters in the set, they correspond to exactly one character in the match. + +So the example below gives no matches: + +```js run +// find "V", then [o or i], then "la" +alert( "Voila".match(/V[oi]la/) ); // null, no matches +``` + +The pattern searches for: + +- `pattern:V`, +- then *one* of the letters `pattern:[oi]`, +- then `pattern:la`. + +So there would be a match for `match:Vola` or `match:Vila`. + +## Ranges + +Square brackets may also contain *character ranges*. + +For instance, `pattern:[a-z]` is a character in range from `a` to `z`, and `pattern:[0-5]` is a digit from `0` to `5`. + +In the example below we're searching for `"x"` followed by two digits or letters from `A` to `F`: + +```js run +alert( "Exception 0xAF".match(/x[0-9A-F][0-9A-F]/g) ); // xAF +``` + +Here `pattern:[0-9A-F]` has two ranges: it searches for a character that is either a digit from `0` to `9` or a letter from `A` to `F`. + +If we'd like to look for lowercase letters as well, we can add the range `a-f`: `pattern:[0-9A-Fa-f]`. Or add the flag `pattern:i`. + +We can also use character classes inside `[…]`. + +For instance, if we'd like to look for a wordly character `pattern:\w` or a hyphen `pattern:-`, then the set is `pattern:[\w-]`. + +Combining multiple classes is also possible, e.g. `pattern:[\s\d]` means "a space character or a digit". + +```smart header="Character classes are shorthands for certain character sets" +For instance: + +- **\d** -- is the same as `pattern:[0-9]`, +- **\w** -- is the same as `pattern:[a-zA-Z0-9_]`, +- **\s** -- is the same as `pattern:[\t\n\v\f\r ]`, plus few other rare unicode space characters. +``` + +### Example: multi-language \w + +As the character class `pattern:\w` is a shorthand for `pattern:[a-zA-Z0-9_]`, it can't find Chinese hieroglyphs, Cyrillic letters, etc. + +We can write a more universal pattern, that looks for wordly characters in any language. That's easy with unicode properties: `pattern:[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]`. + +Let's decipher it. Similar to `pattern:\w`, we're making a set of our own that includes characters with following unicode properties: + +- `Alphabetic` (`Alpha`) - for letters, +- `Mark` (`M`) - for accents, +- `Decimal_Number` (`Nd`) - for digits, +- `Connector_Punctuation` (`Pc`) - for the underscore `'_'` and similar characters, +- `Join_Control` (`Join_C`) - two special codes `200c` and `200d`, used in ligatures, e.g. in Arabic. + +An example of use: + +```js run +let regexp = /[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]/gu; + +let str = `Hi δ½ ε₯½ 12`; + +// finds all letters and digits: +alert( str.match(regexp) ); // H,i,δ½ ,ε₯½,1,2 +``` + +Of course, we can edit this pattern: add unicode properties or remove them. Unicode properties are covered in more details in the article . + +```warn header="Unicode properties aren't supported in Edge and Firefox" +Unicode properties `pattern:p{…}` are not yet implemented in Edge and Firefox. If we really need them, we can use library [XRegExp](http://xregexp.com/). + +Or just use ranges of characters in a language that interests us, e.g. `pattern:[Π°-я]` for Cyrillic letters. +``` + +## Excluding ranges + +Besides normal ranges, there are "excluding" ranges that look like `pattern:[^…]`. + +They are denoted by a caret character `^` at the start and match any character *except the given ones*. + +For instance: + +- `pattern:[^aeyo]` -- any character except `'a'`, `'e'`, `'y'` or `'o'`. +- `pattern:[^0-9]` -- any character except a digit, the same as `pattern:\D`. +- `pattern:[^\s]` -- any non-space character, same as `\S`. + +The example below looks for any characters except letters, digits and spaces: + +```js run +alert( "alice15@gmail.com".match(/[^\d\sA-Z]/gi) ); // @ and . +``` + +## Escaping in […] + +Usually when we want to find exactly a special character, we need to escape it like `pattern:\.`. And if we need a backslash, then we use `pattern:\\`, and so on. + +In square brackets we can use the vast majority of special characters without escaping: + +- Symbols `pattern:. + ( )` never need escaping. +- A hyphen `pattern:-` is not escaped in the beginning or the end (where it does not define a range). +- A caret `pattern:^` is only escaped in the beginning (where it means exclusion). +- The closing square bracket `pattern:]` is always escaped (if we need to look for that symbol). + +In other words, all special characters are allowed without escaping, except when they mean something for square brackets. + +A dot `.` inside square brackets means just a dot. The pattern `pattern:[.,]` would look for one of characters: either a dot or a comma. + +In the example below the regexp `pattern:[-().^+]` looks for one of the characters `-().^+`: + +```js run +// No need to escape +let regexp = /[-().^+]/g; + +alert( "1 + 2 - 3".match(regexp) ); // Matches +, - +``` + +...But if you decide to escape them "just in case", then there would be no harm: + +```js run +// Escaped everything +let regexp = /[\-\(\)\.\^\+]/g; + +alert( "1 + 2 - 3".match(regexp) ); // also works: +, - +``` + +## Ranges and flag "u" + +If there are surrogate pairs in the set, flag `pattern:u` is required for them to work correctly. + +For instance, let's look for `pattern:[𝒳𝒴]` in the string `subject:𝒳`: + +```js run +alert( '𝒳'.match(/[𝒳𝒴]/) ); // shows a strange character, like [?] +// (the search was performed incorrectly, half-character returned) +``` + +The result is incorrect, because by default regular expressions "don't know" about surrogate pairs. + +The regular expression engine thinks that `[𝒳𝒴]` -- are not two, but four characters: +1. left half of `𝒳` `(1)`, +2. right half of `𝒳` `(2)`, +3. left half of `𝒴` `(3)`, +4. right half of `𝒴` `(4)`. + +We can see their codes like this: + +```js run +for(let i=0; i<'𝒳𝒴'.length; i++) { + alert('𝒳𝒴'.charCodeAt(i)); // 55349, 56499, 55349, 56500 +}; +``` + +So, the example above finds and shows the left half of `𝒳`. + +If we add flag `pattern:u`, then the behavior will be correct: + +```js run +alert( '𝒳'.match(/[𝒳𝒴]/u) ); // 𝒳 +``` + +The similar situation occurs when looking for a range, such as `[𝒳-𝒴]`. + +If we forget to add flag `pattern:u`, there will be an error: + +```js run +'𝒳'.match(/[𝒳-𝒴]/); // Error: Invalid regular expression +``` + +The reason is that without flag `pattern:u` surrogate pairs are perceived as two characters, so `[𝒳-𝒴]` is interpreted as `[<55349><56499>-<55349><56500>]` (every surrogate pair is replaced with its codes). Now it's easy to see that the range `56499-55349` is invalid: its starting code `56499` is greater than the end `55349`. That's the formal reason for the error. + +With the flag `pattern:u` the pattern works correctly: + +```js run +// look for characters from 𝒳 to 𝒡 +alert( '𝒴'.match(/[𝒳-𝒡]/u) ); // 𝒴 +``` diff --git a/9-regular-expressions/09-regexp-quantifiers/1-find-text-manydots/solution.md b/9-regular-expressions/09-regexp-quantifiers/1-find-text-manydots/solution.md new file mode 100644 index 000000000..21b8762ec --- /dev/null +++ b/9-regular-expressions/09-regexp-quantifiers/1-find-text-manydots/solution.md @@ -0,0 +1,9 @@ + +Solution: + +```js run +let regexp = /\.{3,}/g; +alert( "Hello!... How goes?.....".match(regexp) ); // ..., ..... +``` + +Please note that the dot is a special character, so we have to escape it and insert as `\.`. diff --git a/9-regular-expressions/09-regexp-quantifiers/1-find-text-manydots/task.md b/9-regular-expressions/09-regexp-quantifiers/1-find-text-manydots/task.md new file mode 100644 index 000000000..4140b4a98 --- /dev/null +++ b/9-regular-expressions/09-regexp-quantifiers/1-find-text-manydots/task.md @@ -0,0 +1,14 @@ +importance: 5 + +--- + +# How to find an ellipsis "..." ? + +Create a regexp to find ellipsis: 3 (or more?) dots in a row. + +Check it: + +```js +let regexp = /your regexp/g; +alert( "Hello!... How goes?.....".match(regexp) ); // ..., ..... +``` diff --git a/9-regular-expressions/09-regexp-quantifiers/2-find-html-colors-6hex/solution.md b/9-regular-expressions/09-regexp-quantifiers/2-find-html-colors-6hex/solution.md new file mode 100644 index 000000000..afee89c50 --- /dev/null +++ b/9-regular-expressions/09-regexp-quantifiers/2-find-html-colors-6hex/solution.md @@ -0,0 +1,31 @@ +We need to look for `#` followed by 6 hexadecimal characters. + +A hexadecimal character can be described as `pattern:[0-9a-fA-F]`. Or if we use the `pattern:i` flag, then just `pattern:[0-9a-f]`. + +Then we can look for 6 of them using the quantifier `pattern:{6}`. + +As a result, we have the regexp: `pattern:/#[a-f0-9]{6}/gi`. + +```js run +let regexp = /#[a-f0-9]{6}/gi; + +let str = "color:#121212; background-color:#AA00ef bad-colors:f#fddee #fd2" + +alert( str.match(regexp) ); // #121212,#AA00ef +``` + +The problem is that it finds the color in longer sequences: + +```js run +alert( "#12345678".match( /#[a-f0-9]{6}/gi ) ) // #123456 +``` + +To fix that, we can add `pattern:\b` to the end: + +```js run +// color +alert( "#123456".match( /#[a-f0-9]{6}\b/gi ) ); // #123456 + +// not a color +alert( "#12345678".match( /#[a-f0-9]{6}\b/gi ) ); // null +``` diff --git a/9-regular-expressions/09-regexp-quantifiers/2-find-html-colors-6hex/task.md b/9-regular-expressions/09-regexp-quantifiers/2-find-html-colors-6hex/task.md new file mode 100644 index 000000000..9a1923a7e --- /dev/null +++ b/9-regular-expressions/09-regexp-quantifiers/2-find-html-colors-6hex/task.md @@ -0,0 +1,15 @@ +# Regexp for HTML colors + +Create a regexp to search HTML-colors written as `#ABCDEF`: first `#` and then 6 hexadecimal characters. + +An example of use: + +```js +let regexp = /...your regexp.../ + +let str = "color:#121212; background-color:#AA00ef bad-colors:f#fddee #fd2 #12345678"; + +alert( str.match(regexp) ) // #121212,#AA00ef +``` + +P.S. In this task we do not need other color formats like `#123` or `rgb(1,2,3)` etc. diff --git a/9-regular-expressions/09-regexp-quantifiers/article.md b/9-regular-expressions/09-regexp-quantifiers/article.md new file mode 100644 index 000000000..1a7eecfeb --- /dev/null +++ b/9-regular-expressions/09-regexp-quantifiers/article.md @@ -0,0 +1,142 @@ +# Quantifiers +, *, ? and {n} + +Let's say we have a string like `+7(903)-123-45-67` and want to find all numbers in it. But unlike before, we are interested not in single digits, but full numbers: `7, 903, 123, 45, 67`. + +A number is a sequence of 1 or more digits `pattern:\d`. To mark how many we need, we can append a *quantifier*. + +## Quantity {n} + +The simplest quantifier is a number in curly braces: `pattern:{n}`. + +A quantifier is appended to a character (or a character class, or a `[...]` set etc) and specifies how many we need. + +It has a few advanced forms, let's see examples: + +The exact count: `pattern:{5}` +: `pattern:\d{5}` denotes exactly 5 digits, the same as `pattern:\d\d\d\d\d`. + + The example below looks for a 5-digit number: + + ```js run + alert( "I'm 12345 years old".match(/\d{5}/) ); // "12345" + ``` + + We can add `\b` to exclude longer numbers: `pattern:\b\d{5}\b`. + +The range: `pattern:{3,5}`, match 3-5 times +: To find numbers from 3 to 5 digits we can put the limits into curly braces: `pattern:\d{3,5}` + + ```js run + alert( "I'm not 12, but 1234 years old".match(/\d{3,5}/) ); // "1234" + ``` + + We can omit the upper limit. + + Then a regexp `pattern:\d{3,}` looks for sequences of digits of length `3` or more: + + ```js run + alert( "I'm not 12, but 345678 years old".match(/\d{3,}/) ); // "345678" + ``` + +Let's return to the string `+7(903)-123-45-67`. + +A number is a sequence of one or more digits in a row. So the regexp is `pattern:\d{1,}`: + +```js run +let str = "+7(903)-123-45-67"; + +let numbers = str.match(/\d{1,}/g); + +alert(numbers); // 7,903,123,45,67 +``` + +## Shorthands + +There are shorthands for most used quantifiers: + +`pattern:+` +: Means "one or more", the same as `pattern:{1,}`. + + For instance, `pattern:\d+` looks for numbers: + + ```js run + let str = "+7(903)-123-45-67"; + + alert( str.match(/\d+/g) ); // 7,903,123,45,67 + ``` + +`pattern:?` +: Means "zero or one", the same as `pattern:{0,1}`. In other words, it makes the symbol optional. + + For instance, the pattern `pattern:ou?r` looks for `match:o` followed by zero or one `match:u`, and then `match:r`. + + So, `pattern:colou?r` finds both `match:color` and `match:colour`: + + ```js run + let str = "Should I write color or colour?"; + + alert( str.match(/colou?r/g) ); // color, colour + ``` + +`pattern:*` +: Means "zero or more", the same as `pattern:{0,}`. That is, the character may repeat any times or be absent. + + For example, `pattern:\d0*` looks for a digit followed by any number of zeroes (may be many or none): + + ```js run + alert( "100 10 1".match(/\d0*/g) ); // 100, 10, 1 + ``` + + Compare it with `pattern:+` (one or more): + + ```js run + alert( "100 10 1".match(/\d0+/g) ); // 100, 10 + // 1 not matched, as 0+ requires at least one zero + ``` + +## More examples + +Quantifiers are used very often. They serve as the main "building block" of complex regular expressions, so let's see more examples. + +**Regexp for decimal fractions (a number with a floating point): `pattern:\d+\.\d+`** + +In action: +```js run +alert( "0 1 12.345 7890".match(/\d+\.\d+/g) ); // 12.345 +``` + +**Regexp for an "opening HTML-tag without attributes", such as `` or `

`.** + +1. The simplest one: `pattern:/<[a-z]+>/i` + + ```js run + alert( " ... ".match(/<[a-z]+>/gi) ); // + ``` + + The regexp looks for character `pattern:'<'` followed by one or more Latin letters, and then `pattern:'>'`. + +2. Improved: `pattern:/<[a-z][a-z0-9]*>/i` + + According to the standard, HTML tag name may have a digit at any position except the first one, like `

`. + + ```js run + alert( "

Hi!

".match(/<[a-z][a-z0-9]*>/gi) ); //

+ ``` + +**Regexp "opening or closing HTML-tag without attributes": `pattern:/<\/?[a-z][a-z0-9]*>/i`** + +We added an optional slash `pattern:/?` near the beginning of the pattern. Had to escape it with a backslash, otherwise JavaScript would think it is the pattern end. + +```js run +alert( "

Hi!

".match(/<\/?[a-z][a-z0-9]*>/gi) ); //

,

+``` + +```smart header="To make a regexp more precise, we often need make it more complex" +We can see one common rule in these examples: the more precise is the regular expression -- the longer and more complex it is. + +For instance, for HTML tags we could use a simpler regexp: `pattern:<\w+>`. But as HTML has stricter restrictions for a tag name, `pattern:<[a-z][a-z0-9]*>` is more reliable. + +Can we use `pattern:<\w+>` or we need `pattern:<[a-z][a-z0-9]*>`? + +In real life both variants are acceptable. Depends on how tolerant we can be to "extra" matches and whether it's difficult or not to remove them from the result by other means. +``` diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/1-lazy-greedy/solution.md b/9-regular-expressions/10-regexp-greedy-and-lazy/1-lazy-greedy/solution.md new file mode 100644 index 000000000..b8e022223 --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/1-lazy-greedy/solution.md @@ -0,0 +1,6 @@ + +The result is: `match:123 4`. + +First the lazy `pattern:\d+?` tries to take as little digits as it can, but it has to reach the space, so it takes `match:123`. + +Then the second `\d+?` takes only one digit, because that's enough. diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/1-lazy-greedy/task.md b/9-regular-expressions/10-regexp-greedy-and-lazy/1-lazy-greedy/task.md new file mode 100644 index 000000000..b46f55917 --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/1-lazy-greedy/task.md @@ -0,0 +1,7 @@ +# A match for /d+? d+?/ + +What's the match here? + +```js +"123 456".match(/\d+? \d+?/g) ); // ? +``` diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/3-find-html-comments/solution.md b/9-regular-expressions/10-regexp-greedy-and-lazy/3-find-html-comments/solution.md new file mode 100644 index 000000000..0244963d1 --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/3-find-html-comments/solution.md @@ -0,0 +1,15 @@ +We need to find the beginning of the comment `match:`. + +An acceptable variant is `pattern:` -- the lazy quantifier makes the dot stop right before `match:-->`. We also need to add flag `pattern:s` for the dot to include newlines. + +Otherwise multiline comments won't be found: + +```js run +let regexp = //gs; + +let str = `... .. .. +`; + +alert( str.match(regexp) ); // '', '' +``` diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/3-find-html-comments/task.md b/9-regular-expressions/10-regexp-greedy-and-lazy/3-find-html-comments/task.md new file mode 100644 index 000000000..551d9c725 --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/3-find-html-comments/task.md @@ -0,0 +1,13 @@ +# Find HTML comments + +Find all HTML comments in the text: + +```js +let regexp = /your regexp/g; + +let str = `... .. .. +`; + +alert( str.match(regexp) ); // '', '' +``` diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/4-find-html-tags-greedy-lazy/solution.md b/9-regular-expressions/10-regexp-greedy-and-lazy/4-find-html-tags-greedy-lazy/solution.md new file mode 100644 index 000000000..b4d9f7496 --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/4-find-html-tags-greedy-lazy/solution.md @@ -0,0 +1,10 @@ + +The solution is `pattern:<[^<>]+>`. + +```js run +let regexp = /<[^<>]+>/g; + +let str = '<> '; + +alert( str.match(regexp) ); // '', '', '' +``` diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/4-find-html-tags-greedy-lazy/task.md b/9-regular-expressions/10-regexp-greedy-and-lazy/4-find-html-tags-greedy-lazy/task.md new file mode 100644 index 000000000..8e96c921d --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/4-find-html-tags-greedy-lazy/task.md @@ -0,0 +1,15 @@ +# Find HTML tags + +Create a regular expression to find all (opening and closing) HTML tags with their attributes. + +An example of use: + +```js run +let regexp = /your regexp/g; + +let str = '<> '; + +alert( str.match(regexp) ); // '', '', '' +``` + +Here we assume that tag attributes may not contain `<` and `>` (inside squotes too), that simplifies things a bit. diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/article.md b/9-regular-expressions/10-regexp-greedy-and-lazy/article.md new file mode 100644 index 000000000..79abc559d --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/article.md @@ -0,0 +1,301 @@ +# Greedy and lazy quantifiers + +Quantifiers are very simple from the first sight, but in fact they can be tricky. + +We should understand how the search works very well if we plan to look for something more complex than `pattern:/\d+/`. + +Let's take the following task as an example. + +We have a text and need to replace all quotes `"..."` with guillemet marks: `Β«...Β»`. They are preferred for typography in many countries. + +For instance: `"Hello, world"` should become `Β«Hello, worldΒ»`. There exist other quotes, such as `β€žWitam, Ε›wiat!”` (Polish) or `γ€Œδ½ ε₯½οΌŒδΈ–η•Œγ€` (Chinese), but for our task let's choose `Β«...Β»`. + +The first thing to do is to locate quoted strings, and then we can replace them. + +A regular expression like `pattern:/".+"/g` (a quote, then something, then the other quote) may seem like a good fit, but it isn't! + +Let's try it: + +```js run +let regexp = /".+"/g; + +let str = 'a "witch" and her "broom" is one'; + +alert( str.match(regexp) ); // "witch" and her "broom" +``` + +...We can see that it works not as intended! + +Instead of finding two matches `match:"witch"` and `match:"broom"`, it finds one: `match:"witch" and her "broom"`. + +That can be described as "greediness is the cause of all evil". + +## Greedy search + +To find a match, the regular expression engine uses the following algorithm: + +- For every position in the string + - Try to match the pattern at that position. + - If there's no match, go to the next position. + +These common words do not make it obvious why the regexp fails, so let's elaborate how the search works for the pattern `pattern:".+"`. + +1. The first pattern character is a quote `pattern:"`. + + The regular expression engine tries to find it at the zero position of the source string `subject:a "witch" and her "broom" is one`, but there's `subject:a` there, so there's immediately no match. + + Then it advances: goes to the next positions in the source string and tries to find the first character of the pattern there, fails again, and finally finds the quote at the 3rd position: + + ![](witch_greedy1.svg) + +2. The quote is detected, and then the engine tries to find a match for the rest of the pattern. It tries to see if the rest of the subject string conforms to `pattern:.+"`. + + In our case the next pattern character is `pattern:.` (a dot). It denotes "any character except a newline", so the next string letter `match:'w'` fits: + + ![](witch_greedy2.svg) + +3. Then the dot repeats because of the quantifier `pattern:.+`. The regular expression engine adds to the match one character after another. + + ...Until when? All characters match the dot, so it only stops when it reaches the end of the string: + + ![](witch_greedy3.svg) + +4. Now the engine finished repeating `pattern:.+` and tries to find the next character of the pattern. It's the quote `pattern:"`. But there's a problem: the string has finished, there are no more characters! + + The regular expression engine understands that it took too many `pattern:.+` and starts to *backtrack*. + + In other words, it shortens the match for the quantifier by one character: + + ![](witch_greedy4.svg) + + Now it assumes that `pattern:.+` ends one character before the string end and tries to match the rest of the pattern from that position. + + If there were a quote there, then the search would end, but the last character is `subject:'e'`, so there's no match. + +5. ...So the engine decreases the number of repetitions of `pattern:.+` by one more character: + + ![](witch_greedy5.svg) + + The quote `pattern:'"'` does not match `subject:'n'`. + +6. The engine keep backtracking: it decreases the count of repetition for `pattern:'.'` until the rest of the pattern (in our case `pattern:'"'`) matches: + + ![](witch_greedy6.svg) + +7. The match is complete. + +8. So the first match is `match:"witch" and her "broom"`. If the regular expression has flag `pattern:g`, then the search will continue from where the first match ends. There are no more quotes in the rest of the string `subject:is one`, so no more results. + +That's probably not what we expected, but that's how it works. + +**In the greedy mode (by default) a quantifier is repeated as many times as possible.** + +The regexp engine adds to the match as many characters as it can for `pattern:.+`, and then shortens that one by one, if the rest of the pattern doesn't match. + +For our task we want another thing. That's where a lazy mode can help. + +## Lazy mode + +The lazy mode of quantifiers is an opposite to the greedy mode. It means: "repeat minimal number of times". + +We can enable it by putting a question mark `pattern:'?'` after the quantifier, so that it becomes `pattern:*?` or `pattern:+?` or even `pattern:??` for `pattern:'?'`. + +To make things clear: usually a question mark `pattern:?` is a quantifier by itself (zero or one), but if added *after another quantifier (or even itself)* it gets another meaning -- it switches the matching mode from greedy to lazy. + +The regexp `pattern:/".+?"/g` works as intended: it finds `match:"witch"` and `match:"broom"`: + +```js run +let regexp = /".+?"/g; + +let str = 'a "witch" and her "broom" is one'; + +alert( str.match(regexp) ); // witch, broom +``` + +To clearly understand the change, let's trace the search step by step. + +1. The first step is the same: it finds the pattern start `pattern:'"'` at the 3rd position: + + ![](witch_greedy1.svg) + +2. The next step is also similar: the engine finds a match for the dot `pattern:'.'`: + + ![](witch_greedy2.svg) + +3. And now the search goes differently. Because we have a lazy mode for `pattern:+?`, the engine doesn't try to match a dot one more time, but stops and tries to match the rest of the pattern `pattern:'"'` right now: + + ![](witch_lazy3.svg) + + If there were a quote there, then the search would end, but there's `'i'`, so there's no match. +4. Then the regular expression engine increases the number of repetitions for the dot and tries one more time: + + ![](witch_lazy4.svg) + + Failure again. Then the number of repetitions is increased again and again... +5. ...Till the match for the rest of the pattern is found: + + ![](witch_lazy5.svg) + +6. The next search starts from the end of the current match and yield one more result: + + ![](witch_lazy6.svg) + +In this example we saw how the lazy mode works for `pattern:+?`. Quantifiers `pattern:*?` and `pattern:??` work the similar way -- the regexp engine increases the number of repetitions only if the rest of the pattern can't match on the given position. + +**Laziness is only enabled for the quantifier with `?`.** + +Other quantifiers remain greedy. + +For instance: + +```js run +alert( "123 456".match(/\d+ \d+?/) ); // 123 4 +``` + +1. The pattern `pattern:\d+` tries to match as many digits as it can (greedy mode), so it finds `match:123` and stops, because the next character is a space `pattern:' '`. +2. Then there's a space in the pattern, it matches. +3. Then there's `pattern:\d+?`. The quantifier is in lazy mode, so it finds one digit `match:4` and tries to check if the rest of the pattern matches from there. + + ...But there's nothing in the pattern after `pattern:\d+?`. + + The lazy mode doesn't repeat anything without a need. The pattern finished, so we're done. We have a match `match:123 4`. + +```smart header="Optimizations" +Modern regular expression engines can optimize internal algorithms to work faster. So they may work a bit differently from the described algorithm. + +But to understand how regular expressions work and to build regular expressions, we don't need to know about that. They are only used internally to optimize things. + +Complex regular expressions are hard to optimize, so the search may work exactly as described as well. +``` + +## Alternative approach + +With regexps, there's often more than one way to do the same thing. + +In our case we can find quoted strings without lazy mode using the regexp `pattern:"[^"]+"`: + +```js run +let regexp = /"[^"]+"/g; + +let str = 'a "witch" and her "broom" is one'; + +alert( str.match(regexp) ); // witch, broom +``` + +The regexp `pattern:"[^"]+"` gives correct results, because it looks for a quote `pattern:'"'` followed by one or more non-quotes `pattern:[^"]`, and then the closing quote. + +When the regexp engine looks for `pattern:[^"]+` it stops the repetitions when it meets the closing quote, and we're done. + +Please note, that this logic does not replace lazy quantifiers! + +It is just different. There are times when we need one or another. + +**Let's see an example where lazy quantifiers fail and this variant works right.** + +For instance, we want to find links of the form ``, with any `href`. + +Which regular expression to use? + +The first idea might be: `pattern://g`. + +Let's check it: +```js run +let str = '......'; +let regexp = //g; + +// Works! +alert( str.match(regexp) ); // +``` + +It worked. But let's see what happens if there are many links in the text? + +```js run +let str = '...... ...'; +let regexp = //g; + +// Whoops! Two links in one match! +alert( str.match(regexp) ); // ... +``` + +Now the result is wrong for the same reason as our "witches" example. The quantifier `pattern:.*` took too many characters. + +The match looks like this: + +```html + +... +``` + +Let's modify the pattern by making the quantifier `pattern:.*?` lazy: + +```js run +let str = '...... ...'; +let regexp = //g; + +// Works! +alert( str.match(regexp) ); // , +``` + +Now it seems to work, there are two matches: + +```html + +... +``` + +...But let's test it on one more text input: + +```js run +let str = '......

...'; +let regexp = //g; + +// Wrong match! +alert( str.match(regexp) ); // ...

+``` + +Now it fails. The match includes not just a link, but also a lot of text after it, including ``. + +Why? + +That's what's going on: + +1. First the regexp finds a link start `match:` (none). +3. Then takes another character into `pattern:.*?`, and so on... until it finally reaches `match:" class="doc">`. + +But the problem is: that's already beyond the link ``, in another tag `

`. Not what we want. + +Here's the picture of the match aligned with the text: + +```html + +...

+``` + +So, we need the pattern to look for ``, but both greedy and lazy variants have problems. + +The correct variant can be: `pattern:href="[^"]*"`. It will take all characters inside the `href` attribute till the nearest quote, just what we need. + +A working example: + +```js run +let str1 = '......

...'; +let str2 = '...... ...'; +let regexp = //g; + +// Works! +alert( str1.match(regexp) ); // null, no matches, that's correct +alert( str2.match(regexp) ); // , +``` + +## Summary + +Quantifiers have two modes of work: + +Greedy +: By default the regular expression engine tries to repeat the quantifier as many times as possible. For instance, `pattern:\d+` consumes all possible digits. When it becomes impossible to consume more (no more digits or string end), then it continues to match the rest of the pattern. If there's no match then it decreases the number of repetitions (backtracks) and tries again. + +Lazy +: Enabled by the question mark `pattern:?` after the quantifier. The regexp engine tries to match the rest of the pattern before each repetition of the quantifier. + +As we've seen, the lazy mode is not a "panacea" from the greedy search. An alternative is a "fine-tuned" greedy search, with exclusions, as in the pattern `pattern:"[^"]+"`. diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy1.svg b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy1.svg new file mode 100644 index 000000000..2eaf636cd --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy1.svg @@ -0,0 +1 @@ +a "witch" and her "broom" is one \ No newline at end of file diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy2.svg b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy2.svg new file mode 100644 index 000000000..0489875a6 --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy2.svg @@ -0,0 +1 @@ +a "witch" and her "broom" is one \ No newline at end of file diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy3.svg b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy3.svg new file mode 100644 index 000000000..f5175e5c3 --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy3.svg @@ -0,0 +1 @@ +a "witch" and her "broom" is one \ No newline at end of file diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy4.svg b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy4.svg new file mode 100644 index 000000000..61b37fb9c --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy4.svg @@ -0,0 +1 @@ +a "witch" and her "broom" is one \ No newline at end of file diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy5.svg b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy5.svg new file mode 100644 index 000000000..a0c5f1fb8 --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy5.svg @@ -0,0 +1 @@ +a "witch" and her "broom" is one \ No newline at end of file diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy6.svg b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy6.svg new file mode 100644 index 000000000..c7cc7537c --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_greedy6.svg @@ -0,0 +1 @@ +a "witch" and her "broom" is one \ No newline at end of file diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/witch_lazy3.svg b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_lazy3.svg new file mode 100644 index 000000000..77d5d1562 --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_lazy3.svg @@ -0,0 +1 @@ +a "witch" and her "broom" is one \ No newline at end of file diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/witch_lazy4.svg b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_lazy4.svg new file mode 100644 index 000000000..6c9cc29cf --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_lazy4.svg @@ -0,0 +1 @@ +a "witch" and her "broom" is one \ No newline at end of file diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/witch_lazy5.svg b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_lazy5.svg new file mode 100644 index 000000000..68c77d27d --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_lazy5.svg @@ -0,0 +1 @@ +a "witch" and her "broom" is one \ No newline at end of file diff --git a/9-regular-expressions/10-regexp-greedy-and-lazy/witch_lazy6.svg b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_lazy6.svg new file mode 100644 index 000000000..2ee64f5b8 --- /dev/null +++ b/9-regular-expressions/10-regexp-greedy-and-lazy/witch_lazy6.svg @@ -0,0 +1 @@ +a "witch" and her "broom" is one \ No newline at end of file diff --git a/9-regular-expressions/11-regexp-groups/01-test-mac/solution.md b/9-regular-expressions/11-regexp-groups/01-test-mac/solution.md new file mode 100644 index 000000000..26f7888f7 --- /dev/null +++ b/9-regular-expressions/11-regexp-groups/01-test-mac/solution.md @@ -0,0 +1,21 @@ +A two-digit hex number is `pattern:[0-9a-f]{2}` (assuming the flag `pattern:i` is set). + +We need that number `NN`, and then `:NN` repeated 5 times (more numbers); + +The regexp is: `pattern:[0-9a-f]{2}(:[0-9a-f]{2}){5}` + +Now let's show that the match should capture all the text: start at the beginning and end at the end. That's done by wrapping the pattern in `pattern:^...$`. + +Finally: + +```js run +let regexp = /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/i; + +alert( regexp.test('01:32:54:67:89:AB') ); // true + +alert( regexp.test('0132546789AB') ); // false (no colons) + +alert( regexp.test('01:32:54:67:89') ); // false (5 numbers, need 6) + +alert( regexp.test('01:32:54:67:89:ZZ') ) // false (ZZ in the end) +``` diff --git a/9-regular-expressions/11-regexp-groups/01-test-mac/task.md b/9-regular-expressions/11-regexp-groups/01-test-mac/task.md new file mode 100644 index 000000000..029a4803a --- /dev/null +++ b/9-regular-expressions/11-regexp-groups/01-test-mac/task.md @@ -0,0 +1,20 @@ +# Check MAC-address + +[MAC-address](https://en.wikipedia.org/wiki/MAC_address) of a network interface consists of 6 two-digit hex numbers separated by a colon. + +For instance: `subject:'01:32:54:67:89:AB'`. + +Write a regexp that checks whether a string is MAC-address. + +Usage: +```js +let regexp = /your regexp/; + +alert( regexp.test('01:32:54:67:89:AB') ); // true + +alert( regexp.test('0132546789AB') ); // false (no colons) + +alert( regexp.test('01:32:54:67:89') ); // false (5 numbers, must be 6) + +alert( regexp.test('01:32:54:67:89:ZZ') ) // false (ZZ ad the end) +``` diff --git a/9-regular-expressions/11-regexp-groups/02-find-webcolor-3-or-6/solution.md b/9-regular-expressions/11-regexp-groups/02-find-webcolor-3-or-6/solution.md new file mode 100644 index 000000000..0806dc4fd --- /dev/null +++ b/9-regular-expressions/11-regexp-groups/02-find-webcolor-3-or-6/solution.md @@ -0,0 +1,27 @@ +A regexp to search 3-digit color `#abc`: `pattern:/#[a-f0-9]{3}/i`. + +We can add exactly 3 more optional hex digits. We don't need more or less. The color has either 3 or 6 digits. + +Let's use the quantifier `pattern:{1,2}` for that: we'll have `pattern:/#([a-f0-9]{3}){1,2}/i`. + +Here the pattern `pattern:[a-f0-9]{3}` is enclosed in parentheses to apply the quantifier `pattern:{1,2}`. + +In action: + +```js run +let regexp = /#([a-f0-9]{3}){1,2}/gi; + +let str = "color: #3f3; background-color: #AA00ef; and: #abcd"; + +alert( str.match(regexp) ); // #3f3 #AA00ef #abc +``` + +There's a minor problem here: the pattern found `match:#abc` in `subject:#abcd`. To prevent that we can add `pattern:\b` to the end: + +```js run +let regexp = /#([a-f0-9]{3}){1,2}\b/gi; + +let str = "color: #3f3; background-color: #AA00ef; and: #abcd"; + +alert( str.match(regexp) ); // #3f3 #AA00ef +``` diff --git a/9-regular-expressions/11-regexp-groups/02-find-webcolor-3-or-6/task.md b/9-regular-expressions/11-regexp-groups/02-find-webcolor-3-or-6/task.md new file mode 100644 index 000000000..09108484a --- /dev/null +++ b/9-regular-expressions/11-regexp-groups/02-find-webcolor-3-or-6/task.md @@ -0,0 +1,14 @@ +# Find color in the format #abc or #abcdef + +Write a RegExp that matches colors in the format `#abc` or `#abcdef`. That is: `#` followed by 3 or 6 hexadecimal digits. + +Usage example: +```js +let regexp = /your regexp/g; + +let str = "color: #3f3; background-color: #AA00ef; and: #abcd"; + +alert( str.match(regexp) ); // #3f3 #AA00ef +``` + +P.S. This should be exactly 3 or 6 hex digits. Values with 4 digits, such as `#abcd`, should not match. diff --git a/9-regular-expressions/11-regexp-groups/03-find-decimal-numbers/solution.md b/9-regular-expressions/11-regexp-groups/03-find-decimal-numbers/solution.md new file mode 100644 index 000000000..c4349f9a0 --- /dev/null +++ b/9-regular-expressions/11-regexp-groups/03-find-decimal-numbers/solution.md @@ -0,0 +1,11 @@ +A positive number with an optional decimal part is (per previous task): `pattern:\d+(\.\d+)?`. + +Let's add the optional `pattern:-` in the beginning: + +```js run +let regexp = /-?\d+(\.\d+)?/g; + +let str = "-1.5 0 2 -123.4."; + +alert( str.match(regexp) ); // -1.5, 0, 2, -123.4 +``` diff --git a/9-regular-expressions/11-regexp-groups/03-find-decimal-numbers/task.md b/9-regular-expressions/11-regexp-groups/03-find-decimal-numbers/task.md new file mode 100644 index 000000000..4f5a73fff --- /dev/null +++ b/9-regular-expressions/11-regexp-groups/03-find-decimal-numbers/task.md @@ -0,0 +1,13 @@ +# Find all numbers + +Write a regexp that looks for all decimal numbers including integer ones, with the floating point and negative ones. + +An example of use: + +```js +let regexp = /your regexp/g; + +let str = "-1.5 0 2 -123.4."; + +alert( str.match(regexp) ); // -1.5, 0, 2, -123.4 +``` diff --git a/9-regular-expressions/11-regexp-groups/04-parse-expression/solution.md b/9-regular-expressions/11-regexp-groups/04-parse-expression/solution.md new file mode 100644 index 000000000..130c57be3 --- /dev/null +++ b/9-regular-expressions/11-regexp-groups/04-parse-expression/solution.md @@ -0,0 +1,56 @@ +A regexp for a number is: `pattern:-?\d+(\.\d+)?`. We created it in previous tasks. + +An operator is `pattern:[-+*/]`. The hyphen `pattern:-` goes first in the square brackets, because in the middle it would mean a character range, while we just want a character `-`. + +The slash `/` should be escaped inside a JavaScript regexp `pattern:/.../`, we'll do that later. + +We need a number, an operator, and then another number. And optional spaces between them. + +The full regular expression: `pattern:-?\d+(\.\d+)?\s*[-+*/]\s*-?\d+(\.\d+)?`. + +It has 3 parts, with `pattern:\s*` between them: +1. `pattern:-?\d+(\.\d+)?` - the first number, +1. `pattern:[-+*/]` - the operator, +1. `pattern:-?\d+(\.\d+)?` - the second number. + +To make each of these parts a separate element of the result array, let's enclose them in parentheses: `pattern:(-?\d+(\.\d+)?)\s*([-+*/])\s*(-?\d+(\.\d+)?)`. + +In action: + +```js run +let regexp = /(-?\d+(\.\d+)?)\s*([-+*\/])\s*(-?\d+(\.\d+)?)/; + +alert( "1.2 + 12".match(regexp) ); +``` + +The result includes: + +- `result[0] == "1.2 + 12"` (full match) +- `result[1] == "1.2"` (first group `(-?\d+(\.\d+)?)` -- the first number, including the decimal part) +- `result[2] == ".2"` (second group`(\.\d+)?` -- the first decimal part) +- `result[3] == "+"` (third group `([-+*\/])` -- the operator) +- `result[4] == "12"` (forth group `(-?\d+(\.\d+)?)` -- the second number) +- `result[5] == undefined` (fifth group `(\.\d+)?` -- the last decimal part is absent, so it's undefined) + +We only want the numbers and the operator, without the full match or the decimal parts, so let's "clean" the result a bit. + +The full match (the arrays first item) can be removed by shifting the array `result.shift()`. + +Groups that contain decimal parts (number 2 and 4) `pattern:(.\d+)` can be excluded by adding `pattern:?:` to the beginning: `pattern:(?:\.\d+)?`. + +The final solution: + +```js run +function parse(expr) { + let regexp = /(-?\d+(?:\.\d+)?)\s*([-+*\/])\s*(-?\d+(?:\.\d+)?)/; + + let result = expr.match(regexp); + + if (!result) return []; + result.shift(); + + return result; +} + +alert( parse("-1.23 * 3.45") ); // -1.23, *, 3.45 +``` diff --git a/9-regular-expressions/11-regexp-groups/04-parse-expression/task.md b/9-regular-expressions/11-regexp-groups/04-parse-expression/task.md new file mode 100644 index 000000000..8b54d4683 --- /dev/null +++ b/9-regular-expressions/11-regexp-groups/04-parse-expression/task.md @@ -0,0 +1,28 @@ +# Parse an expression + +An arithmetical expression consists of 2 numbers and an operator between them, for instance: + +- `1 + 2` +- `1.2 * 3.4` +- `-3 / -6` +- `-2 - 2` + +The operator is one of: `"+"`, `"-"`, `"*"` or `"/"`. + +There may be extra spaces at the beginning, at the end or between the parts. + +Create a function `parse(expr)` that takes an expression and returns an array of 3 items: + +1. The first number. +2. The operator. +3. The second number. + +For example: + +```js +let [a, op, b] = parse("1.2 * 3.4"); + +alert(a); // 1.2 +alert(op); // * +alert(b); // 3.4 +``` diff --git a/9-regular-expressions/11-regexp-groups/article.md b/9-regular-expressions/11-regexp-groups/article.md new file mode 100644 index 000000000..e559fd87c --- /dev/null +++ b/9-regular-expressions/11-regexp-groups/article.md @@ -0,0 +1,364 @@ +# Capturing groups + +A part of a pattern can be enclosed in parentheses `pattern:(...)`. This is called a "capturing group". + +That has two effects: + +1. It allows to get a part of the match as a separate item in the result array. +2. If we put a quantifier after the parentheses, it applies to the parentheses as a whole. + +## Examples + +Let's see how parentheses work in examples. + +### Example: gogogo + +Without parentheses, the pattern `pattern:go+` means `subject:g` character, followed by `subject:o` repeated one or more times. For instance, `match:goooo` or `match:gooooooooo`. + +Parentheses group characters together, so `pattern:(go)+` means `match:go`, `match:gogo`, `match:gogogo` and so on. + +```js run +alert( 'Gogogo now!'.match(/(go)+/ig) ); // "Gogogo" +``` + +### Example: domain + +Let's make something more complex -- a regular expression to search for a website domain. + +For example: + +``` +mail.com +users.mail.com +smith.users.mail.com +``` + +As we can see, a domain consists of repeated words, a dot after each one except the last one. + +In regular expressions that's `pattern:(\w+\.)+\w+`: + +```js run +let regexp = /(\w+\.)+\w+/g; + +alert( "site.com my.site.com".match(regexp) ); // site.com,my.site.com +``` + +The search works, but the pattern can't match a domain with a hyphen, e.g. `my-site.com`, because the hyphen does not belong to class `pattern:\w`. + +We can fix it by replacing `pattern:\w` with `pattern:[\w-]` in every word except the last one: `pattern:([\w-]+\.)+\w+`. + +### Example: email + +The previous example can be extended. We can create a regular expression for emails based on it. + +The email format is: `name@domain`. Any word can be the name, hyphens and dots are allowed. In regular expressions that's `pattern:[-.\w]+`. + +The pattern: + +```js run +let regexp = /[-.\w]+@([\w-]+\.)+[\w-]+/g; + +alert("my@mail.com @ his@site.com.uk".match(regexp)); // my@mail.com, his@site.com.uk +``` + +That regexp is not perfect, but mostly works and helps to fix accidental mistypes. The only truly reliable check for an email can only be done by sending a letter. + +## Parentheses contents in the match + +Parentheses are numbered from left to right. The search engine memorizes the content matched by each of them and allows to get it in the result. + +The method `str.match(regexp)`, if `regexp` has no flag `g`, looks for the first match and returns it as an array: + +1. At index `0`: the full match. +2. At index `1`: the contents of the first parentheses. +3. At index `2`: the contents of the second parentheses. +4. ...and so on... + +For instance, we'd like to find HTML tags `pattern:<.*?>`, and process them. It would be convenient to have tag content (what's inside the angles), in a separate variable. + +Let's wrap the inner content into parentheses, like this: `pattern:<(.*?)>`. + +Now we'll get both the tag as a whole `match:

` and its contents `match:h1` in the resulting array: + +```js run +let str = '

Hello, world!

'; + +let tag = str.match(/<(.*?)>/); + +alert( tag[0] ); //

+alert( tag[1] ); // h1 +``` + +### Nested groups + +Parentheses can be nested. In this case the numbering also goes from left to right. + +For instance, when searching a tag in `subject:` we may be interested in: + +1. The tag content as a whole: `match:span class="my"`. +2. The tag name: `match:span`. +3. The tag attributes: `match:class="my"`. + +Let's add parentheses for them: `pattern:<(([a-z]+)\s*([^>]*))>`. + +Here's how they are numbered (left to right, by the opening paren): + +![](regexp-nested-groups-pattern.svg) + +In action: + +```js run +let str = ''; + +let regexp = /<(([a-z]+)\s*([^>]*))>/; + +let result = str.match(regexp); +alert(result[0]); // +alert(result[1]); // span class="my" +alert(result[2]); // span +alert(result[3]); // class="my" +``` + +The zero index of `result` always holds the full match. + +Then groups, numbered from left to right by an opening paren. The first group is returned as `result[1]`. Here it encloses the whole tag content. + +Then in `result[2]` goes the group from the second opening paren `pattern:([a-z]+)` - tag name, then in `result[3]` the tag: `pattern:([^>]*)`. + +The contents of every group in the string: + +![](regexp-nested-groups-matches.svg) + +### Optional groups + +Even if a group is optional and doesn't exist in the match (e.g. has the quantifier `pattern:(...)?`), the corresponding `result` array item is present and equals `undefined`. + +For instance, let's consider the regexp `pattern:a(z)?(c)?`. It looks for `"a"` optionally followed by `"z"` optionally followed by `"c"`. + +If we run it on the string with a single letter `subject:a`, then the result is: + +```js run +let match = 'a'.match(/a(z)?(c)?/); + +alert( match.length ); // 3 +alert( match[0] ); // a (whole match) +alert( match[1] ); // undefined +alert( match[2] ); // undefined +``` + +The array has the length of `3`, but all groups are empty. + +And here's a more complex match for the string `subject:ac`: + +```js run +let match = 'ac'.match(/a(z)?(c)?/) + +alert( match.length ); // 3 +alert( match[0] ); // ac (whole match) +alert( match[1] ); // undefined, because there's nothing for (z)? +alert( match[2] ); // c +``` + +The array length is permanent: `3`. But there's nothing for the group `pattern:(z)?`, so the result is `["ac", undefined, "c"]`. + +## Searching for all matches with groups: matchAll + +```warn header="`matchAll` is a new method, polyfill may be needed" +The method `matchAll` is not supported in old browsers. + +A polyfill may be required, such as . +``` + +When we search for all matches (flag `pattern:g`), the `match` method does not return contents for groups. + +For example, let's find all tags in a string: + +```js run +let str = '

'; + +let tags = str.match(/<(.*?)>/g); + +alert( tags ); //

,

+``` + +The result is an array of matches, but without details about each of them. But in practice we usually need contents of capturing groups in the result. + +To get them, we should search using the method `str.matchAll(regexp)`. + +It was added to JavaScript language long after `match`, as its "new and improved version". + +Just like `match`, it looks for matches, but there are 3 differences: + +1. It returns not an array, but an iterable object. +2. When the flag `pattern:g` is present, it returns every match as an array with groups. +3. If there are no matches, it returns not `null`, but an empty iterable object. + +For instance: + +```js run +let results = '

'.matchAll(/<(.*?)>/gi); + +// results - is not an array, but an iterable object +alert(results); // [object RegExp String Iterator] + +alert(results[0]); // undefined (*) + +results = Array.from(results); // let's turn it into array + +alert(results[0]); //

,h1 (1st tag) +alert(results[1]); //

,h2 (2nd tag) +``` + +As we can see, the first difference is very important, as demonstrated in the line `(*)`. We can't get the match as `results[0]`, because that object isn't pseudoarray. We can turn it into a real `Array` using `Array.from`. There are more details about pseudoarrays and iterables in the article . + +There's no need in `Array.from` if we're looping over results: + +```js run +let results = '

'.matchAll(/<(.*?)>/gi); + +for(let result of results) { + alert(result); + // first alert:

,h1 + // second:

,h2 +} +``` + +...Or using destructuring: + +```js +let [tag1, tag2] = '

'.matchAll(/<(.*?)>/gi); +``` + +Every match, returned by `matchAll`, has the same format as returned by `match` without flag `pattern:g`: it's an array with additional properties `index` (match index in the string) and `input` (source string): + +```js run +let results = '

'.matchAll(/<(.*?)>/gi); + +let [tag1, tag2] = results; + +alert( tag1[0] ); //

+alert( tag1[1] ); // h1 +alert( tag1.index ); // 0 +alert( tag1.input ); //

+``` + +```smart header="Why is a result of `matchAll` an iterable object, not an array?" +Why is the method designed like that? The reason is simple - for the optimization. + +The call to `matchAll` does not perform the search. Instead, it returns an iterable object, without the results initially. The search is performed each time we iterate over it, e.g. in the loop. + +So, there will be found as many results as needed, not more. + +E.g. there are potentially 100 matches in the text, but in a `for..of` loop we found 5 of them, then decided it's enough and make a `break`. Then the engine won't spend time finding other 95 matches. +``` + +## Named groups + +Remembering groups by their numbers is hard. For simple patterns it's doable, but for more complex ones counting parentheses is inconvenient. We have a much better option: give names to parentheses. + +That's done by putting `pattern:?` immediately after the opening paren. + +For example, let's look for a date in the format "year-month-day": + +```js run +*!* +let dateRegexp = /(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})/; +*/!* +let str = "2019-04-30"; + +let groups = str.match(dateRegexp).groups; + +alert(groups.year); // 2019 +alert(groups.month); // 04 +alert(groups.day); // 30 +``` + +As you can see, the groups reside in the `.groups` property of the match. + +To look for all dates, we can add flag `pattern:g`. + +We'll also need `matchAll` to obtain full matches, together with groups: + +```js run +let dateRegexp = /(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})/g; + +let str = "2019-10-30 2020-01-01"; + +let results = str.matchAll(dateRegexp); + +for(let result of results) { + let {year, month, day} = result.groups; + + alert(`${day}.${month}.${year}`); + // first alert: 30.10.2019 + // second: 01.01.2020 +} +``` + +## Capturing groups in replacement + +Method `str.replace(regexp, replacement)` that replaces all matches with `regexp` in `str` allows to use parentheses contents in the `replacement` string. That's done using `pattern:$n`, where `pattern:n` is the group number. + +For example, + +```js run +let str = "John Bull"; +let regexp = /(\w+) (\w+)/; + +alert( str.replace(regexp, '$2, $1') ); // Bull, John +``` + +For named parentheses the reference will be `pattern:$`. + +For example, let's reformat dates from "year-month-day" to "day.month.year": + +```js run +let regexp = /(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})/g; + +let str = "2019-10-30, 2020-01-01"; + +alert( str.replace(regexp, '$.$.$') ); +// 30.10.2019, 01.01.2020 +``` + +## Non-capturing groups with ?: + +Sometimes we need parentheses to correctly apply a quantifier, but we don't want their contents in results. + +A group may be excluded by adding `pattern:?:` in the beginning. + +For instance, if we want to find `pattern:(go)+`, but don't want the parentheses contents (`go`) as a separate array item, we can write: `pattern:(?:go)+`. + +In the example below we only get the name `match:John` as a separate member of the match: + +```js run +let str = "Gogogo John!"; + +*!* +// ?: exludes 'go' from capturing +let regexp = /(?:go)+ (\w+)/i; +*/!* + +let result = str.match(regexp); + +alert( result[0] ); // Gogogo John (full match) +alert( result[1] ); // John +alert( result.length ); // 2 (no more items in the array) +``` + +## Summary + +Parentheses group together a part of the regular expression, so that the quantifier applies to it as a whole. + +Parentheses groups are numbered left-to-right, and can optionally be named with `(?...)`. + +The content, matched by a group, can be obtained in the results: + +- The method `str.match` returns capturing groups only without flag `pattern:g`. +- The method `str.matchAll` always returns capturing groups. + +If the parentheses have no name, then their contents is available in the match array by its number. Named parentheses are also available in the property `groups`. + +We can also use parentheses contents in the replacement string in `str.replace`: by the number `$n` or the name `$`. + +A group may be excluded from numbering by adding `pattern:?:` in its start. That's used when we need to apply a quantifier to the whole group, but don't want it as a separate item in the results array. We also can't reference such parentheses in the replacement string. diff --git a/9-regular-expressions/11-regexp-groups/regexp-nested-groups-matches.svg b/9-regular-expressions/11-regexp-groups/regexp-nested-groups-matches.svg new file mode 100644 index 000000000..ce61ff3a7 --- /dev/null +++ b/9-regular-expressions/11-regexp-groups/regexp-nested-groups-matches.svg @@ -0,0 +1 @@ +< (( [a-z]+ ) \s* ( [^>]* )) >1span class="my"2span3class="my" \ No newline at end of file diff --git a/9-regular-expressions/11-regexp-groups/regexp-nested-groups-pattern.svg b/9-regular-expressions/11-regexp-groups/regexp-nested-groups-pattern.svg new file mode 100644 index 000000000..ce61ff3a7 --- /dev/null +++ b/9-regular-expressions/11-regexp-groups/regexp-nested-groups-pattern.svg @@ -0,0 +1 @@ +< (( [a-z]+ ) \s* ( [^>]* )) >1span class="my"2span3class="my" \ No newline at end of file diff --git a/9-regular-expressions/12-regexp-backreferences/article.md b/9-regular-expressions/12-regexp-backreferences/article.md new file mode 100644 index 000000000..b80fa85cf --- /dev/null +++ b/9-regular-expressions/12-regexp-backreferences/article.md @@ -0,0 +1,72 @@ +# Backreferences in pattern: \N and \k + +We can use the contents of capturing groups `pattern:(...)` not only in the result or in the replacement string, but also in the pattern itself. + +## Backreference by number: \N + +A group can be referenced in the pattern using `pattern:\N`, where `N` is the group number. + +To make clear why that's helpful, let's consider a task. + +We need to find quoted strings: either single-quoted `subject:'...'` or a double-quoted `subject:"..."` -- both variants should match. + +How to find them? + +We can put both kinds of quotes in the square brackets: `pattern:['"](.*?)['"]`, but it would find strings with mixed quotes, like `match:"...'` and `match:'..."`. That would lead to incorrect matches when one quote appears inside other ones, like in the string `subject:"She's the one!"`: + +```js run +let str = `He said: "She's the one!".`; + +let regexp = /['"](.*?)['"]/g; + +// The result is not what we'd like to have +alert( str.match(regexp) ); // "She' +``` + +As we can see, the pattern found an opening quote `match:"`, then the text is consumed till the other quote `match:'`, that closes the match. + +To make sure that the pattern looks for the closing quote exactly the same as the opening one, we can wrap it into a capturing group and backreference it: `pattern:(['"])(.*?)\1`. + +Here's the correct code: + +```js run +let str = `He said: "She's the one!".`; + +*!* +let regexp = /(['"])(.*?)\1/g; +*/!* + +alert( str.match(regexp) ); // "She's the one!" +``` + +Now it works! The regular expression engine finds the first quote `pattern:(['"])` and memorizes its content. That's the first capturing group. + +Further in the pattern `pattern:\1` means "find the same text as in the first group", exactly the same quote in our case. + +Similar to that, `pattern:\2` would mean the contents of the second group, `pattern:\3` - the 3rd group, and so on. + +```smart +If we use `?:` in the group, then we can't reference it. Groups that are excluded from capturing `(?:...)` are not memorized by the engine. +``` + +```warn header="Don't mess up: in the pattern `pattern:\1`, in the replacement: `pattern:$1`" +In the replacement string we use a dollar sign: `pattern:$1`, while in the pattern - a backslash `pattern:\1`. +``` + +## Backreference by name: `\k` + +If a regexp has many parentheses, it's convenient to give them names. + +To reference a named group we can use `pattern:\k`. + +In the example below the group with quotes is named `pattern:?`, so the backreference is `pattern:\k`: + +```js run +let str = `He said: "She's the one!".`; + +*!* +let regexp = /(?['"])(.*?)\k/g; +*/!* + +alert( str.match(regexp) ); // "She's the one!" +``` diff --git a/9-regular-expressions/13-regexp-alternation/01-find-programming-language/solution.md b/9-regular-expressions/13-regexp-alternation/01-find-programming-language/solution.md new file mode 100644 index 000000000..e33f9cf2f --- /dev/null +++ b/9-regular-expressions/13-regexp-alternation/01-find-programming-language/solution.md @@ -0,0 +1,33 @@ + +The first idea can be to list the languages with `|` in-between. + +But that doesn't work right: + +```js run +let regexp = /Java|JavaScript|PHP|C|C\+\+/g; + +let str = "Java, JavaScript, PHP, C, C++"; + +alert( str.match(regexp) ); // Java,Java,PHP,C,C +``` + +The regular expression engine looks for alternations one-by-one. That is: first it checks if we have `match:Java`, otherwise -- looks for `match:JavaScript` and so on. + +As a result, `match:JavaScript` can never be found, just because `match:Java` is checked first. + +The same with `match:C` and `match:C++`. + +There are two solutions for that problem: + +1. Change the order to check the longer match first: `pattern:JavaScript|Java|C\+\+|C|PHP`. +2. Merge variants with the same start: `pattern:Java(Script)?|C(\+\+)?|PHP`. + +In action: + +```js run +let regexp = /Java(Script)?|C(\+\+)?|PHP/g; + +let str = "Java, JavaScript, PHP, C, C++"; + +alert( str.match(regexp) ); // Java,JavaScript,PHP,C,C++ +``` diff --git a/9-regular-expressions/13-regexp-alternation/01-find-programming-language/task.md b/9-regular-expressions/13-regexp-alternation/01-find-programming-language/task.md new file mode 100644 index 000000000..e0f7af95c --- /dev/null +++ b/9-regular-expressions/13-regexp-alternation/01-find-programming-language/task.md @@ -0,0 +1,11 @@ +# Find programming languages + +There are many programming languages, for instance Java, JavaScript, PHP, C, C++. + +Create a regexp that finds them in the string `subject:Java JavaScript PHP C++ C`: + +```js +let regexp = /your regexp/g; + +alert("Java JavaScript PHP C++ C".match(regexp)); // Java JavaScript PHP C++ C +``` diff --git a/9-regular-expressions/13-regexp-alternation/02-find-matching-bbtags/solution.md b/9-regular-expressions/13-regexp-alternation/02-find-matching-bbtags/solution.md new file mode 100644 index 000000000..9b3fa1877 --- /dev/null +++ b/9-regular-expressions/13-regexp-alternation/02-find-matching-bbtags/solution.md @@ -0,0 +1,23 @@ + +Opening tag is `pattern:\[(b|url|quote)\]`. + +Then to find everything till the closing tag -- let's use the pattern `pattern:.*?` with flag `pattern:s` to match any character including the newline and then add a backreference to the closing tag. + +The full pattern: `pattern:\[(b|url|quote)\].*?\[/\1\]`. + +In action: + +```js run +let regexp = /\[(b|url|quote)\].*?\[\/\1\]/gs; + +let str = ` + [b]hello![/b] + [quote] + [url]http://google.com[/url] + [/quote] +`; + +alert( str.match(regexp) ); // [b]hello![/b],[quote][url]http://google.com[/url][/quote] +``` + +Please note that besides escaping `pattern:[` and `pattern:]`, we had to escape a slash for the closing tag `pattern:[\/\1]`, because normally the slash closes the pattern. diff --git a/9-regular-expressions/13-regexp-alternation/02-find-matching-bbtags/task.md b/9-regular-expressions/13-regexp-alternation/02-find-matching-bbtags/task.md new file mode 100644 index 000000000..72d715afd --- /dev/null +++ b/9-regular-expressions/13-regexp-alternation/02-find-matching-bbtags/task.md @@ -0,0 +1,48 @@ +# Find bbtag pairs + +A "bb-tag" looks like `[tag]...[/tag]`, where `tag` is one of: `b`, `url` or `quote`. + +For instance: +``` +[b]text[/b] +[url]http://google.com[/url] +``` + +BB-tags can be nested. But a tag can't be nested into itself, for instance: + +``` +Normal: +[url] [b]http://google.com[/b] [/url] +[quote] [b]text[/b] [/quote] + +Can't happen: +[b][b]text[/b][/b] +``` + +Tags can contain line breaks, that's normal: + +``` +[quote] + [b]text[/b] +[/quote] +``` + +Create a regexp to find all BB-tags with their contents. + +For instance: + +```js +let regexp = /your regexp/flags; + +let str = "..[url]http://google.com[/url].."; +alert( str.match(regexp) ); // [url]http://google.com[/url] +``` + +If tags are nested, then we need the outer tag (if we want we can continue the search in its content): + +```js +let regexp = /your regexp/flags; + +let str = "..[url][b]http://google.com[/b][/url].."; +alert( str.match(regexp) ); // [url][b]http://google.com[/b][/url] +``` diff --git a/9-regular-expressions/13-regexp-alternation/03-match-quoted-string/solution.md b/9-regular-expressions/13-regexp-alternation/03-match-quoted-string/solution.md new file mode 100644 index 000000000..5a007aee0 --- /dev/null +++ b/9-regular-expressions/13-regexp-alternation/03-match-quoted-string/solution.md @@ -0,0 +1,17 @@ +The solution: `pattern:/"(\\.|[^"\\])*"/g`. + +Step by step: + +- First we look for an opening quote `pattern:"` +- Then if we have a backslash `pattern:\\` (we technically have to double it in the pattern, because it is a special character, so that's a single backslash in fact), then any character is fine after it (a dot). +- Otherwise we take any character except a quote (that would mean the end of the string) and a backslash (to prevent lonely backslashes, the backslash is only used with some other symbol after it): `pattern:[^"\\]` +- ...And so on till the closing quote. + +In action: + +```js run +let regexp = /"(\\.|[^"\\])*"/g; +let str = ' .. "test me" .. "Say \\"Hello\\"!" .. "\\\\ \\"" .. '; + +alert( str.match(regexp) ); // "test me","Say \"Hello\"!","\\ \"" +``` diff --git a/9-regular-expressions/13-regexp-alternation/03-match-quoted-string/task.md b/9-regular-expressions/13-regexp-alternation/03-match-quoted-string/task.md new file mode 100644 index 000000000..ad41d91b1 --- /dev/null +++ b/9-regular-expressions/13-regexp-alternation/03-match-quoted-string/task.md @@ -0,0 +1,32 @@ +# Find quoted strings + +Create a regexp to find strings in double quotes `subject:"..."`. + +The strings should support escaping, the same way as JavaScript strings do. For instance, quotes can be inserted as `subject:\"` a newline as `subject:\n`, and the slash itself as `subject:\\`. + +```js +let str = "Just like \"here\"."; +``` + +Please note, in particular, that an escaped quote `subject:\"` does not end a string. + +So we should search from one quote to the other ignoring escaped quotes on the way. + +That's the essential part of the task, otherwise it would be trivial. + +Examples of strings to match: +```js +.. *!*"test me"*/!* .. +.. *!*"Say \"Hello\"!"*/!* ... (escaped quotes inside) +.. *!*"\\"*/!* .. (double slash inside) +.. *!*"\\ \""*/!* .. (double slash and an escaped quote inside) +``` + +In JavaScript we need to double the slashes to pass them right into the string, like this: + +```js run +let str = ' .. "test me" .. "Say \\"Hello\\"!" .. "\\\\ \\"" .. '; + +// the in-memory string +alert(str); // .. "test me" .. "Say \"Hello\"!" .. "\\ \"" .. +``` diff --git a/9-regular-expressions/13-regexp-alternation/04-match-exact-tag/solution.md b/9-regular-expressions/13-regexp-alternation/04-match-exact-tag/solution.md new file mode 100644 index 000000000..5d4ba8d96 --- /dev/null +++ b/9-regular-expressions/13-regexp-alternation/04-match-exact-tag/solution.md @@ -0,0 +1,16 @@ + +The pattern start is obvious: `pattern:`, because `match:` would match it. + +We need either a space after `match:`. + +In the regexp language: `pattern:|\s.*?>)`. + +In action: + +```js run +let regexp = /|\s.*?>)/g; + +alert( '