Compare commits
2 Commits
9d03ff01dc
...
2465a18235
Author | SHA1 | Date |
---|---|---|
Danil Negrienko | 2465a18235 | |
Danil Negrienko | db731118fc |
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"neenjaw"
|
||||||
|
],
|
||||||
|
"contributors": [
|
||||||
|
"angelikatyborska",
|
||||||
|
"NobbZ"
|
||||||
|
],
|
||||||
|
"files": {
|
||||||
|
"solution": [
|
||||||
|
"lib/dna.ex"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"test/dna_test.exs"
|
||||||
|
],
|
||||||
|
"exemplar": [
|
||||||
|
".meta/exemplar.ex"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"language_versions": ">=1.10",
|
||||||
|
"blurb": "Learn about bitstrings and tail call recursion by encoding DNA sequences as binary data."
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
{"track":"elixir","exercise":"dna-encoding","id":"cf5029ade3444553984645359b133fcc","url":"https://exercism.org/tracks/elixir/exercises/dna-encoding","handle":"negrienko","is_requester":true,"auto_approve":false}
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
|
@ -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").
|
||||||
|
bitstrings-*.tar
|
||||||
|
|
|
@ -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/dna.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.
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Hints
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
- Use `?` to work with the character [code points][codepoint].
|
||||||
|
- `\s` can be used to represent a space.
|
||||||
|
- Use [integer binary notation][integer-literal] for working with the codes.
|
||||||
|
- Try to use the tail call recursion strategy.
|
||||||
|
|
||||||
|
## 1. Encode nucleic acid to binary value
|
||||||
|
|
||||||
|
- This function needs to map one integer to another.
|
||||||
|
- This function doesn't need recursion.
|
||||||
|
- Making use of multiple clause functions may make this easier by breaking it down.
|
||||||
|
|
||||||
|
## 2. Decode the binary value to the nucleic acid
|
||||||
|
|
||||||
|
- This function is the opposite of part 1's function.
|
||||||
|
- This function doesn't need recursion.
|
||||||
|
- Making use of multiple clause functions may make this easier by breaking it down.
|
||||||
|
|
||||||
|
## 3. Encode a DNA charlist
|
||||||
|
|
||||||
|
- Create a tail-recursive function which takes a code point from the charlist and recursively builds the bitstring result.
|
||||||
|
- Tail-recursive functions need an accumulator.
|
||||||
|
- Remember, a [charlist][charlist] is a list of [integer code points][codepoint].
|
||||||
|
- You can get the first and remaining items from a list using a built-in [`Kernel` module][kernel] function.
|
||||||
|
- You can also pattern match on a list using the [`[head | tail]`][list] notation.
|
||||||
|
- Use multiple clause functions to separate the base case from the recursive cases.
|
||||||
|
- Do not forget to specify the types of bitstring segments using the `::` operator.
|
||||||
|
|
||||||
|
## 4. Decode a DNA bitstring
|
||||||
|
|
||||||
|
- Create a tail-recursive function which [matches the first 4 bits][bitstring-matching] from the [bitstring][bitstring] and recursively builds the [charlist][charlist] result.
|
||||||
|
- Tail-recursive functions need an accumulator.
|
||||||
|
- Remember the [bitstring special form][bitstring-form] can be used for matching on bitstrings.
|
||||||
|
- Do not forget to specify the types of bitstring segments using the `::` operator.
|
||||||
|
- You will need to reverse the accumulator at the end. Write a private tail-recursive `reverse` function to do that and use it in the base-case of the `decode` function.
|
||||||
|
|
||||||
|
[integer-literal]: https://hexdocs.pm/elixir/syntax-reference.html#integers-in-other-bases-and-unicode-code-points
|
||||||
|
[codepoint]: https://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html#unicode-and-code-points
|
||||||
|
[charlist]: https://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html#charlists
|
||||||
|
[bitstring]: https://elixir-lang.org/getting-started/binaries-strings-and-char-lists.html#bitstrings
|
||||||
|
[bitstring-form]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#%3C%3C%3E%3E/1
|
||||||
|
[bitstring-matching]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#%3C%3C%3E%3E/1-binary-bitstring-matching
|
||||||
|
[type-operator]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#::/2
|
||||||
|
[recursion-tco]: https://en.wikipedia.org/wiki/Tail_call
|
||||||
|
[list]: https://hexdocs.pm/elixir/List.html#content
|
||||||
|
[kernel]: https://hexdocs.pm/elixir/Kernel.html#functions
|
|
@ -0,0 +1,99 @@
|
||||||
|
# DNA Encoding
|
||||||
|
|
||||||
|
Welcome to DNA Encoding 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
|
||||||
|
|
||||||
|
## Tail Call Recursion
|
||||||
|
|
||||||
|
When [recursing][exercism-recursion] through enumerables (lists, bitstrings, strings), there are often two concerns:
|
||||||
|
|
||||||
|
- how much memory is required to store the trail of recursive function calls
|
||||||
|
- how to build the solution efficiently
|
||||||
|
|
||||||
|
To deal with these concerns an _accumulator_ may be used.
|
||||||
|
|
||||||
|
An accumulator is a variable that is passed along in addition to the data. It is used to pass the current state of the function's execution, from function call to function call, until the _base case_ is reached. In the base case, the accumulator is used to return the final value of the recursive function call.
|
||||||
|
|
||||||
|
Accumulators should be initialized by the function's author, not the function's user. To achieve this, declare two functions - a public function that takes just the necessary data as arguments and initializes the accumulator, and a private function that also takes an accumulator. In Elixir, it is a common pattern to prefix the private function's name with `do_`.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Count the length of a list without an accumulator
|
||||||
|
def count([]), do: 0
|
||||||
|
def count([_head | tail]), do: 1 + count(tail)
|
||||||
|
|
||||||
|
# Count the length of a list with an accumulator
|
||||||
|
def count(list), do: do_count(list, 0)
|
||||||
|
|
||||||
|
defp do_count([], count), do: count
|
||||||
|
defp do_count([_head | tail], count), do: do_count(tail, count + 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
The usage of an accumulator allows us to turn recursive functions into _tail-recursive_ functions. A function is tail-recursive if the _last_ thing executed by the function is a call to itself.
|
||||||
|
|
||||||
|
[exercism-recursion]: https://exercism.org/tracks/elixir/concepts/recursion
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
In your DNA research lab, you have been working through various ways to compress your research data to save storage space. One teammate suggests converting the DNA data to a binary representation:
|
||||||
|
|
||||||
|
| Nucleic Acid | Code |
|
||||||
|
| ------------ | ------ |
|
||||||
|
| a space | `0000` |
|
||||||
|
| A | `0001` |
|
||||||
|
| C | `0010` |
|
||||||
|
| G | `0100` |
|
||||||
|
| T | `1000` |
|
||||||
|
|
||||||
|
You ponder this, as it will potentially halve the required data storage costs, but at the expense of human readability. You decide to write a module to encode and decode your data to benchmark your savings.
|
||||||
|
|
||||||
|
## 1. Encode nucleic acid to binary value
|
||||||
|
|
||||||
|
Implement `encode_nucleotide/1` to accept the code point for the nucleic acid and return the integer value of the encoded code.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
DNA.encode_nucleotide(?A)
|
||||||
|
# => 1
|
||||||
|
# (which is equal to 0b0001)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Decode the binary value to the nucleic acid
|
||||||
|
|
||||||
|
Implement `decode_nucleotide/1` to accept the integer value of the encoded code and return the code point for the nucleic acid.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
DNA.decode_nucleotide(0b0001)
|
||||||
|
# => 65
|
||||||
|
# (which is equal to ?A)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Encode a DNA charlist
|
||||||
|
|
||||||
|
Implement `encode/1` to accept a charlist representing nucleic acids and gaps and return a bitstring of the encoded data.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
DNA.encode(~c"AC GT")
|
||||||
|
# => <<18, 4, 8::size(4)>>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Decode a DNA bitstring
|
||||||
|
|
||||||
|
Implement `decode/1` to accept a bitstring representing nucleic acids and gaps and return the decoded data as a charlist.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
DNA.decode(<<132, 2, 1::size(4)>>)
|
||||||
|
# => ~c"TG CA"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
### Created by
|
||||||
|
|
||||||
|
- @neenjaw
|
||||||
|
|
||||||
|
### Contributed to by
|
||||||
|
|
||||||
|
- @angelikatyborska
|
||||||
|
- @NobbZ
|
|
@ -0,0 +1,29 @@
|
||||||
|
defmodule DNA do
|
||||||
|
def encode_nucleotide(?\s), do: 0b0000
|
||||||
|
def encode_nucleotide(?A), do: 0b0001
|
||||||
|
def encode_nucleotide(?C), do: 0b0010
|
||||||
|
def encode_nucleotide(?G), do: 0b0100
|
||||||
|
def encode_nucleotide(?T), do: 0b1000
|
||||||
|
|
||||||
|
def decode_nucleotide(0b0000), do: ?\s
|
||||||
|
def decode_nucleotide(0b0001), do: ?A
|
||||||
|
def decode_nucleotide(0b0010), do: ?C
|
||||||
|
def decode_nucleotide(0b0100), do: ?G
|
||||||
|
def decode_nucleotide(0b1000), do: ?T
|
||||||
|
|
||||||
|
def encode(dna), do: do_encode(dna)
|
||||||
|
|
||||||
|
defp do_encode(list, acc \\ <<0::size(0)>>)
|
||||||
|
defp do_encode([], acc), do: acc
|
||||||
|
defp do_encode([nucleotide | tail], acc) do
|
||||||
|
do_encode(tail, <<acc::bitstring, encode_nucleotide(nucleotide)::size(4)>>)
|
||||||
|
end
|
||||||
|
|
||||||
|
def decode(dna), do: do_decode(dna)
|
||||||
|
|
||||||
|
defp do_decode(list, acc \\ [])
|
||||||
|
defp do_decode(<<>>, acc), do: acc
|
||||||
|
defp do_decode(<<nucleotide::4, rest::bitstring>>, acc) do
|
||||||
|
do_decode(rest, acc ++ [decode_nucleotide(nucleotide)])
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,28 @@
|
||||||
|
defmodule DNA.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :dna,
|
||||||
|
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
|
|
@ -0,0 +1,75 @@
|
||||||
|
defmodule DNATest do
|
||||||
|
use ExUnit.Case
|
||||||
|
|
||||||
|
describe "encode to 4-bit encoding" do
|
||||||
|
@tag task_id: 1
|
||||||
|
test "?\\s to 0b0000", do: assert(DNA.encode_nucleotide(?\s) == 0b0000)
|
||||||
|
@tag task_id: 1
|
||||||
|
test "?A to 0b0001", do: assert(DNA.encode_nucleotide(?A) == 0b0001)
|
||||||
|
@tag task_id: 1
|
||||||
|
test "?C to 0b0010", do: assert(DNA.encode_nucleotide(?C) == 0b0010)
|
||||||
|
@tag task_id: 1
|
||||||
|
test "?G to 0b0100", do: assert(DNA.encode_nucleotide(?G) == 0b0100)
|
||||||
|
@tag task_id: 1
|
||||||
|
test "?T to 0b1000", do: assert(DNA.encode_nucleotide(?T) == 0b1000)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "decode to code point" do
|
||||||
|
@tag task_id: 2
|
||||||
|
test "0b0000 to ?\\s", do: assert(DNA.decode_nucleotide(0b0000) == ?\s)
|
||||||
|
@tag task_id: 2
|
||||||
|
test "0b0001 to ?A", do: assert(DNA.decode_nucleotide(0b0001) == ?A)
|
||||||
|
@tag task_id: 2
|
||||||
|
test "0b0010 to ?C", do: assert(DNA.decode_nucleotide(0b0010) == ?C)
|
||||||
|
@tag task_id: 2
|
||||||
|
test "0b0100 to ?G", do: assert(DNA.decode_nucleotide(0b0100) == ?G)
|
||||||
|
@tag task_id: 2
|
||||||
|
test "0b1000 to ?T", do: assert(DNA.decode_nucleotide(0b1000) == ?T)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "encoding" do
|
||||||
|
@tag task_id: 3
|
||||||
|
test "' '", do: assert(DNA.encode(~c" ") == <<0b0000::4>>)
|
||||||
|
@tag task_id: 3
|
||||||
|
test "'A'", do: assert(DNA.encode(~c"A") == <<0b0001::4>>)
|
||||||
|
@tag task_id: 3
|
||||||
|
test "'C'", do: assert(DNA.encode(~c"C") == <<0b0010::4>>)
|
||||||
|
@tag task_id: 3
|
||||||
|
test "'G'", do: assert(DNA.encode(~c"G") == <<0b0100::4>>)
|
||||||
|
@tag task_id: 3
|
||||||
|
test "'T'", do: assert(DNA.encode(~c"T") == <<0b1000::4>>)
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "' ACGT'",
|
||||||
|
do:
|
||||||
|
assert(DNA.encode(~c" ACGT") == <<0b0000::4, 0b0001::4, 0b0010::4, 0b0100::4, 0b1000::4>>)
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "'TGCA '",
|
||||||
|
do:
|
||||||
|
assert(DNA.encode(~c"TGCA ") == <<0b1000::4, 0b0100::4, 0b0010::4, 0b0001::4, 0b0000::4>>)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "decoding" do
|
||||||
|
@tag task_id: 4
|
||||||
|
test "' '", do: assert(DNA.decode(<<0b0000::4>>) == ~c" ")
|
||||||
|
@tag task_id: 4
|
||||||
|
test "'A'", do: assert(DNA.decode(<<0b0001::4>>) == ~c"A")
|
||||||
|
@tag task_id: 4
|
||||||
|
test "'C'", do: assert(DNA.decode(<<0b0010::4>>) == ~c"C")
|
||||||
|
@tag task_id: 4
|
||||||
|
test "'G'", do: assert(DNA.decode(<<0b0100::4>>) == ~c"G")
|
||||||
|
@tag task_id: 4
|
||||||
|
test "'T'", do: assert(DNA.decode(<<0b1000::4>>) == ~c"T")
|
||||||
|
|
||||||
|
@tag task_id: 4
|
||||||
|
test "' ACGT'",
|
||||||
|
do:
|
||||||
|
assert(DNA.decode(<<0b0000::4, 0b0001::4, 0b0010::4, 0b0100::4, 0b1000::4>>) == ~c" ACGT")
|
||||||
|
|
||||||
|
@tag task_id: 4
|
||||||
|
test "'TGCA '",
|
||||||
|
do:
|
||||||
|
assert(DNA.decode(<<0b1000::4, 0b0100::4, 0b0010::4, 0b0001::4, 0b0000::4>>) == ~c"TGCA ")
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,2 @@
|
||||||
|
ExUnit.start()
|
||||||
|
ExUnit.configure(exclude: :pending, trace: true, seed: 0)
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"angelikatyborska"
|
||||||
|
],
|
||||||
|
"contributors": [
|
||||||
|
"neenjaw"
|
||||||
|
],
|
||||||
|
"files": {
|
||||||
|
"solution": [
|
||||||
|
"lib/library_fees.ex"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"test/library_fees_test.exs"
|
||||||
|
],
|
||||||
|
"exemplar": [
|
||||||
|
".meta/exemplar.ex"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"language_versions": ">=1.10",
|
||||||
|
"blurb": "Learn about dates and time by calculating late fees for the local library."
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
{"track":"elixir","exercise":"library-fees","id":"ca5ec29bebbb49669bd4607297760349","url":"https://exercism.org/tracks/elixir/exercises/library-fees","handle":"negrienko","is_requester":true,"auto_approve":false}
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
|
@ -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
|
||||||
|
|
|
@ -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/library_fees.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.
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Hints
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
- Review the functions available in the [`NaiveDateTime` module][naive-date-time], the [`Date` module][date], and the [`Time` module][time].
|
||||||
|
|
||||||
|
## 1. Parse the stored datetimes
|
||||||
|
|
||||||
|
- There is a [built-in function][naive-date-time-from-iso8601] that parses an ISO8601 datetime string and returns a `NaiveDateTime` struct.
|
||||||
|
|
||||||
|
## 2. Determine if a book was checked out before noon
|
||||||
|
|
||||||
|
- You can define a `Time` literal using the [`~T` sigil][time-sigil].
|
||||||
|
- There is a [built-in function][naive-date-time-to-time] that changes a `NaiveDateTime` struct to a `Time` struct.
|
||||||
|
- There is a [built-in function][time-compare] that checks which one of two `Time` structs is bigger.
|
||||||
|
|
||||||
|
## 3. Calculate the return date
|
||||||
|
|
||||||
|
- A day has `24 * 60 * 60` seconds.
|
||||||
|
- There is a [built-in function][naive-date-time-add] that adds a given number of seconds to a `NaiveDateTime` struct.
|
||||||
|
- There is a [built-in function][naive-date-time-to-date] that changes a `NaiveDateTime` struct to a `Date` struct.
|
||||||
|
|
||||||
|
## 4. Determine how late the return of the book was
|
||||||
|
|
||||||
|
- There is a [built-in function][naive-date-time-to-date] that changes a `NaiveDateTime` struct to a `Date` struct.
|
||||||
|
- There is a [built-in function][date-diff] that calculates the difference in days between two `Date` structs.
|
||||||
|
|
||||||
|
## 5. Determine if the book was returned on a Monday
|
||||||
|
|
||||||
|
- There is a [built-in function][naive-date-time-to-date] that changes a `NaiveDateTime` struct to a `Date` struct.
|
||||||
|
- There is a [built-in function][date-day-of-week] that returns the day of week for a given `Date` struct.
|
||||||
|
|
||||||
|
## 6. Calculate the late fee
|
||||||
|
|
||||||
|
- Combine together all of the functions that you defined in previous steps.
|
||||||
|
|
||||||
|
[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-from-iso8601]: https://hexdocs.pm/elixir/NaiveDateTime.html#from_iso8601!/2
|
||||||
|
[naive-date-time-to-time]: https://hexdocs.pm/elixir/NaiveDateTime.html#to_time/1
|
||||||
|
[naive-date-time-to-date]: https://hexdocs.pm/elixir/NaiveDateTime.html#to_date/1
|
||||||
|
[naive-date-time-add]: https://hexdocs.pm/elixir/NaiveDateTime.html#add/3
|
||||||
|
[time-sigil]: https://hexdocs.pm/elixir/Kernel.html#sigil_T/2
|
||||||
|
[time-compare]: https://hexdocs.pm/elixir/Time.html#compare/2
|
||||||
|
[date-diff]: https://hexdocs.pm/elixir/Date.html#diff/2
|
||||||
|
[date-day-of-week]: https://hexdocs.pm/elixir/Date.html#day_of_week/2
|
|
@ -0,0 +1,124 @@
|
||||||
|
# Library Fees
|
||||||
|
|
||||||
|
Welcome to Library Fees 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
|
||||||
|
|
||||||
|
## Dates and Time
|
||||||
|
|
||||||
|
Elixir's standard library offers 4 different modules for working with dates and time, each with its own struct.
|
||||||
|
|
||||||
|
- The `Date` module. A `Date` struct can be created with the `~D` sigil.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
~D[2021-01-01]
|
||||||
|
```
|
||||||
|
|
||||||
|
- The `Time` module. A `Time` struct can be created with the `~T` sigil.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
~T[12:00:00]
|
||||||
|
```
|
||||||
|
|
||||||
|
- The `NaiveDateTime` module for datetimes without a timezone. A `NaiveDateTime` struct can be created with the `~N` sigil.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
~N[2021-01-01 12:00:00]
|
||||||
|
```
|
||||||
|
|
||||||
|
- The `DateTime` module for datetimes with a timezone. Using this module for timezones other than UTC requires an external dependency, a timezone database.
|
||||||
|
|
||||||
|
### Comparisons
|
||||||
|
|
||||||
|
To compare dates or times to one another, look for a `compare` or `diff` function in the corresponding module. Comparison operators such as `==`, `>`, and `<` _seem_ to work, but they don't do a correct semantic comparison for those structs.
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
Your librarian friend has asked you to extend her library software to automatically calculate late fees.
|
||||||
|
Her current system stores the exact date and time of a book checkout as an [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) datetime string.
|
||||||
|
She runs a local library in a small town in Ghana, which uses the GMT timezone (UTC +0), doesn't use daylight saving time, and doesn't need to worry about other timezones.
|
||||||
|
|
||||||
|
## 1. Parse the stored datetimes
|
||||||
|
|
||||||
|
Implement the `LibraryFees.datetime_from_string/1` function. It should take an ISO8601 datetime string as an argument, and return a `NaiveDateTime` struct.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
LibraryFees.datetime_from_string("2021-01-01T13:30:45Z")
|
||||||
|
# => ~N[2021-01-01 13:30:45]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Determine if a book was checked out before noon
|
||||||
|
|
||||||
|
If a book was checked out before noon, the reader has 28 days to return it. If it was checked out at or after noon, it's 29 days.
|
||||||
|
|
||||||
|
Implement the `LibraryFees.before_noon?/1` function. It should take a `NaiveDateTime` struct and return a boolean.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
LibraryFees.before_noon?(~N[2021-01-12 08:23:03])
|
||||||
|
# => true
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Calculate the return date
|
||||||
|
|
||||||
|
Based on the checkout datetime, calculate the return date.
|
||||||
|
|
||||||
|
Implement the `LibraryFees.return_date/1` function. It should take a `NaiveDateTime` struct and return a `Date` struct, either 28 or 29 days later.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
LibraryFees.return_date(~N[2020-11-28 15:55:33])
|
||||||
|
# => ~D[2020-12-27]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Determine how late the return of the book was
|
||||||
|
|
||||||
|
The library has a flat rate for late returns. To be able to calculate the fee, we need to know how many days after the return date the book was actually returned.
|
||||||
|
|
||||||
|
Implement the `LibraryFees.days_late/2` function. It should take a `Date` struct - the planned return date, and a `NaiveDateTime` struct - the actual return datetime.
|
||||||
|
|
||||||
|
If the actual return date is on an earlier or the same day as the planned return datetime, the function should return 0. Otherwise, the function should return the difference between those two dates in days.
|
||||||
|
|
||||||
|
The library tracks both the date and time of the actual return of the book for statistical purposes, but doesn't use the time when calculating late fees.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
LibraryFees.days_late(~D[2020-12-27], ~N[2021-01-03 09:23:36])
|
||||||
|
# => 7
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Determine if the book was returned on a Monday
|
||||||
|
|
||||||
|
The library has a special offer for returning books on Mondays.
|
||||||
|
|
||||||
|
Implement the `LibraryFees.monday?/1` function. It should take a `NaiveDateTime` struct and return a boolean.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
LibraryFees.monday?(~N[2021-01-03 13:30:45Z])
|
||||||
|
# => false
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Calculate the late fee
|
||||||
|
|
||||||
|
Implement the `LibraryFees.calculate_late_fee/3` function. It should take three arguments - two ISO8601 datetime strings, checkout datetime and actual return datetime, and the late fee for one day. It should return the total late fee according to how late the actual return of the book was.
|
||||||
|
|
||||||
|
Include the special Monday offer. If you return the book on Monday, your late fee is 50% off, rounded down.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Sunday, 7 days late
|
||||||
|
LibraryFees.calculate_late_fee("2020-11-28T15:55:33Z", "2021-01-03T13:30:45Z", 100)
|
||||||
|
# => 700
|
||||||
|
|
||||||
|
# one day later, Monday, 8 days late
|
||||||
|
LibraryFees.calculate_late_fee("2020-11-28T15:55:33Z", "2021-01-04T09:02:11Z", 100)
|
||||||
|
# => 400
|
||||||
|
```
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
### Created by
|
||||||
|
|
||||||
|
- @angelikatyborska
|
||||||
|
|
||||||
|
### Contributed to by
|
||||||
|
|
||||||
|
- @neenjaw
|
|
@ -0,0 +1,55 @@
|
||||||
|
defmodule LibraryFees do
|
||||||
|
def datetime_from_string(string) do
|
||||||
|
case NaiveDateTime.from_iso8601(string) do
|
||||||
|
{:ok, naive_date_time} -> naive_date_time
|
||||||
|
_ -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def before_noon?(datetime) do
|
||||||
|
datetime.hour < 12
|
||||||
|
end
|
||||||
|
|
||||||
|
defp days_to_return(checkout_datetime) do
|
||||||
|
if(before_noon?(checkout_datetime), do: 28, else: 29)
|
||||||
|
end
|
||||||
|
|
||||||
|
def return_date(checkout_datetime) do
|
||||||
|
checkout_datetime
|
||||||
|
|> NaiveDateTime.add(days_to_return(checkout_datetime), :day)
|
||||||
|
|> NaiveDateTime.to_date()
|
||||||
|
end
|
||||||
|
|
||||||
|
def days_late(planned_return_date, actual_return_datetime) do
|
||||||
|
actual_return_datetime
|
||||||
|
|> NaiveDateTime.to_date()
|
||||||
|
|> Date.diff(planned_return_date)
|
||||||
|
|> case do
|
||||||
|
late_in_days when late_in_days > 0 -> late_in_days
|
||||||
|
_late_in_days -> 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def monday?(datetime) do
|
||||||
|
datetime
|
||||||
|
|> NaiveDateTime.to_date()
|
||||||
|
|> Date.day_of_week(:monday)
|
||||||
|
|> case do
|
||||||
|
1 -> true
|
||||||
|
_ -> false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_late_fee(checkout, return, rate) do
|
||||||
|
checkout_date_time = datetime_from_string(checkout)
|
||||||
|
actual_return_date_time = datetime_from_string(return)
|
||||||
|
planed_return_date_time = return_date(checkout_date_time)
|
||||||
|
|
||||||
|
fee = days_late(planed_return_date_time, actual_return_date_time) * rate
|
||||||
|
|
||||||
|
case monday?(actual_return_date_time) do
|
||||||
|
true -> floor(fee * 0.5)
|
||||||
|
_ -> fee
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,28 @@
|
||||||
|
defmodule LibraryFees.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :library_fees,
|
||||||
|
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
|
|
@ -0,0 +1,171 @@
|
||||||
|
defmodule LibraryFeesTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
|
||||||
|
describe "datetime_from_string/1" do
|
||||||
|
@tag task_id: 1
|
||||||
|
test "returns NaiveDateTime" do
|
||||||
|
result = LibraryFees.datetime_from_string("2021-01-01T12:00:00Z")
|
||||||
|
assert result.__struct__ == NaiveDateTime
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 1
|
||||||
|
test "parses an ISO8601 string" do
|
||||||
|
result = LibraryFees.datetime_from_string("2019-12-24T13:15:45Z")
|
||||||
|
assert result == ~N[2019-12-24 13:15:45Z]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "before_noon?/1" do
|
||||||
|
@tag task_id: 2
|
||||||
|
test "returns true if the given NaiveDateTime is before 12:00" do
|
||||||
|
assert LibraryFees.before_noon?(~N[2020-06-06 11:59:59Z]) == true
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 2
|
||||||
|
test "returns false if the given NaiveDateTime is after 12:00" do
|
||||||
|
assert LibraryFees.before_noon?(~N[2021-01-03 12:01:01Z]) == false
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 2
|
||||||
|
test "returns false if the given NaiveDateTime is exactly at 12:00" do
|
||||||
|
assert LibraryFees.before_noon?(~N[2018-11-17 12:00:00Z]) == false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "return_date/1" do
|
||||||
|
@tag task_id: 3
|
||||||
|
test "adds 28 days if the given NaiveDateTime is before 12:00" do
|
||||||
|
result = LibraryFees.return_date(~N[2020-02-14 11:59:59Z])
|
||||||
|
assert result == ~D[2020-03-13]
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "adds 29 days if the given NaiveDateTime is after 12:00" do
|
||||||
|
result = LibraryFees.return_date(~N[2021-01-03 12:01:01Z])
|
||||||
|
assert result == ~D[2021-02-01]
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "adds 29 days if the given NaiveDateTime is exactly at 12:00" do
|
||||||
|
result = LibraryFees.return_date(~N[2018-12-01 12:00:00Z])
|
||||||
|
assert result == ~D[2018-12-30]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "days_late/2" do
|
||||||
|
@tag task_id: 4
|
||||||
|
test "returns 0 when identical datetimes" do
|
||||||
|
result = LibraryFees.days_late(~D[2021-02-01], ~N[2021-02-01 12:00:00Z])
|
||||||
|
assert result == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 4
|
||||||
|
test "returns 0 when identical dates, but different times" do
|
||||||
|
result = LibraryFees.days_late(~D[2019-03-11], ~N[2019-03-11 12:00:00Z])
|
||||||
|
assert result == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 4
|
||||||
|
test "returns 0 when planned return date is later than actual return date" do
|
||||||
|
result = LibraryFees.days_late(~D[2020-12-03], ~N[2020-11-29 16:00:00Z])
|
||||||
|
assert result == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 4
|
||||||
|
test "returns date difference in numbers of days when planned return date is earlier than actual return date" do
|
||||||
|
result = LibraryFees.days_late(~D[2020-06-12], ~N[2020-06-21 16:00:00Z])
|
||||||
|
assert result == 9
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 4
|
||||||
|
test "a new day starts at midnight" do
|
||||||
|
result = LibraryFees.days_late(~D[2020-06-12], ~N[2020-06-12 23:59:59Z])
|
||||||
|
assert result == 0
|
||||||
|
|
||||||
|
result = LibraryFees.days_late(~D[2020-06-12], ~N[2020-06-13 00:00:00Z])
|
||||||
|
assert result == 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "monday?" do
|
||||||
|
@tag task_id: 5
|
||||||
|
test "2021-02-01 was a Monday" do
|
||||||
|
assert LibraryFees.monday?(~N[2021-02-01 14:01:00Z]) == true
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 5
|
||||||
|
test "2020-03-16 was a Monday" do
|
||||||
|
assert LibraryFees.monday?(~N[2020-03-16 09:23:52Z]) == true
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 5
|
||||||
|
test "2020-04-22 was a Monday" do
|
||||||
|
assert LibraryFees.monday?(~N[2019-04-22 15:44:03Z]) == true
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 5
|
||||||
|
test "2021-02-02 was a Tuesday" do
|
||||||
|
assert LibraryFees.monday?(~N[2021-02-02 15:07:00Z]) == false
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 5
|
||||||
|
test "2020-03-14 was a Friday" do
|
||||||
|
assert LibraryFees.monday?(~N[2020-03-14 08:54:51Z]) == false
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 5
|
||||||
|
test "2019-04-28 was a Sunday" do
|
||||||
|
assert LibraryFees.monday?(~N[2019-04-28 11:37:12Z]) == false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "calculate_late_fee/2" do
|
||||||
|
@tag task_id: 6
|
||||||
|
test "returns 0 if the book was returned less than 28 days after a morning checkout" do
|
||||||
|
result = LibraryFees.calculate_late_fee("2018-11-01T09:00:00Z", "2018-11-13T14:12:00Z", 123)
|
||||||
|
assert result == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "returns 0 if the book was returned exactly 28 days after a morning checkout" do
|
||||||
|
result = LibraryFees.calculate_late_fee("2018-11-01T09:00:00Z", "2018-11-29T14:12:00Z", 123)
|
||||||
|
assert result == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "returns the rate for one day if the book was returned exactly 29 days after a morning checkout" do
|
||||||
|
result = LibraryFees.calculate_late_fee("2018-11-01T09:00:00Z", "2018-11-30T14:12:00Z", 320)
|
||||||
|
assert result == 320
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "returns 0 if the book was returned less than 29 days after an afternoon checkout" do
|
||||||
|
result = LibraryFees.calculate_late_fee("2019-05-01T16:12:00Z", "2019-05-17T14:32:45Z", 400)
|
||||||
|
assert result == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "returns 0 if the book was returned exactly 29 days after an afternoon checkout" do
|
||||||
|
result = LibraryFees.calculate_late_fee("2019-05-01T16:12:00Z", "2019-05-30T14:32:45Z", 313)
|
||||||
|
assert result == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "returns the rate for one day if the book was returned exactly 30 days after an afternoon checkout" do
|
||||||
|
result = LibraryFees.calculate_late_fee("2019-05-01T16:12:00Z", "2019-05-31T14:32:45Z", 234)
|
||||||
|
assert result == 234
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "multiplies the number of days late by the rate for one day" do
|
||||||
|
result = LibraryFees.calculate_late_fee("2021-01-01T08:00:00Z", "2021-02-13T08:00:00Z", 111)
|
||||||
|
assert result == 111 * 15
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 6
|
||||||
|
test "late fees are 50% off (rounded down) when the book is returned on a Monday" do
|
||||||
|
result = LibraryFees.calculate_late_fee("2021-01-01T08:00:00Z", "2021-02-15T08:00:00Z", 111)
|
||||||
|
assert result == trunc(111 * 17 * 0.5)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,2 @@
|
||||||
|
ExUnit.start()
|
||||||
|
ExUnit.configure(exclude: :pending, trace: true, seed: 0)
|
Loading…
Reference in New Issue