new-passport

This commit is contained in:
Danil Negrienko 2024-03-07 05:10:54 -05:00
parent 8a0a02f996
commit 5704073c97
11 changed files with 487 additions and 0 deletions

View File

@ -0,0 +1,21 @@
{
"authors": [
"jiegillet"
],
"contributors": [
"angelikatyborska"
],
"files": {
"solution": [
"lib/new_passport.ex"
],
"test": [
"test/new_passport_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
]
},
"language_versions": ">=1.10",
"blurb": "Learn about `with` to concentrate on the happy path and manage a stressful day of facing bureaucracy."
}

View File

@ -0,0 +1 @@
{"track":"elixir","exercise":"new-passport","id":"ffbdd008e588434f83784fc853a67f8f","url":"https://exercism.org/tracks/elixir/exercises/new-passport","handle":"negrienko","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
elixir/new-passport/.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

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/new_passport.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)
- The [Elixir track's programming category on the forum](https://forum.exercism.org/c/programming/elixir)
- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
- 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.

View File

@ -0,0 +1,38 @@
# Hints
## General
- Read about using `with` in the [official Getting Started guide][getting-started-with].
- Review the functions available in the [`NaiveDateTime` module][naive-date-time], the [`Date` module][date], and the [`Time` module][time].
## 1. Get into the building
- Match the `:ok` tuple returned by `enter_building/1` in `with` with `<-`.
- In the `do` part of `with`, return an `:ok` tuple with the value you just matched.
- Since you don't need to modify the error, you don't need an `else` block.
## 2. Go to the information desk and find which counter you should go to
- Match the `:ok` tuple returned by `find_counter_information/1` in `with` with `<-`.
- Apply the anonymous function your just matched and match the result with `<-`.
- In the `do` part of `with`, return an `:ok` tuple with the counter you obtained.
- Add an `else` block that will expect a `:coffee_break` tuple and return a `:retry` tuple with a `NaiveDateTime`.
- A minute has `60` seconds.
- There is a [built-in function][naive-date-time-add] that adds a given number of seconds to a `NaiveDateTime` struct.
- Other errors should be returned as they are.
## 3. Go to the counter and get your form stamped
- Match the `:ok` tuple returned by `stamp_form/3` in `with` with `<-`.
- In the `do` part of `with`, return an `:ok` tuple with the checksum.
## 4. Receive your new passport
- In the `do` part of `with`, use `get_new_passport_number/3` and return the result in an `:ok` tuple.
[with]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1
[getting-started-with]: https://hexdocs.pm/elixir/docs-tests-and-with.html#with
[naive-date-time]: https://hexdocs.pm/elixir/NaiveDateTime.html
[time]: https://hexdocs.pm/elixir/Time.html
[date]: https://hexdocs.pm/elixir/Date.html
[naive-date-time-add]: https://hexdocs.pm/elixir/NaiveDateTime.html#add/3

View File

@ -0,0 +1,78 @@
# New Passport
Welcome to New Passport 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
## With
The [special form with][with] provides a way to focus on the "happy path" of a series of potentially failing steps and deal with the failures later.
```elixir
with {:ok, id} <- get_id(username),
{:ok, avatar} <- fetch_avatar(id),
{:ok, image_type} <- check_valid_image_type(avatar) do
{:ok, image_type, avatar}
else
:not_found ->
{:error, "invalid username"}
{:error, "not an image"} ->
{:error, "avatar associated to #{username} is not an image"}
err ->
err
end
```
At each step, if a clause matches, the chain will continue until the `do` block is executed. If one match fails, the chain stops and the non-matching clause is returned. You have the option of using an `else` block to catch failed matches and modify the return value.
[with]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#with/1
## Instructions
Your passport is about to expire, so you need to drop by the city office to renew it. You know from previous experience that your city office is not necessarily the easiest to deal with, so you decide to do your best to always "focus on the happy path".
You print out the form you need to get your new passport, fill it out, jump into your car, drive around the block, park and head to the office.
All the following tasks will require implementing and extending `get_new_passport/3`.
## 1. Get into the building
It turns out that the building is only open in the afternoon, and not at the same time everyday.
Call the function `enter_building/1` with the current time (given to you as first argument of `get_new_passport/3`). If the building is open, the function will return a tuple with `:ok` and a timestamp that you will need later, otherwise a tuple with `:error` and a message. For now, the happy path can return the `:ok` tuple.
If you get an `:error` tuple, use the `else` block to return it.
## 2. Go to the information desk and find which counter you should go to
The information desk is notorious for taking long coffee breaks. If you are lucky enough to find someone there, they will give you an instruction manual which will explain which counter you need to go to depending on your birth date.
Call the function `find_counter_information/1` with the current time. You will get either a tuple with `:ok` and a manual, represented by an anonymous function, or a tuple with `:coffee_break` and more instructions. In your happy path where you receive the manual, apply it to your birthday (second argument of `get_new_passport/3`). It will return the number of the counter where you need to go. Return an `:ok` tuple with that counter number.
If you get a `:coffee_break` message, return a tuple with `:retry` and a `NaiveDateTime` pointing to 15 minutes after the current time. As before, if you get an `:error` tuple, return it.
## 3. Go to the counter and get your form stamped
For some reason, different counters require forms of different colors. Of course, you printed the first one you found on the website, so you focus on your happy path and hope for the best.
Call the function `stamp_form/3` with the timestamp you received at the entrance, the counter and the form you brought (last argument of `get_new_passport/3`). You will get either a tuple with `:ok` and a checksum that will be used to verify your passport number or a tuple with `:error` and a message. Have your happy path return an `:ok` tuple with the checksum. If you get an `:error` tuple, return it.
## 4. Receive your new passport
Finally, you have all the documents you need.
Call `get_new_passport_number/3` with the timestamp, the counter and the checksum you received earlier. You will receive a string with your final passport number, all that is left to do is to return that string in a tuple with `:ok` and go home.
## Source
### Created by
- @jiegillet
### Contributed to by
- @angelikatyborska

View File

@ -0,0 +1,61 @@
defmodule NewPassport do
def get_new_passport(now, birthday, form) do
with {:ok, timestamp} <- enter_building(now),
{:ok, manual} <- find_counter_information(now),
counter <- manual.(birthday),
{:ok, checksum} <- stamp_form(timestamp, counter, form),
passport_number <- get_new_passport_number(timestamp, counter, checksum) do
{:ok, passport_number}
else
{:coffee_break, _message} -> {:retry, NaiveDateTime.add(now, 15, :minute)}
{:error, _message} = error -> error
end
end
# Do not modify the functions below
defp enter_building(%NaiveDateTime{} = datetime) do
day = Date.day_of_week(datetime)
time = NaiveDateTime.to_time(datetime)
cond do
day <= 4 and time_between(time, ~T[13:00:00], ~T[15:30:00]) ->
{:ok, datetime |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_unix()}
day == 5 and time_between(time, ~T[13:00:00], ~T[14:30:00]) ->
{:ok, datetime |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_unix()}
true ->
{:error, "city office is closed"}
end
end
@eighteen_years 18 * 365
defp find_counter_information(%NaiveDateTime{} = datetime) do
time = NaiveDateTime.to_time(datetime)
if time_between(time, ~T[14:00:00], ~T[14:20:00]) do
{:coffee_break, "information counter staff on coffee break, come back in 15 minutes"}
else
{:ok, fn %Date{} = birthday -> 1 + div(Date.diff(datetime, birthday), @eighteen_years) end}
end
end
defp stamp_form(timestamp, counter, :blue) when rem(counter, 2) == 1 do
{:ok, 3 * (timestamp + counter) + 1}
end
defp stamp_form(timestamp, counter, :red) when rem(counter, 2) == 0 do
{:ok, div(timestamp + counter, 2)}
end
defp stamp_form(_timestamp, _counter, _form), do: {:error, "wrong form color"}
defp get_new_passport_number(timestamp, counter, checksum) do
"#{timestamp}-#{counter}-#{checksum}"
end
defp time_between(time, from, to) do
Time.compare(from, time) != :gt and Time.compare(to, time) == :gt
end
end

View File

@ -0,0 +1,28 @@
defmodule NewPassport.MixProject do
use Mix.Project
def project do
[
app: :new_passport,
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,155 @@
defmodule NewPassportTest do
use ExUnit.Case
describe "getting into the building" do
@tag task_id: 1
test "building is closed in the morning" do
assert NewPassport.get_new_passport(~N[2021-10-11 10:30:00], ~D[1984-09-14], :blue) ==
{:error, "city office is closed"}
end
@tag task_id: 1
test "building is closed early on Friday afternoon" do
assert NewPassport.get_new_passport(~N[2021-10-08 15:00:00], ~D[1984-09-14], :blue) ==
{:error, "city office is closed"}
end
@tag task_id: 1
test "entering during business hour" do
assert {:ok, _} =
NewPassport.get_new_passport(~N[2021-10-11 15:00:00], ~D[1984-09-14], :blue)
end
end
describe "find the right counter" do
@tag task_id: 2
test "information staff on coffee break" do
assert NewPassport.get_new_passport(~N[2021-10-11 14:10:00], ~D[1984-09-14], :blue) ==
{:retry, ~N[2021-10-11 14:25:00]}
end
@tag task_id: 2
test "information staff on coffee break, retry at given time" do
assert {:ok, _} =
NewPassport.get_new_passport(~N[2021-10-11 14:25:00], ~D[1984-09-14], :blue)
end
@tag task_id: 2
test "information staff on coffee break on Friday 15 minutes before closing time" do
assert NewPassport.get_new_passport(~N[2021-10-08 14:15:00], ~D[1984-09-14], :blue) ==
{:retry, ~N[2021-10-08 14:30:00]}
end
@tag task_id: 2
test "retry after previous attempt, hit closing time" do
assert NewPassport.get_new_passport(~N[2021-10-08 14:30:00], ~D[1984-09-14], :blue) ==
{:error, "city office is closed"}
end
end
describe "get the passport form stamped" do
@tag task_id: 3
test "illegal form color" do
assert NewPassport.get_new_passport(
~N[2021-10-11 14:25:00],
~D[1984-09-14],
:orange_and_purple
) == {:error, "wrong form color"}
end
@tag task_id: 3
test "wrong form color" do
assert NewPassport.get_new_passport(~N[2021-10-11 14:25:00], ~D[1984-09-14], :red) ==
{:error, "wrong form color"}
end
@tag task_id: 3
test "correct form color" do
assert {:ok, _} =
NewPassport.get_new_passport(~N[2021-10-11 14:25:00], ~D[1984-09-14], :blue)
end
end
describe "receive the new passport number" do
@tag task_id: 4
test "get the right timestamp" do
assert {:ok, passport_number} =
NewPassport.get_new_passport(~N[2021-10-11 13:00:00], ~D[1984-09-14], :blue)
[timestamp, _counter, _checksum] = String.split(passport_number, "-")
assert timestamp == "1633957200"
end
@tag task_id: 4
test "get the right timestamp after waiting for coffee break" do
assert NewPassport.get_new_passport(~N[2021-10-11 14:15:00], ~D[1984-09-14], :blue) ==
{:retry, ~N[2021-10-11 14:30:00]}
assert {:ok, passport_number} =
NewPassport.get_new_passport(~N[2021-10-11 14:30:00], ~D[1984-09-14], :blue)
[timestamp, _counter, _checksum] = String.split(passport_number, "-")
assert timestamp == "1633962600"
end
@tag task_id: 4
test "get the right timestamp after waiting twice for coffee break" do
assert NewPassport.get_new_passport(~N[2021-10-11 14:00:00], ~D[1984-09-14], :blue) ==
{:retry, ~N[2021-10-11 14:15:00]}
assert NewPassport.get_new_passport(~N[2021-10-11 14:15:00], ~D[1984-09-14], :blue) ==
{:retry, ~N[2021-10-11 14:30:00]}
assert {:ok, passport_number} =
NewPassport.get_new_passport(~N[2021-10-11 14:30:00], ~D[1984-09-14], :blue)
[timestamp, _counter, _checksum] = String.split(passport_number, "-")
assert timestamp == "1633962600"
end
@tag task_id: 4
test "16 year old finds the right counter" do
assert {:ok, passport_number} =
NewPassport.get_new_passport(~N[2021-10-11 14:30:00], ~D[2005-09-14], :blue)
[_timestamp, counter, _checksum] = String.split(passport_number, "-")
assert counter == "1"
end
@tag task_id: 4
test "34 year old finds the right counter" do
assert {:ok, passport_number} =
NewPassport.get_new_passport(~N[2021-10-11 14:30:00], ~D[1987-09-14], :red)
[_timestamp, counter, _checksum] = String.split(passport_number, "-")
assert counter == "2"
end
@tag task_id: 4
test "get the right passport number" do
assert NewPassport.get_new_passport(~N[2021-10-11 15:00:00], ~D[1984-09-14], :blue) ==
{:ok, "1633964400-3-4901893210"}
end
@tag task_id: 4
test "get a passport number after waiting for a coffee break" do
assert NewPassport.get_new_passport(~N[2021-10-11 14:15:00], ~D[1984-09-14], :blue) ==
{:retry, ~N[2021-10-11 14:30:00]}
assert NewPassport.get_new_passport(~N[2021-10-11 14:30:00], ~D[1984-09-14], :blue) ==
{:ok, "1633962600-3-4901887810"}
end
@tag task_id: 4
test "get a passport number after two coffee breaks" do
assert NewPassport.get_new_passport(~N[2021-10-11 14:00:00], ~D[1964-09-14], :red) ==
{:retry, ~N[2021-10-11 14:15:00]}
assert NewPassport.get_new_passport(~N[2021-10-11 14:15:00], ~D[1964-09-14], :red) ==
{:retry, ~N[2021-10-11 14:30:00]}
assert NewPassport.get_new_passport(~N[2021-10-12 14:30:00], ~D[1964-09-14], :red) ==
{:ok, "1634049000-4-817024502"}
end
end
end

View File

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