diff --git a/README.md b/README.md index d5b0614..e55980f 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ by adding `expression` to your list of dependencies in `mix.exs`: ```elixir def deps do [ - {:expression, "~> 3.0.0-rc.1"} + {:expression, "~> 3.0.0-rc.2"} ] end ``` diff --git a/lib/literal_helpers.ex b/lib/literal_helpers.ex index 1761f3f..16b8730 100644 --- a/lib/literal_helpers.ex +++ b/lib/literal_helpers.ex @@ -4,24 +4,31 @@ defmodule Expression.LiteralHelpers do def int do optional(string("-")) - |> times( - integer(min: 1) - |> concat(optional(ignore(utf8_char([?_])))), - min: 1 - ) + |> concat(digits_with_underscores()) |> reduce({Enum, :join, [""]}) |> map({String, :to_integer, []}) end def float do optional(string("-")) - |> concat(utf8_string([?0..?9], min: 1)) + |> concat(digits_with_underscores()) |> concat(string(".")) - |> concat(utf8_string([?0..?9], min: 1)) + |> concat(digits_with_underscores()) |> reduce({Enum, :join, [""]}) |> map({String, :to_float, []}) end + # Digit groups must be captured as strings, not with `integer/1`: parsing + # "000" in "180_000" as an integer collapses it to 0, so the joined number + # became 1800 instead of 180000. + defp digits_with_underscores do + times( + ascii_string([?0..?9], min: 1) + |> concat(optional(ignore(utf8_char([?_])))), + min: 1 + ) + end + def numeric do choice([int(), float()]) end diff --git a/mix.exs b/mix.exs index 0045fe1..11a20c3 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Expression.MixProject do use Mix.Project - @version "3.0.0-rc.1" + @version "3.0.0-rc.2" def project do [ diff --git a/test/expression/parser_test.exs b/test/expression/parser_test.exs index bd735ff..e47713d 100644 --- a/test/expression/parser_test.exs +++ b/test/expression/parser_test.exs @@ -89,6 +89,12 @@ defmodule Expression.ParserTest do assert_ast([expression: [literal: 11]], "@(1_1)") assert_ast([expression: [[]], text: "(1__1)"], "@(1__1)") assert_ast([expression: [literal: 111]], "@(1_1_1)") + # digit groups with leading zeros must keep them: 180_000 used to parse as 1800 + assert_ast([expression: [literal: 180_000]], "@(180_000)") + assert_ast([expression: [literal: 1_000_000]], "@(1_000_000)") + assert_ast([expression: [literal: -1_000_000]], "@(-1_000_000)") + assert_ast([expression: [literal: 1_234_567.89]], "@(1_234_567.89)") + assert_ast([expression: [literal: 0.000_5]], "@(0.000_5)") assert_ast( [expression: [literal: ~U[2022-05-24 00:00:00.0Z]]], diff --git a/test/expression_test.exs b/test/expression_test.exs index ecad346..2d98135 100644 --- a/test/expression_test.exs +++ b/test/expression_test.exs @@ -800,6 +800,17 @@ defmodule ExpressionTest do "message" => "Invalid enumerable" } end + + test "underscored number literals keep their value in comparisons and arithmetic" do + # 180_000 used to parse as 1800 (each digit group was parsed as an + # integer before joining, dropping leading zeros), which made + # `5000 > 180_000` evaluate to true + assert Expression.evaluate_block!("5000 > 180_000") == false + assert Expression.evaluate_block!("180_000 < 5000") == false + assert Expression.evaluate_block!("200_000 + 1") == 200_001 + assert Expression.evaluate_block!("1_000_000") == 1_000_000 + assert Expression.evaluate_block!("1_234_567.89") == 1_234_567.89 + end end describe "evaluate_as_string!" do