Feel.Runtime.Regex (feel v0.1.0)

Copy Markdown View Source

The regular expressions of matches, replace and split (DMN 1.6, section 10.3.4.3, footnote 2 of Table 75; D-80, was D-80).

The specification says that pattern, replacement and flags conform to clause 7.6 of XQuery 1.0 and XPath 2.0 Functions and Operators, and that where XPath gives an error FEEL gives null. That dialect is the regular expressions of XML Schema (appendix F of XSD part 2) with a few additions, and it is not the one of Erlang (PCRE): \s is four characters and not the Unicode spaces, . leaves out the carriage return, $ is only the end of the string, there are character classes with a subtraction ([A-Z-[OI]]) and blocks (\p{IsBasicLatin}), and there is no look-around, no \b, no inline flags and no possessive quantifier. So a pattern is read here, strictly, in the dialect of XPath, and written as a PCRE pattern that means the same; a pattern that is not in the dialect is :error, and the function is null. Nothing of the pattern reaches the engine as it was written.

The dialect

  • Atoms. A character (not one of . \ ? * + { } ( ) | [ ] ^ $), . (any character but a line feed or a carriage return, or any with the flag s), an escape, a class, or a group. The escapes are \n \r \t, a character that is special (\\ \| \. \- \^ \? \* \+ \{ \} \( \) \[ \]), \s \S \d \D \w \W (as XML Schema has them: \s is a space, a tab, a line feed or a carriage return), \i \I \c \C (the characters of XML names), \p{..} and \P{..} (a category such as L or Nd, or a block such as IsBasicLatin), and \1 to \9, which is the text of a group that has been closed.

  • Classes. [a-z0-9_], [^...], and [base-[subtracted]], the characters of the first that are not in the second. A - that is not the dash of a range or of a subtraction is a character only first in the class or last; anywhere else it has to be escaped ([a-c-e] is an error, as XML Schema has it).
  • Groups. ( ) captures and (?: ) does not. Any other (? is an error.
  • Quantifiers. ? * + {n} {n,} {n,m}, each with an optional ? after it for the shortest match.
  • Anchors. ^ and $ are the start and the end of the string; with the flag m, the start and the end of a line (a line ends at a line feed).
  • Flags. s (a dot is any character), m, i (a letter is its other case too, for the whole of Unicode: the Kelvin sign is a k) and x (the white space of the pattern is taken out first, but for what is in a class). Another character is an error.

A range whose end is before its start, a quantifier that has nothing to repeat, a back-reference to a group that is not closed, an unbalanced bracket or parenthesis, an escape that is none of the above, and a block that is not listed (the names of blocks/0, which are most of the scripts) are errors.

What is done with a pattern

compile/2 reads it. matches?/3 is true if some part of the input matches (the pattern is not anchored). replace/4 replaces every match, left to right, the replacement having $0, $1, ... for the text of a group (empty if the group did not take part or does not exist; the number takes as many digits as there are groups to hold it), \$ for a dollar and \\ for a backslash, and any other $ or \ an error. split/3 cuts the input at every match, and keeps the empty parts, those at the end included. replace and split need a pattern that does not match the empty string (a* does), which is an error as in XPath. The input is data and not a pattern.

The engine is told how long it may look (regex_match_limit of Feel.Limits); a match that takes more is {:error, :limit}. It has to be asked to say so (:report_errors): otherwise the engine answers that there is no match, which is a wrong answer and not an end of the run.

The size of the result

replace/5, split/4 and scan/4 take the most that their result may have (bytes for replace/5, items for the others) and answer {:error, {:over, size}} where it would be more, with the count at which it passed. The walk over the matches adds up what the result is as it goes and ends at the first match that makes it too long, and nothing is built that is not within the cap: a replacement of 100 KB for each of a million matches is 100 GB, and the limit of a finished string would look at it after it was made.

A pattern that is only text (,, ab, é: no character of the syntax, and no flag i or x) is searched for one match at a time with :binary.match/3, so the walk costs what it walks: a megabyte of a split by a ends at the 100,001st part, in 26 ms and a few megabytes, where the engine took 600 ms and 380 MB for the million.

Any other pattern is run by the engine one match at a time too, so the walk costs what it walks (D-215): the engine is asked for the first match from a byte offset of the whole text (the text is not cut, so ^, $ and the characters before the offset mean what they mean in the whole), and for the next from where that one ended, with an empty match stepped over as the engine's own :global run steps over it. The public :re.run/3 cannot do this cheaply: a run for each match checks the whole text for UTF-8 again every time (80 KB took 3.5 s, and 1 MB does not end). The function internal_run of :re (four arguments) is what the engine's own loop calls for each match after the first, and its last argument says that the text has been checked; the first call checks it, as a :global run does, and a text that is not UTF-8 raises as before. That function is not documented, and the loop that steps over an empty match is that of OTP 28 and later, so where a version of OTP does not have the function, or is before 28, the matches are listed whole as before (about 360 bytes of heap for a match, 360 MB for a megabyte of a and the pattern [a]), and the cap and the engine's limit are crossed in the list, not on the walk.

Summary

Types

t()

A pattern that is read: the program of the engine, the number of groups, and the text it is when it is only that text (nil if it is more).

Functions

The names of the blocks that a pattern may use after Is, sorted.

Reads a pattern and its flags (nil for none), or says that they are not in the dialect.

Whether some part of the input matches. {:error, :limit} if the engine gave up (limit is the regex_match_limit of the run).

Whether the pattern matches the empty string, which replace and split do not allow.

Replaces every match with the replacement, or :error if the replacement is not in the dialect (a $ not followed by a digit, a \\ not followed by a $ or a \\) or the pattern matches the empty string.

The text of every match, left to right: none is {:ok, []}. A pattern that matches the empty string is :error, as for replace/5 and split/4: it would match everywhere.

Cuts the input at every match, keeping the empty parts. The empty string is no parts.

Types

t()

@type t() :: %Feel.Runtime.Regex{
  groups: non_neg_integer(),
  literal: String.t() | nil,
  program: :re.mp()
}

A pattern that is read: the program of the engine, the number of groups, and the text it is when it is only that text (nil if it is more).

Functions

blocks()

@spec blocks() :: [String.t()]

The names of the blocks that a pattern may use after Is, sorted.

compile(pattern, flags)

@spec compile(String.t(), String.t() | nil) :: {:ok, t()} | :error

Reads a pattern and its flags (nil for none), or says that they are not in the dialect.

iex> {:ok, regex} = Feel.Runtime.Regex.compile("a(b)c", nil)
iex> regex.groups
1
iex> Feel.Runtime.Regex.compile("a(", nil)
:error
iex> Feel.Runtime.Regex.compile("a", "q")
:error

matches?(regex, input, limit)

@spec matches?(t(), String.t(), non_neg_integer() | :infinity) ::
  {:ok, boolean()} | {:error, :limit}

Whether some part of the input matches. {:error, :limit} if the engine gave up (limit is the regex_match_limit of the run).

iex> {:ok, regex} = Feel.Runtime.Regex.compile("^fo*b", nil)
iex> Feel.Runtime.Regex.matches?(regex, "foobar", :infinity)
{:ok, true}
iex> Feel.Runtime.Regex.matches?(regex, "a foobar", :infinity)
{:ok, false}

matches_empty?(regex)

@spec matches_empty?(t()) :: boolean()

Whether the pattern matches the empty string, which replace and split do not allow.

iex> {:ok, regex} = Feel.Runtime.Regex.compile("a*", nil)
iex> Feel.Runtime.Regex.matches_empty?(regex)
true

replace(regex, input, replacement, limit, max \\ :infinity)

@spec replace(
  t(),
  String.t(),
  String.t(),
  non_neg_integer() | :infinity,
  non_neg_integer() | :infinity
) ::
  {:ok, String.t()}
  | :error
  | {:error, :limit}
  | {:error, {:over, pos_integer()}}

Replaces every match with the replacement, or :error if the replacement is not in the dialect (a $ not followed by a digit, a \\ not followed by a $ or a \\) or the pattern matches the empty string.

max is the most bytes the text may have (:infinity for no cap): a result that would be longer is {:error, {:over, size}}, where size is the count of bytes when it passed max, and is not made.

iex> {:ok, regex} = Feel.Runtime.Regex.compile("(ab)|(a)", nil)
iex> Feel.Runtime.Regex.replace(regex, "abcd", "[1=$1][2=$2]", :infinity)
{:ok, "[1=ab][2=]cd"}
iex> Feel.Runtime.Regex.replace(regex, "abcd", "$", :infinity)
:error
iex> Feel.Runtime.Regex.replace(regex, "abcd", "xyz", :infinity, 4)
{:error, {:over, 5}}

scan(regex, input, limit, max \\ :infinity)

@spec scan(
  t(),
  String.t(),
  non_neg_integer() | :infinity,
  non_neg_integer() | :infinity
) ::
  {:ok, [String.t()]}
  | :error
  | {:error, :limit}
  | {:error, {:over, pos_integer()}}

The text of every match, left to right: none is {:ok, []}. A pattern that matches the empty string is :error, as for replace/5 and split/4: it would match everywhere.

max is the most matches there may be (:infinity for no cap): more are {:error, {:over, count}}, and are not made.

iex> {:ok, regex} = Feel.Runtime.Regex.compile("12[0-9]*", nil)
iex> Feel.Runtime.Regex.scan(regex, "references are 1234, 1256, 1378", :infinity)
{:ok, ["1234", "1256"]}
iex> Feel.Runtime.Regex.scan(regex, "nothing", :infinity)
{:ok, []}
iex> Feel.Runtime.Regex.scan(regex, "references are 1234, 1256, 1378", :infinity, 1)
{:error, {:over, 2}}
iex> {:ok, empty} = Feel.Runtime.Regex.compile("a*", nil)
iex> Feel.Runtime.Regex.scan(empty, "baaa", :infinity)
:error

split(regex, input, limit, max \\ :infinity)

@spec split(
  t(),
  String.t(),
  non_neg_integer() | :infinity,
  non_neg_integer() | :infinity
) ::
  {:ok, [String.t()]}
  | :error
  | {:error, :limit}
  | {:error, {:over, pos_integer()}}

Cuts the input at every match, keeping the empty parts. The empty string is no parts.

max is the most parts there may be (:infinity for no cap): more are {:error, {:over, count}}, and are not made.

iex> {:ok, regex} = Feel.Runtime.Regex.compile(";", nil)
iex> Feel.Runtime.Regex.split(regex, "a;b;c;;", :infinity)
{:ok, ["a", "b", "c", "", ""]}
iex> Feel.Runtime.Regex.split(regex, "", :infinity)
{:ok, []}
iex> Feel.Runtime.Regex.split(regex, "a;b;c;;", :infinity, 4)
{:error, {:over, 5}}