From 7d6747cbd32db7f303afe8eab5a0aef65564a64f Mon Sep 17 00:00:00 2001 From: Federico Meini Date: Wed, 12 Aug 2026 18:43:42 +0200 Subject: [PATCH 1/2] fix: underscored number literals losing leading zeros of digit groups The int parser captured each digit group between underscores with integer/1, which parses "000" as 0, so 180_000 was joined into 1800 and 1_000_000 into 100. Elixir's code formatter writes numbers longer than five digits with underscore separators, so expressions generated from Elixir ASTs hit this constantly: 5000 > 180_000 evaluated to true. Capture digit groups as strings so leading zeros survive the join, and give floats the same underscore support since the formatter emits underscores for large floats too. --- lib/literal_helpers.ex | 21 ++++++++++++++------- test/expression/parser_test.exs | 6 ++++++ test/expression_test.exs | 11 +++++++++++ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/lib/literal_helpers.ex b/lib/literal_helpers.ex index 1761f3f4..16b87302 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/test/expression/parser_test.exs b/test/expression/parser_test.exs index bd735ff7..e47713d0 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 ecad3469..2d98135b 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 From 0c50d69154afa4d75e206e1e365f36454573a3ee Mon Sep 17 00:00:00 2001 From: Federico Meini Date: Thu, 13 Aug 2026 17:38:21 +0200 Subject: [PATCH 2/2] chore: bump version to 3.0.0-rc.2 --- README.md | 2 +- mix.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d5b06141..e55980f7 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/mix.exs b/mix.exs index 0045fe19..11a20c30 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 [