# Elex
[](https://github.com/bandesz/elex/actions/workflows/ci.yml)
Elex is a powerful expression language library for Elixir that provides parsing, validation, and evaluation of mathematical and logical expressions.
## Documentation
Full guides are available on [hexdocs.pm](https://hexdocs.pm/elex):
- [Getting Started](https://hexdocs.pm/elex/getting-started.html)
- [Expression Language](https://hexdocs.pm/elex/expression-language.html)
- [Functions](https://hexdocs.pm/elex/functions.html)
- [Ash Integration](https://hexdocs.pm/elex/ash-integration.html)
- [Advanced Topics](https://hexdocs.pm/elex/advanced.html)
## Features
- **Arithmetic Operations**: `+`, `-`, `*`, `/`, `%` (modulo), unary `-`
- **Comparison Operators**: `<`, `>`, `<=`, `>=`, `==`, `!=` (numbers and strings)
- **Logical Operations**: `and`, `or`, `not` (with short-circuit evaluation)
- **Literals**: Decimal numbers, booleans (`true`/`false`, `yes`/`no`), strings, and `null`
- **Variables**: Dynamic variable substitution
- **Functions**: Built-in math, string, and utility functions (see [Functions](#functions)) and custom functions via `Elex.Function`
- **Type System**: Static type checking and validation
- **Decimal Precision**: Uses `Decimal` for accurate arithmetic
- **Expression Inversion**: Solve for variables in simple expressions
- **Ash Integration**: Optional Ash validation for resource attributes
## Installation
Add `elex` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:elex, "~> 0.2.0"}
]
end
```
## Quick Start
```elixir
# Create a context with variables
context = Elex.new_context()
|> Elex.add_variable("price", 100)
|> Elex.add_variable("tax_rate", 0.08)
# Evaluate an expression
{:ok, result} = Elex.evaluate("price * (1 + tax_rate)", context)
# result => Decimal.new("108")
# Validate expression type
{:ok, :decimal} = Elex.validate("price + 10", context)
{:ok, :boolean} = Elex.validate("price > 50", context)
# Extract variables from an expression
{:ok, ["price", "quantity"]} = Elex.extract_variables("price * quantity")
```
## Expression Syntax
### Literals
```elixir
# Numbers (decimal)
Elex.evaluate("42", Elex.new_context())
Elex.evaluate("3.14", Elex.new_context())
Elex.evaluate("-5.5", Elex.new_context())
# Booleans
Elex.evaluate("true", Elex.new_context())
Elex.evaluate("false", Elex.new_context())
Elex.evaluate("yes", Elex.new_context()) # alias for true
Elex.evaluate("no", Elex.new_context()) # alias for false
# Strings
Elex.evaluate("\"hello\"", Elex.new_context())
# Null
Elex.evaluate("null", Elex.new_context())
```
`null` compares equal only to `null` or `nil` variables (`null == null`, `x == null` when `x` is nil). It cannot be compared to numbers, booleans, or strings.
### Variables
Variable names must start with a lowercase letter and may contain letters, digits, and underscores. The words `and`, `or`, `not`, `null`, `true`, `false`, `yes`, and `no` are reserved and cannot be used as variable names.
```elixir
context =
Elex.new_context()
|> Elex.add_variable("price", 100)
|> Elex.add_variable("tax_rate", 0.08)
{:ok, result} = Elex.evaluate("price * (1 + tax_rate)", context)
# result => #Decimal<108>
```
### Arithmetic
```elixir
context = Elex.new_context()
{:ok, result} = Elex.evaluate("10 + 5", context) # => #Decimal<15>
{:ok, result} = Elex.evaluate("10 - 5", context) # => #Decimal<5>
{:ok, result} = Elex.evaluate("10 * 5", context) # => #Decimal<50>
{:ok, result} = Elex.evaluate("10 / 5", context) # => #Decimal<2>
{:ok, result} = Elex.evaluate("10 % 3", context) # => #Decimal<1>
{:ok, result} = Elex.evaluate("2 + 3 * 4", context) # => #Decimal<14> (respects precedence)
{:ok, result} = Elex.evaluate("-5", context) # => #Decimal<-5> (unary minus)
{:ok, result} = Elex.evaluate("-(1 + 2)", context) # => #Decimal<-3>
```
> **Note:** `Elex.evaluate/2` returns `{:ok, result}` on success or `{:error, reason}` on failure. Arithmetic operations use `Decimal` and return `Decimal` values. The `%` operator has the same precedence as `*` and `/`.
### Comparisons
```elixir
{:ok, true} = Elex.evaluate("10 > 5", Elex.new_context())
{:ok, false} = Elex.evaluate("10 < 5", Elex.new_context())
{:ok, true} = Elex.evaluate("10 >= 10", Elex.new_context())
{:ok, false} = Elex.evaluate("10 <= 5", Elex.new_context())
{:ok, true} = Elex.evaluate("10 == 10", Elex.new_context())
{:ok, true} = Elex.evaluate("10 != 5", Elex.new_context())
# String ordering (lexicographic)
{:ok, true} = Elex.evaluate(~s["a" < "b"], Elex.new_context())
{:ok, true} = Elex.evaluate(~s["b" >= "a"], Elex.new_context())
```
Comparison operands must have the same type (decimal, boolean, string, or null).
### Logical Operations
```elixir
{:ok, false} = Elex.evaluate("true and false", Elex.new_context())
{:ok, true} = Elex.evaluate("true or false", Elex.new_context())
{:ok, false} = Elex.evaluate("not true", Elex.new_context())
```
`and`, `or`, and `if(condition, a, b)` use short-circuit evaluation: the right-hand operand (or unselected branch) is not evaluated when its result cannot change the outcome. This avoids errors such as division by zero in guard expressions:
```elixir
{:ok, false} = Elex.evaluate("false and (1 / 0 > 0)", Elex.new_context())
{:ok, true} = Elex.evaluate("true or (1 / 0 > 0)", Elex.new_context())
{:ok, result} = Elex.evaluate("if(false, 1 / 0, 2)", Elex.new_context()) # => #Decimal<2>
```
### Functions
#### Math
| Function | Description |
|----------|-------------|
| `abs(x)` | Absolute value |
| `ceil(x)`, `floor(x)`, `round(x)` | Rounding |
| `sqrt(x)` | Square root |
| `pow(base, exp)` | Exponentiation |
| `rem(a, b)` | Remainder (sign follows the dividend; same as `%`) |
| `mod(a, b)` | Floored modulo (sign follows the divisor) |
| `max(a, b, …)`, `min(a, b, …)` | Largest or smallest of two or more numbers (variadic) |
| `clamp(x, min, max)` | Clamp `x` to an inclusive range |
| `between(x, low, high)` | `true` when `x` is in the inclusive range |
| `pi()` | Mathematical constant π |
| `if(cond, a, b)` | Conditional (short-circuits; branches must share a type) |
```elixir
context = Elex.new_context()
{:ok, result} = Elex.evaluate("max(10, 20)", context) # => #Decimal<20>
{:ok, result} = Elex.evaluate("max(3, 7, 9)", context) # => #Decimal<9>
{:ok, result} = Elex.evaluate("min(10, 20)", context) # => #Decimal<10>
{:ok, result} = Elex.evaluate("abs(-5)", context) # => #Decimal<5>
{:ok, result} = Elex.evaluate("pow(2, 3)", context) # => #Decimal<8>
{:ok, result} = Elex.evaluate("mod(10, 3)", context) # => #Decimal<1>
{:ok, result} = Elex.evaluate("clamp(15, 0, 10)", context) # => #Decimal<10>
{:ok, result} = Elex.evaluate("between(5, 0, 10)", context) # => true
{:ok, result} = Elex.evaluate("ceil(3.2)", context) # => #Decimal<4>
{:ok, result} = Elex.evaluate("floor(3.8)", context) # => #Decimal<3>
{:ok, result} = Elex.evaluate("round(3.5)", context) # => #Decimal<4>
{:ok, result} = Elex.evaluate("sqrt(16)", context) # => #Decimal<4>
{:ok, result} = Elex.evaluate("rem(10, 3)", context) # => #Decimal<1>
{:ok, result} = Elex.evaluate("pi()", context) # => #Decimal<3.14159…>
{:ok, result} = Elex.evaluate("if(10 > 5, 1, 0)", context) # => #Decimal<1>
```
#### Strings
| Function | Description |
|----------|-------------|
| `concat(a, b)` | Concatenate two strings |
| `length(s)` | String length (character count) |
| `contains(haystack, needle)` | Substring search |
| `starts_with(s, prefix)`, `ends_with(s, suffix)` | Prefix/suffix test |
| `lower(s)`, `upper(s)`, `trim(s)` | Case and whitespace transforms |
| `coalesce(a, b, …)` | First non-null argument (variadic; short-circuits) |
```elixir
context = Elex.new_context()
{:ok, result} = Elex.evaluate(~s[concat("hello", " world")], context) # => "hello world"
{:ok, result} = Elex.evaluate(~s[length("abc")], context) # => #Decimal<3>
{:ok, result} = Elex.evaluate(~s[contains("hello", "ell")], context) # => true
{:ok, result} = Elex.evaluate(~s[starts_with("hello", "he")], context) # => true
{:ok, result} = Elex.evaluate(~s[ends_with("hello", "lo")], context) # => true
{:ok, result} = Elex.evaluate(~s[lower("ABC")], context) # => "abc"
{:ok, result} = Elex.evaluate(~s[upper("abc")], context) # => "ABC"
{:ok, result} = Elex.evaluate(~s[trim(" x ")], context) # => "x"
{:ok, result} = Elex.evaluate("coalesce(null, 5)", context) # => #Decimal<5>
```
## Ash Integration
Elex provides an optional Ash validation for validating expressions in resource attributes:
```elixir
defmodule MyApp.Resource do
use Ash.Resource
attributes do
attribute :formula, :string do
allow_nil? false
end
end
validations do
validate Elex.AshValidation,
attribute: :formula,
context: Elex.new_context(),
expected_type: :decimal
end
end
```
The `expected_type` option accepts `:decimal`, `:boolean`, or `:string`. Use `add_value_type_from_attribute` to inject a `value` variable typed from another attribute — useful when validating formulas that reference the current value.
## Expression Inversion
Elex can invert simple arithmetic expressions to solve for a variable:
```elixir
alias Elex.{Parser, Inverter}
context = Elex.new_context()
{:ok, ast, _type} = Parser.parse("value * 2 + 5", context, validate: false)
{:ok, inverted_ast} = Inverter.invert(ast, "value")
# The inverted expression solves for "value":
# value = (result - 5) / 2
```
## Custom Functions
Implement the `Elex.Function` behaviour and register your module with `Elex.Context.add_function/2`:
```elixir
defmodule MyApp.Functions.Double do
@behaviour Elex.Function
@impl true
def signature, do: %{name: :double, arity: 1}
@impl true
def validate([arg], ctx), do: Elex.Validator.validate(arg, ctx)
@impl true
def call([arg]), do: {:ok, Decimal.mult(arg, Decimal.new(2))}
@impl true
def documentation, do: %{signature: "double(x)", description: "doubles a number"}
end
context =
Elex.new_context()
|> Elex.Context.add_function(MyApp.Functions.Double)
{:ok, result} = Elex.evaluate("double(5)", context)
```
## Development
### Setup
```bash
mix deps.get
mix test
```
### Git Hooks
To enable the pre-commit hook that runs quality checks before each commit:
```bash
git config core.hooksPath .git-hooks
```
The pre-commit hook runs:
- Code compilation with warnings as errors
- Code formatting
- Credo linting
- Sobelow security checks
- Dependency audit
- All tests
You can also run these checks manually:
```bash
mix precommit
```
### Code Quality
```bash
mix check # Run all quality checks
mix format # Format code
mix credo --strict # Run Credo linter
mix dialyzer # Run Dialyzer type checker
mix sobelow # Run security analysis
```
## License
Copyright (c) 2025 bandesz
See [LICENSE](LICENSE) for details.