diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index f5efe93..e76bdb2 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -5,6 +5,13 @@ on: pull_request_target: types: [opened, closed, synchronize] +# explicitly configure permissions, in case your GITHUB_TOKEN workflow permissions are set to read-only in repository settings +permissions: + actions: write + contents: write # this can be 'read' if the signatures are in remote repository + pull-requests: write + statuses: write + jobs: CLAssistant: runs-on: ubuntu-latest diff --git a/lib/expression/callbacks/standard.ex b/lib/expression/callbacks/standard.ex index dfd554f..c599a3a 100644 --- a/lib/expression/callbacks/standard.ex +++ b/lib/expression/callbacks/standard.ex @@ -116,6 +116,75 @@ defmodule Expression.Callbacks.Standard do end end + @expression_doc doc: + ~s[The SWITCH function evaluates one value (called the expression) against a list of values, and returns the result corresponding to the first matching value. If there is no match, an optional default value (the last one in the list if the list is odd) may be returned], + expression: ~s[SWITCH(1, 1, "Sunday", 2, "Monday", 3, "Tuesday", "No match")], + result: "Sunday" + @expression_doc doc: + ~s[The SWITCH function evaluates one value (called the expression) against a list of values, and returns the result corresponding to the first matching value. If there is no match, an optional default value (the last one in the list if the list is odd) may be returned], + expression: ~s[SWITCH(5, 1, "Sunday", 2, "Monday", 3, "Tuesday", "No match")], + result: "No match" + def switch_vargs(ctx, arguments) do + [key | options] = eval_args!(arguments, ctx) + optional = if rem(length(options), 2) == 1, do: List.last(options), else: nil + + options + |> Enum.chunk_every(2, 2, :discard) + |> Map.new(fn [a, b] -> {a, b} end) + |> Map.get(key, optional) + end + + @expression_doc doc: + "The ROUND function rounds a number to a specified number of digits. For example, if cell A1 contains 23.7825, and you want to round that value to two decimal places you can do ROUND(23.7825, 2)", + expression: "ROUND(23.7825, 2)", + result: "23.78" + @expression_doc doc: + "The ROUND function rounds a number to a specified number of digits. For example, if cell A1 contains 23.7825, and you want to round that value to zero decimal places you can do ROUND(23.7825)", + expression: "ROUND(23.7825)", + result: "24" + def round(ctx, value) do + [value] = eval_args!([value], ctx) + + value + |> Decimal.from_float() + |> Decimal.round(0) + |> Decimal.to_string(:normal) + end + + def round(ctx, value, places) do + [value, places] = eval_args!([value, places], ctx) + + value + |> Decimal.from_float() + |> Decimal.round(places) + |> Decimal.to_string(:normal) + end + + @doc """ + MID extracts part of a string, starting at a specified position and for a specified length. + + It correctly handles Unicode characters. For example, taking the first three characters from "héllo" returns "hél". + + If the starting position is beyond the string length, it returns an empty string. + + Implementation based on https://support.microsoft.com/en-us/office/mid-function-2eba57be-0c05-4bdc-bf81-5ecf4421eb8a + """ + @expression_doc doc: + "MID returns a specific number of characters from a text string, starting at the position you specify, based on the number of characters you specify.", + expression: ~s[MID("Fluid", 1, 5)], + result: "Fluid" + + @expression_doc expression: ~s[MID("Fluid Flow", 7, 20)], + result: "Flow" + + @expression_doc expression: ~s[MID("Fluid Flow", 20, 5)], + result: "" + + def mid(ctx, text, start_num, num_chars) do + [text, start_num, num_chars] = eval_args!([text, start_num, num_chars], ctx) + String.slice(to_string(text), start_num - 1, num_chars) + end + @doc """ Converts date stored in text to an actual date object and formats it using `strftime` formatting. @@ -519,9 +588,12 @@ defmodule Expression.Callbacks.Standard do @expression_doc expression: "clean(value)", context: %{"value" => <<65, 0, 66, 0, 67>>}, result: "ABC" + @expression_doc expression: "clean(nil)", + result: "" def clean(ctx, binary) do binary |> eval!(ctx) + |> to_string() |> String.graphemes() |> Enum.filter(&String.printable?/1) |> Enum.join("") @@ -537,9 +609,15 @@ defmodule Expression.Callbacks.Standard do """ @expression_doc expression: "code(\"A\")", result: 65 + @expression_doc expression: "code(nil)", + result: nil def code(ctx, code_ast) do - <> = eval!(code_ast, ctx) - code + code = eval!(code_ast, ctx) + + if code do + <> = code + code + end end @doc """ @@ -598,6 +676,8 @@ defmodule Expression.Callbacks.Standard do @doc """ Returns the first characters in a text string. This is Unicode safe. + + It will return `nil` if the given string is `nil` """ @expression_doc expression: "left(\"foobar\", 4)", result: "foob" @@ -605,20 +685,28 @@ defmodule Expression.Callbacks.Standard do @expression_doc expression: "left(\"Умерла Мадлен Олбрайт - первая женщина на посту главы Госдепа США\", 20)", result: "Умерла Мадлен Олбрай" + @expression_doc expression: "left(nil, 4)", + result: nil def left(ctx, binary, size) do [binary, size] = eval_args!([binary, size], ctx) - String.slice(binary, 0, size) + + if is_binary(binary) do + String.slice(binary, 0, size) + end end @doc """ - Returns the number of characters in a text string + Returns the number of characters in a text string, + returns 0 if the string is null or empty """ @expression_doc expression: "len(\"foo\")", result: 3 @expression_doc expression: "len(\"zoë\")", result: 3 + @expression_doc expression: "len(nil)", + result: 0 def len(ctx, binary) do - String.length(eval!(binary, ctx)) + String.length(to_string(eval!(binary, ctx))) end @doc """ @@ -626,8 +714,14 @@ defmodule Expression.Callbacks.Standard do """ @expression_doc expression: "lower(\"Foo Bar\")", result: "foo bar" + @expression_doc expression: "lower(nil)", + result: nil def lower(ctx, binary) do - String.downcase(eval!(binary, ctx)) + text = eval!(binary, ctx) + + if is_binary(text) do + String.downcase(text) + end end @doc """ @@ -635,11 +729,17 @@ defmodule Expression.Callbacks.Standard do """ @expression_doc expression: "proper(\"foo bar\")", result: "Foo Bar" + @expression_doc expression: "proper(nil)", + result: nil def proper(ctx, binary) do - binary - |> eval!(ctx) - |> String.split(" ") - |> Enum.map_join(" ", &String.capitalize/1) + text = eval!(binary, ctx) + + if is_binary(text) do + text + |> String.capitalize() + |> String.split(" ") + |> Enum.map_join(" ", &String.capitalize/1) + end end @doc """ @@ -647,9 +747,15 @@ defmodule Expression.Callbacks.Standard do """ @expression_doc expression: "rept(\"*\", 10)", result: "**********" + @expression_doc expression: "rept(nil, 10)", + result: nil def rept(ctx, value, amount) do - [value, amount] = eval_args!([value, amount], ctx) - String.duplicate(value, amount) + text = eval!(value, ctx) + + if is_binary(text) do + [amount] = eval_args!([amount], ctx) + String.duplicate(text, amount) + end end @doc """ @@ -661,9 +767,15 @@ defmodule Expression.Callbacks.Standard do @expression_doc expression: "right(\"Умерла Мадлен Олбрайт - первая женщина на посту главы Госдепа США\", 20)", result: "ту главы Госдепа США" + @expression_doc expression: "right(nil, 3)", + result: nil def right(ctx, binary, size) do - [binary, size] = eval_args!([binary, size], ctx) - String.slice(binary, -size, size) + text = eval!(binary, ctx) + + if is_binary(text) do + size = eval!(size, ctx) + String.slice(text, -size, size) + end end @doc """ @@ -671,9 +783,15 @@ defmodule Expression.Callbacks.Standard do """ @expression_doc expression: "substitute(\"I can't\", \"can't\", \"can do\")", result: "I can do" + @expression_doc expression: "substitute(nil, \"can't\", \"can do\")", + result: nil def substitute(ctx, subject, pattern, replacement) do - [subject, pattern, replacement] = eval_args!([subject, pattern, replacement], ctx) - String.replace(subject, pattern, replacement) + text = eval!(subject, ctx) + + if is_binary(text) do + [pattern, replacement] = eval_args!([pattern, replacement], ctx) + String.replace(text, pattern, replacement) + end end @doc """ @@ -701,8 +819,14 @@ defmodule Expression.Callbacks.Standard do """ @expression_doc expression: "upper(\"foo\")", result: "FOO" + @expression_doc expression: "upper(nil)", + result: nil def upper(ctx, binary) do - String.upcase(eval!(binary, ctx)) + text = eval!(binary, ctx) + + if is_binary(text) do + String.upcase(text) + end end @doc """ @@ -710,8 +834,10 @@ defmodule Expression.Callbacks.Standard do """ @expression_doc expression: "first_word(\"foo bar baz\")", result: "foo" + @expression_doc expression: "first_word(nil)", + result: "" def first_word(ctx, binary) do - [word | _] = String.split(eval!(binary, ctx), " ") + [word | _] = String.split(to_string(eval!(binary, ctx)), " ") word end @@ -733,6 +859,7 @@ defmodule Expression.Callbacks.Standard do Formats digits in text for reading in TTS """ @expression_doc expression: "read_digits(\"+271\")", result: "plus two seven one" + @expression_doc expression: "read_digits(nil)", result: "" def read_digits(ctx, binary) do map = %{ "+" => "plus", @@ -750,6 +877,7 @@ defmodule Expression.Callbacks.Standard do binary |> eval!(ctx) + |> to_string() |> String.graphemes() |> Enum.map(fn grapheme -> Map.get(map, grapheme, nil) end) |> Enum.reject(&is_nil/1) @@ -761,15 +889,58 @@ defmodule Expression.Callbacks.Standard do """ @expression_doc expression: "remove_first_word(\"foo bar\")", result: "bar" @expression_doc expression: "remove_first_word(\"foo-bar\", \"-\")", result: "bar" + @expression_doc expression: "remove_first_word(nil)", result: "" + @expression_doc expression: "remove_first_word(nil, \"-\")", result: "" def remove_first_word(ctx, binary) do - binary = eval!(binary, ctx) separator = " " - tl(String.split(binary, separator)) |> Enum.join(separator) + + binary + |> eval!(ctx) + |> to_string() + |> String.split(separator) + |> tl() + |> Enum.join(separator) end def remove_first_word(ctx, binary, separator) do [binary, separator] = eval_args!([binary, separator], ctx) - tl(String.split(binary, separator)) |> Enum.join(separator) + + binary + |> to_string() + |> String.split(separator) + |> tl() + |> Enum.join(separator) + end + + @expression_doc doc: + "Remove the last word from a list of words, using the specified separator ", + expression: ~s{remove_last_word("foo-bar", "-")}, + result: "foo" + @expression_doc doc: + "Remove the last word from a list of words, using spaces as separator between words ", + expression: "remove_last_word(\"foo bar\")", + result: "foo" + def remove_last_word(ctx, binary) do + [binary] = eval_args!([binary], ctx) + separator = " " + + binary + |> String.split(separator) + |> Enum.reverse() + |> tl() + |> Enum.reverse() + |> Enum.join(separator) + end + + def remove_last_word(ctx, binary, separator) do + [binary, separator] = eval_args!([binary, separator], ctx) + + binary + |> String.split(separator) + |> Enum.reverse() + |> tl() + |> Enum.reverse() + |> Enum.join(separator) end @doc """ @@ -781,11 +952,13 @@ defmodule Expression.Callbacks.Standard do @expression_doc expression: "word(\"hello cow-boy\", 2)", result: "cow" @expression_doc expression: "word(\"hello cow-boy\", 2, true)", result: "cow-boy" @expression_doc expression: "word(\"hello cow-boy\", -1)", result: "boy" + @expression_doc expression: "word(nil, 1)", result: "" def word(ctx, binary, n) do [binary, n] = eval_args!([binary, n], ctx) - parts = String.split(binary, @punctuation_pattern) + parts = String.split(to_string(binary), @punctuation_pattern) # This slicing seems off. + # but we copy it from the standard lib [part] = if n < 0 do Enum.slice(parts, n, 1) @@ -799,7 +972,7 @@ defmodule Expression.Callbacks.Standard do def word(ctx, binary, n, by_spaces) do [binary, n, by_spaces] = eval_args!([binary, n, by_spaces], ctx) splitter = if(by_spaces, do: " ", else: @punctuation_pattern) - parts = String.split(binary, splitter) + parts = String.split(to_string(binary), splitter) # This slicing seems off. [part] = @@ -822,20 +995,32 @@ defmodule Expression.Callbacks.Standard do """ @expression_doc expression: "word_count(\"hello cow-boy\")", result: 3 @expression_doc expression: "word_count(\"hello cow-boy\", true)", result: 2 + @expression_doc expression: "word_count(nil)", result: 0 def word_count(ctx, binary) do - binary - |> eval!(ctx) - |> String.split(@punctuation_pattern) - |> Enum.count() + text = eval!(binary, ctx) + + if is_nil(text) do + 0 + else + text + |> String.split(@punctuation_pattern) + |> Enum.count() + end end def word_count(ctx, binary, by_spaces) do - [binary, by_spaces] = eval_args!([binary, by_spaces], ctx) - splitter = if(by_spaces, do: " ", else: @punctuation_pattern) + text = eval!(binary, ctx) - binary - |> String.split(splitter) - |> Enum.count() + if is_nil(text) do + 0 + else + [by_spaces] = eval_args!([by_spaces], ctx) + splitter = if(by_spaces, do: " ", else: @punctuation_pattern) + + text + |> String.split(splitter) + |> Enum.count() + end end @doc """ @@ -853,12 +1038,13 @@ defmodule Expression.Callbacks.Standard do result: "FLOIP expressions" @expression_doc expression: "word_slice(\"FLOIP expressions are fun\", -1)", result: "fun" + @expression_doc expression: "word_slice(nil, -1)", + result: "" def word_slice(ctx, binary, start) do [binary, start] = eval_args!([binary, start], ctx) parts = - binary - |> String.split(" ") + String.split(to_string(binary), " ") cond do start > 0 -> @@ -879,12 +1065,14 @@ defmodule Expression.Callbacks.Standard do cond do stop > 0 -> binary + |> to_string() |> String.split(@punctuation_pattern) |> Enum.slice((start - 1)..(stop - 2)//1) |> Enum.join(" ") stop < 0 -> binary + |> to_string() |> String.split(@punctuation_pattern) |> Enum.slice((start - 1)..(stop - 1)//1) |> Enum.join(" ") @@ -898,12 +1086,14 @@ defmodule Expression.Callbacks.Standard do case stop do stop when stop > 0 -> binary + |> to_string() |> String.split(splitter) |> Enum.slice((start - 1)..(stop - 2)) |> Enum.join(" ") stop when stop < 0 -> binary + |> to_string() |> String.split(splitter) |> Enum.slice((start - 1)..(stop - 1)) |> Enum.join(" ") @@ -979,11 +1169,17 @@ defmodule Expression.Callbacks.Standard do """ @expression_doc expression: "has_all_words(\"the quick brown FOX\", \"the fox\")", result: true @expression_doc expression: "has_all_words(\"the quick brown FOX\", \"red fox\")", result: false + @expression_doc expression: "has_all_words(nil, \"red fox\")", result: false def has_all_words(ctx, haystack, words) do [haystack, words] = eval_args!([haystack, words], ctx) - {patterns, results} = search_words(haystack, words) - # future match result: Enum.join(results, " ") - Enum.count(patterns) == Enum.count(results) + + if is_binary(haystack) and is_binary(words) do + {patterns, results} = search_words(haystack, words) + # future match result: Enum.join(results, " ") + Enum.count(patterns) == Enum.count(results) + else + false + end end @doc """ @@ -996,27 +1192,31 @@ defmodule Expression.Callbacks.Standard do @expression_doc expression: "has_any_word(\"The Quick Brown Fox\", \"yellow\")", result: %{"__value__" => false, "match" => nil} def has_any_word(ctx, haystack, words) do - [haystack, words] = eval_args!([haystack, words], ctx) - haystack_words = String.split(haystack) - haystacks_lowercase = Enum.map(haystack_words, &String.downcase/1) - words_lowercase = String.split(words) |> Enum.map(&String.downcase/1) - - matched_indices = - haystacks_lowercase - |> Enum.with_index() - |> Enum.filter(fn {haystack_word, _index} -> - Enum.member?(words_lowercase, haystack_word) - end) - |> Enum.map(fn {_haystack_word, index} -> index end) + haystack = eval!(haystack, ctx) - matched_haystack_words = Enum.map(matched_indices, &Enum.at(haystack_words, &1)) + if is_binary(haystack) do + [words] = eval_args!([words], ctx) + haystack_words = String.split(haystack) + haystacks_lowercase = Enum.map(haystack_words, &String.downcase/1) + words_lowercase = words |> String.split() |> Enum.map(&String.downcase/1) - match? = Enum.any?(matched_haystack_words) + matched_indices = + haystacks_lowercase + |> Enum.with_index() + |> Enum.filter(fn {haystack_word, _index} -> + Enum.member?(words_lowercase, haystack_word) + end) + |> Enum.map(fn {_haystack_word, index} -> index end) - %{ - "__value__" => match?, - "match" => if(match?, do: Enum.join(matched_haystack_words, " "), else: nil) - } + matched_haystack_words = Enum.map(matched_indices, &Enum.at(haystack_words, &1)) + + match? = Enum.any?(matched_haystack_words) + + %{ + "__value__" => match?, + "match" => if(match?, do: Enum.join(matched_haystack_words, " "), else: nil) + } + end end @doc """ @@ -1215,11 +1415,13 @@ defmodule Expression.Callbacks.Standard do result: %{"__value__" => true, "email" => "foo1@bar.com"} @expression_doc expression: "has_email(\"i'm not sharing my email\")", result: %{"__value__" => false, "email" => nil} + @expression_doc expression: "has_email(nil)", + result: %{"__value__" => false, "email" => nil} def has_email(ctx, expression) do expression = eval!(expression, ctx) email = - case Regex.run(~r/([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)/, expression) do + case Regex.run(~r/([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)/, to_string(expression)) do # future match result: match [match | _] -> match nil -> nil @@ -1262,6 +1464,9 @@ defmodule Expression.Callbacks.Standard do !!group end + defp extract_numberish(nil), do: nil + defp extract_numberish(value) when is_number(value), do: value + defp extract_numberish(expression) do with [match] <- Regex.run(~r/([0-9]+\.?[0-9]*)/u, replace_arabic_numerals(expression), capture: :first), @@ -1478,7 +1683,7 @@ defmodule Expression.Callbacks.Standard do @expression_doc expression: "has_only_text(\"foo\", \"FOO\")", result: false def has_only_text(ctx, expression_one, expression_two) do [expression_one, expression_two] = eval_args!([expression_one, expression_two], ctx) - expression_one == expression_two + to_string(expression_one) == to_string(expression_two) end @doc """ @@ -1488,10 +1693,12 @@ defmodule Expression.Callbacks.Standard do """ @expression_doc expression: "has_pattern(\"Buy cheese please\", \"buy (\\w+)\")", result: true @expression_doc expression: "has_pattern(\"Sell cheese please\", \"buy (\\w+)\")", result: false + @expression_doc expression: "has_pattern(nil, \"buy (\\w+)\")", result: false def has_pattern(ctx, expression, pattern) do [expression, pattern] = eval_args!([expression, pattern], ctx) - with {:ok, regex} <- Regex.compile(String.trim(pattern), "i"), + with true <- is_binary(expression), + {:ok, regex} <- Regex.compile(String.trim(pattern), "i"), [[_first | _remainder]] <- Regex.scan(regex, String.trim(expression), capture: :all) do # Future match result: first true @@ -1512,16 +1719,18 @@ defmodule Expression.Callbacks.Standard do result: %{"__value__" => true, "phonenumber" => "+12067799294"} @expression_doc expression: "has_phone(\"my number is none of your business\", \"US\")", result: %{"__value__" => false, "phonenumber" => nil} + @expression_doc expression: "has_phone(nil, \"US\")", + result: %{"__value__" => false, "phonenumber" => nil} def has_phone(ctx, expression) do [expression] = eval_args!([expression], ctx) - letters_removed = Regex.replace(~r/[a-z]/i, expression, "") + letters_removed = Regex.replace(~r/[a-z]/i, to_string(expression), "") parse_phone_number(letters_removed, "") end def has_phone(ctx, expression, country_code) do [expression, country_code] = eval_args!([expression, country_code], ctx) - letters_removed = Regex.replace(~r/[a-z]/i, expression, "") + letters_removed = Regex.replace(~r/[a-z]/i, to_string(expression), "") parse_phone_number(letters_removed, country_code) end @@ -1559,9 +1768,14 @@ defmodule Expression.Callbacks.Standard do @expression_doc expression: "has_text(\"\")", result: false @expression_doc expression: "has_text(\" \n\")", result: false @expression_doc expression: "has_text(123)", result: true + @expression_doc expression: "has_text(nil)", result: false def has_text(ctx, expression) do - expression = eval!(expression, ctx) |> to_string() - String.trim(expression) != "" + expression = + expression + |> eval!(ctx) + |> to_string() + + String.trim(to_string(expression)) != "" end @doc """ diff --git a/lib/expression/v2/callbacks/standard.ex b/lib/expression/v2/callbacks/standard.ex index 398ff58..eac9473 100644 --- a/lib/expression/v2/callbacks/standard.ex +++ b/lib/expression/v2/callbacks/standard.ex @@ -1001,6 +1001,7 @@ defmodule Expression.V2.Callbacks.Standard do !!group end + defp extract_numberish(nil), do: nil defp extract_numberish(value) when is_number(value), do: value defp extract_numberish(expression) do @@ -1054,6 +1055,8 @@ defmodule Expression.V2.Callbacks.Standard do @expression_doc expression: "has_number(\"العدد ٤٢\")", result: true @expression_doc expression: "has_number(\"٠.٥\")", result: true @expression_doc expression: "has_number(\"0.6\")", result: true + @expression_doc expression: "has_number(\"\")", result: false + @expression_doc expression: "has_number(value)", context: %{"value" => nil}, result: false def has_number(_ctx, expression) do number = extract_numberish(expression) diff --git a/mix.exs b/mix.exs index c53aa17..cc1782d 100644 --- a/mix.exs +++ b/mix.exs @@ -55,6 +55,7 @@ defmodule Expression.MixProject do {:mix_test_watch, "~> 1.0", only: :dev, runtime: false}, {:nimble_parsec, "~> 1.1"}, {:number, "~> 1.0"}, + {:decimal, "~> 2.0"}, {:timex, "~> 3.7"}, {:version_tasks, "~> 0.12.0", only: [:dev], runtime: false} ] diff --git a/mix.lock b/mix.lock index 1166658..9f19e06 100644 --- a/mix.lock +++ b/mix.lock @@ -10,7 +10,7 @@ "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, - "ex_phone_number": {:hex, :ex_phone_number, "0.4.4", "8e994abe583496a3308cf56af013dca9b47a0424b0a9940af41cb0d66b848dd3", [:mix], [{:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}], "hexpm", "a59875692ec57b3392959a7740f3e9a5cb08da88bcaee4efd480c770f5bb0f2c"}, + "ex_phone_number": {:hex, :ex_phone_number, "0.4.5", "2065cc48c3e9d1ed9821f50877c32f2f6898362cb990f44147ca217c5d1374ed", [:mix], [{:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}], "hexpm", "67163f8706f8cbfef1b1f4b9230c461f19786d0d79fd0b22cbeeefc6f0b99d4a"}, "expo": {:hex, :expo, "0.4.0", "bbe4bf455e2eb2ebd2f1e7d83530ce50fb9990eb88fc47855c515bfdf1c6626f", [:mix], [], "hexpm", "a8ed1683ec8b7c7fa53fd7a41b2c6935f539168a6bb0616d7fd6b58a36f3abf2"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "gettext": {:hex, :gettext, "0.22.1", "e7942988383c3d9eed4bdc22fc63e712b655ae94a672a27e4900e3d4a2c43581", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "ad105b8dab668ee3f90c0d3d94ba75e9aead27a62495c101d94f2657a190ac5d"}, diff --git a/test/expression_test.exs b/test/expression_test.exs index f6a2047..cdc136b 100644 --- a/test/expression_test.exs +++ b/test/expression_test.exs @@ -34,6 +34,21 @@ defmodule ExpressionTest do Expression.evaluate_as_boolean!("@has_beginning(contact.number, \"123\")", %{ "contact" => %{"number" => 123_456} }) + + assert true == + Expression.evaluate_as_boolean!( + "@has_pattern('Buy cheese please', 'buy (\\w+)')", + %{} + ) + + assert false == + Expression.evaluate_as_boolean!( + "@has_pattern('Sell cheese please', 'buy (\\w+)')", + %{} + ) + + assert false == + Expression.evaluate_as_boolean!("@has_pattern(nil, 'buy (\\w+)')", %{}) end test "evaluate_as_boolean! with kernel operators" do @@ -431,6 +446,21 @@ defmodule ExpressionTest do assert {:ok, expected} == Expression.evaluate("@(DATEVALUE(NOW(), \"%Y-%m-%d\"))") end + test "lazy argument evaluation in ifs" do + context = %{ + "status" => nil + } + + assert false == + Expression.evaluate!(~S|@if(status, left(status, 2), false)|, context) + + assert false == + Expression.evaluate!(~S|@if(isstring(status), left(status, 2), false)|, context) + + assert false == + Expression.evaluate!(~S|@if(LEN(status) > 0, LEFT(status, 2), false)|, context) + end + test "checking for nil vars with if" do assert 1 == Expression.evaluate!("@IF(value, value, 0)", %{