Packages
expression
2.5.4
2.49.1
2.49.0
2.48.0
2.47.5
2.47.4
2.47.3
2.47.2
2.47.1
2.47.0
2.46.0
2.45.1
2.45.0
2.44.1
2.44.0
2.43.0
2.42.1
2.41.4
2.41.3
2.41.2
2.41.0
2.40.0
2.38.0
2.37.0
2.36.2
2.36.1
2.36.0
2.35.0
2.34.0
2.33.1
2.33.0
2.32.1
2.31.6
2.31.3
2.31.0
2.30.0
2.28.0
2.27.3
2.27.2
2.27.1
2.27.0
2.26.1
2.26.0
2.25.0
2.24.2
2.24.1
2.24.0
2.23.9
2.23.8
2.23.7
2.23.6
2.23.4
2.23.3
2.23.2
2.23.1
2.23.0
2.22.2
2.22.0
2.21.0
2.20.0
2.19.0
2.18.0
2.17.0
2.16.0
2.14.0
2.13.0
2.12.0
2.11.0
2.10.1
2.10.0
2.9.0
2.8.0
2.6.0
2.5.8
2.5.7
2.5.6
2.5.5
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.4.1
2.4.0
2.3.2
2.3.1
2.3.0
2.2.1
2.2.0
2.1.1
2.1.0
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.1.1
1.1.0
1.0.0
0.7.2
0.7.1
0.7.0
0.6.0
0.5.0
0.4.3
0.4.2
0.4.1
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.1.0
A Excel like expression parser, compatible with FLOIP Expression language.
Current section
Files
Jump to
Current section
Files
lib/expression/parser.ex
defmodule Expression.Parser do
@moduledoc """
Expression.Parser is responsible for accepting a string
containing an expression and returning the abstract syntax
tree (AST) representing the expression.
The AST generated by this module can be evaluated by
Expression.Eval
# Example
iex(1)> Expression.Parser.parse("hello @world")
{:ok, [text: "hello ", expression: [atom: "world"]], "", %{}, {1, 0}, 12}
"""
import NimbleParsec
import Expression.BooleanHelpers
import Expression.DateHelpers
import Expression.LiteralHelpers
import Expression.OperatorHelpers
# literal = 1, 2.1, "three", 'four', true, false, ISO dates
defparsec(
:literal,
choice([
datetime(),
decimal(),
int(),
boolean(),
single_quoted_string(),
double_quoted_string()
])
|> unwrap_and_tag(:literal)
)
# atom = atom
atom =
ascii_string([?a..?z, ?A..?Z, ?0..?9], min: 1)
|> ascii_string([?a..?z, ?A..?Z, ?0..?9, ?_, ?-], min: 0)
|> map({String, :downcase, []})
|> reduce({Enum, :join, []})
range =
int()
|> ignore(string(".."))
|> concat(int())
|> optional(
ignore(string("//"))
|> concat(int())
)
|> tag(:range)
whitespace = choice([string(" "), string("\n"), string("\r")])
ignore_surrounding_whitespace = fn p ->
ignore(repeat(whitespace))
|> concat(p)
|> ignore(repeat(whitespace))
end
# argument separator = ", "
argument_separator =
string(",")
|> ignore_surrounding_whitespace.()
lambda_capture =
ignore(string("&"))
|> concat(int())
|> unwrap_and_tag(:capture)
# arguments = expression "," expression
defparsec(
:arguments,
parsec(:aexpr)
|> optional(ignore(argument_separator) |> parsec(:arguments))
)
primitives =
choice([
lambda_capture,
range,
parsec(:lambda),
parsec(:function),
parsec(:literal),
parsec(:variable),
parsec(:list)
])
defparsec(
:aexpr_factor,
choice([
primitives,
ignore(string("(")) |> parsec(:aexpr) |> ignore(string(")"))
])
|> ignore_surrounding_whitespace.()
)
defparsec(
:key,
ignore(ascii_char([91]))
|> replace(:key)
|> parsec(:aexpr)
|> ignore(ascii_char([93]))
|> label("[..]")
)
defparsec(
:attribute,
ascii_char([?.])
|> replace(:attribute)
|> label(".")
)
attribute_or_key =
repeat(
choice([
parsec(:attribute) |> parsec(:aexpr_factor),
parsec(:key)
])
)
# The difference between this one and the one above
# is that this one does not allow for spaces or arithmatic
# which makes it suitable for use in `@foo` type expressions
# because otherwise `info@support.com for` (note the space)
# is parsed as being part of the expression.
#
# That would be wrong since spaces are only allowed in
# expressions starting with brackets like `@( ... )`
attribute_or_key_with_primitives_only =
repeat(
choice([
parsec(:attribute) |> concat(primitives),
parsec(:key)
])
)
defparsec(
:aexpr_exponent,
parsec(:aexpr_factor)
|> optional(attribute_or_key)
|> repeat(
exponent()
|> parsec(:aexpr_factor)
|> optional(attribute_or_key)
)
|> reduce(:fold_infixl)
)
defparsec(
:aexpr_term,
parsec(:aexpr_exponent)
|> repeat(
choice([times(), divide()])
|> ignore_surrounding_whitespace.()
|> parsec(:aexpr_exponent)
)
|> reduce(:fold_infixl)
)
defparsec(
:aexpr,
parsec(:aexpr_term)
|> repeat(
choice([
plus(),
minus(),
concatenate(),
gte(),
lte(),
neq(),
gt(),
lt(),
eq()
])
|> parsec(:aexpr_term)
|> ignore_surrounding_whitespace.()
)
|> reduce(:fold_infixl)
)
def fold_infixl(acc) do
acc
|> Enum.reverse()
|> Enum.chunk_every(2)
|> List.foldr([], fn
[l], [] -> l
[r, op], l -> {op, [l, r]}
end)
end
defparsec(
:list,
ignore(string("["))
|> optional(parsec(:arguments) |> tag(:args))
|> ignore(string("]"))
|> tag(:list)
)
# function = "(" arguments ")"
defparsec(
:function,
atom
|> unwrap_and_tag(:name)
|> ignore(string("("))
|> optional(parsec(:arguments) |> tag(:args))
|> ignore(string(")"))
|> tag(:function)
)
defparsec(
:lambda,
ignore(string("&"))
|> optional(parsec(:arguments) |> tag(:args))
|> tag(:lambda)
)
# variable = atom
defparsec(
:variable,
atom
|> unwrap_and_tag(:atom)
)
expression_block =
ignore(string("@"))
|> lookahead_not(string("@"))
|> ignore(string("("))
|> parsec(:aexpr)
|> ignore(string(")"))
|> tag(:expression)
expression =
ignore(string("@"))
|> lookahead_not(string("@"))
|> repeat(
choice([
parsec(:list),
parsec(:function),
parsec(:variable)
])
|> optional(attribute_or_key_with_primitives_only)
)
|> reduce(:fold_infixl)
|> tag(:expression)
escaped_at =
ignore(string("@"))
|> string("@")
|> unwrap_and_tag(:text)
text =
empty()
|> lookahead_not(string("@"))
|> utf8_string([], 1)
|> times(min: 1)
|> reduce({Enum, :join, []})
|> unwrap_and_tag(:text)
defparsec(:parse, repeat(choice([expression_block, expression, escaped_at, text])))
end