captains_log

This commit is contained in:
Danil Negrienko 2024-03-06 23:45:39 -05:00
parent eab9b54f20
commit c62233f40f
11 changed files with 439 additions and 0 deletions

View File

@ -0,0 +1,21 @@
{
"authors": [
"angelikatyborska"
],
"contributors": [
"neenjaw"
],
"files": {
"solution": [
"lib/captains_log.ex"
],
"test": [
"test/captains_log_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
]
},
"language_versions": ">=1.10",
"blurb": "Learn about randomness and using Erlang libraries from Elixir by helping Mary generate stardates and starship registry numbers for her Star Trek themed pen-and-paper role playing sessions."
}

View File

@ -0,0 +1 @@
{"track":"elixir","exercise":"captains-log","id":"9270dfe97e544e6e83b53ccdf38c582e","url":"https://exercism.org/tracks/elixir/exercises/captains-log","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/captains-log/.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/captains_log.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 Erlang libraries in the [official Getting Started guide][getting-started-erlang-libraries], and in particular about [formatting strings][getting-started-formatted-text-output].
## 1. Generate a random planet
- Use the provided module attribute with a list of letters representing planetary classes.
- There is a [built-in function][enum-random] for choosing an element from a list at random.
## 2. Generate a random starship registry number
- There is a [built-in function][enum-random] for choosing an element from a range at random.
## 3. Generate a random stardate
- There is no Elixir function that would return a random float.
- There is a [built-in Erlang function][erl-rand-uniform] that returns a random float x where `0.0 <= x < 1.0`.
- If `x` belongs to a range `0.0 <= x < 1.0`, but you need a number from a different range `a <= x < b`, you can shift x's range by multiplying it by the range's width (`b - a`) and adding the range's start (`a`). That is: `x * (b - a) + a`.
## 4. Format the stardate
- There is no Elixir function that would be able to, in a single step, format a float as a string with a given precision.
- There is a [built-in Erlang function][erl-io-lib-format] that takes a format string and a list of data, and returns a charlist.
- There is a [built-in function][to-string] that changes a charlist to a string.
- The format string of that function contains control sequences.
- A control sequence starts with `~` and has the pattern `~F.P.PadModC`, where `F` stands for the width of the output, `P` stands for the precision, `Pad` stands for the padding character, `Mod` stands for the control sequence modifier, and `C` is the type of the control sequence.
- To format a float with a desired precision, a control sequence with the pattern `~.PC` will suffice.
- The control sequence type for floats is `f`.
- The exact format string you need is `~.1f`.
[getting-started-erlang-libraries]: https://hexdocs.pm/elixir/erlang-libraries.html
[getting-started-formatted-text-output]: https://hexdocs.pm/elixir/erlang-libraries.html#formatted-text-output
[enum-random]: https://hexdocs.pm/elixir/Enum.html#random/1
[erl-rand-uniform]: https://www.erlang.org/doc/man/rand.html#uniform-0
[erl-io-lib-format]: https://www.erlang.org/doc/man/io_lib.html#format-2
[to-string]: https://hexdocs.pm/elixir/Kernel.html#to_string/1

View File

@ -0,0 +1,107 @@
# Captain's Log
Welcome to Captain's Log 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
## Randomness
In Elixir, to choose a random element from an enumerable data structure (e.g. list, range), we use `Enum.random`. This function will pick a single element, with every element having equal probability of being picked.
Elixir does not have its own functions for picking a random float. To do that, we have to use Erlang directly.
## Erlang Libraries
Elixir code runs in the BEAM virtual machine. BEAM is part of the Erlang Run-Time System. Being inspired by Erlang, and sharing its run environment, Elixir provides great interoperability with Erlang libraries. This means that Elixir developers can use Erlang libraries from within their Elixir code. In fact, writing Elixir libraries for functionality already provided by Erlang libraries is discouraged in the Elixir community.
As a result, certain functionality, like mathematical operations or timer functions, is only available in Elixir via Erlang.
Erlang's standard library is available for use in our Elixir code without any extra steps necessary.
Erlang functions can be called in the same way we call Elixir functions, with one small difference. Erlang module names are `snake_case` atoms. For example, to call the Erlang `pi/0` function from the `math` module, one would write:
```elixir
:math.pi()
# => 3.141592653589793
```
The most commonly used Erlang functions that do not have an Elixir equivalent are:
- `:timer.sleep/1` which suspends a process for the given amount of milliseconds.
- `:rand.uniform/0` which generates a random float `x`, where `0.0 <= x < 1.0`.
- `:io_lib.format/2` which provides C-style string formatting (using control sequences). Using this function, we could for example print an integer in any base between 2 and 36 or format a float with desired precision. Note that this function, like many Erlang functions, returns a charlist.
- The `math` module that provides mathematical functions such as `sin/1`, `cos/1`, `log2/1`, `log10/1`, `pow/2`, and more.
To discover Erlang's standard library, explore the [STDLIB Reference Manual][erl-stdlib-ref].
[erl-stdlib-ref]: https://www.erlang.org/doc/apps/stdlib/index.html
## Instructions
Mary is a big fan of the TV series _Star Trek: The Next Generation_. She often plays pen-and-paper role playing games, where she and her friends pretend to be the crew of the _Starship Enterprise_. Mary's character is Captain Picard, which means she has to keep the captain's log. She loves the creative part of the game, but doesn't like to generate random data on the spot.
Help Mary by creating random generators for data commonly appearing in the captain's log.
## 1. Generate a random planet
The _Starship Enterprise_ encounters many planets in its travels. Planets in the Star Trek universe are split into categories based on their properties. For example, Earth is a class M planet. All possible planetary classes are: D, H, J, K, L, M, N, R, T, and Y.
Implement the `random_planet_class/0` function. It should return one of the planetary classes at random.
```elixir
CaptainsLog.random_planet_class()
# => "K"
```
## 2. Generate a random starship registry number
Enterprise (registry number NCC-1701) is not the only starship flying around! When it rendezvous with another starship, Mary needs to log the registry number of that starship.
Registry numbers start with the prefix "NCC-" and then use a number from 1000 to 9999 (inclusive).
Implement the `random_ship_registry_number/0` function that returns a random starship registry number.
```elixir
CaptainsLog.random_ship_registry_number()
# => "NCC-1947"
```
## 3. Generate a random stardate
What's the use of a log if it doesn't include dates?
A stardate is a floating point number. The adventures of the _Starship Enterprise_ from the first season of _The Next Generation_ take place between the stardates 41000.0 and 42000.0. The "4" stands for the 24th century, the "1" for the first season.
Implement the function `random_stardate/0` that returns a floating point number between 41000.0 (inclusive) and 42000.0 (exclusive).
The implementation should use an Erlang function.
```elixir
CaptainsLog.random_stardate()
# => 41458.15721310934
```
## 4. Format the stardate
In the captain's log, stardates are usually rounded to a single decimal place.
Implement the `format_stardate/1` function that will take a floating point number and return a string with the number rounded to a single decimal place.
The implementation should use an Erlang function.
```elixir
CaptainsLog.format_stardate(41458.15721310934)
# => "41458.2"
```
## Source
### Created by
- @angelikatyborska
### Contributed to by
- @neenjaw

View File

@ -0,0 +1,20 @@
defmodule CaptainsLog do
@planetary_classes ["D", "H", "J", "K", "L", "M", "N", "R", "T", "Y"]
def random_planet_class() do
Enum.random(@planetary_classes)
end
def random_ship_registry_number() do
"NCC-#{Enum.random(1_000..9_999)}"
end
def random_stardate() do
:rand.uniform() * 1_000 + 41_000
end
def format_stardate(stardate) do
:io_lib.format('~.1f', [stardate])
|> Kernel.to_string()
end
end

View File

@ -0,0 +1,28 @@
defmodule CaptainsLog.MixProject do
use Mix.Project
def project do
[
app: :captains_log,
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,119 @@
defmodule CaptainsLogTest do
use ExUnit.Case
describe "random_planet_class" do
@tag task_id: 1
test "it always returns one of the letters: D, H, J, K, L, M, N, R, T, Y" do
planetary_classes = ["D", "H", "J", "K", "L", "M", "N", "R", "T", "Y"]
Enum.each(0..100, fn _ ->
assert CaptainsLog.random_planet_class() in planetary_classes
end)
end
@tag task_id: 1
test "it will eventually return each of the letters at least once" do
planetary_classes = ["D", "H", "J", "K", "L", "M", "N", "R", "T", "Y"]
never_returned_planetary_classes =
Enum.reduce_while(0..1000, planetary_classes, fn _, remaining_planetary_classes ->
if remaining_planetary_classes == [] do
{:halt, remaining_planetary_classes}
else
{:cont, remaining_planetary_classes -- [CaptainsLog.random_planet_class()]}
end
end)
assert never_returned_planetary_classes == []
end
end
describe "random_ship_registry_number" do
@tag task_id: 2
test "start with \"NCC-\"" do
assert String.starts_with?(CaptainsLog.random_ship_registry_number(), "NCC-")
end
@tag task_id: 2
test "ends with a random integer between 1000 and 9999" do
Enum.each(0..100, fn _ ->
random_ship_registry_number = CaptainsLog.random_ship_registry_number()
just_the_number = String.replace(random_ship_registry_number, "NCC-", "")
case Integer.parse(just_the_number) do
{integer, ""} ->
assert integer >= 1000
assert integer <= 9999
_ ->
flunk("Expected #{just_the_number} to be an integer")
end
end)
end
end
describe "random_stardate" do
@tag task_id: 3
test "is a float" do
assert is_float(CaptainsLog.random_stardate())
end
@tag task_id: 3
test "is equal to or greater than 41_000.0" do
Enum.each(0..100, fn _ ->
assert CaptainsLog.random_stardate() >= 41_000.0
end)
end
@tag task_id: 3
test "is less than 42_000.0" do
Enum.each(0..100, fn _ ->
assert CaptainsLog.random_stardate() < 42_000.0
end)
end
@tag task_id: 3
test "consecutive calls return floats with different fractional parts" do
decimal_parts =
Enum.map(0..10, fn _ ->
random_stardate = CaptainsLog.random_stardate()
Float.ceil(random_stardate) - random_stardate
end)
assert Enum.count(Enum.uniq(decimal_parts)) > 3
end
@tag task_id: 3
test "returns floats with fractional parts with more than one decimal place" do
decimal_parts =
Enum.map(0..10, fn _ ->
random_stardate = CaptainsLog.random_stardate()
Float.ceil(random_stardate * 10) - random_stardate * 10
end)
assert Enum.count(Enum.uniq(decimal_parts)) > 3
end
end
describe "format_stardate" do
@tag task_id: 4
test "returns a string" do
assert is_bitstring(CaptainsLog.format_stardate(41010.7))
end
@tag task_id: 4
test "formats floats" do
assert CaptainsLog.format_stardate(41543.3) == "41543.3"
end
@tag task_id: 4
test "rounds floats to one decimal place" do
assert CaptainsLog.format_stardate(41032.4512) == "41032.5"
end
@tag task_id: 4
test "does not accept integers" do
assert_raise ArgumentError, fn -> CaptainsLog.format_stardate(41411) end
end
end
end

View File

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