From 433b03d6cd13fdf661f32543e016f83e26d24945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Thallis?= Date: Mon, 21 Sep 2026 08:42:23 -0300 Subject: [PATCH] Round whole numbers and numeric strings in ROUND `ROUND` piped its argument straight into `Decimal.from_float/1`, which only accepts a float. So `ROUND(5)`, `ROUND(2 * 3)` and a variable holding a whole number all raised a `FunctionClauseError`, while the same expression with a fractional value worked. That error is not one `evaluate_block!/4` converts into an `Expression.Error`, so it crashed the caller. Coerce the value with the helper `FIXED` already uses, then build the decimal from either an integer or a float. A whole number now rounds to itself, a numeric string rounds like the number it holds, and a value that is not a number raises the standard "expression is not a number" error. Release as 3.0.0-rc.5. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 10 ++++++++++ README.md | 2 +- lib/expression/callbacks/standard.ex | 9 +++++++-- mix.exs | 2 +- test/expression_test.exs | 29 ++++++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a5ccab..7af5f35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## v3.0.0-rc.5 + +### Fixed + +- `ROUND` now rounds whole numbers and numeric strings. It used to accept only + floats, so `ROUND(5)`, `ROUND(2 * 3)` and a whole-number variable all raised a + `FunctionClauseError`. A value that is not a number now surfaces as the + standard "expression is not a number" `Expression.Error`, as `FIXED` already + does. + ## v3.0.0-rc.4 ### Fixed diff --git a/README.md b/README.md index c5bed4a..4bfed26 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.4"} + {:expression, "~> 3.0.0-rc.5"} ] end ``` diff --git a/lib/expression/callbacks/standard.ex b/lib/expression/callbacks/standard.ex index 5e67eb3..8f15540 100644 --- a/lib/expression/callbacks/standard.ex +++ b/lib/expression/callbacks/standard.ex @@ -351,7 +351,8 @@ defmodule Expression.Callbacks.Standard do [value] = eval_args!([value], ctx) value - |> Decimal.from_float() + |> coerce_to_number!() + |> number_to_decimal() |> Decimal.round(0) |> Decimal.to_string(:normal) end @@ -361,11 +362,15 @@ defmodule Expression.Callbacks.Standard do [value, places] = eval_args!([value, places], ctx) value - |> Decimal.from_float() + |> coerce_to_number!() + |> number_to_decimal() |> Decimal.round(places) |> Decimal.to_string(:normal) end + defp number_to_decimal(number) when is_float(number), do: Decimal.from_float(number) + defp number_to_decimal(number) when is_integer(number), do: Decimal.new(number) + @doc """ MID extracts part of a string, starting at a specified position and for a specified length. diff --git a/mix.exs b/mix.exs index 85a0c50..4dff4d1 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Expression.MixProject do use Mix.Project - @version "3.0.0-rc.4" + @version "3.0.0-rc.5" def project do [ diff --git a/test/expression_test.exs b/test/expression_test.exs index a9fd15c..629e011 100644 --- a/test/expression_test.exs +++ b/test/expression_test.exs @@ -760,6 +760,35 @@ defmodule ExpressionTest do %Expression.Error{message: "expression is not a number: `\"not a number\"`"}} = Expression.evaluate_block("fixed(value, 2, true)", %{"value" => "not a number"}) end + + test "round/1 rounds a whole number to itself" do + assert {:ok, "4"} == Expression.evaluate_block("round(value)", %{"value" => 4}) + end + + test "round/2 rounds a whole number to the given places" do + assert {:ok, "4.00"} == Expression.evaluate_block("round(value, 2)", %{"value" => 4}) + end + + test "round/1 with a numeric string still rounds the number" do + assert {:ok, "4"} == Expression.evaluate_block("round(value)", %{"value" => "3.7"}) + end + + test "round/1 with a non-numeric value returns an error tuple instead of crashing" do + assert {:error, + %Expression.Error{message: "expression is not a number: `\"not a number\"`"}} = + Expression.evaluate_block("round(value)", %{"value" => "not a number"}) + end + + test "round/1 with a nil value returns an error tuple instead of crashing" do + assert {:error, %Expression.Error{message: "expression is not a number: `nil`"}} = + Expression.evaluate_block("round(value)", %{"value" => nil}) + end + + test "round/2 with a non-numeric value returns an error tuple instead of crashing" do + assert {:error, + %Expression.Error{message: "expression is not a number: `\"not a number\"`"}} = + Expression.evaluate_block("round(value, 2)", %{"value" => "not a number"}) + end end test "escaping" do