pain-by-number

This commit is contained in:
Danil Negrienko 2023-12-19 09:38:41 -05:00
parent 3001618707
commit 9d03ff01dc
12 changed files with 699 additions and 0 deletions

View File

@ -0,0 +1,27 @@
{
"authors": [
"angelikatyborska"
],
"contributors": [
"neenjaw",
"meatball133",
"vaeng",
"glennj"
],
"files": {
"solution": [
"lib/paint_by_number.ex"
],
"test": [
"test/paint_by_number_test.exs"
],
"exemplar": [
".meta/exemplar.ex"
],
"editor": [
"lib/math.ex"
]
},
"language_versions": ">=1.10",
"blurb": "Learn about bitstrings by creating binary files with pictures for a paint-by-number app."
}

View File

@ -0,0 +1 @@
{"track":"elixir","exercise":"paint-by-number","id":"5739ec34143e406c9f072ea94450aa45","url":"https://exercism.org/tracks/elixir/exercises/paint-by-number","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/paint-by-number/.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").
paint-by-number-*.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/paint_by_number.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,57 @@
# Hints
## General
- Read about [bitstrings][bitstring] in the Getting Started guide.
- Review the documentation for the [bitstring special form][bitstring-form].
- Review [how to convert decimal numbers to binary numbers][decimal-to-binary-youtube].
- Elixir supports [different ways of writing integers][integer-literal], including binary notation.
## 1. Calculate palette bit size
- This task doesn't use the bitstring data type.
- Find the smallest positive number `n` such that 2 raised to the power of `n` is greater than or equal than the number of colors we need to represent.
- Create a recursive function for this.
- Choose a starting bit size of 1. If 2 raised to the power of the bit size is greater than or equal to the color count, we've found our bit size. If not, add 1 to the bit size and check again (recursion).
- Use [`Integer.pow/2`][integer-pow] to raise 2 to a given power.
## 2. Create an empty picture
- Use the [bitstring special form][bitstring-form] to create a new bitstring.
## 3. Create a test picture
- Use the [bitstring special form][bitstring-form] to create a new bitstring.
- Use the [type operator][type-operator] to specify the bit size of each fragment.
## 4. Prepend a pixel to a picture
- The [bitstring special form][bitstring-form] can be used to append a new fragment to an existing bitstring.
- Use the [type operator][type-operator] to specify the bit size of the new fragment as well as the existing bitstring.
- Use the previously implemented `PaintByNumber.palette_bit_size/1` function to get the bit size of the new fragment.
- Use the special `::bitstring` type to specify that the old fragment is of unknown size.
## 5. Get the first pixel from a picture
- The [bitstring special form][bitstring-form] can be used to [pattern match bitstrings][bitstring-matching].
- Use the previously implemented `PaintByNumber.palette_bit_size/1` function to get the bit size of one pixel.
- Use the special `::bitstring` type to specify that the rest of the bitstring is of unknown size.
## 6. Drop the first pixel from a picture
- The [bitstring special form][bitstring-form] can be used to [pattern match bitstrings][bitstring-matching].
- Use the previously implemented `PaintByNumber.palette_bit_size/1` function to get the bit size of one pixel.
- Use the special `::bitstring` type to specify that the rest of the bitstring is of unknown size.
## 7. Concatenate two pictures
- The [bitstring special form][bitstring-form] can be used to concatenate two bitstrings.
- Use the special `::bitstring` type to specify that each of the bitstring fragments is of unknown size.
[decimal-to-binary-youtube]: https://www.youtube.com/watch?v=gGiEu7QTi68
[integer-literal]: https://hexdocs.pm/elixir/syntax-reference.html#integers-in-other-bases-and-unicode-code-points
[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
[integer-pow]: https://hexdocs.pm/elixir/Integer.html#pow/2

View File

@ -0,0 +1,226 @@
# Paint By Number
Welcome to Paint By Number 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
## Bitstrings
Working with binary data is an important concept in any language, and Elixir provides an elegant syntax to write, match, and construct binary data.
In Elixir, binary data is referred to as the bitstring type. The binary data _type_ (not to be confused with binary data in general) is a specific form of a bitstring, which we will discuss in a later exercise.
Bitstring literals are defined using the bitstring special form `<<>>`. When defining a bitstring literal, it is defined in segments. Each segment has a value and type, separated by the `::` operator. The type specifies how many bits will be used to encode the value. The type can be omitted completely, which will default to a 8-bit integer value.
```elixir
# This defines a bitstring with three segments of a single bit each
<<0::1, 1::1, 0::1>>
```
Specifying the type as `::1` is a shorthand for writing `::size(1)`. You need to use the longer syntax if the bit size comes from a variable.
### Binary
When writing binary integer literals, we can write them directly in base-2 notation by prefixing the literal with `0b`. Note that they will be anyway displayed as decimal numbers when printed in tests results or when using iex.
```elixir
<<0b1011::4>> == <<11::4>>
# => true
```
### Truncating
If the value of the segment overflows the capacity of the segment's type, it will be truncated from the left.
```elixir
<<0b1011::3>> == <<0b0011::3>>
# => true
```
### Prepending and appending
You can both prepend and append to an existing bitstring using the special form. The `::bitstring` type must be used on the existing bitstring if it's of unknown size.
```elixir
value = <<0b110::3, 0b001::3>>
new_value = <<0b011::3, value::bitstring, 0b000::3>>
# => <<120, 8::size(4)>>
```
### Concatenating
We can concatenate bitstrings stored in variables using the special form. The `::bitstring` type must be used when concatenating two bitstrings of unknown sizes.
```elixir
first = <<0b110::3>>
second = <<0b001::3>>
concatenated = <<first::bitstring, second::bitstring>>
# => <<49::size(6)>>
```
### Pattern matching
Pattern matching can also be done to obtain values from the special form. You have to know the number of bits for each fragment you want to capture, with one exception: the `::bitstring` type can be used to pattern match on a bitstring of an unknown size, but this can only be used for the last fragment.
```elixir
<<value::4, rest::bitstring>> = <<0b01101001::8>>
value == 0b0110
# => true
```
### Inspecting bitstrings
~~~~exercism/note
Bitstrings might be printed (by the test runner or in iex) in a
different format than the format that was used to create them. This often
causes confusion when learning bitstrings.
~~~~
By default, bitstrings are displayed in fragments of 8 bits (a byte), even if you created them with fragments of a different size.
```elixir
<<2011::11>>
# => <<251, 3::size(3)>>
```
If you create a bitstring that represents a printable UTF-8 encoded string, it gets displayed as a string.
```elixir
<<>>
# => ""
<<65, 66, 67>>
# => "ABC"
```
## Instructions
[Paint by number][paint-by-number] (also known as _color by number_)
are black-and-white pictures meant for coloring.
The different areas of the picture are annotated with different numbers,
the numbers correspond to specific colors in a predefined color palette.
The goal is to fill the areas with the right colors,
revealing a beautiful colorful picture at the end.
It's a relaxing activity for both kids and adults.
You have been tasked with writing a paint-by-number app in Elixir.
You want your app to be able to import and export pictures in a custom data format.
You have decided to use [binary files][binary-file] to store your picture data.
~~~~exercism/note
This exercise assumes that you're familiar with [binary numbers](https://en.wikipedia.org/wiki/Binary_number)
and understand the principles behind changing binary numbers to decimal numbers
and decimal numbers to binary numbers.
~~~~
Let's imagine you have a picture of a smiley, like the one shown below.
The picture has a white background. The smiley has a black border and a yellow fill color.
This picture uses 3 colors.
Let's say we assign indices to those colors:
- `0` (binary: `0b00`) for white,
- `1` (binary: `0b01`) for black,
- `2` (binary: `0b10`) for yellow.
We can now use those color indices to represent the color of each pixel.
| Smiley | Smiley with color indices |
|-------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
| ![](https://assets.exercism.org/images/exercises/paint-by-number/smiley.png) | ![](https://assets.exercism.org/images/exercises/paint-by-number/smiley-numbers.png) |
## 1. Calculate palette bit size
Implement the `PaintByNumber.palette_bit_size/1` function. It should take the count of colors in the palette and return how many bits are necessary to represent that many color indices as binary numbers. Color indices always start at 0 and are continuous ascending integers.
For example, representing 13 different colors require 4 bits. 4 bits can store up to 16 color indices (2^4). 3 bits would not be enough because 3 bits can only store up to 8 color indices (2^3).
```elixir
PaintByNumber.palette_bit_size(13)
# => 4
```
Note: there is no `log2` function in the Elixir standard library. You will later learn how to use [Erlang libraries][erlang-libraries] from Elixir where you can find this function. Now, solve this task with recursion and the [`Integer.pow/2`][integer-pow] function instead. If you're solving this exercise on your own computer using an older Elixir version (1.11 or lower) that doesn't have `Integer.pow/2` function, use the `Math.pow/2` function we provided in the `lib/math.ex` file for this exercise.
## 2. Create an empty picture
Implement the `PaintByNumber.empty_picture/0` function. It should return an empty bitstring.
## 3. Create a test picture
A predefined test picture will be used for manual testing of basic features of your app.
The test picture consists of 4 pixels with 4 different colors.
Implement the `PaintByNumber.test_picture/0` function. It should return a bitstring that consists of 4 segments.
Each segment should have a bit size of 2. The segments should have values 0, 1, 2, and 3.
## 4. Prepend a pixel to a picture
Implement the `PaintByNumber.prepend_pixel/3` function. It should take three arguments: a bitstring with the picture to which we're prepending, the count of colors in the palette, and the index of the color for the new pixel. It should return a bitstring with a picture with the new pixel added to the beginning.
```elixir
picture = <<2::4, 0::4>>
color_count = 13
pixel_color_index = 11
PaintByNumber.prepend_pixel(picture, color_count, pixel_color_index)
# => <<178, 0::size(4)>>
# (which is equal to <<11::4, 2::4, 0::4>>)
```
## 5. Get the first pixel from a picture
Implement the `PaintByNumber.get_first_pixel/2` function. It should take two arguments: a bitstring with the picture from which we're reading, and the count of colors in the palette. It should return the color index of the first pixel in the given picture. When given an empty picture, it should return `nil`.
```elixir
picture = <<19::5, 2::5, 18::5>>
color_count = 20
PaintByNumber.get_first_pixel(picture, color_count)
# => 19
```
## 6. Drop the first pixel from a picture
Implement the `PaintByNumber.drop_first_pixel/2` function. It should take two arguments: a bitstring with the picture from which we're removing a pixel, and the count of colors in the palette. It should return the picture without the first pixel. When given an empty picture, it should return an empty picture.
```elixir
picture = <<2::3, 5::3, 5::3, 0::3>>
color_count = 6
PaintByNumber.drop_first_pixel(picture, color_count)
# => <<180, 0::size(1)>>
# (which is equal to <<5::3, 5::3, 0::3>>)
```
## 7. Concatenate two pictures
Implement the `PaintByNumber.concat_pictures/2` function. It should take two arguments, two bitstrings. It should return a bitstrings that is the result of prepending the first argument to the second argument.
```elixir
picture1 = <<52::6, 51::6>>
picture2 = <<0::6, 34::6, 12::6>>
PaintByNumber.concat_pictures(picture1, picture2)
# => <<211, 48, 34, 12::size(6)>>
# (which is equal to <<52::6, 51::6, 0::6, 34::6, 12::6>>)
```
[paint-by-number]: https://en.wikipedia.org/wiki/Paint_by_number
[binary-file]: https://en.wikipedia.org/wiki/Binary_file
[erlang-libraries]: https://exercism.org/tracks/elixir/concepts/erlang-libraries
[integer-pow]: https://hexdocs.pm/elixir/Integer.html#pow/2
## Source
### Created by
- @angelikatyborska
### Contributed to by
- @neenjaw
- @meatball133
- @vaeng
- @glennj

View File

@ -0,0 +1,18 @@
# You can ignore this file if you're solving this exercise in the web editor,
# or on your own computer if you have Elixir version 1.12 or higher.
# You can check which Elixir version you have by running `elixir -v` in your terminal.
defmodule Math do
import Bitwise
# copied from https://github.com/elixir-lang/elixir/blob/v1.12.0/lib/elixir/lib/integer.ex#L103-L114
def pow(base, exponent) when is_integer(base) and is_integer(exponent) do
if exponent < 0, do: raise("exponent cannot be negative")
guarded_pow(base, exponent)
end
# https://en.wikipedia.org/wiki/Exponentiation_by_squaring
defp guarded_pow(_, 0), do: 1
defp guarded_pow(b, 1), do: b
defp guarded_pow(b, e) when (e &&& 1) == 0, do: guarded_pow(b * b, e >>> 1)
defp guarded_pow(b, e), do: b * guarded_pow(b * b, e >>> 1)
end

View File

@ -0,0 +1,41 @@
defmodule PaintByNumber do
def palette_bit_size(color_count, pow \\ 1) do
if color_count <= 2 ** pow do
pow
else
palette_bit_size(color_count, pow + 1)
end
end
def empty_picture() do
<<>>
end
def test_picture() do
<<0::2, 1::2, 2::2, 3::2>>
end
def prepend_pixel(picture, color_count, pixel_color_index) do
size = palette_bit_size(color_count)
pixel = <<pixel_color_index::size(size)>>
<<pixel::bitstring, picture::bitstring>>
end
def get_first_pixel(<<>>, _color_count), do: nil
def get_first_pixel(picture, color_count) do
size = palette_bit_size(color_count)
<<value::size(size), _rest::bitstring>> = picture
value
end
def drop_first_pixel(<<>>, _color_count), do: <<>>
def drop_first_pixel(picture, color_count) do
size = palette_bit_size(color_count)
<<_value::size(size), rest::bitstring>> = picture
rest
end
def concat_pictures(picture1, picture2) do
<<picture1::bitstring, picture2::bitstring>>
end
end

View File

@ -0,0 +1,28 @@
defmodule PaintByNumber.MixProject do
use Mix.Project
def project do
[
app: :paint_by_number,
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,196 @@
defmodule PaintByNumberTest do
use ExUnit.Case
describe "palette_bit_size/1" do
@tag task_id: 1
test "needs 1 bit to encode two colors" do
color_count = 2
assert PaintByNumber.palette_bit_size(color_count) == 1
end
@tag task_id: 1
test "needs 2 bits to encode three colors" do
color_count = 3
assert PaintByNumber.palette_bit_size(color_count) == 2
end
@tag task_id: 1
test "needs 2 bits to encode four colors" do
color_count = 4
assert PaintByNumber.palette_bit_size(color_count) == 2
end
@tag task_id: 1
test "needs 3 bits to encode seven colors" do
color_count = 7
assert PaintByNumber.palette_bit_size(color_count) == 3
end
@tag task_id: 1
test "needs 3 bits to encode eight colors" do
color_count = 8
assert PaintByNumber.palette_bit_size(color_count) == 3
end
@tag task_id: 1
test "needs 4 bits to encode nine colors" do
color_count = 9
assert PaintByNumber.palette_bit_size(color_count) == 4
end
@tag task_id: 1
test "needs 4 bits to encode fourteen colors" do
color_count = 14
assert PaintByNumber.palette_bit_size(color_count) == 4
end
@tag task_id: 1
test "needs 6 bits to encode fifty colors" do
color_count = 50
assert PaintByNumber.palette_bit_size(color_count) == 6
end
@tag task_id: 1
test "needs 20 bits to encode 1 million colors" do
color_count = 1_000_000
assert PaintByNumber.palette_bit_size(color_count) == 20
end
end
describe "empty_picture/0" do
@tag task_id: 2
test "returns an empty bitstring" do
assert PaintByNumber.empty_picture() == <<>>
end
end
describe "test_picture/0" do
@tag task_id: 3
test "returns the numbers 0, 1, 2, and 3 as fragments of size 2" do
assert PaintByNumber.test_picture() == <<0::2, 1::2, 2::2, 3::2>>
end
end
describe "prepend_pixel/3" do
@tag task_id: 4
test "works with an empty picture" do
picture = <<>>
color_count = 16
pixel_color_index = 1
assert PaintByNumber.prepend_pixel(picture, color_count, pixel_color_index) == <<1::4>>
end
@tag task_id: 4
test "works with a non-empty picture" do
picture = <<3::3, 2::3, 2::3>>
color_count = 7
pixel_color_index = 0
assert PaintByNumber.prepend_pixel(picture, color_count, pixel_color_index) ==
<<0::3, 3::3, 2::3, 2::3>>
end
@tag task_id: 4
test "pixel color overflows if it doesn't fit in palette size" do
picture = <<3::6>>
color_count = 64
pixel_color_index = 64
assert PaintByNumber.prepend_pixel(picture, color_count, pixel_color_index) ==
<<0::6, 3::6>>
end
end
describe "get_first_pixel/2" do
@tag task_id: 5
test "returns nil if empty picture" do
picture = <<>>
color_count = 16
assert PaintByNumber.get_first_pixel(picture, color_count) == nil
end
@tag task_id: 5
test "works with a non-empty picture" do
picture = <<1::2, 0::2, 0::2, 2::2>>
color_count = 3
assert PaintByNumber.get_first_pixel(picture, color_count) == 1
end
@tag task_id: 5
test "can reinterpret picture data with a different color count" do
picture = <<0b01::2, 0b10::2, 0b00::2, 0b10::2>>
# Color count of 8 means 3 bits.
color_count = 8
# We take bits from segments until we have 3 bits.
# First, we take `01` from the first segment. Then, `1` from the second segment.
# This gives us the binary number `011`, which is equal to the decimal number 5.
assert PaintByNumber.get_first_pixel(picture, color_count) == 0b011
end
end
describe "drop_first_pixel/2" do
@tag task_id: 6
test "returns empty picture if empty picture" do
picture = <<>>
color_count = 5
assert PaintByNumber.drop_first_pixel(picture, color_count) == <<>>
end
@tag task_id: 6
test "works with a non-empty picture" do
picture = <<23::5, 21::5, 15::5, 3::5>>
color_count = 32
assert PaintByNumber.drop_first_pixel(picture, color_count) == <<21::5, 15::5, 3::5>>
end
@tag task_id: 6
test "can reinterpret picture data with a different color count" do
picture = <<0b011011::6, 0b110001::6>>
# Color count of 4 means 2 bits.
color_count = 4
# We remove the first 2 bits from the first segment.
assert PaintByNumber.drop_first_pixel(picture, color_count) == <<0b1011::4, 0b110001::6>>
end
end
describe "concat_pictures/2" do
@tag task_id: 7
test "two empty pictures result in an empty picture" do
picture1 = <<>>
picture2 = <<>>
assert PaintByNumber.concat_pictures(picture1, picture2) == <<>>
end
@tag task_id: 7
test "a non-picture with an empty picture" do
picture1 = <<5::3, 2::3, 2::3, 4::3>>
picture2 = <<>>
assert PaintByNumber.concat_pictures(picture1, picture2) == picture1
end
@tag task_id: 7
test "an empty picture with a non-empty picture" do
picture1 = <<>>
picture2 = <<13::4, 11::4, 0::4>>
assert PaintByNumber.concat_pictures(picture1, picture2) == picture2
end
@tag task_id: 7
test "two non-empty pictures" do
picture1 = <<2::4, 2::4, 1::4, 14::4>>
picture2 = <<15::4, 14::4>>
assert PaintByNumber.concat_pictures(picture1, picture2) ==
<<2::4, 2::4, 1::4, 14::4, 15::4, 14::4>>
end
@tag task_id: 7
test "two non-empty pictures with different palette bit sizes" do
picture1 = <<0b00::2, 0b01::2, 0b11::2, 0b01::2>>
picture2 = <<0b10101::5, 0b10011::5>>
assert PaintByNumber.concat_pictures(picture1, picture2) ==
<<0b00011101::8, 0b10101100::8, 0b11::2>>
end
end
end

View File

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