-
-
Notifications
You must be signed in to change notification settings - Fork 398
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Raindrops 48in24 approaches #1422
base: main
Are you sure you want to change the base?
Changes from 5 commits
51b120b
4bc6253
d0685c9
d37fe8e
c8008f4
6da6b4a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
# Pattern Matching | ||
|
||
```elixir | ||
defmodule Raindrops do | ||
@spec convert(pos_integer) :: String.t() | ||
def convert(number) do | ||
case {rem(number, 3), rem(number, 5), rem(number, 7)} do | ||
{0, 0, 0} -> "PlingPlangPlong" | ||
{0, 0, _} -> "PlingPlang" | ||
{0, _, 0} -> "PlingPlong" | ||
{_, 0, 0} -> "PlangPlong" | ||
{0, _, _} -> "Pling" | ||
{_, 0, _} -> "Plang" | ||
{_, _, 0} -> "Plong" | ||
_ -> Integer.to_string(number) | ||
end | ||
end | ||
end | ||
``` | ||
|
||
## Case | ||
The case allows us to evaluate if a number is divisible by 3, 5 and 7 once, and then match the results to various combinations of the possible outcomes. | ||
The advantage of using a `case` on a tuple, like in the example above, is that the `rem` functions are executed only once. | ||
|
||
## Cond | ||
We can use `cond do`, too. | ||
However, first, let's look at the maths to make the solution more compact. | ||
|
||
A number is divisible by `a`, `b`, and `c` only when it is divisible by `a*b*c`. | ||
So, instead of | ||
```elixir | ||
rem(number, 3) == 0 and rem(number, 5) == 0 and rem(number, 7) == 0 | ||
``` | ||
we can write | ||
```elixir | ||
rem(number, 3*5*7) == 0 | ||
``` | ||
|
||
Now, let's look at this pattern matching with `cond`. | ||
|
||
```elixir | ||
def convert(number) do | ||
cond do | ||
rem(number, 3*5*7) == 0 -> "PlingPlangPlong" | ||
rem(number, 3*5) == 0 -> "PlingPlang" | ||
rem(number, 3*7) == 0 -> "PlingPlong" | ||
rem(number, 5*7) == 0 -> "PlangPlong" | ||
rem(number, 3) == 0 -> "Pling" | ||
rem(number, 5) == 0 -> "Plang" | ||
rem(number, 7) == 0 -> "Plong" | ||
true -> Integer.to_string(number) | ||
end | ||
end | ||
``` | ||
|
||
## Multiple-clause functions | ||
We can do something very similar by using guards in multi-clause functions. | ||
We use different feautre of the language, but at its core, the approach is the same. | ||
|
||
```elixir | ||
defmodule Raindrops do | ||
@spec convert(pos_integer) :: String.t() | ||
def convert(number) when rem(number, 3*5*7) == 0, do: "PlingPlangPlong" | ||
def convert(number) when rem(number, 3*5) == 0, do: "PlingPlang" | ||
def convert(number) when rem(number, 3*7) == 0, do: "PlingPlong" | ||
michalporeba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def convert(number) when rem(number, 5*7) == 0, do: "PlangPlong" | ||
def convert(number) when rem(number, 3) == 0, do: "Pling" | ||
def convert(number) when rem(number, 5) == 0, do: "Plang" | ||
def convert(number) when rem(number, 7) == 0, do: "Plong" | ||
def convert(number), do: Integer.to_string(number) | ||
end | ||
``` | ||
|
||
We can use different features of the language, but at its core, the approach is the same. | ||
We check a set of conditions that leads us to the exact answer. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
def convert(number) do | ||
case {rem(number, 3), rem(number, 5), rem(number, 7)} do | ||
{0, 0, 0} -> "PlingPlangPlong" | ||
{0, 0, _} -> "PlingPlang" | ||
{0, _, 0} -> "PlingPlong" | ||
{_, 0, 0} -> "PlangPlong" | ||
{0, _, _} -> "Pling" | ||
{_, 0, _} -> "Plang" | ||
{_, _, 0} -> "Plong" | ||
_ -> Integer.to_string(number) | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
{ | ||
"approaches": [ | ||
{ | ||
michalporeba marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"uuid": "fbfbabb4-f4e6-4329-b2f5-a93e3199b809", | ||
"slug": "check-every-possibility", | ||
"title": "Every Possibility", | ||
"blurb": "Check every possibility.", | ||
"authors": [ | ||
"michalporeba" | ||
] | ||
}, | ||
{ | ||
"uuid": "925ccb59-3414-472b-9054-1cdfc5e44fad", | ||
"slug": "step-by-step", | ||
"title": "Step By Step", | ||
"blurb": "Perform the checks one by one, step by step.", | ||
"authors": [ | ||
"michalporeba" | ||
] | ||
} | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# Introduction | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think my preference would be to try to start the approaches documents with the "most reasonable" solution first, and explore the more exotic solutions further down. I know that's very subjective, but I hope we can agree on which one is the best 😁 I have a strong opinion for this exercise that the best solution uses a list that pairs divisors to sounds, and then iterates over them like this one: https://exercism.org/tracks/elixir/exercises/raindrops/solutions/SaberCon I don't know if you would qualify that as a variant of a "step by step" approach? You didn't mention anything with Btw. In this exercise it's important to note that maps in Elixir are not sorted. This solution for example https://exercism.org/tracks/elixir/exercises/raindrops/solutions/alkhulaifi has to sort the map first before iterating, which clearly suggests a list of two-tuples is a better data structure here. |
||
|
||
## Check every possibility | ||
|
||
The output of the `convert` method depends on three conditions which can be either true or false. | ||
This gives only eight possibilities and we can check them all. | ||
|
||
```elixir | ||
def convert(number) do | ||
case {rem(number, 3), rem(number, 5), rem(number, 7)} do | ||
{0, 0, 0} -> "PlingPlangPlong" | ||
{0, 0, _} -> "PlingPlang" | ||
{0, _, 0} -> "PlingPlong" | ||
{_, 0, 0} -> "PlangPlong" | ||
{0, _, _} -> "Pling" | ||
{_, 0, _} -> "Plang" | ||
{_, _, 0} -> "Plong" | ||
_ -> Integer.to_string(number) | ||
end | ||
Comment on lines
+10
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was actually shocked to see this approach, but people actually use it 😮 (found: Can we put it at the bottom of the document? |
||
end | ||
``` | ||
|
||
We can use a few Elixir features to do more or less the same and we explore them in the [check every possibility approach][check-every-possibility-approach]. | ||
|
||
## Step by step | ||
|
||
An alternative approach is to consider each condition one at a time. | ||
At each step we return either a sound (i.e. "Pling", "Plang", or "Plong"), or an empty string. | ||
We can then concatenate the strings together. | ||
|
||
```elixir | ||
def convert(number) do | ||
pling = if rem(number, 3) == 0, do: "Pling", else: "" | ||
plang = if rem(number, 5) == 0, do: "Plang", else: "" | ||
plong = if rem(number, 7) == 0, do: "Plong", else: "" | ||
result = pling <> plang <> plong | ||
|
||
if result == "" do | ||
Integer.to_string(number) | ||
else | ||
result | ||
end | ||
end | ||
``` | ||
|
||
Let's have a look at a few variations of this [step by step approach][step-by-step-approach]. | ||
|
||
[check-every-possibility-approach]: https://exercism.org/tracks/elixir/exercises/raindrops/approaches/check-every-possibility | ||
[step-by-step-approach]: https://exercism.org/tracks/elixir/exercises/raindrops/approaches/step-by-step |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
# Step By Step | ||
|
||
```elixir | ||
defmodule Raindrops do | ||
@spec convert(pos_integer) :: String.t() | ||
def convert(number) do | ||
pling = if rem(number, 3) == 0, do: "Pling", else: "" | ||
plang = if rem(number, 5) == 0, do: "Plang", else: "" | ||
plong = if rem(number, 7) == 0, do: "Plong", else: "" | ||
sound = pling <> plang <> plong | ||
|
||
if sound == "" do | ||
Integer.to_string(number) | ||
else | ||
sound | ||
end | ||
end | ||
end | ||
``` | ||
|
||
In this approach, we test each condition only once, similar to using the `case` on a tuple in the [pattern matching approach][pattern-matching-approach]. | ||
However, this time, if a condition is true, we capture the sound component, and if it is not true, we capture the sound as an empty string. | ||
|
||
Once this is done, we can concatenate all three conditions to get the full sound. | ||
Finally, if the `sound` is empty, we can return the number or, alternatively, the calculated `sound`. | ||
|
||
## Functions | ||
|
||
We can create private functions to test for component sounds. | ||
|
||
```elixir | ||
defp pling(n) when rem(n, 3) == 0, do: "Pling" | ||
defp pling(_), do: "" | ||
defp plang(n) when rem(n, 5) == 0, do: "Plang" | ||
defp plang(_), do: "" | ||
defp plong(n) when rem(n, 7) == 0, do: "Plong" | ||
defp plong(_), do: "" | ||
defp sound(sound, number) when sound == "", do: Integer.to_string(number) | ||
defp sound(sound, _number), do: sound | ||
``` | ||
|
||
Now the solution can look like this: | ||
```elixir | ||
def convert(number) do | ||
sound(pling(number) <> plang(number) <> plong(number), number) | ||
end | ||
``` | ||
Comment on lines
+1
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. String concatenation isn't used by Elixir devs that often. It has a big disadvantage - it crashes on Using string interpolation in both of those solutions would make them slightly shorter: defmodule Raindrops do
@spec convert(pos_integer) :: String.t()
def convert(number) do
pling = if rem(number, 3) == 0, do: "Pling"
plang = if rem(number, 5) == 0, do: "Plang"
plong = if rem(number, 7) == 0, do: "Plong"
sound = "#{pling}#{plang}#{plong}"
if sound == "" do
Integer.to_string(number)
else
sound
end
end
end defp pling(n), do: if(rem(n, 3) == 0, do: "Pling")
defp plang(n), do: if(rem(n, 5) == 0, do: "Plang")
defp plong(n), do: if(rem(n, 7) == 0, do: "Plong")
defp sound(sound, number) when sound == "", do: Integer.to_string(number)
defp sound(sound, _number), do: sound
def convert(number) do
sound("#{pling(number)}#{plang(number)}#{plong(number)}", number)
end |
||
|
||
## The pipe operator | ||
|
||
With a slightly different design of the functions we can use the pipe operator to have a very clean-looking code | ||
|
||
```elixir | ||
@spec convert(pos_integer) :: String.t() | ||
def convert(number) do | ||
{"", number} | ||
|> pling | ||
|> plang | ||
|> plong | ||
|> sound | ||
end | ||
``` | ||
At least in the `convert` functions. The `pling`, `plang`, `plong` become a bit more complex: | ||
```elixir | ||
defp pling({ s, n }) when rem(n, 3) == 0, do: { s <> "Pling", n } | ||
defp pling({ s, n }), do: { s, n } | ||
defp plang({ s, n }) when rem(n, 5) == 0, do: { s <> "Plang", n } | ||
defp plang({ s, n }), do: { s, n } | ||
defp plong({ s, n }) when rem(n, 7) == 0, do: { s <> "Plong", n } | ||
defp plong({ s, n }), do: { s, n } | ||
defp sound({ s, n }) when s == "" , do: n |> Integer.to_string | ||
defp sound({ s, _ }), do: s | ||
``` | ||
|
||
All the examples above, at their core, represent the same approach of doing the check step by step. | ||
|
||
[pattern-matching-approach]: https://exercism.org/tracks/elixir/exercises/raindrops/approaches/pattern-matching | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the pattern matching approach for this exercise? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
def convert(number) do | ||
pling = if rem(number, 3) == 0, do: "Pling", else: "" | ||
plang = if rem(number, 5) == 0, do: "Plang", else: "" | ||
plong = if rem(number, 7) == 0, do: "Plong", else: "" | ||
result = pling <> plang <> plong | ||
|
||
if result == "" do | ||
Integer.to_string(number) | ||
else | ||
result | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrong heading