diff --git a/README.md b/README.md index a5a227c..7c76fd7 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,21 @@ iex> Complex.new(0, 1) %Complex{im: 1.0, re: 0.0} ``` +### Sigil + +`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> ~COMPLEX(3 4) +%Complex{im: 4, re: 3} + +iex> ~POLAR(2 1.57) +Complex.from_polar(2, 1.57) +``` + ## License Copyright 2015 Thomas Krauss 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/test/complex/sigils_test.exs b/test/complex/sigils_test.exs new file mode 100644 index 0000000..3668acb --- /dev/null +++ b/test/complex/sigils_test.exs @@ -0,0 +1,43 @@ +defmodule Complex.SigilsTest do + use ExUnit.Case, async: true + + import Complex, only: :sigils + + test "~COMPLEX builds a complex number from integers" do + assert ~COMPLEX(3 4) == Complex.new(3, 4) + end + + 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 ~COMPLEX( 3 4 ) == Complex.new(3, 4) + end + + 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 -> + 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 -> Complex.sigil_COMPLEX("3", []) end + assert_raise MatchError, fn -> Complex.sigil_POLAR("1 2 3", []) end + 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