Packages
expression
0.3.2
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/callbacks.ex
defmodule Expression.Callbacks do
@moduledoc """
The function callbacks for the standard function set available
in FLOIP expressions.
This should be relatively swappable with another implementation.
The only requirement is the `handle/3` function.
FLOIP functions are case insensitive. All functions in this callback
module are implemented as lowercase names.
Some functions accept a variable amount of arguments. Elixir doesn't
support variable arguments in functions.
If a function accepts a variable number of arguments the convention
is to call the `<function_name>_vargs/2` callback where the context
is given as the first argument and the argument list as a second
argument.
Reserved names such as `and`, `if`, and `or` are suffixed with an
underscore.
"""
@reserved_words ~w[and if or]
@punctuation_pattern ~r/\s*[,:;!?.-]\s*|\s/
@doc """
Convert a string function name into an atom meant to handle
that function
Reserved words such as `and`, `if`, and `or` are automatically suffixed
with an `_` underscore.
"""
def atom_function_name(function_name) when function_name in @reserved_words,
do: atom_function_name("#{function_name}_")
def atom_function_name(function_name) do
String.to_atom(function_name)
end
@doc """
Handle a function call while evaluating the AST.
Handlers in this module are either:
1. The function name as is
2. The function name with an underscore suffix if the function name is a reserved word
3. The function name suffixed with `_vargs` if the takes a variable set of arguments
"""
@spec handle(function_name :: binary, arguments :: [any], context :: map) ::
{:ok, any} | {:error, :not_implemented}
def handle(function_name, arguments, context) do
exact_function_name = atom_function_name(function_name)
vargs_function_name = atom_function_name("#{function_name}_vargs")
cond do
# Check if the exact function signature has been implemented
function_exported?(__MODULE__, exact_function_name, length(arguments) + 1) ->
{:ok, apply(__MODULE__, exact_function_name, [context] ++ arguments)}
# Check if it's been implemented to accept a variable amount of arguments
function_exported?(__MODULE__, vargs_function_name, 2) ->
{:ok, apply(__MODULE__, vargs_function_name, [context, arguments])}
# Otherwise fail
true ->
{:error, "#{function_name} is not implemented."}
end
end
@doc """
Defines a new date value
```
This is a date @DATE(2012, 12, 25)
```
# Example
iex> to_string(Expression.Callbacks.date(%{}, 2012, 12, 25))
"2012-12-25 00:00:00Z"
"""
def date(_ctx, year, month, day) do
fields = [
calendar: Calendar.ISO,
year: year,
month: month,
day: day,
hour: 0,
minute: 0,
second: 0,
time_zone: "Etc/UTC",
zone_abbr: "UTC",
utc_offset: 0,
std_offset: 0
]
struct(DateTime, fields)
end
@doc """
Converts date stored in text to an actual date,
using `strftime` formatting.
It will fallback to "%Y-%m-%d %H:%M:%S" if no formatting is supplied
```
You joined on @DATEVALUE(contact.joined_date, "%Y-%m%-d")
```
# Example
iex> date = Expression.Callbacks.date(%{}, 2020, 12, 20)
iex> Expression.Callbacks.datevalue(%{}, date)
"2020-12-20 00:00:00"
iex> Expression.Callbacks.datevalue(%{}, date, "%Y-%m-%d")
"2020-12-20"
"""
def datevalue(ctx, date, format \\ "%Y-%m-%d %H:%M:%S")
def datevalue(_ctx, date, format) do
Timex.format!(date, format, :strftime)
end
@doc """
Returns only the day of the month of a date (1 to 31)
```
The current day is @DAY(contact.joined_date)
```
# Example
iex> now = DateTime.utc_now()
iex> day = Expression.Callbacks.day(%{}, now)
iex> day == now.day
true
"""
def day(_ctx, %{day: day} = _date) do
day
end
@doc """
Moves a date by the given number of months
```
Next month's meeting will be on @EDATE(date.today, 1)
```
# Example
iex> now = DateTime.utc_now()
iex> future = Timex.shift(now, months: 1)
iex> date = Expression.Callbacks.edate(%{}, now, 1)
iex> future == date
true
"""
def edate(_ctx, date, months) do
date |> Timex.shift(months: months)
end
@doc """
Returns only the hour of a datetime (0 to 23)
```
The current hour is @HOUR(NOW())
```
# Example
iex> now = DateTime.utc_now()
iex> hour = Expression.Callbacks.hour(%{}, now)
iex> now.hour == hour
true
"""
def hour(_ctx, %{hour: hour} = _date) do
hour
end
@doc """
Returns only the minute of a datetime (0 to 59)
```
The current minute is @MINUTE(NOW())
```
# Example
iex> now = DateTime.utc_now()
iex> minute = Expression.Callbacks.minute(%{}, now)
iex> now.minute == minute
true
"""
def minute(_ctx, %{minute: minute} = _date) do
minute
end
@doc """
Returns only the month of a date (1 to 12)
```
The current month is @MONTH(NOW())
```
# Example
iex> now = DateTime.utc_now()
iex> month = Expression.Callbacks.month(%{}, now)
iex> now.month == month
true
"""
def month(_ctx, %{month: month} = _date) do
month
end
@doc """
Returns the current date time as UTC
```
It is currently @NOW()
```
# Example
iex> DateTime.utc_now() == Expression.Callbacks.now(%{})
"""
def now(_ctx) do
DateTime.utc_now()
end
@doc """
Returns only the second of a datetime (0 to 59)
```
The current second is @SECOND(NOW())
```
# Example
iex> now = DateTime.utc_now()
iex> second = Expression.Callbacks.second(%{}, now)
iex> now.second == second
true
"""
def second(_ctx, %{second: second} = _date) do
second
end
@doc """
Defines a time value which can be used for time arithmetic
```
2 hours and 30 minutes from now is @(date.now + TIME(2, 30, 0))
```
# Example
iex> Expression.Callbacks.time(%{}, 12, 13, 14)
%Time{hour: 12, minute: 13, second: 14}
"""
def time(_ctx, hours, minutes, seconds) do
%Time{hour: hours, minute: minutes, second: seconds}
end
@doc """
Converts time stored in text to an actual time
```
Your appointment is at @(date.today + TIME("2:30"))
```
# Example
iex> Expression.Callbacks.timevalue(%{}, "2:30")
%Time{hour: 2, minute: 30, second: 0}
iex> Expression.Callbacks.timevalue(%{}, "2:30:55")
%Time{hour: 2, minute: 30, second: 55}
"""
def timevalue(_ctx, expression) do
parts =
expression
|> String.split(":")
|> Enum.map(&String.to_integer/1)
defaults = [
hour: 0,
minute: 0,
second: 0
]
fields =
[:hour, :minute, :second]
|> Enum.zip(parts)
struct(Time, Keyword.merge(defaults, fields))
end
@doc """
Returns the current date
```
Today's date is @TODAY()
```
# Example
iex> today = Date.utc_today()
iex> today == Expression.Callbacks.today(%{})
true
"""
def today(_ctx) do
Date.utc_today()
end
@doc """
Returns the day of the week of a date (1 for Sunday to 7 for Saturday)
```
Today is day no. @WEEKDAY(TODAY()) in the week
```
# Example
iex> today = DateTime.utc_now()
iex> expected = Timex.weekday(today)
iex> weekday = Expression.Callbacks.weekday(%{}, today)
iex> weekday == expected
true
"""
def weekday(_ctx, date) do
Timex.weekday(date)
end
@doc """
Returns only the year of a date
```
The current year is @YEAR(NOW())
```
# Example
iex> %{year: year} = now = DateTime.utc_now()
iex> year == Expression.Callbacks.year(%{}, now)
"""
def year(_ctx, %{year: year} = _date) do
year
end
@doc """
Returns TRUE if and only if all its arguments evaluate to TRUE
```
@AND(contact.gender = "F", contact.age >= 18)
```
# Example
iex> Expression.Callbacks.handle("and", [true, true], %{})
{:ok, true}
iex> Expression.Callbacks.and_vargs(%{}, [true, true])
true
iex> Expression.Callbacks.and_vargs(%{}, [true, false])
false
iex> Expression.Callbacks.and_vargs(%{}, [false, false])
false
"""
def and_vargs(_ctx, arguments) do
Enum.all?(arguments, fn
true -> true
_other -> false
end)
end
@doc """
Returns one value if the condition evaluates to TRUE, and another value if it evaluates to FALSE
```
Dear @IF(contact.gender = "M", "Sir", "Madam")
```
# Example
iex> Expression.Callbacks.handle("if", [true, "Yes", "No"], %{})
{:ok, "Yes"}
iex> Expression.Callbacks.handle("if", [false, "Yes", "No"], %{})
{:ok, "No"}
"""
def if_(_ctx, condition, yes, no) do
if(condition, do: yes, else: no)
end
@doc """
Returns TRUE if any argument is TRUE
```
@OR(contact.state = "GA", contact.state = "WA", contact.state = "IN")
```
# Example
iex> Expression.Callbacks.handle("or", [true, false], %{})
{:ok, true}
iex> Expression.Callbacks.handle("or", [true, true], %{})
{:ok, true}
iex> Expression.Callbacks.handle("or", [false, false], %{})
{:ok, false}
"""
def or_vargs(_ctx, arguments) do
Enum.any?(arguments, fn
true -> true
_anything_else -> false
end)
end
@doc """
Returns the absolute value of a number
```
The absolute value of -1 is @ABS(-1)
```
# Example
iex> Expression.Callbacks.abs(%{}, -1)
1
"""
def abs(_ctx, number) do
abs(number)
end
@doc """
Returns the maximum value of all arguments
```
Please complete at most @MAX(flow.questions, 10) questions
```
# Example
iex> Expression.Callbacks.handle("max", [1, 2, 3], %{})
{:ok, 3}
"""
def max_vargs(_ctx, arguments) do
Enum.max(arguments)
end
@doc """
Returns the minimum value of all arguments
```
Please complete at least @MIN(flow.questions, 10) questions
```
# Example
iex> Expression.Callbacks.handle("min", [1, 2, 3], %{})
{:ok, 1}
"""
def min_vargs(_ctx, arguments) do
Enum.min(arguments)
end
@doc """
Returns the result of a number raised to a power - equivalent to the ^ operator
```
2 to the power of 3 is @POWER(2, 3)
```
"""
def power(_ctx, a, b) do
:math.pow(a, b)
end
@doc """
Returns the sum of all arguments, equivalent to the + operator
```
You have @SUM(contact.reports, contact.forms) reports and forms
```
# Example
iex> Expression.Callbacks.handle("sum", [1, 2, 3], %{})
{:ok, 6}
"""
def sum_vargs(_ctx, arguments) do
Enum.sum(arguments)
end
@doc """
Returns the character specified by a number
```
As easy as @CHAR(65), @CHAR(66), @CHAR(67)
```
# Example
iex> Expression.Callbacks.char(%{}, 65)
"A"
"""
def char(_ctx, code) do
<<code>>
end
@doc """
Removes all non-printable characters from a text string
```
You entered @CLEAN(step.value)
```
# Example
iex> Expression.Callbacks.clean(%{}, <<65, 0, 66, 0, 67>>)
"ABC"
"""
def clean(_ctx, binary) do
binary
|> String.graphemes()
|> Enum.filter(&String.printable?/1)
|> Enum.join("")
end
@doc """
Returns a numeric code for the first character in a text string
```
The numeric code of A is @CODE("A")
```
# Example
iex> Expression.Callbacks.code(%{}, "A")
65
"""
def code(_ctx, <<code>>) do
code
end
@doc """
Joins text strings into one text string
```
Your name is @CONCATENATE(contact.first_name, " ", contact.last_name)
```
# Example
iex> Expression.Callbacks.handle("concatenate", ["name", " ", "surname"], %{})
{:ok, "name surname"}
"""
def concatenate_vargs(_ctx, arguments) do
Enum.join(arguments, "")
end
@doc """
Formats the given number in decimal format using a period and commas
```
You have @FIXED(contact.balance, 2) in your account
```
# Example
iex> Expression.Callbacks.fixed(%{}, 4.209922, 2, false)
"4.21"
iex> Expression.Callbacks.fixed(%{}, 4000.424242, 4, true)
"4,000.4242"
iex> Expression.Callbacks.fixed(%{}, 3.7979, 2, false)
"3.80"
iex> Expression.Callbacks.fixed(%{}, 3.7979, 2)
"3.80"
"""
def fixed(_ctx, number, precision, no_commas \\ false)
def fixed(_ctx, number, precision, true) do
Number.Delimit.number_to_delimited(number,
precision: precision,
delimiter: ",",
separator: "."
)
end
def fixed(_ctx, number, precision, false) do
Number.Delimit.number_to_delimited(number, precision: precision)
end
@doc """
Returns the first characters in a text string
```
You entered PIN @LEFT(step.value, 4)
```
# Example
iex> Expression.Callbacks.left(%{}, "foobar", 4)
"foob"
"""
def left(_ctx, binary, size) do
binary_part(binary, 0, size)
end
@doc """
Returns the number of characters in a text string
```
You entered @LEN(step.value) characters
```
# Example
iex> Expression.Callbacks.len(%{}, "foo")
3
iex> Expression.Callbacks.len(%{}, "zoë")
3
"""
def len(_ctx, binary) do
String.length(binary)
end
@doc """
Converts a text string to lowercase
````
Welcome @LOWER(contact)
```
# Example
iex> Expression.Callbacks.lower(%{}, "Foo Bar")
"foo bar"
"""
def lower(_ctx, binary) do
String.downcase(binary)
end
@doc """
Capitalizes the first letter of every word in a text string
```
Your name is @PROPER(contact)
```
# Example
iex> Expression.Callbacks.proper(%{}, "foo bar")
"Foo Bar"
"""
def proper(_ctx, binary) do
binary
|> String.split(" ")
|> Enum.map(&String.capitalize/1)
|> Enum.join(" ")
end
@doc """
Repeats text a given number of times
```
Stars! @REPT("*", 10)
```
# Example
iex> Expression.Callbacks.rept(%{}, "*", 10)
"**********"
"""
def rept(_ctx, value, amount) do
String.duplicate(value, amount)
end
@doc """
Returns the last characters in a text string
```
Your input ended with ...@RIGHT(step.value, 3)
```
# Example
iex> Expression.Callbacks.right(%{}, "testing", 3)
"ing"
"""
def right(_ctx, binary, size) do
String.slice(binary, -size, size)
end
@doc """
Substitutes new_text for old_text in a text string. If instance_num is given, then only that instance will be substituted
```
@SUBSTITUTE(step.value, "can't", "can")
```
# Example
iex> Expression.Callbacks.substitute(%{}, "I can't", "can't", "can do")
"I can do"
"""
def substitute(%{}, subject, pattern, replacement) do
String.replace(subject, pattern, replacement)
end
@doc """
Returns the unicode character specified by a number
```
As easy as @UNICHAR(65), @UNICHAR(66) , @UNICHAR(67)
```
# Example
iex> Expression.Callbacks.unichar(%{}, 65)
"A"
iex> Expression.Callbacks.unichar(%{}, 233)
"é"
"""
def unichar(_ctx, code) do
<<code::utf8>>
end
@doc """
Returns a numeric code for the first character in a text string
```
The numeric code of A is @UNICODE("A")
```
# Example
iex> Expression.Callbacks.unicode(%{}, "A")
65
iex> Expression.Callbacks.unicode(%{}, "é")
233
"""
def unicode(_ctx, <<code::utf8>>) do
code
end
@doc """
Converts a text string to uppercase
```
WELCOME @UPPER(contact)!!
```
# Example
iex> Expression.Callbacks.upper(%{}, "foo")
"FOO"
"""
def upper(_ctx, binary) do
String.upcase(binary)
end
@doc """
Returns the first word in the given text - equivalent to WORD(text, 1)
```
The first word you entered was @FIRST_WORD(step.value)
```
# Example
iex> Expression.Callbacks.first_word(%{}, "foo bar baz")
"foo"
"""
def first_word(_ctx, binary) do
[word | _] = String.split(binary, " ")
word
end
@doc """
Formats a number as a percentage
```
You've completed @PERCENT(contact.reports_done / 10) reports
```
# Example
iex> Expression.Callbacks.percent(%{}, 2/10)
"20%"
iex> Expression.Callbacks.percent(%{}, "0.2")
"20%"
iex> Expression.Callbacks.percent(%{}, Decimal.new("0.2"))
"20%"
"""
@spec percent(Expression.Context.t(), float) :: binary
def percent(ctx, float) when is_float(float) do
percent(ctx, Decimal.from_float(float))
end
@spec percent(Expression.Context.t(), binary) :: binary
def percent(ctx, binary) when is_binary(binary) do
percent(ctx, Decimal.new(binary))
end
@spec percent(Expression.Context.t(), Decimal.t()) :: binary
def percent(_ctx, decimal) do
Number.Percentage.number_to_percentage(Decimal.mult(decimal, 100), precision: 0)
end
@doc """
Formats digits in text for reading in TTS
```
Your number is @READ_DIGITS(contact.tel_e164)
```
# Example
iex> Expression.Callbacks.read_digits(%{}, "+271")
"plus two seven one"
"""
def read_digits(_ctx, binary) do
map = %{
"+" => "plus",
"0" => "zero",
"1" => "one",
"2" => "two",
"3" => "three",
"4" => "four",
"5" => "five",
"6" => "six",
"7" => "seven",
"8" => "eight",
"9" => "nine"
}
binary
|> String.graphemes()
|> Enum.map(fn grapheme -> Map.get(map, grapheme, nil) end)
|> Enum.reject(&is_nil/1)
|> Enum.join(" ")
end
@doc """
Removes the first word from the given text. The remaining text will be unchanged
```
You entered @REMOVE_FIRST_WORD(step.value)
```
# Example
iex> Expression.Callbacks.remove_first_word(%{}, "foo bar")
"bar"
iex> Expression.Callbacks.remove_first_word(%{}, "foo-bar", "-")
"bar"
"""
def remove_first_word(_ctx, binary, separator \\ " ")
def remove_first_word(_ctx, binary, separator) do
tl(String.split(binary, separator)) |> Enum.join(separator)
end
@doc """
Extracts the nth word from the given text string. If stop is a negative number,
then it is treated as count backwards from the end of the text. If by_spaces is
specified and is TRUE then the function splits the text into words only by spaces.
Otherwise the text is split by punctuation characters as well
# Example
iex> Expression.Callbacks.word(%{}, "hello cow-boy", 2)
"cow"
iex> Expression.Callbacks.word(%{}, "hello cow-boy", 2, true)
"cow-boy"
iex> Expression.Callbacks.word(%{}, "hello cow-boy", -1)
"boy"
"""
def word(ctx, binary, n, by_spaces \\ false)
def word(_ctx, binary, n, by_spaces) do
splitter = if(by_spaces, do: " ", else: @punctuation_pattern)
parts = String.split(binary, splitter)
# This slicing seems off.
[part] =
if n < 0 do
Enum.slice(parts, n, 1)
else
Enum.slice(parts, n - 1, 1)
end
part
end
@doc """
Returns the number of words in the given text string. If by_spaces is specified and is TRUE then the function splits the text into words only by spaces. Otherwise the text is split by punctuation characters as well
```
You entered @WORD_COUNT(step.value) words
```
# Example
iex> Expression.Callbacks.word_count(%{}, "hello cow-boy")
3
iex> Expression.Callbacks.word_count(%{}, "hello cow-boy", true)
2
"""
def word_count(ctx, binary, by_spaces \\ false)
def word_count(_ctx, binary, by_spaces) do
splitter = if(by_spaces, do: " ", else: @punctuation_pattern)
binary
|> String.split(splitter)
|> Enum.count()
end
@doc """
Extracts a substring of the words beginning at start, and up to but not-including stop.
If stop is omitted then the substring will be all words from start until the end of the text.
If stop is a negative number, then it is treated as count backwards from the end of the text.
If by_spaces is specified and is TRUE then the function splits the text into words only by spaces.
Otherwise the text is split by punctuation characters as well
# Example
iex> Expression.Callbacks.word_slice(%{}, "RapidPro expressions are fun", 2, 4)
"expressions are"
iex> Expression.Callbacks.word_slice(%{}, "RapidPro expressions are fun", 2)
"expressions are fun"
iex> Expression.Callbacks.word_slice(%{}, "RapidPro expressions are fun", 1, -2)
"RapidPro expressions"
iex> Expression.Callbacks.word_slice(%{}, "RapidPro expressions are fun", -1)
"fun"
"""
def word_slice(_ctx, binary, start) when start > 0 do
parts =
binary
|> String.split(" ")
parts
|> Enum.slice(start - 1, length(parts))
|> Enum.join(" ")
end
def word_slice(_ctx, binary, start) when start < 0 do
parts =
binary
|> String.split(" ")
parts
|> Enum.slice(start..length(parts))
|> Enum.join(" ")
end
def word_slice(_ctx, binary, start, stop, by_spaces \\ false)
def word_slice(_ctx, binary, start, stop, by_spaces) when stop > 0 do
splitter = if(by_spaces, do: " ", else: @punctuation_pattern)
binary
|> String.split(splitter)
|> Enum.slice((start - 1)..(stop - 2))
|> Enum.join(" ")
end
def word_slice(_ctx, binary, start, stop, by_spaces) when stop < 0 do
splitter = if(by_spaces, do: " ", else: @punctuation_pattern)
binary
|> String.split(splitter)
|> Enum.slice((start - 1)..(stop - 1))
|> Enum.join(" ")
end
@doc """
Returns TRUE if the argument is a number.
```
@ISNUMBER(contact.age) will return TRUE if the contact's age is a number.
```
# Example
iex> Expression.Callbacks.isnumber(%{}, 1)
true
iex> Expression.Callbacks.isnumber(%{}, 1.0)
true
iex> Expression.Callbacks.isnumber(%{}, Decimal.new("1.0"))
true
iex> Expression.Callbacks.isnumber(%{}, "1.0")
true
iex> Expression.Callbacks.isnumber(%{}, "a")
false
"""
def isnumber(_ctx, var) when is_float(var) or is_integer(var), do: true
def isnumber(_ctx, %{__struct__: Decimal}), do: true
def isnumber(_ctx, var) when is_binary(var) do
Decimal.new(var)
true
rescue
Decimal.Error ->
false
end
def isnumber(_ctx, _var), do: false
@doc """
Returns TRUE if the argument is a boolean.
```
@ISBOOL(block.value) will return TRUE if the block returned a boolean value.
```
# Example
iex> Expression.Callbacks.isbool(%{}, true)
true
iex> Expression.Callbacks.isbool(%{}, false)
true
iex> Expression.Callbacks.isbool(%{}, 1)
false
iex> Expression.Callbacks.isbool(%{}, 0)
false
iex> Expression.Callbacks.isbool(%{}, "true")
false
iex> Expression.Callbacks.isbool(%{}, "false")
false
"""
def isbool(_ctx, var) when var in [true, false], do: true
def isbool(_ctx, _var), do: false
@doc """
Returns TRUE if the argument is a string.
```
@ISSTRING(contact.name) will return TRUE if the contact's name is a string.
```
# Example
iex> Expression.Callbacks.isstring(%{}, "hello")
true
iex> Expression.Callbacks.isstring(%{}, false)
false
iex> Expression.Callbacks.isstring(%{}, 1)
false
iex> Expression.Callbacks.isstring(%{}, Decimal.new("1.0"))
false
"""
def isstring(_ctx, binary), do: is_binary(binary)
defp search_words(haystack, words) do
patterns =
words
|> String.split(" ")
|> Enum.map(&Regex.escape/1)
|> Enum.map(&Regex.compile!(&1, "i"))
results =
patterns
|> Enum.map(&Regex.run(&1, haystack))
|> Enum.map(fn
[match] -> match
nil -> nil
end)
|> Enum.reject(&is_nil/1)
{patterns, results}
end
@doc """
Tests whether all the words are contained in text
The words can be in any order and may appear more than once.
```
@(has_all_words("the quick brown FOX", "the fox")) → true
@(has_all_words("the quick brown fox", "red fox")) → false
```
NOTE: the flowspec supports `.match` which isn't support here yet.
```
@(has_all_words("the quick brown FOX", "the fox").match) → the FOX
```
# Example
iex> Expression.Callbacks.has_all_words(%{}, "the quick brown FOX", "the fox")
{:ok, true}
iex> Expression.Callbacks.has_all_words(%{}, "the quick brown FOX", "red fox")
{:ok, false}
"""
def has_all_words(_ctx, haystack, words) do
{patterns, results} = search_words(haystack, words)
# future match result: Enum.join(results, " ")
{:ok, Enum.count(patterns) == Enum.count(results)}
end
@doc """
Tests whether any of the words are contained in the text
Only one of the words needs to match and it may appear more than once.
```
@(has_any_word("The Quick Brown Fox", "fox quick")) → true
```
Unsupported:
```
@(has_any_word("The Quick Brown Fox", "fox quick").match) → Quick Fox
@(has_any_word("The Quick Brown Fox", "red fox").match) → Fox
```
# Example
iex> Expression.Callbacks.has_any_word(%{}, "The Quick Brown Fox", "fox quick")
{:ok, true}
iex> Expression.Callbacks.has_any_word(%{}, "The Quick Brown Fox", "yellow")
{:ok, false}
"""
def has_any_word(_ctx, haystack, words) do
{_patterns, results} = search_words(haystack, words)
# future match result Enum.join(results, " ")
{:ok, Enum.any?(results)}
end
@doc """
Tests whether text starts with beginning
Both text values are trimmed of surrounding whitespace, but otherwise matching is
strict without any tokenization.
Supported:
```
@(has_beginning("The Quick Brown", "the quick")) → true
@(has_beginning("The Quick Brown", "the quick")) → false
@(has_beginning("The Quick Brown", "quick brown")) → false
```
Unsupported
```
@(has_beginning("The Quick Brown", "the quick").match) → The Quick
```
# Example
iex> Expression.Callbacks.has_beginning(%{}, "The Quick Brown", "the quick")
{:ok, true}
iex> Expression.Callbacks.has_beginning(%{}, "The Quick Brown", "the quick")
{:ok, false}
iex> Expression.Callbacks.has_beginning(%{}, "The Quick Brown", "quick brown")
{:ok, false}
"""
def has_beginning(_ctx, text, beginning) do
case Regex.run(~r/^#{Regex.escape(beginning)}/i, text) do
# future match result: first
[_first | _remainder] -> {:ok, true}
nil -> {:ok, false}
end
end
defp extract_dateish(expression) do
expression = Regex.replace(~r/[a-z]/u, expression, "")
case DateTimeParser.parse_date(expression) do
{:ok, date} -> date
{:error, _} -> nil
end
end
@doc """
Tests whether `expression` contains a date formatted according to our environment
This is very naively implemented with a regular expression.
Supported:
```
@(has_date("the date is 15/01/2017")) → true
@(has_date("there is no date here, just a year 2017")) → false
```
Unsupported:
```
@(has_date("the date is 15/01/2017").match) → 2017-01-15T13:24:30.123456-05:00
```
# Example
iex> Expression.Callbacks.has_date(%{}, "the date is 15/01/2017")
{:ok, true}
iex> Expression.Callbacks.has_date(%{}, "there is no date here, just a year 2017")
{:ok, false}
"""
def has_date(_, expression) do
date = extract_dateish(expression)
# future match result: date
{:ok, !!date}
end
@doc """
Tests whether `expression` is a date equal to `date_string`
Supported:
```
@(has_date_eq("the date is 15/01/2017", "2017-01-15")) → true
@(has_date_eq("there is no date here, just a year 2017", "2017-06-01")) → false
@(has_date_eq("there is no date here, just a year 2017", "not date")) → ERROR
```
Not supported:
```
@(has_date_eq("the date is 15/01/2017", "2017-01-15").match) → 2017-01-15T13:24:30.123456-05:00
@(has_date_eq("the date is 15/01/2017 15:00", "2017-01-15").match) → 2017-01-15T15:00:00.000000-05:00
```
# Examples
iex> Expression.Callbacks.has_date_eq(%{}, "the date is 15/01/2017", "2017-01-15")
{:ok, true}
iex> Expression.Callbacks.has_date_eq(%{}, "there is no date here, just a year 2017", "2017-01-15")
{:ok, false}
"""
def has_date_eq(_ctx, expression, date_string) do
found_date = extract_dateish(expression)
test_date = extract_dateish(date_string)
# Future match result: found_date
{:ok, found_date == test_date}
end
@doc """
Tests whether `expression` is a date after the date `date_string`
```
@(has_date_gt("the date is 15/01/2017", "2017-01-01")) → true
@(has_date_gt("the date is 15/01/2017", "2017-03-15")) → false
@(has_date_gt("there is no date here, just a year 2017", "2017-06-01")) → false
@(has_date_gt("there is no date here, just a year 2017", "not date")) → ERROR
```
Not supported:
```
@(has_date_gt("the date is 15/01/2017", "2017-01-01").match) → 2017-01-15T13:24:30.123456-05:00
```
# Example
iex> Expression.Callbacks.has_date_gt(%{}, "the date is 15/01/2017", "2017-01-01")
{:ok, true}
iex> Expression.Callbacks.has_date_gt(%{}, "the date is 15/01/2017", "2017-03-15")
{:ok, false}
"""
def has_date_gt(_ctx, expression, date_string) do
found_date = extract_dateish(expression)
test_date = extract_dateish(date_string)
# future match result: found_date
{:ok, Date.compare(found_date, test_date) == :gt}
end
@doc """
Tests whether `expression` contains a date before the date `date_string`
```
@(has_date_lt("the date is 15/01/2017", "2017-06-01")) → true
@(has_date_lt("there is no date here, just a year 2017", "2017-06-01")) → false
@(has_date_lt("there is no date here, just a year 2017", "not date")) → ERROR
```
Not supported:
```
@(has_date_lt("the date is 15/01/2017", "2017-06-01").match) → 2017-01-15T13:24:30.123456-05:00
```
# Example
iex> Expression.Callbacks.has_date_lt(%{}, "the date is 15/01/2017", "2017-06-01")
{:ok, true}
iex> Expression.Callbacks.has_date_lt(%{}, "the date is 15/01/2021", "2017-03-15")
{:ok, false}
"""
def has_date_lt(_ctx, expression, date_string) do
found_date = extract_dateish(expression)
test_date = extract_dateish(date_string)
# future match result: found_date
{:ok, Date.compare(found_date, test_date) == :lt}
end
@doc """
Tests whether an email is contained in text
```
@(has_email("my email is foo1@bar.com, please respond")) → true
@(has_email("i'm not sharing my email")) → false
```
Not supported:
```
@(has_email("my email is foo1@bar.com, please respond").match) → foo1@bar.com
@(has_email("my email is <foo@bar2.com>").match) → foo@bar2.com
```
# Example:
iex> Expression.Callbacks.has_email(%{}, "my email is foo1@bar.com, please respond")
{:ok, true}
iex> Expression.Callbacks.has_email(%{}, "i'm not sharing my email")
{:ok, false}
"""
def has_email(_ctx, expression) do
case Regex.run(~r/([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)/, expression) do
# future match result: match
[_match | _] -> {:ok, true}
nil -> {:ok, false}
end
end
@doc """
Returns whether the contact is part of group with the passed in UUID
```
@(has_group(array(), "97fe7029-3a15-4005-b0c7-277b884fc1d5")) → false
```
Not supported:
```
@(has_group(contact.groups, "b7cf0d83-f1c9-411c-96fd-c511a4cfa86d").match) → {name: Testers, uuid: b7cf0d83-f1c9-411c-96fd-c511a4cfa86d}
```
# Example:
iex> contact = %{
...> "groups" => [%{
...> "uuid" => "b7cf0d83-f1c9-411c-96fd-c511a4cfa86d"
...> }]
...> }
iex> Expression.Callbacks.has_group(%{}, contact["groups"], "b7cf0d83-f1c9-411c-96fd-c511a4cfa86d")
{:ok, true}
iex> Expression.Callbacks.has_group(%{}, contact["groups"], "00000000-0000-0000-0000-000000000000")
{:ok, false}
"""
def has_group(_ctx, groups, uuid) do
group = Enum.find(groups, nil, &(&1["uuid"] == uuid))
# future match result: group
{:ok, !!group}
end
defp extract_numberish(expression) do
with [match] <-
Regex.run(~r/([0-9]+\.?[0-9]+)/u, replace_arabic_numerals(expression), capture: :first),
{decimal, ""} <- Decimal.parse(match) do
decimal
else
# Regex can return nil
nil -> nil
# Decimal parsing can return :error
:error -> nil
end
end
defp replace_arabic_numerals(expression) do
replace_numerals(expression, %{
"٠" => "0",
"١" => "1",
"٢" => "2",
"٣" => "3",
"٤" => "4",
"٥" => "5",
"٦" => "6",
"٧" => "7",
"٨" => "8",
"٩" => "9"
})
end
defp replace_numerals(expression, mapping) do
mapping
|> Enum.reduce(expression, fn {rune, replacement}, expression ->
String.replace(expression, rune, replacement)
end)
end
defp parse_decimal(float) when is_float(float), do: Decimal.from_float(float)
defp parse_decimal(number) when is_number(number), do: Decimal.new(number)
defp parse_decimal(binary) when is_binary(binary) do
case Decimal.parse(binary) do
{decimal, ""} -> decimal
:error -> :error
end
end
@doc """
Tests whether `expression` contains a number
```
@(has_number("the number is 42")) → true
@(has_number("the number is forty two")) → false
```
Not supported:
```
@(has_number("the number is 42").match) → 42
@(has_number("العدد ٤٢").match) → 42
```
# Example
iex> {:ok, true} = Expression.Callbacks.has_number(%{}, "the number is 42 and 5")
iex> {:ok, true} = Expression.Callbacks.has_number(%{}, "العدد ٤٢")
iex> {:ok, true} = Expression.Callbacks.has_number(%{}, "٠.٥")
iex> {:ok, true} = Expression.Callbacks.has_number(%{}, "0.6")
"""
def has_number(_ctx, expression) do
number = extract_numberish(expression)
# future match result: number
{:ok, !!number}
end
@doc """
Tests whether `expression` contains a number equal to the value
```
@(has_number_eq("the number is 42", 42)) → true
@(has_number_eq("the number is 42", 40)) → false
@(has_number_eq("the number is not there", 40)) → false
@(has_number_eq("the number is not there", "foo")) → ERROR
```
Not supported:
```
@(has_number_eq("the number is 42", 42).match) → 42
```
# Example
iex> {:ok, true} = Expression.Callbacks.has_number_eq(%{}, "the number is 42", 42)
iex> {:ok, true} = Expression.Callbacks.has_number_eq(%{}, "the number is 42", 42.0)
iex> {:ok, true} = Expression.Callbacks.has_number_eq(%{}, "the number is 42", "42")
iex> {:ok, true} = Expression.Callbacks.has_number_eq(%{}, "the number is 42.0", "42")
iex> {:ok, false} = Expression.Callbacks.has_number_eq(%{}, "the number is 40", "42")
iex> {:ok, false} = Expression.Callbacks.has_number_eq(%{}, "the number is 40", "foo")
iex> {:ok, false} = Expression.Callbacks.has_number_eq(%{}, "four hundred", "foo")
"""
def has_number_eq(_ctx, expression, decimal) do
with %Decimal{} = number <- extract_numberish(expression),
%Decimal{} = decimal <- parse_decimal(decimal) do
# Future match result: number
{:ok, Decimal.eq?(number, decimal)}
else
nil -> {:ok, false}
:error -> {:ok, false}
end
end
@doc """
Tests whether `expression` contains a number greater than min
```
@(has_number_gt("the number is 42", 40)) → true
@(has_number_gt("the number is 42", 42)) → false
@(has_number_gt("the number is not there", 40)) → false
@(has_number_gt("the number is not there", "foo")) → ERROR
```
Not supported:
```
@(has_number_gt("the number is 42", 40).match) → 42
```
# Example
iex> {:ok, true} = Expression.Callbacks.has_number_gt(%{}, "the number is 42", 40)
iex> {:ok, true} = Expression.Callbacks.has_number_gt(%{}, "the number is 42", 40.0)
iex> {:ok, true} = Expression.Callbacks.has_number_gt(%{}, "the number is 42", "40")
iex> {:ok, true} = Expression.Callbacks.has_number_gt(%{}, "the number is 42.0", "40")
iex> {:ok, false} = Expression.Callbacks.has_number_gt(%{}, "the number is 40", "40")
iex> {:ok, false} = Expression.Callbacks.has_number_gt(%{}, "the number is 40", "foo")
iex> {:ok, false} = Expression.Callbacks.has_number_gt(%{}, "four hundred", "foo")
"""
def has_number_gt(_ctx, expression, decimal) do
with %Decimal{} = number <- extract_numberish(expression),
%Decimal{} = decimal <- parse_decimal(decimal) do
# Future match result: number
{:ok, Decimal.gt?(number, decimal)}
else
nil -> {:ok, false}
:error -> {:ok, false}
end
end
@doc """
Tests whether `expression` contains a number greater than or equal to min
```
@(has_number_gte("the number is 42", 42)) → true
@(has_number_gte("the number is 42", 45)) → false
@(has_number_gte("the number is not there", 40)) → false
@(has_number_gte("the number is not there", "foo")) → ERROR
```
Not supported:
```
@(has_number_gte("the number is 42", 42).match) → 42
```
# Example
iex> {:ok, true} = Expression.Callbacks.has_number_gte(%{}, "the number is 42", 42)
iex> {:ok, true} = Expression.Callbacks.has_number_gte(%{}, "the number is 42", 42.0)
iex> {:ok, true} = Expression.Callbacks.has_number_gte(%{}, "the number is 42", "42")
iex> {:ok, false} = Expression.Callbacks.has_number_gte(%{}, "the number is 42.0", "45")
iex> {:ok, false} = Expression.Callbacks.has_number_gte(%{}, "the number is 40", "45")
iex> {:ok, false} = Expression.Callbacks.has_number_gte(%{}, "the number is 40", "foo")
iex> {:ok, false} = Expression.Callbacks.has_number_gte(%{}, "four hundred", "foo")
"""
def has_number_gte(_ctx, expression, decimal) do
with %Decimal{} = number <- extract_numberish(expression),
%Decimal{} = decimal <- parse_decimal(decimal) do
# Future match result: number
{:ok, Decimal.gt?(number, decimal) || Decimal.eq?(number, decimal)}
else
nil -> {:ok, false}
:error -> {:ok, false}
end
end
@doc """
Tests whether `expression` contains a number less than max
```
@(has_number_lt("the number is 42", 44)) → true
@(has_number_lt("the number is 42", 40)) → false
@(has_number_lt("the number is not there", 40)) → false
@(has_number_lt("the number is not there", "foo")) → ERROR
```
Not supported:
```
@(has_number_lt("the number is 42", 44).match) → 42
```
# Example
iex> {:ok, true} = Expression.Callbacks.has_number_lt(%{}, "the number is 42", 44)
iex> {:ok, true} = Expression.Callbacks.has_number_lt(%{}, "the number is 42", 44.0)
iex> {:ok, false} = Expression.Callbacks.has_number_lt(%{}, "the number is 42", "40")
iex> {:ok, false} = Expression.Callbacks.has_number_lt(%{}, "the number is 42.0", "40")
iex> {:ok, false} = Expression.Callbacks.has_number_lt(%{}, "the number is 40", "40")
iex> {:ok, false} = Expression.Callbacks.has_number_lt(%{}, "the number is 40", "foo")
iex> {:ok, false} = Expression.Callbacks.has_number_lt(%{}, "four hundred", "foo")
"""
def has_number_lt(_ctx, expression, decimal) do
with %Decimal{} = number <- extract_numberish(expression),
%Decimal{} = decimal <- parse_decimal(decimal) do
# Future match result: number
{:ok, Decimal.lt?(number, decimal)}
else
nil -> {:ok, false}
:error -> {:ok, false}
end
end
@doc """
Tests whether `expression` contains a number less than or equal to max
```
@(has_number_lte("the number is 42", 42)) → true
@(has_number_lte("the number is 42", 40)) → false
@(has_number_lte("the number is not there", 40)) → false
@(has_number_lte("the number is not there", "foo")) → ERROR
```
Not supported:
```
@(has_number_lte("the number is 42", 42).match) → 42
```
# Example
iex> {:ok, true} = Expression.Callbacks.has_number_lte(%{}, "the number is 42", 42)
iex> {:ok, true} = Expression.Callbacks.has_number_lte(%{}, "the number is 42", 42.0)
iex> {:ok, true} = Expression.Callbacks.has_number_lte(%{}, "the number is 42", "42")
iex> {:ok, false} = Expression.Callbacks.has_number_lte(%{}, "the number is 42.0", "40")
iex> {:ok, false} = Expression.Callbacks.has_number_lte(%{}, "the number is 40", "foo")
iex> {:ok, false} = Expression.Callbacks.has_number_lte(%{}, "four hundred", "foo")
"""
def has_number_lte(_ctx, expression, decimal) do
with %Decimal{} = number <- extract_numberish(expression),
%Decimal{} = decimal <- parse_decimal(decimal) do
# Future match result: number
{:ok, Decimal.lt?(number, decimal) || Decimal.eq?(number, decimal)}
else
nil -> {:ok, false}
:error -> {:ok, false}
end
end
@doc """
Tests whether the text contains only phrase
The phrase must be the only text in the text to match
```
@(has_only_phrase("Quick Brown", "quick brown")) → true
@(has_only_phrase("The Quick Brown Fox", "quick brown")) → false
@(has_only_phrase("the Quick Brown fox", "")) → false
@(has_only_phrase("", "").match) →
@(has_only_phrase("The Quick Brown Fox", "red fox")) → false
```
Not supported:
```
@(has_only_phrase("Quick Brown", "quick brown").match) → Quick Brown
```
# Example
iex> Expression.Callbacks.has_only_phrase(%{}, "Quick Brown", "quick brown")
{:ok, true}
iex> Expression.Callbacks.has_only_phrase(%{}, "", "")
{:ok, true}
iex> Expression.Callbacks.has_only_phrase(%{}, "The Quick Brown Fox", "quick brown")
{:ok, false}
"""
def has_only_phrase(_ctx, expression, phrase) do
case Enum.map([expression, phrase], &String.downcase/1) do
# Future match result: expression
[same, same] -> {:ok, true}
_anything_else -> {:ok, false}
end
end
@doc """
Returns whether two text values are equal (case sensitive). In the case that they are, it will return the text as the match.
```
@(has_only_text("foo", "foo")) → true
@(has_only_text("foo", "FOO")) → false
@(has_only_text("foo", "bar")) → false
@(has_only_text("foo", " foo ")) → false
@(has_only_text(results.webhook.category, "Failure")) → false
```
Not supported:
```
@(has_only_text("foo", "foo").match) → foo
@(has_only_text(run.status, "completed").match) → completed
@(has_only_text(results.webhook.category, "Success").match) → Success
```
# Example
iex> Expression.Callbacks.has_only_text(%{}, "foo", "foo")
{:ok, true}
iex> Expression.Callbacks.has_only_text(%{}, "", "")
{:ok, true}
iex> Expression.Callbacks.has_only_text(%{}, "foo", "FOO")
{:ok, false}
"""
def has_only_text(_ctx, expression, expression) when is_binary(expression),
# future match result: expression
do: {:ok, true}
def has_only_text(_ctx, _expression, _something_else),
# Future match result: expression
do: {:ok, false}
@doc """
Tests whether `expression` matches the regex pattern
Both text values are trimmed of surrounding whitespace and matching is case-insensitive.
```
@(has_pattern("Buy cheese please", "buy (\w+)")) → true
@(has_pattern("Sell cheese please", "buy (\w+)")) → false
```
Not supported:
```
@(has_pattern("Buy cheese please", "buy (\w+)").match) → Buy cheese
@(has_pattern("Buy cheese please", "buy (\w+)").extra) → {0: Buy cheese, 1: cheese}
```
# Examples
iex> Expression.Callbacks.has_pattern(%{}, "Buy cheese please", "buy (\\\\w+)")
{:ok, true}
iex> Expression.Callbacks.has_pattern(%{}, "Sell cheese please", "buy (\\\\w+)")
{:ok, false}
"""
def has_pattern(_ctx, expression, pattern) do
with {:ok, regex} <- Regex.compile(String.trim(pattern), "i"),
[[_first | _remainder]] <- Regex.scan(regex, String.trim(expression), capture: :all) do
# Future match result: first
{:ok, true}
else
_ -> {:ok, false}
end
end
@doc """
Tests whether `expresssion` contains a phone number.
The optional country_code argument specifies the country to use for parsing.
```
@(has_phone("my number is +12067799294 thanks")) → true
@(has_phone("my number is none of your business", "US")) → false
```
Not supported:
```
@(has_phone("my number is +12067799294").match) → +12067799294
@(has_phone("my number is 2067799294", "US").match) → +12067799294
@(has_phone("my number is 206 779 9294", "US").match) → +12067799294
```
# Example
iex> Expression.Callbacks.has_phone(%{}, "my number is +12067799294 thanks")
{:ok, true}
iex> Expression.Callbacks.has_phone(%{}, "my number is 2067799294 thanks", "US")
{:ok, true}
iex> Expression.Callbacks.has_phone(%{}, "my number is 206 779 9294 thanks", "US")
{:ok, true}
iex> Expression.Callbacks.has_phone(%{}, "my number is none of your business", "US")
{:ok, false}
"""
def has_phone(%{}, expression, country_code \\ "") do
letters_removed = Regex.replace(~r/[a-z]/i, expression, "")
case ExPhoneNumber.parse(letters_removed, country_code) do
# Future match result: ExPhoneNumber.format(pn, :es164)
{:ok, _pn} -> {:ok, true}
_ -> {:ok, false}
end
end
@doc """
Tests whether phrase is contained in `expression`
The words in the test phrase must appear in the same order with no other words in between.
```
@(has_phrase("the quick brown fox", "brown fox")) → true
@(has_phrase("the Quick Brown fox", "quick fox")) → false
@(has_phrase("the Quick Brown fox", "").match) →
```
Not supported:
```
@(has_phrase("the quick brown fox", "brown fox").match) → brown fox
```
# Examples
iex> Expression.Callbacks.has_phrase(%{}, "the quick brown fox", "brown fox")
{:ok, true}
iex> Expression.Callbacks.has_phrase(%{}, "the quick brown fox", "quick fox")
{:ok, false}
iex> Expression.Callbacks.has_phrase(%{}, "the quick brown fox", "")
{:ok, true}
"""
def has_phrase(_ctx, expression, phrase) do
lower_expression = String.downcase(expression)
lower_phrase = String.downcase(phrase)
found? = String.contains?(lower_expression, lower_phrase)
# Future match result: phrase
{:ok, found?}
end
@doc """
Tests whether there the `expression` has any characters in it
```
@(has_text("quick brown")) → true
@(has_text("")) → false
@(has_text(" \n")) → false
@(has_text(contact.fields.not_set)) → false
```
Not supported:
```
@(has_text("quick brown").match) → quick brown
@(has_text(123).match) → 123
```
# Examples
iex> Expression.Callbacks.has_text(%{}, "quick brown")
{:ok, true}
iex> Expression.Callbacks.has_text(%{}, "")
{:ok, false}
iex> Expression.Callbacks.has_text(%{}, " \\n")
{:ok, false}
iex> Expression.Callbacks.has_text(%{}, 123)
{:ok, true}
iex> Expression.Callbacks.has_text(%{}, nil)
{:ok, false}
"""
def has_text(ctx, expression) when not is_binary(expression),
do: has_text(ctx, to_string(expression))
def has_text(_ctx, expression) when is_binary(expression) do
case String.trim(expression) do
"" -> {:ok, false}
# Future match result: any_other_binary
_any_other_binary -> {:ok, true}
end
end
@doc """
Tests whether `expression` contains a time.
```
@(has_time("the time is 10:30")) → true
@(has_time("the time is 10:30:45").match) → 10:30:45.000000
@(has_time("there is no time here, just the number 25")) → false
```
Not supported:
```
@(has_time("the time is 10:30").match) → 10:30:00.000000
@(has_time("the time is 10 PM").match) → 22:00:00.000000
```
# Examples
iex> Expression.Callbacks.has_time(%{}, "the time is 10:30")
{:ok, true}
iex> Expression.Callbacks.has_time(%{}, "the time is 10:00 pm")
{:ok, true}
iex> Expression.Callbacks.has_time(%{}, "the time is 10:30:45")
{:ok, true}
iex> Expression.Callbacks.has_time(%{}, "there is no time here, just the number 25")
{:ok, false}
"""
def has_time(_ctx, expression) do
case DateTimeParser.parse_time(expression) do
# Future match result: time
{:ok, _time} -> {:ok, true}
_ -> {:ok, false}
end
end
end