From 1a48c6ed2269fe9de700eef5232f460c8095fe01 Mon Sep 17 00:00:00 2001 From: anquegi Date: Mon, 9 Nov 2026 19:50:12 +0100 Subject: [PATCH 1/3] feat: add ~z sigil for cartesian and polar complex numbers Co-authored-by: Claude Sonnet 5.5 --- README.md | 15 ++++++++++ lib/complex/kernel.ex | 4 ++- lib/complex/sigils.ex | 45 ++++++++++++++++++++++++++++++ test/complex/kernel_sigil_test.exs | 9 ++++++ test/complex/sigils_test.exs | 34 ++++++++++++++++++++++ 5 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 lib/complex/sigils.ex create mode 100644 test/complex/kernel_sigil_test.exs create mode 100644 test/complex/sigils_test.exs diff --git a/README.md b/README.md index a5a227c..2287ba9 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,21 @@ iex> Complex.new(0, 1) %Complex{im: 1.0, re: 0.0} ``` +### Sigil + +`use Complex.Kernel` (or `import Complex.Sigils`) gives you the `~z` sigil. +The two numbers are separated by whitespace, mirroring Common Lisp's +`#c(re im)` reader syntax. With the `p` modifier they are the magnitude and +the angle (radians) instead: + +```elixir +iex> ~z(3 4) +%Complex{im: 4, re: 3} + +iex> ~z(2 1.57)p +Complex.from_polar(2, 1.57) +``` + ## License Copyright 2015 Thomas Krauss diff --git a/lib/complex/kernel.ex b/lib/complex/kernel.ex index e9f7597..8d15cec 100644 --- a/lib/complex/kernel.ex +++ b/lib/complex/kernel.ex @@ -1,6 +1,7 @@ defmodule Complex.Kernel do @moduledoc """ - Provides operator overloading for Elixir operators. + Provides operator overloading for Elixir operators and the `~z` sigil + (see `Complex.Sigils`). When you `use Complex.Kernel`, be aware that the arithmetic operators won't work in clause guards. For that you need to use the fully qualified @@ -18,6 +19,7 @@ defmodule Complex.Kernel do quote do import Kernel, except: unquote(operators) import Complex.Kernel, only: unquote(operators) + import Complex.Sigils, only: [sigil_z: 2] end end diff --git a/lib/complex/sigils.ex b/lib/complex/sigils.ex new file mode 100644 index 0000000..df2ba68 --- /dev/null +++ b/lib/complex/sigils.ex @@ -0,0 +1,45 @@ +defmodule Complex.Sigils do + @moduledoc """ + Sigils to build complex numbers. + """ + + @doc ~S""" + Handles the sigil `~z` for complex numbers. + + The two numbers are separated by whitespace, mirroring Common Lisp's + `#c(re im)` reader syntax. Without modifiers they are the real and + imaginary parts. With the `p` modifier (an extension specific to this + library) they are the magnitude and the angle (radians). + + ## Examples + + import Complex.Sigils + + ~z(3 4) + #=> %Complex{re: 3, im: 4} + + ~z(2 1.57)p + #=> Complex.from_polar(2, 1.57) + """ + def sigil_z(string, []) do + [re, im] = parse(string) + Complex.new(re, im) + end + + def sigil_z(string, [?p]) do + [r, phi] = parse(string) + Complex.from_polar(r, phi) + end + + defp parse(string), do: string |> String.split() |> Enum.map(&to_number/1) + + defp to_number(str) do + with :error <- parse_exact(Integer.parse(str)), + :error <- parse_exact(Float.parse(str)) do + raise ArgumentError, "invalid number in ~z sigil: #{inspect(str)}" + end + end + + defp parse_exact({n, ""}), do: n + defp parse_exact(_), do: :error +end diff --git a/test/complex/kernel_sigil_test.exs b/test/complex/kernel_sigil_test.exs new file mode 100644 index 0000000..1a117ad --- /dev/null +++ b/test/complex/kernel_sigil_test.exs @@ -0,0 +1,9 @@ +defmodule Complex.KernelSigilTest do + use ExUnit.Case, async: true + use Complex.Kernel + + test "use Complex.Kernel imports the ~z sigil" do + assert ~z(3 4) == Complex.new(3, 4) + assert ~z(2 1.57)p == Complex.from_polar(2, 1.57) + end +end diff --git a/test/complex/sigils_test.exs b/test/complex/sigils_test.exs new file mode 100644 index 0000000..c899ff0 --- /dev/null +++ b/test/complex/sigils_test.exs @@ -0,0 +1,34 @@ +defmodule Complex.SigilsTest do + use ExUnit.Case, async: true + + import Complex.Sigils + + test "cartesian form with integers" do + assert ~z(3 4) == Complex.new(3, 4) + end + + test "cartesian form keeps integers and floats as written" do + assert ~z(3 -4.5) === Complex.new(3, -4.5) + end + + test "extra whitespace is ignored" do + assert ~z( 3 4 ) == Complex.new(3, 4) + end + + test "polar form with the p modifier" do + assert ~z(2 1.57)p == Complex.from_polar(2, 1.57) + end + + test "it raises on an invalid number" do + assert_raise ArgumentError, ~r/invalid number/, fn -> sigil_z("3 x", []) end + end + + test "it raises when there are not exactly two numbers" do + assert_raise MatchError, fn -> sigil_z("3", []) end + assert_raise MatchError, fn -> sigil_z("1 2 3", []) end + end + + test "it raises on an unknown modifier" do + assert_raise FunctionClauseError, fn -> sigil_z("3 4", ~c"x") end + end +end From 0c08ae0707635a76f19fa1103cf9c102798f1048 Mon Sep 17 00:00:00 2001 From: anquegi Date: Mon, 9 Nov 2026 21:29:25 +0100 Subject: [PATCH 2/3] refactor: do not import ~z from Complex.Kernel Co-authored-by: Claude Sonnet 5.5 --- README.md | 2 +- lib/complex/kernel.ex | 4 +--- test/complex/kernel_sigil_test.exs | 9 --------- 3 files changed, 2 insertions(+), 13 deletions(-) delete mode 100644 test/complex/kernel_sigil_test.exs diff --git a/README.md b/README.md index 2287ba9..c41d207 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ iex> Complex.new(0, 1) ### Sigil -`use Complex.Kernel` (or `import Complex.Sigils`) gives you the `~z` sigil. +`import Complex.Sigils` gives you the `~z` sigil. The two numbers are separated by whitespace, mirroring Common Lisp's `#c(re im)` reader syntax. With the `p` modifier they are the magnitude and the angle (radians) instead: diff --git a/lib/complex/kernel.ex b/lib/complex/kernel.ex index 8d15cec..e9f7597 100644 --- a/lib/complex/kernel.ex +++ b/lib/complex/kernel.ex @@ -1,7 +1,6 @@ defmodule Complex.Kernel do @moduledoc """ - Provides operator overloading for Elixir operators and the `~z` sigil - (see `Complex.Sigils`). + Provides operator overloading for Elixir operators. When you `use Complex.Kernel`, be aware that the arithmetic operators won't work in clause guards. For that you need to use the fully qualified @@ -19,7 +18,6 @@ defmodule Complex.Kernel do quote do import Kernel, except: unquote(operators) import Complex.Kernel, only: unquote(operators) - import Complex.Sigils, only: [sigil_z: 2] end end diff --git a/test/complex/kernel_sigil_test.exs b/test/complex/kernel_sigil_test.exs deleted file mode 100644 index 1a117ad..0000000 --- a/test/complex/kernel_sigil_test.exs +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Complex.KernelSigilTest do - use ExUnit.Case, async: true - use Complex.Kernel - - test "use Complex.Kernel imports the ~z sigil" do - assert ~z(3 4) == Complex.new(3, 4) - assert ~z(2 1.57)p == Complex.from_polar(2, 1.57) - end -end From ceed5d47f163046387690a0d48b515fa93da7414 Mon Sep 17 00:00:00 2001 From: anquegi Date: Mon, 9 Nov 2026 23:06:50 +0100 Subject: [PATCH 3/3] refactor: replace ~z with ~COMPLEX and ~POLAR sigils in Complex Co-authored-by: Claude Sonnet 5.5 --- README.md | 12 +++---- lib/complex.ex | 63 ++++++++++++++++++++++++++++++++++++ lib/complex/sigils.ex | 45 -------------------------- test/complex/sigils_test.exs | 35 ++++++++++++-------- 4 files changed, 91 insertions(+), 64 deletions(-) delete mode 100644 lib/complex/sigils.ex diff --git a/README.md b/README.md index c41d207..7c76fd7 100644 --- a/README.md +++ b/README.md @@ -46,16 +46,16 @@ iex> Complex.new(0, 1) ### Sigil -`import Complex.Sigils` gives you the `~z` sigil. -The two numbers are separated by whitespace, mirroring Common Lisp's -`#c(re im)` reader syntax. With the `p` modifier they are the magnitude and -the angle (radians) instead: +`import Complex, only: :sigils` gives you the `~COMPLEX` and `~POLAR` sigils. +The two numbers are separated by whitespace; `~COMPLEX` mirrors Common +Lisp's `#c(re im)` reader syntax. They are meant for literals; for +variables use `Complex.new/2` and `Complex.from_polar/2`: ```elixir -iex> ~z(3 4) +iex> ~COMPLEX(3 4) %Complex{im: 4, re: 3} -iex> ~z(2 1.57)p +iex> ~POLAR(2 1.57) Complex.from_polar(2, 1.57) ``` diff --git a/lib/complex.ex b/lib/complex.ex index 1b79384..ef8f8ff 100644 --- a/lib/complex.ex +++ b/lib/complex.ex @@ -288,6 +288,69 @@ defmodule Complex do end end + @doc ~S""" + Handles the sigil `~COMPLEX` for complex numbers in cartesian form. + + The real and imaginary parts are separated by whitespace, mirroring + Common Lisp's `#c(re im)` reader syntax. Uppercase sigils do not + interpolate, so for variables use `new/2`. + + Use `import Complex, only: :sigils` to make it available. + + ### See also + + `new/2`, `sigil_POLAR/2` + + ### Examples + + import Complex, only: :sigils + + ~COMPLEX(3 4) + #=> %Complex{re: 3, im: 4} + """ + @spec sigil_COMPLEX(String.t(), []) :: t + def sigil_COMPLEX(string, []) do + [re, im] = sigil_numbers(string) + new(re, im) + end + + @doc ~S""" + Handles the sigil `~POLAR` for complex numbers in polar form. + + The magnitude and the angle (in radians) are separated by whitespace. + Uppercase sigils do not interpolate, so for variables use `from_polar/2`. + + Use `import Complex, only: :sigils` to make it available. + + ### See also + + `from_polar/2`, `sigil_COMPLEX/2` + + ### Examples + + import Complex, only: :sigils + + ~POLAR(2 1.57) + #=> Complex.from_polar(2, 1.57) + """ + @spec sigil_POLAR(String.t(), []) :: t + def sigil_POLAR(string, []) do + [r, phi] = sigil_numbers(string) + from_polar(r, phi) + end + + defp sigil_numbers(string), do: string |> String.split() |> Enum.map(&sigil_number/1) + + defp sigil_number(str) do + with :error <- exact_parse(Integer.parse(str)), + :error <- exact_parse(Float.parse(str)) do + raise ArgumentError, "invalid number in complex sigil: #{inspect(str)}" + end + end + + defp exact_parse({n, ""}), do: n + defp exact_parse(_), do: :error + @doc """ Returns the phase angle of the supplied complex, in radians. diff --git a/lib/complex/sigils.ex b/lib/complex/sigils.ex deleted file mode 100644 index df2ba68..0000000 --- a/lib/complex/sigils.ex +++ /dev/null @@ -1,45 +0,0 @@ -defmodule Complex.Sigils do - @moduledoc """ - Sigils to build complex numbers. - """ - - @doc ~S""" - Handles the sigil `~z` for complex numbers. - - The two numbers are separated by whitespace, mirroring Common Lisp's - `#c(re im)` reader syntax. Without modifiers they are the real and - imaginary parts. With the `p` modifier (an extension specific to this - library) they are the magnitude and the angle (radians). - - ## Examples - - import Complex.Sigils - - ~z(3 4) - #=> %Complex{re: 3, im: 4} - - ~z(2 1.57)p - #=> Complex.from_polar(2, 1.57) - """ - def sigil_z(string, []) do - [re, im] = parse(string) - Complex.new(re, im) - end - - def sigil_z(string, [?p]) do - [r, phi] = parse(string) - Complex.from_polar(r, phi) - end - - defp parse(string), do: string |> String.split() |> Enum.map(&to_number/1) - - defp to_number(str) do - with :error <- parse_exact(Integer.parse(str)), - :error <- parse_exact(Float.parse(str)) do - raise ArgumentError, "invalid number in ~z sigil: #{inspect(str)}" - end - end - - defp parse_exact({n, ""}), do: n - defp parse_exact(_), do: :error -end diff --git a/test/complex/sigils_test.exs b/test/complex/sigils_test.exs index c899ff0..3668acb 100644 --- a/test/complex/sigils_test.exs +++ b/test/complex/sigils_test.exs @@ -1,34 +1,43 @@ defmodule Complex.SigilsTest do use ExUnit.Case, async: true - import Complex.Sigils + import Complex, only: :sigils - test "cartesian form with integers" do - assert ~z(3 4) == Complex.new(3, 4) + test "~COMPLEX builds a complex number from integers" do + assert ~COMPLEX(3 4) == Complex.new(3, 4) end - test "cartesian form keeps integers and floats as written" do - assert ~z(3 -4.5) === Complex.new(3, -4.5) + test "~COMPLEX keeps integers and floats as written" do + assert ~COMPLEX(3 -4.5) === Complex.new(3, -4.5) end test "extra whitespace is ignored" do - assert ~z( 3 4 ) == Complex.new(3, 4) + assert ~COMPLEX( 3 4 ) == Complex.new(3, 4) end - test "polar form with the p modifier" do - assert ~z(2 1.57)p == Complex.from_polar(2, 1.57) + test "~POLAR builds a complex number from magnitude and angle" do + assert ~POLAR(2 1.57) == Complex.from_polar(2, 1.57) end test "it raises on an invalid number" do - assert_raise ArgumentError, ~r/invalid number/, fn -> sigil_z("3 x", []) end + assert_raise ArgumentError, ~r/invalid number/, fn -> + Complex.sigil_COMPLEX("3 x", []) + end + + assert_raise ArgumentError, ~r/invalid number/, fn -> + Complex.sigil_POLAR("3 x", []) + end end test "it raises when there are not exactly two numbers" do - assert_raise MatchError, fn -> sigil_z("3", []) end - assert_raise MatchError, fn -> sigil_z("1 2 3", []) end + assert_raise MatchError, fn -> Complex.sigil_COMPLEX("3", []) end + assert_raise MatchError, fn -> Complex.sigil_POLAR("1 2 3", []) end end - test "it raises on an unknown modifier" do - assert_raise FunctionClauseError, fn -> sigil_z("3 4", ~c"x") end + test "it raises on modifiers" do + # apply/3 keeps the compiler's type checker from flagging the call, + # which can never match the `[]` modifiers clause + assert_raise FunctionClauseError, fn -> apply(Complex, :sigil_COMPLEX, ["3 4", ~c"x"]) end + assert_raise FunctionClauseError, fn -> apply(Complex, :sigil_POLAR, ["3 4", ~c"p"]) end end end