This commit is contained in:
Ruidy 2022-03-11 23:18:34 -04:00
parent dba21bcf5c
commit c43da91eb3
31 changed files with 1230 additions and 0 deletions

View file

@ -0,0 +1,35 @@
{
"blurb": "Convert a long phrase to its acronym.",
"authors": [
"Teapane"
],
"contributors": [
"angelikatyborska",
"Cohen-Carlisle",
"dalexj",
"devonestes",
"gmile",
"henrik",
"jwworth",
"lpil",
"martinsvalin",
"neenjaw",
"parkerl",
"rubysolo",
"sotojuan",
"waiting-for-dev"
],
"files": {
"solution": [
"lib/acronym.ex"
],
"test": [
"test/acronym_test.exs"
],
"example": [
".meta/example.ex"
]
},
"source": "Julien Vanier",
"source_url": "https://github.com/monkbroc"
}

View file

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

4
acronym/.formatter.exs Normal file
View file

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

24
acronym/.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").
acronym-*.tar

75
acronym/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/acronym.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
acronym/README.md Normal file
View file

@ -0,0 +1,40 @@
# Acronym
Welcome to Acronym on Exercism's Elixir Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
## Instructions
Convert a phrase to its acronym.
Techies love their TLA (Three Letter Acronyms)!
Help generate some jargon by writing a program that converts a long name
like Portable Network Graphics to its acronym (PNG).
## Source
### Created by
- @Teapane
### Contributed to by
- @angelikatyborska
- @Cohen-Carlisle
- @dalexj
- @devonestes
- @gmile
- @henrik
- @jwworth
- @lpil
- @martinsvalin
- @neenjaw
- @parkerl
- @rubysolo
- @sotojuan
- @waiting-for-dev
### Based on
Julien Vanier - https://github.com/monkbroc

12
acronym/lib/acronym.ex Normal file
View file

@ -0,0 +1,12 @@
defmodule Acronym do
@doc """
Generate an acronym from a string.
"This is a string" => "TIAS"
"""
@spec abbreviate(String.t()) :: String.t()
def abbreviate(string) do
String.split(string, [" ", "-", "_"])
|> Enum.map_join(&String.at(&1, 0))
|> String.upcase()
end
end

28
acronym/mix.exs Normal file
View file

@ -0,0 +1,28 @@
defmodule Acronym.MixProject do
use Mix.Project
def project do
[
app: :acronym,
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,41 @@
defmodule AcronymTest do
use ExUnit.Case
test "it produces acronyms from title case" do
assert Acronym.abbreviate("Portable Networks Graphic") === "PNG"
end
test "it produces acronyms from lower case" do
assert Acronym.abbreviate("Ruby on Rails") === "ROR"
end
test "it ignores punctuation" do
assert Acronym.abbreviate("First in, First out") === "FIFO"
end
test "it produces acronyms from phrases with acronyms" do
assert Acronym.abbreviate("GNU Image Manipulation Program") === "GIMP"
end
test "it produces acronyms ignoring punctuation and casing" do
assert Acronym.abbreviate("Complementary Metal-Oxide semiconductor") === "CMOS"
end
test "it produces a very long abbreviation" do
assert Acronym.abbreviate(
"Rolling On The Floor Laughing So Hard That My Dogs Came Over And Licked Me"
) === "ROTFLSHTMDCOALM"
end
test "it produces acronyms from phrases with consecutive delimiters" do
assert Acronym.abbreviate("Something - I made up from thin air") === "SIMUFTA"
end
test "it produces acronyms from phrases with apostrophes" do
assert Acronym.abbreviate("Halley's Comet") === "HC"
end
test "it produces acronyms from phrases with underscore emphasis" do
assert Acronym.abbreviate("The Road _Not_ Taken") === "TRNT"
end
end

View file

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

View file

@ -0,0 +1,41 @@
{
"blurb": "Given a word and a list of possible anagrams, select the correct sublist.",
"authors": [
"rubysolo"
],
"contributors": [
"andrewsardone",
"angelikatyborska",
"Br1ght0ne",
"Cohen-Carlisle",
"crazymykl",
"dalexj",
"devonestes",
"henrik",
"jeremy-w",
"jinyeow",
"kytrinyx",
"lpil",
"markijbema",
"neenjaw",
"parkerl",
"pminten",
"sotojuan",
"Teapane",
"tjcelaya",
"waiting-for-dev"
],
"files": {
"solution": [
"lib/anagram.ex"
],
"test": [
"test/anagram_test.exs"
],
"example": [
".meta/example.ex"
]
},
"source": "Inspired by the Extreme Startup game",
"source_url": "https://github.com/rchatley/extreme_startup"
}

View file

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

4
anagram/.formatter.exs Normal file
View file

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

24
anagram/.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").
anagram-*.tar

75
anagram/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/anagram.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).

51
anagram/README.md Normal file
View file

@ -0,0 +1,51 @@
# Anagram
Welcome to Anagram on Exercism's Elixir Track.
If you need help running the tests or submitting your code, check out `HELP.md`.
## Instructions
An anagram is a rearrangement of letters to form a new word: for example `"owns"` is an anagram of `"snow"`.
A word is not its own anagram: for example, `"stop"` is not an anagram of `"stop"`.
Given a target word and a set of candidate words, this exercise requests the anagram set: the subset of the candidates that are anagrams of the target.
The target and candidates are words of one or more ASCII alphabetic characters (`A`-`Z` and `a`-`z`).
Lowercase and uppercase characters are equivalent: for example, `"PoTS"` is an anagram of `"sTOp"`, but `StoP` is not an anagram of `sTOp`.
The anagram set is the subset of the candidate set that are anagrams of the target (in any order).
Words in the anagram set should have the same letter case as in the candidate set.
Given the target `"stone"` and candidates `"stone"`, `"tones"`, `"banana"`, `"tons"`, `"notes"`, `"Seton"`, the anagram set is `"tones"`, `"notes"`, `"Seton"`.
## Source
### Created by
- @rubysolo
### Contributed to by
- @andrewsardone
- @angelikatyborska
- @Br1ght0ne
- @Cohen-Carlisle
- @crazymykl
- @dalexj
- @devonestes
- @henrik
- @jeremy-w
- @jinyeow
- @kytrinyx
- @lpil
- @markijbema
- @neenjaw
- @parkerl
- @pminten
- @sotojuan
- @Teapane
- @tjcelaya
- @waiting-for-dev
### Based on
Inspired by the Extreme Startup game - https://github.com/rchatley/extreme_startup

8
anagram/lib/anagram.ex Normal file
View file

@ -0,0 +1,8 @@
defmodule Anagram do
@doc """
Returns all candidates that are anagrams of, but not equal to, 'base'.
"""
@spec match(String.t(), [String.t()]) :: [String.t()]
def match(base, candidates) do
end
end

28
anagram/mix.exs Normal file
View file

@ -0,0 +1,28 @@
defmodule Anagram.MixProject do
use Mix.Project
def project do
[
app: :anagram,
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,87 @@
defmodule AnagramTest do
use ExUnit.Case
# @tag :pending
test "no matches" do
matches = Anagram.match("diaper", ~w(hello world zombies pants))
assert matches == []
end
@tag :pending
test "detects two anagrams" do
matches = Anagram.match("solemn", ~w(lemons cherry melons))
assert matches == ~w(lemons melons)
end
@tag :pending
test "does not detect anagram subsets" do
matches = Anagram.match("good", ~w(dog goody))
assert matches == []
end
@tag :pending
test "detects anagram" do
matches = Anagram.match("listen", ~w(enlists google inlets banana))
assert matches == ~w(inlets)
end
@tag :pending
test "detects three anagrams" do
matches = Anagram.match("allergy", ~w(gallery ballerina regally clergy largely leading))
assert matches == ~w(gallery regally largely)
end
@tag :pending
test "detects multiple anagrams with different case" do
matches = Anagram.match("nose", ~w(Eons ONES))
assert matches == ~w(Eons ONES)
end
@tag :pending
test "does not detect non-anagrams with identical checksum" do
matches = Anagram.match("mass", ~w(last))
assert matches == []
end
@tag :pending
test "detect anagrams case-insensitively" do
matches = Anagram.match("orchestra", ~w(cashregister Carthorse radishes))
assert matches == ~w(Carthorse)
end
@tag :pending
test "detects anagrams using case-insensitive subject" do
matches = Anagram.match("Orchestra", ~w(cashregister carthorse radishes))
assert matches == ~w(carthorse)
end
@tag :pending
test "detects anagrams using case-insensitive possible matches" do
matches = Anagram.match("orchestra", ~w(cashregister Carthorse radishes))
assert matches == ~w(Carthorse)
end
@tag :pending
test "does not detect an anagram if the original word is repeated" do
matches = Anagram.match("go", ~w(go Go GO))
assert matches == []
end
@tag :pending
test "anagrams must use all letters exactly once" do
matches = Anagram.match("tapper", ~w(patter))
assert matches == []
end
@tag :pending
test "words are not anagrams of themselves (case-insensitive)" do
matches = Anagram.match("BANANA", ~w(BANANA Banana banana))
assert matches == []
end
@tag :pending
test "words other than themselves can be anagrams" do
matches = Anagram.match("LISTEN", ~w(Listen Silent LISTEN))
assert matches == ~w(Silent)
end
end

View file

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

View file

@ -0,0 +1,19 @@
{
"blurb": "Learn about the Abstract Syntax Tree (AST) by helping decode secret messages from Agent Ex.",
"authors": [
"jiegillet",
"angelikatyborska"
],
"files": {
"solution": [
"lib/top_secret.ex"
],
"test": [
"test/top_secret_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
]
},
"language_versions": ">=1.10"
}

View file

@ -0,0 +1 @@
{"track":"elixir","exercise":"top-secret","id":"5b9a04b7076a477189cf300bf55d6c31","url":"https://exercism.org/tracks/elixir/exercises/top-secret","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
top-secret/.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").
maps-*.tar

75
top-secret/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/top_secret.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).

51
top-secret/HINTS.md Normal file
View file

@ -0,0 +1,51 @@
# Hints
## General
- Read about quoting in the [official Getting Started guide][getting-started-quote].
- Read the [introduction to Elixir AST by Lucas San Román][ast-intro-lucas].
- Read [the official documentation for `quote`][doc-quote].
- Inspect the output of [`quote`][doc-quote] to familiarize yourself with how ASTs look like for specific code snippets.
## 1. Turn code into data
- There is a [built-in function][doc-code-string-to-quoted] that turns a string with code into an AST.
## 2. Parse a single AST node
- Inspect the output of [`quote`][doc-quote] to familiarize yourself with how ASTs look like for specific code snippets.
- The operations that define a function are `:def` and `:defp`.
- The operation is the first element in a three-element AST node tuple.
- You can ignore the second element in the tuple in this exercise completely.
- The third element in the tuple is the argument list of the operation that defines the function.
- The first element on that list is the function's name, and the second element is the function's body.
## 3. Decode the secret message part from function definition
- Inspect the output of [`quote`][doc-quote] to familiarize yourself with how ASTs look like for specific code snippets.
- The AST node that contains the function's name also contains the function's argument list as the third element.
- The arity of a function is the length of its argument list.
- There is a [built-in function in the `String` module][string-slice] that can get the first `n` characters from a string.
- A function without arguments written without parentheses will not have a list as argument but an atom.
## 4. Fix the decoding for functions with guards
- Inspect the output of [`quote`][doc-quote] to familiarize yourself with how ASTs look like for specific code snippets.
- When a function has a guard, the third element in the tuple for the `:def/:defp` operation is a bit different.
- That third element is a list with two elements, the first one is the tuple for the `:when` operation, and the second one is the function's body.
- The `:when` operation's arguments are a two-element list, where the first argument is the function's name, and the second is the guard expression.
## 5. Decode the full secret message
- Use the function `to_ast/1` that you implemented in the first task to create the AST.
- There is a [built-in function][macro-prewalk] that can visit each node in an AST with an accumulator.
- Use the function `decode_secret_message_part/2` that you implemented in previous tasks to prewalk the AST.
- To reverse the accumulator at the end and turn it into a string, refresh your knowledge of the [`Enum` module][enum].
[getting-started-quote]: https://elixir-lang.org/getting-started/meta/quote-and-unquote.html#quoting
[doc-quote]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#quote/2
[ast-intro-lucas]: https://dorgan.ar/posts/2021/04/the_elixir_ast/
[doc-code-string-to-quoted]: https://hexdocs.pm/elixir/Code.html#string_to_quoted/2
[string-slice]: https://hexdocs.pm/elixir/String.html#slice/2
[macro-prewalk]: https://hexdocs.pm/elixir/Macro.html#prewalk/3
[enum]: https://hexdocs.pm/elixir/Enum.html

137
top-secret/README.md Normal file
View file

@ -0,0 +1,137 @@
# Top Secret
Welcome to Top Secret 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
## AST
The Abstract Syntax Tree (AST), also called a _quoted expression_, is a way to represent code as data.
Each node in the AST is a three-element tuple.
```elixir
# AST representation of:
# 2 + 3
{:+, [], [2, 3]}
```
The first element, an atom, is the operation. The second element, a keyword list, is the metadata. The third element is a list of arguments, which contains other nodes. Literal values such as integers, atoms, and strings are represented in the AST as themselves instead of three-element tuples.
### Turning code into ASTs
Changing Elixir code to ASTs and ASTs back to code is part of the standard library. You can find functions for working with ASTs in the modules `Code` (e.g. to change a string with code to an AST) and `Macro` (e.g. to traverse the AST or change it to a string).
Note that all of the functions in the standard library use the name "quoted" to mean the AST (short for _quoted expression_).
The special form for turning code into an AST is called `quote`. It accepts a code block and returns its AST.
```elixir
quote do
2 + 3 - 1
end
# => {:-, [], [
# {:+, [], [2, 3]},
# 1
# ]}
```
### Use cases
The ability to represent code as an AST is at the heart of metaprogramming in Elixir. _Macros_, which is a way to write Elixir code that produces Elixir code, work by returning ASTs as output.
Another use case for ASTs is static code analysis, like Exercism's own tool, the Analyzer, which you might already know as the little bot that leaves comments on your solutions.
## Instructions
You're part of a task force fighting against corporate espionage. You have a secret informer at Shady Company X, which you suspect of stealing secrets from its competitors.
Your informer, Agent Ex, is an Elixir developer. She is encoding secret messages in her code.
To decode her secret messages:
- Take all functions (public and private) in the order they're defined in.
- For each function, take the first `n` characters from its name, where `n` is the function's arity.
## 1. Turn code into data
Implement the `TopSecret.to_ast/1` function. It should take a string with Elixir code and return its AST.
```elixir
TopSecret.to_ast("div(4, 3)")
# => {:div, [line: 1], [4, 3]}
```
## 2. Parse a single AST node
Implement the `TopSecret.decode_secret_message_part/2` function. It should take an AST node and an accumulator for the secret message (a list). It should return a tuple with the AST node unchanged as the first element, and the accumulator as the second element.
If the operation of the AST node is defining a function (`def` or `defp`), prepend the function name (changed to a string) to the accumulator. If the operation is something else, return the accumulator unchanged.
```elixir
ast_node = TopSecret.to_ast("defp cat(a, b, c), do: nil")
TopSecret.decode_secret_message_part(ast_node, ["day"])
# => {ast_node, ["cat", "day"]}
ast_node = TopSecret.to_ast("10 + 3")
TopSecret.decode_secret_message_part(ast_node, ["day"])
# => {ast_node, ["day"]}
```
This function doesn't need to do any recursive calls to check the whole AST, only the given node. We will traverse the whole AST with built-in tools in the last step.
## 3. Decode the secret message part from function definition
Extend the `TopSecret.decode_secret_message_part/2` function. If the operation in the AST node is defining a function, don't return the whole function name. Instead, check the function's arity. Then, return only first `n` character from the name, where `n` is the arity.
```elixir
ast_node = TopSecret.to_ast("defp cat(a, b), do: nil")
TopSecret.decode_secret_message_part(ast_node, ["day"])
# => {ast_node, ["ca", "day"]}
ast_node = TopSecret.to_ast("defp cat(), do: nil")
TopSecret.decode_secret_message_part(ast_node, ["day"])
# => {ast_node, ["", "day"]}
```
## 4. Fix the decoding for functions with guards
Extend the `TopSecret.decode_secret_message_part/2` function. Make sure the function's name and arity is correctly detected for function definitions that use guards.
```elixir
ast_node = TopSecret.to_ast("defp cat(a, b) when is_nil(a), do: nil")
TopSecret.decode_secret_message_part(ast_node, ["day"])
# => {ast_node, ["ca", "day"]}
```
## 5. Decode the full secret message
Implement the `TopSecret.decode_secret_message/1` function. It should take a string with Elixir code and return the secret message as a string decoded from all function definitions found in the code. Make sure to reuse functions defined in previous steps.
```elixir
code = """
defmodule MyCalendar do
def busy?(date, time) do
Date.day_of_week(date) != 7 and
time.hour in 10..16
end
def yesterday?(date) do
Date.diff(Date.utc_today, date)
end
end
"""
TopSecret.decode_secret_message(code)
# => "buy"
```
## Source
### Created by
- @jiegillet
- @angelikatyborska

View file

@ -0,0 +1,26 @@
defmodule TopSecret do
def to_ast(string), do: Code.string_to_quoted!(string)
def decode_secret_message_part(ast, acc) do
case ast do
{:def, _, [{_, _, nil} | _]} ->
{ast, [""] ++ acc}
{:def, _, [{name, _, args} | _]} ->
{ast, [String.slice(to_string(name), 0, length(args))] ++ acc}
{:defp, _, [{_, _, nil} | _]} ->
{ast, [""] ++ acc}
{:defp, _, [{name, _, args} | _]} ->
{ast, [String.slice(to_string(name), 0, length(args))] ++ acc}
_ ->
{ast, acc}
end
end
def decode_secret_message(string) do
# Please implement the decode_secret_message/1 function
end
end

28
top-secret/mix.exs Normal file
View file

@ -0,0 +1,28 @@
defmodule TopSecret.MixProject do
use Mix.Project
def project do
[
app: :top_secret,
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,2 @@
ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true, seed: 0)

View file

@ -0,0 +1,280 @@
defmodule TopSecretTest do
use ExUnit.Case
describe "to_ast/1" do
@tag task_id: 1
test "handles an empty string" do
string = ""
ast = {:__block__, [], []}
assert TopSecret.to_ast(string) == ast
end
@tag task_id: 1
test "handles a small code snippet" do
string = """
x = 7
y = x - 2
"""
ast =
{:__block__, [],
[
{:=, [line: 1], [{:x, [line: 1], nil}, 7]},
{:=, [line: 2], [{:y, [line: 2], nil}, {:-, [line: 2], [{:x, [line: 2], nil}, 2]}]}
]}
assert TopSecret.to_ast(string) == ast
end
@tag task_id: 1
test "handles a bigger code snippet" do
string = """
defmodule List do
@spec delete([], any) :: []
@spec delete([...], any) :: list
def delete(list, element)
end
"""
ast = {
:defmodule,
[line: 1],
[
{:__aliases__, [line: 1], [:List]},
[
do: {
:__block__,
[],
[
{:@, [line: 2],
[
{:spec, [line: 2],
[{:"::", [line: 2], [{:delete, [line: 2], [[], {:any, [line: 2], nil}]}, []]}]}
]},
{:@, [line: 3],
[
{:spec, [line: 3],
[
{:"::", [line: 3],
[
{:delete, [line: 3], [[{:..., [line: 3], nil}], {:any, [line: 3], nil}]},
{:list, [line: 3], nil}
]}
]}
]},
{:def, [line: 4],
[{:delete, [line: 4], [{:list, [line: 4], nil}, {:element, [line: 4], nil}]}]}
]
}
]
]
}
assert TopSecret.to_ast(string) == ast
end
end
describe "decode_secret_message_part/2" do
@tag task_id: 2
test "returns the AST and accumulator unchanged" do
string = "2 + 3"
ast = TopSecret.to_ast(string)
acc = ["le", "mo"]
{actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc)
assert actual_ast == ast
assert actual_acc == acc
end
@tag task_id: 2
test "appends a public function name to the accumulator" do
string = "def fit(a, b, c), do: :scale"
ast = TopSecret.to_ast(string)
acc = ["at"]
{actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc)
assert actual_ast == ast
assert actual_acc == ["fit", "at"]
end
@tag task_id: 2
test "appends a private function name to the accumulator" do
string = "defp op(a, b), do: 2"
ast = TopSecret.to_ast(string)
acc = ["e", "ced"]
{actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc)
assert actual_ast == ast
assert actual_acc == ["op", "e", "ced"]
end
@tag task_id: 2
test "ignores not top-level function definition" do
string = """
defmodule Math do
def sin(x), do: do_sin(x)
defp do_sin(x), do: nil
end
"""
ast = TopSecret.to_ast(string)
acc = []
{actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc)
assert actual_ast == ast
assert actual_acc == acc
end
@tag task_id: 3
test "function arity affects message part length" do
string = "def adjust(a, b), do: :scale"
ast = TopSecret.to_ast(string)
acc = ["re"]
{actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc)
assert actual_ast == ast
assert actual_acc == ["ad", "re"]
end
@tag task_id: 3
test "function arity 0 results in empty string" do
string = "def adjust(), do: :scale"
ast = TopSecret.to_ast(string)
acc = ["re"]
{actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc)
assert actual_ast == ast
assert actual_acc == ["", "re"]
end
@tag task_id: 3
test "function arity 0 and no parentheses results in empty string" do
string = "def adjust, do: :scale"
ast = TopSecret.to_ast(string)
acc = ["re"]
{actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc)
assert actual_ast == ast
assert actual_acc == ["", "re"]
end
@tag task_id: 4
test "works for public functions with a guard" do
string = "def sign(a) when a >= 0, do: :+"
ast = TopSecret.to_ast(string)
acc = ["e"]
{actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc)
assert actual_ast == ast
assert actual_acc == ["s", "e"]
end
@tag task_id: 4
test "works for private functions with a guard" do
string = "defp do_sign(a) when a < 0, do: :-"
ast = TopSecret.to_ast(string)
acc = ["e"]
{actual_ast, actual_acc} = TopSecret.decode_secret_message_part(ast, acc)
assert actual_ast == ast
assert actual_acc == ["d", "e"]
end
end
describe "decode_secret_message/1" do
@tag task_id: 5
test "decodes a secret message from a single function definition" do
code = """
defmodule Notebook do
def note(notebook, text) do
add_to_notebook(notebook, text, append: true)
end
end
"""
secret_message = "no"
assert TopSecret.decode_secret_message(code) == secret_message
end
@tag task_id: 5
test "decodes a secret message from a two function definitions" do
code = """
defmodule MyCalendar do
def busy?(date, time) do
Date.day_of_week(date) != 7 and
time.hour in 10..16
end
def yesterday?(date) do
Date.diff(Date.utc_today, date)
end
end
"""
secret_message = "buy"
assert TopSecret.decode_secret_message(code) == secret_message
end
@tag task_id: 5
test "decodes a secret message from many function definitions" do
code = """
defmodule TotallyNotTopSecret do
def force(mass, acceleration), do: mass * acceleration
def uniform(from, to), do: rand.uniform(to - from) + from
def data(%{metadata: metadata}, _opts), do: model(metadata)
defp model(metadata, _opts), do: metadata |> less_data |> Enum.reverse() |> Enum.take(3)
defp less_data(data, _opts), do: Enum.reject(data, &is_nil/1)
end
"""
secret_message = "foundamole"
assert TopSecret.decode_secret_message(code) == secret_message
end
@tag task_id: 5
test "decodes a secret message without a module definition" do
code = """
def force(mass, acceleration), do: mass * acceleration
def uniform(from, to), do: rand.uniform(to - from) + from
def data(%{metadata: metadata}, _opts), do: model(metadata)
defp model(metadata, _opts), do: metadata |> less_data |> Enum.reverse() |> Enum.take(3)
defp less_data(data, _opts), do: Enum.reject(data, &is_nil/1)
"""
secret_message = "foundamole"
assert TopSecret.decode_secret_message(code) == secret_message
end
@tag task_id: 5
test "decodes another secret message from multiple modules" do
code = """
defmodule IOHelpers do
def inspect(x, opts), do: IO.inspect(x, opts)
def vi_or_vim(_env, _preference), do: :vim
def signal(pid, string), do: send(pid, {:signal, string})
def black(text, label), do: IO.ANSI.black <> label <> text <> IO.ANSI.reset()
end
defmodule TimeHelpers do
defp est_to_cet(time), do: Time.add(time, 6 * 60 * 60)
end
defmodule ASTHelpers do
def submodule?(m, _f, _args), do: String.contains?(m, ".")
def module({m, _f, _args}), do: m
def arity(_m, _f, args), do: length(args)
defp nested?(x, y) when is_list(y), do: x in y
end
"""
secret_message = "invisiblesubmarine"
assert TopSecret.decode_secret_message(code) == secret_message
end
end
end