chessboard

This commit is contained in:
Ruidy 2022-02-16 16:32:44 -04:00
parent 3b7a535a6e
commit c6177f9612
34 changed files with 1151 additions and 0 deletions

View file

@ -0,0 +1,27 @@
{
"blurb": "Convert a number, represented as a sequence of digits in one base, to any other base.",
"authors": [
"ananthamapod"
],
"contributors": [
"angelikatyborska",
"ChristianTovar",
"Cohen-Carlisle",
"devonestes",
"neenjaw",
"parkerl",
"sotojuan",
"ybod"
],
"files": {
"solution": [
"lib/all_your_base.ex"
],
"test": [
"test/all_your_base_test.exs"
],
"example": [
".meta/example.ex"
]
}
}

View file

@ -0,0 +1 @@
{"track":"elixir","exercise":"all-your-base","id":"7674c05295ec47618e00a878143e5422","url":"https://exercism.org/tracks/elixir/exercises/all-your-base","handle":"rjNemo","is_requester":true,"auto_approve":false}

View file

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
all-your-base/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
all_your_base-*.tar

75
all-your-base/HELP.md Normal file
View file

@ -0,0 +1,75 @@
# Help
## Running the tests
From the terminal, change to the base directory of the exercise then execute the tests with:
```bash
$ mix test
```
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
Documentation:
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
## Pending tests
In test suites of practice exercises, all but the first test have been tagged to be skipped.
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
For example:
```elixir
# @tag :pending
test "shouting" do
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end
```
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
```bash
$ mix test --include pending
```
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
```elixir
# ExUnit.configure(exclude: :pending, trace: true)
```
## Useful `mix test` options
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
* `--failed` - runs only tests that failed the last time they ran
* `--max-failures` - the suite stops evaluating tests when this number of test failures
is reached
* `--seed 0` - disables randomization so the tests in a single file will always be ran
in the same order they were defined in
## Submitting your solution
You can submit your solution using the `exercism submit lib/all_your_base.ex` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).

54
all-your-base/README.md Normal file
View file

@ -0,0 +1,54 @@
# All Your Base
Welcome to All Your Base on Exercism's Elixir Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
## Instructions
Convert a number, represented as a sequence of digits in one base, to any other base.
Implement general base conversion. Given a number in base **a**,
represented as a sequence of digits, convert it to base **b**.
## Note
- Try to implement the conversion yourself.
Do not use something else to perform the conversion for you.
## About [Positional Notation](https://en.wikipedia.org/wiki/Positional_notation)
In positional notation, a number in base **b** can be understood as a linear
combination of powers of **b**.
The number 42, *in base 10*, means:
(4 \* 10^1) + (2 \* 10^0)
The number 101010, *in base 2*, means:
(1 \* 2^5) + (0 \* 2^4) + (1 \* 2^3) + (0 \* 2^2) + (1 \* 2^1) + (0 \* 2^0)
The number 1120, *in base 3*, means:
(1 \* 3^3) + (1 \* 3^2) + (2 \* 3^1) + (0 \* 3^0)
I think you got the idea!
*Yes. Those three numbers above are exactly the same. Congratulations!*
## Source
### Created by
- @ananthamapod
### Contributed to by
- @angelikatyborska
- @ChristianTovar
- @Cohen-Carlisle
- @devonestes
- @neenjaw
- @parkerl
- @sotojuan
- @ybod

View file

@ -0,0 +1,27 @@
defmodule AllYourBase do
@doc """
Given a number in input base, represented as a sequence of digits, converts it to output base,
or returns an error tuple if either of the bases are less than 2
"""
@spec convert(list, integer, integer) :: {:ok, list} | {:error, String.t()}
def convert(digits, input_base, 10), do: {:ok, [to_decimal(digits, input_base)]}
def convert(digits, input_base, output_base) do
number = to_decimal(digits, input_base)
do_convert(digits, input_base, output_base,[rem(number, output_base)])
end
defp do_convert(digits,input_base, output_base,converted) do
end
defp to_decimal(digits, input_base) do
n = length(digits)
for {d, i} <- Enum.with_index(digits), reduce: 0 do
acc -> d * input_base ** (n - i - 1) + acc
end
end
end

28
all-your-base/mix.exs Normal file
View file

@ -0,0 +1,28 @@
defmodule AllYourBase.MixProject do
use Mix.Project
def project do
[
app: :all_your_base,
version: "0.1.0",
# elixir: "~> 1.8",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

View file

@ -0,0 +1,107 @@
defmodule AllYourBaseTest do
use ExUnit.Case
test "convert single bit one to decimal" do
assert AllYourBase.convert([1], 2, 10) == {:ok, [1]}
end
test "convert binary to single decimal" do
assert AllYourBase.convert([1, 0, 1], 2, 10) == {:ok, [5]}
end
test "convert single decimal to binary" do
assert AllYourBase.convert([5], 10, 2) == {:ok, [1, 0, 1]}
end
@tag :pending
test "convert binary to multiple decimal" do
assert AllYourBase.convert([1, 0, 1, 0, 1, 0], 2, 10) == {:ok, [4, 2]}
end
@tag :pending
test "convert decimal to binary" do
assert AllYourBase.convert([4, 2], 10, 2) == {:ok, [1, 0, 1, 0, 1, 0]}
end
@tag :pending
test "convert trinary to hexadecimal" do
assert AllYourBase.convert([1, 1, 2, 0], 3, 16) == {:ok, [2, 10]}
end
@tag :pending
test "convert hexadecimal to trinary" do
assert AllYourBase.convert([2, 10], 16, 3) == {:ok, [1, 1, 2, 0]}
end
@tag :pending
test "convert 15-bit integer" do
assert AllYourBase.convert([3, 46, 60], 97, 73) == {:ok, [6, 10, 45]}
end
@tag :pending
test "convert empty list" do
assert AllYourBase.convert([], 2, 10) == {:ok, [0]}
end
@tag :pending
test "convert single zero" do
assert AllYourBase.convert([0], 10, 2) == {:ok, [0]}
end
@tag :pending
test "convert multiple zeros" do
assert AllYourBase.convert([0, 0, 0], 10, 2) == {:ok, [0]}
end
@tag :pending
test "convert leading zeros" do
assert AllYourBase.convert([0, 6, 0], 7, 10) == {:ok, [4, 2]}
end
@tag :pending
test "convert first base is one" do
assert AllYourBase.convert([0], 1, 10) == {:error, "input base must be >= 2"}
end
@tag :pending
test "convert first base is zero" do
assert AllYourBase.convert([], 0, 10) == {:error, "input base must be >= 2"}
end
@tag :pending
test "convert first base is negative" do
assert AllYourBase.convert([1], -2, 10) == {:error, "input base must be >= 2"}
end
@tag :pending
test "convert negative digit" do
assert AllYourBase.convert([1, -1, 1, 0, 1, 0], 2, 10) ==
{:error, "all digits must be >= 0 and < input base"}
end
@tag :pending
test "convert invalid positive digit" do
assert AllYourBase.convert([1, 2, 1, 0, 1, 0], 2, 10) ==
{:error, "all digits must be >= 0 and < input base"}
end
@tag :pending
test "convert second base is one" do
assert AllYourBase.convert([1, 0, 1, 0, 1, 0], 2, 1) == {:error, "output base must be >= 2"}
end
@tag :pending
test "convert second base is zero" do
assert AllYourBase.convert([7], 10, 0) == {:error, "output base must be >= 2"}
end
@tag :pending
test "convert second base is negative" do
assert AllYourBase.convert([1], 2, -7) == {:error, "output base must be >= 2"}
end
@tag :pending
test "convert both bases are negative" do
assert AllYourBase.convert([1], -2, -7) == {:error, "output base must be >= 2"}
end
end

View file

@ -0,0 +1,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true)

View file

@ -0,0 +1,21 @@
{
"blurb": "Learn about ranges by generating a chessboard.",
"authors": [
"angelikatyborska"
],
"contributors": [
"neenjaw"
],
"files": {
"solution": [
"lib/chessboard.ex"
],
"test": [
"test/chessboard_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
]
},
"language_versions": ">=1.10"
}

View file

@ -0,0 +1 @@
{"track":"elixir","exercise":"chessboard","id":"5ffb191bbfc94a8eadae73a4626d115e","url":"https://exercism.org/tracks/elixir/exercises/chessboard","handle":"rjNemo","is_requester":true,"auto_approve":false}

View file

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
chessboard/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
match_binary-*.tar

75
chessboard/HELP.md Normal file
View file

@ -0,0 +1,75 @@
# Help
## Running the tests
From the terminal, change to the base directory of the exercise then execute the tests with:
```bash
$ mix test
```
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
Documentation:
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
## Pending tests
In test suites of practice exercises, all but the first test have been tagged to be skipped.
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
For example:
```elixir
# @tag :pending
test "shouting" do
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end
```
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
```bash
$ mix test --include pending
```
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
```elixir
# ExUnit.configure(exclude: :pending, trace: true)
```
## Useful `mix test` options
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
* `--failed` - runs only tests that failed the last time they ran
* `--max-failures` - the suite stops evaluating tests when this number of test failures
is reached
* `--seed 0` - disables randomization so the tests in a single file will always be ran
in the same order they were defined in
## Submitting your solution
You can submit your solution using the `exercism submit lib/chessboard.ex` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).

32
chessboard/HINTS.md Normal file
View file

@ -0,0 +1,32 @@
# Hints
## General
- Read the official documentation for [ranges][range].
## 1. Define the rank range
- There is a [special operator][range-creation-operator] for creating ranges.
## 2. Define the file range
- There is a [special operator][range-creation-operator] for creating ranges.
- There is a [special syntax][unicode-code-points] to write a character code point without explicitly knowing its value.
## 3. Transform the rank range into a list of ranks
- Ranges implement the `Enumerable` protocol.
- There is a [built-in function][enum-to-list] to change an enumerable data structure to a list.
## 4. Transform the file range into a list of files
- Ranges implement the `Enumerable` protocol.
- There is a [built-in function][enum-map] to change an enumerable data structure to a list while modifying its elements.
- The [bitstring special form][bitstring-special-form] can be used to turn a code point into a string.
[range]: https://hexdocs.pm/elixir/Range.html
[range-creation-operator]: https://hexdocs.pm/elixir/Kernel.html#../2
[unicode-code-points]: https://hexdocs.pm/elixir/syntax-reference.html#integers-in-other-bases-and-unicode-code-points
[enum-to-list]: https://hexdocs.pm/elixir/Enum.html#to_list/1
[enum-map]: https://hexdocs.pm/elixir/Enum.html#map/2
[bitstring-special-form]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#%3C%3C%3E%3E/1

71
chessboard/README.md Normal file
View file

@ -0,0 +1,71 @@
# Chessboard
Welcome to Chessboard on Exercism's Elixir Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
## Introduction
## Ranges
Ranges represent a sequence of one or many consecutive integers. They are created by connecting two integers with `..`.
```elixir
1..5
```
Ranges can be ascending or descending. They are always inclusive of the first and last values.
A range implements the _Enumerable protocol_, which means functions in the `Enum` module can be used to work with ranges.
## Instructions
As a chess enthusiast, you would like to write your own version of the game. Yes, there maybe plenty of implementations of chess available online already, but yours will be unique!
But before you can let your imagination run wild, you need to take care of the basics. Let's start by generating the board.
Each square of the chessboard is identified by a letter-number pair. The vertical columns of squares, called files, are labeled A through H. The horizontal rows of squares, called ranks, are numbered 1 to 8.
## 1. Define the rank range
Implement the `rank_range/0` function. It should return a range of integers, from 1 to 8.
```elixir
Chessboard.rank_range()
```
## 2. Define the file range
Implement the `file_range/0` function. It should return a range of integers, from the code point of the uppercase letter A, to the code point of the uppercase letter H.
```elixir
Chessboard.file_range()
```
## 3. Transform the rank range into a list of ranks
Implement the `ranks/0` function. It should return a list of integers, from 1 to 8. Do not write the list by hand, generate it from the range returned by the `rank_range/0` function.
```elixir
Chessboard.ranks()
# => [1, 2, 3, 4, 5, 6, 7, 8]
```
## 4. Transform the file range into a list of files
Implement the `files/0` function. It should return a list of letters (strings), from "A" to "H". Do not write the list by hand, generate it from the range returned by the `file_range/0` function.
```elixir
Chessboard.files()
# => ["A", "B", "C", "D", "E", "F", "G", "H"]
```
## Source
### Created by
- @angelikatyborska
### Contributed to by
- @neenjaw

View file

@ -0,0 +1,11 @@
defmodule Chessboard do
def rank_range, do: 1..8
def file_range, do: ?A..?H
def ranks, do: Enum.to_list(rank_range())
def files do
file_range() |> Enum.map(&<<&1>>)
end
end

28
chessboard/mix.exs Normal file
View file

@ -0,0 +1,28 @@
defmodule Chessboard.MixProject do
use Mix.Project
def project do
[
app: :chessboard,
version: "0.1.0",
# elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

View file

@ -0,0 +1,23 @@
defmodule ChessboardTest do
use ExUnit.Case
@tag task_id: 1
test "rank_range is a range from 1 to 8" do
assert Chessboard.rank_range() == 1..8
end
@tag task_id: 2
test "file_range is a range from ?A to ?H" do
assert Chessboard.file_range() == ?A..?H
end
@tag task_id: 3
test "ranks is a list of integers from 1 to 8" do
assert Chessboard.ranks() == [1, 2, 3, 4, 5, 6, 7, 8]
end
@tag task_id: 4
test "files is a list of letters (strings) from A to H" do
assert Chessboard.files() == ["A", "B", "C", "D", "E", "F", "G", "H"]
end
end

View file

@ -0,0 +1,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true, seed: 0)

View file

@ -0,0 +1,21 @@
{
"blurb": "Learn about working with files by sending out a newsletter.",
"authors": [
"angelikatyborska"
],
"contributors": [
"neenjaw"
],
"files": {
"solution": [
"lib/newsletter.ex"
],
"test": [
"test/newsletter_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
]
},
"language_versions": ">=1.10"
}

View file

@ -0,0 +1 @@
{"track":"elixir","exercise":"newsletter","id":"ce997462e3674d26b979803a6147c540","url":"https://exercism.org/tracks/elixir/exercises/newsletter","handle":"rjNemo","is_requester":true,"auto_approve":false}

View file

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

24
newsletter/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
match_binary-*.tar

75
newsletter/HELP.md Normal file
View file

@ -0,0 +1,75 @@
# Help
## Running the tests
From the terminal, change to the base directory of the exercise then execute the tests with:
```bash
$ mix test
```
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
Documentation:
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
## Pending tests
In test suites of practice exercises, all but the first test have been tagged to be skipped.
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
For example:
```elixir
# @tag :pending
test "shouting" do
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end
```
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
```bash
$ mix test --include pending
```
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
```elixir
# ExUnit.configure(exclude: :pending, trace: true)
```
## Useful `mix test` options
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
* `--failed` - runs only tests that failed the last time they ran
* `--max-failures` - the suite stops evaluating tests when this number of test failures
is reached
* `--seed 0` - disables randomization so the tests in a single file will always be ran
in the same order they were defined in
## Submitting your solution
You can submit your solution using the `exercism submit lib/newsletter.ex` command.
This command will upload your solution to the Exercism website and print the solution page's URL.
It's possible to submit an incomplete solution which allows you to:
- See how others have completed the exercise
- Request help from a mentor
## Need to get help?
If you'd like help solving the exercise, check the following pages:
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
- [Exercism's support channel on gitter](https://gitter.im/exercism/support)
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.
If you can't find what you're looking for in the documentation, feel free to ask help in the Exercism's BEAM [gitter channel](https://gitter.im/exercism/xerlang).

40
newsletter/HINTS.md Normal file
View file

@ -0,0 +1,40 @@
# Hints
## 1. General
- Read about files in the official [Getting Started guide][getting-started-file].
- Read about files on [joyofelixir.com][joy-of-elixir-file].
- Take a look at the [documentation of the `File` module][file].
## 1. Read email addresses from a file
- There is a [built-in function][file-read] for reading the contents of a file all at once.
## 2. Open a log file for writing
- There is a [built-in function][file-open] for opening a file.
- The second argument of that function is a list of modes which allows specifying that the file should be opened for writing.
## 3. Log a sent email
- Functions for reading and writing to a file opened with [`File.open!/1`][file-open] can be found in the [`IO`][io] module.
- There is a [built-in function][io-puts] for writing a string to a file, followed by a newline.
## 4. Close the log file
- There is a [built-in function][file-close] for closing a file.
## 5. Send the newsletter
- All the necessary operations on files were already implemented in the previous steps.
- Before writing to a file, the file must be opened.
- After all write operations to a file finished, the file should be closed.
[getting-started-file]: https://elixir-lang.org/getting-started/io-and-the-file-system.html#the-file-module
[joy-of-elixir-file]: https://joyofelixir.com/11-files/
[file]: https://hexdocs.pm/elixir/File.html
[file-read]: https://hexdocs.pm/elixir/File.html#read!/1
[file-open]: https://hexdocs.pm/elixir/File.html#open!/1
[file-close]: https://hexdocs.pm/elixir/File.html#close/1
[io]: https://hexdocs.pm/elixir/IO.html
[io-puts]: https://hexdocs.pm/elixir/IO.html#puts/2

90
newsletter/README.md Normal file
View file

@ -0,0 +1,90 @@
# Newsletter
Welcome to Newsletter on Exercism's Elixir Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
## Introduction
## File
Functions for working with files are provided by the `File` module.
To read a whole file, use `File.read/1`. To write to a file, use `File.write/2`.
Every time a file is written to with `File.write/2`, a file descriptor is opened and a new Elixir process is spawned. For this reason, writing to a file in a loop using `File.write/2` should be avoided.
Instead, a file can be opened using `File.open/2`. The second argument to `File.open/2` is a list of modes, which allows you to specify if you want to open the file for reading or for writing.
`File.open/2` returns a PID of a process that handles the file. To read and write to the file, use functions from the `IO` module and pass this PID as the IO device.
When you're finished working with the file, close it with `File.close/1`.
All the mentioned functions from the `File` module also have a `!` variant that raises an error instead of returning an error tuple (e.g. `File.read!/1`). Use that variant if you don't intend to handle errors such as missing files or lack of permissions.
## Instructions
You're a big model train enthusiast and have decided to share your passion with the world by starting a newsletter. You'll start by sending the first issue of your newsletter to your friends and acquaintances that share your hobby. You have a text file with a list of their email addresses.
## 1. Read email addresses from a file
Implement the `Newsletter.read_emails/1` function. It should take a file path. The file is a text file that contains email addresses separated by newlines. The function should return a list of the email addresses from the file.
```elixir
Newsletter.read_emails("/home/my_user/documents/model_train_friends_emails.txt")
# => ["rick@example.com", "choochoo42@example.com", "anna@example.com"]
```
## 2. Open a log file for writing
Sending an email is a task that might fail for many unpredictable reasons, like a typo in the email address or temporary network issues. To ensure that you can retry sending the emails to all your friends without sending duplicates, you need to log the email addresses that already received the email. For this, you'll need a log file.
Implement the `Newsletter.open_log/1` function. It should take a file path, open the file for writing, and return the PID of the process that handles the file.
```elixir
Newsletter.open_log("/home/my_user/documents/newsletter_issue1_log.txt")
# => #PID<0.145.0>
```
## 3. Log a sent email
Implement the `Newsletter.log_sent_email/2` function. It should take a PID of the process that handles the file and a string with the email address. It should write the email address to the file, followed by a newline.
```elixir
Newsletter.log_sent_email(pid, "joe@example.com")
# => :ok
```
## 4. Close the log file
Implement the `Newsletter.close_log/1` function. It should take a PID of the process that handles the file and close the file.
```elixir
Newsletter.close_log(pid)
# => :ok
```
## 5. Send the newsletter
Now that you have all of the building blocks of the email sending procedure, you need to combine them together in a single function.
Implement the `Newsletter.send_newsletter/3` function. It should take a path of the file with email addresses, a path of a log file, and an anonymous function that sends an email to a given email address. It should read all the email addresses from the given file and attempt to send an email to every one of them. If the anonymous function that sends the email returns `:ok`, write the email address to the log file, followed by a new line. Make sure to do it as soon as the email is sent. Afterwards, close the log file.
```elixir
Newsletter.send_newsletter(
"model_train_friends_emails.txt",
"newsletter_issue1_log.txt",
fn email -> :ok end
)
# => :ok
```
## Source
### Created by
- @angelikatyborska
### Contributed to by
- @neenjaw

View file

@ -0,0 +1,4 @@
alice@example.com
bob@example.com
charlie@example.com
dave@example.com

View file

View file

@ -0,0 +1,15 @@
defmodule Newsletter do
def read_emails(path), do: File.read!(path) |> String.split("\n", trim: true)
def open_log(path), do: File.open!(path, [:write])
def log_sent_email(pid, email), do: IO.puts(pid, email)
def close_log(pid), do: File.close(pid)
def send_newsletter(emails_path, log_path, send_fun) do
pid = open_log(log_path)
read_emails(emails_path)
|> Enum.each(&(send_fun.(&1) == :ok and log_sent_email(pid, &1)))
close_log(pid)
end
end

28
newsletter/mix.exs Normal file
View file

@ -0,0 +1,28 @@
defmodule Newsletter.MixProject do
use Mix.Project
def project do
[
app: :newsletter,
version: "0.1.0",
# elixir: "~> 1.10",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end

View file

@ -0,0 +1,206 @@
defmodule NewsletterTest do
# run test synchronously to be able to use the same file path for all tests without write conflicts
use ExUnit.Case, async: false
@temp_file_path Path.join(["assets", "temp.txt"])
setup do
File.write!(@temp_file_path, "")
on_exit(fn -> File.rm!(@temp_file_path) end)
end
describe "read_emails" do
@tag task_id: 1
test "returns a list of all lines in a file" do
emails_file_path = Path.join(["assets", "emails.txt"])
assert Newsletter.read_emails(emails_file_path) == [
"alice@example.com",
"bob@example.com",
"charlie@example.com",
"dave@example.com"
]
end
@tag task_id: 1
test "returns an empty list if the file is empty" do
empty_file_path = Path.join(["assets", "empty.txt"])
assert Newsletter.read_emails(empty_file_path) == []
end
end
describe "open_log" do
@tag task_id: 2
test "returns a pid" do
file = Newsletter.open_log(@temp_file_path)
assert is_pid(file)
File.close(file)
end
@tag task_id: 2
test "opens the file for writing" do
file = Newsletter.open_log(@temp_file_path)
assert IO.write(file, "hello") == :ok
assert File.read!(@temp_file_path) == "hello"
File.close(file)
end
end
describe "log_sent_email" do
@tag task_id: 3
test "returns ok" do
file = File.open!(@temp_file_path, [:write])
assert Newsletter.log_sent_email(file, "janice@example.com") == :ok
File.close(file)
end
@tag task_id: 3
test "writes the email address to the given file" do
file = File.open!(@temp_file_path, [:write])
Newsletter.log_sent_email(file, "joe@example.com")
assert File.read!(@temp_file_path) == "joe@example.com\n"
File.close(file)
end
@tag task_id: 3
test "writes many email addresses to the given file" do
file = File.open!(@temp_file_path, [:write])
Newsletter.log_sent_email(file, "joe@example.com")
Newsletter.log_sent_email(file, "kathrine@example.com")
Newsletter.log_sent_email(file, "lina@example.com")
assert File.read!(@temp_file_path) ==
"joe@example.com\nkathrine@example.com\nlina@example.com\n"
File.close(file)
end
end
describe "close_log" do
@tag task_id: 4
test "returns ok" do
file = File.open!(@temp_file_path, [:write])
assert Newsletter.close_log(file) == :ok
end
@tag task_id: 4
test "closes the file" do
file = File.open!(@temp_file_path, [:read])
assert Newsletter.close_log(file) == :ok
assert IO.read(file, :all) == {:error, :terminated}
end
end
describe "send_newsletter" do
@tag task_id: 5
test "returns ok" do
send_fun = fn _ -> :ok end
assert Newsletter.send_newsletter(
Path.join(["assets", "emails.txt"]),
@temp_file_path,
send_fun
) == :ok
end
@tag task_id: 5
test "calls send function for every email from the emails file" do
send_fun = fn email -> send(self(), {:send, email}) && :ok end
Newsletter.send_newsletter(Path.join(["assets", "emails.txt"]), @temp_file_path, send_fun)
assert_received {:send, "alice@example.com"}
assert_received {:send, "bob@example.com"}
assert_received {:send, "charlie@example.com"}
assert_received {:send, "dave@example.com"}
end
@tag task_id: 5
test "logs emails that were sent" do
send_fun = fn _ -> :ok end
Newsletter.send_newsletter(Path.join(["assets", "emails.txt"]), @temp_file_path, send_fun)
assert File.read!(@temp_file_path) ==
"""
alice@example.com
bob@example.com
charlie@example.com
dave@example.com
"""
end
@tag task_id: 5
test "does not log emails that could not be sent" do
send_fun = fn
"bob@example.com" -> :error
"charlie@example.com" -> :error
_ -> :ok
end
Newsletter.send_newsletter(Path.join(["assets", "emails.txt"]), @temp_file_path, send_fun)
assert File.read!(@temp_file_path) == """
alice@example.com
dave@example.com
"""
end
@tag task_id: 5
test "sending the same newsletter twice resets the log" do
send_fun = fn _ -> :ok end
Newsletter.send_newsletter(Path.join(["assets", "emails.txt"]), @temp_file_path, send_fun)
Newsletter.send_newsletter(Path.join(["assets", "emails.txt"]), @temp_file_path, send_fun)
assert File.read!(@temp_file_path) ==
"""
alice@example.com
bob@example.com
charlie@example.com
dave@example.com
"""
end
@tag task_id: 5
test "logs the email immediately after it was sent" do
send_fun = fn email ->
case email do
"alice@example.com" ->
:ok
"bob@example.com" ->
assert File.read!(@temp_file_path) == """
alice@example.com
"""
:ok
"charlie@example.com" ->
assert File.read!(@temp_file_path) == """
alice@example.com
bob@example.com
"""
:error
"dave@example.com" ->
assert File.read!(@temp_file_path) == """
alice@example.com
bob@example.com
"""
:ok
end
end
Newsletter.send_newsletter(Path.join(["assets", "emails.txt"]), @temp_file_path, send_fun)
assert File.read!(@temp_file_path) ==
"""
alice@example.com
bob@example.com
dave@example.com
"""
end
end
end

View file

@ -0,0 +1,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true, seed: 0)