Trabalhar com dados binários é um conceito importante em qualquer linguagem, e o Elixir oferece uma sintaxe elegante para escrever, casar e construir dados binários.
No Elixir, os dados binários são chamados de tipo bitstring. O tipo de dados binários (que não deve ser confundido com dados binários em geral) é uma forma específica de bitstring, que discutiremos em um exercício mais adiante.
Os literais de bitstring são definidos usando a forma especial de bitstring <<>>. Ao definir um literal de bitstring, ele é definido em segmentos. Cada segmento tem um valor e um tipo, separados pelo operador ::. O tipo especifica quantos bits serão usados para codificar o valor. O tipo pode ser omitido por completo, e o padrão será um valor inteiro de 8 bits.
# This defines a bitstring with three segments of a single bit each
<<0::1, 1::1, 0::1>>
Especificar o tipo como ::1 é uma forma abreviada de escrever ::size(1). Você precisa usar a sintaxe mais longa se o tamanho em bits vier de uma variável.
Ao escrever literais inteiros binários, podemos escrevê-los diretamente na notação de base 2, prefixando o literal com 0b. Repare que eles serão, de qualquer forma, exibidos como números decimais quando impressos nos resultados dos testes ou ao usar o iex.
<<0b1011::4>> == <<11::4>>
# => true
Se o valor do segmento ultrapassar a capacidade do tipo do segmento, ele será truncado a partir da esquerda.
<<0b1011::3>> == <<0b0011::3>>
# => true
Você pode adicionar tanto no início quanto no fim de um bitstring existente usando a forma especial. O tipo ::bitstring deve ser usado no bitstring existente se ele tiver tamanho desconhecido.
value = <<0b110::3, 0b001::3>>
new_value = <<0b011::3, value::bitstring, 0b000::3>>
# => <<120, 8::size(4)>>
Podemos concatenar bitstrings armazenados em variáveis usando a forma especial. O tipo ::bitstring deve ser usado ao concatenar dois bitstrings de tamanhos desconhecidos.
first = <<0b110::3>>
second = <<0b001::3>>
concatenated = <<first::bitstring, second::bitstring>>
# => <<49::size(6)>>
O casamento de padrões também pode ser feito para obter valores da forma especial. Você precisa saber o número de bits de cada fragmento que quer capturar, com uma exceção: o tipo ::bitstring pode ser usado para fazer casamento de padrões em um bitstring de tamanho desconhecido, mas isso só pode ser usado no último fragmento.
<<value::4, rest::bitstring>> = <<0b01101001::8>>
value == 0b0110
# => true
Os bitstrings podem ser impressos (pelo executor de testes ou no iex) em um formato diferente do formato usado para criá-los. Isso costuma causar confusão ao aprender bitstrings.
Por padrão, os bitstrings são exibidos em fragmentos de 8 bits (um byte), mesmo que você os tenha criado com fragmentos de tamanho diferente.
<<2011::11>>
# => <<251, 3::size(3)>>
Se você criar um bitstring que representa uma string imprimível codificada em UTF-8, ele será exibido como uma string.
<<>>
# => ""
<<65, 66, 67>>
# => "ABC"
Pintura por números (também conhecida como colorir por números) são imagens em preto e branco feitas para colorir. As diferentes áreas da imagem são anotadas com números diferentes, e os números correspondem a cores específicas de uma paleta de cores predefinida. O objetivo é preencher as áreas com as cores certas, revelando uma bela imagem colorida no final. É uma atividade relaxante tanto para crianças quanto para adultos.
Você recebeu a tarefa de escrever um app de pintura por números em Elixir. Você quer que seu app consiga importar e exportar imagens em um formato de dados personalizado. Você decidiu usar arquivos binários para armazenar os dados das suas imagens.
Este exercício presume que você conheça números binários e entenda os princípios por trás da conversão de números binários em números decimais e de números decimais em números binários.
Vamos imaginar que você tem uma imagem de uma carinha sorridente, como a mostrada abaixo. A imagem tem um fundo branco. A carinha sorridente tem uma borda preta e uma cor de preenchimento amarela.
Esta imagem usa 3 cores. Digamos que atribuímos índices a essas cores:
0 (binário: 0b00) para branco,1 (binário: 0b01) para preto,2 (binário: 0b10) para amarelo.Agora podemos usar esses índices de cor para representar a cor de cada pixel.
| Carinha sorridente | Carinha sorridente com índices de cor |
|---|---|
![]() |
![]() |
Implemente a função PaintByNumber.palette_bit_size/1. Ela deve receber a quantidade de cores da paleta e retornar quantos bits são necessários para representar essa quantidade de índices de cor como números binários. Os índices de cor sempre começam em 0 e são inteiros crescentes e contínuos.
Por exemplo, representar 13 cores diferentes exige 4 bits. 4 bits podem armazenar até 16 índices de cor (2^4). 3 bits não seriam suficientes, porque 3 bits só conseguem armazenar até 8 índices de cor (2^3).
PaintByNumber.palette_bit_size(13)
# => 4
Observação: não existe uma função log2 na biblioteca padrão do Elixir. Mais adiante você vai aprender a usar as bibliotecas do Erlang a partir do Elixir, onde é possível encontrar essa função. Por enquanto, resolva esta tarefa com recursão e com o operador de potência **.
Implemente a função PaintByNumber.empty_picture/0. Ela deve retornar uma bitstring vazia.
Uma imagem de teste predefinida será usada para testar manualmente os recursos básicos do seu app. A imagem de teste consiste em 4 pixels com 4 cores diferentes.
Implemente a função PaintByNumber.test_picture/0. Ela deve retornar uma bitstring composta por 4 segmentos.
Cada segmento deve ter tamanho de 2 bits. Os segmentos devem ter os valores 0, 1, 2 e 3.
Implemente a função PaintByNumber.prepend_pixel/3. Ela deve receber três argumentos: uma bitstring com a imagem à qual estamos adicionando o pixel, a quantidade de cores da paleta e o índice da cor do novo pixel. Ela deve retornar uma bitstring com a imagem e o novo pixel adicionado ao início.
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>>)
Implemente a função PaintByNumber.get_first_pixel/2. Ela deve receber dois argumentos: uma bitstring com a imagem da qual estamos lendo e a quantidade de cores da paleta. Ela deve retornar o índice de cor do primeiro pixel da imagem fornecida. Quando receber uma imagem vazia, deve retornar nil.
picture = <<19::5, 2::5, 18::5>>
color_count = 20
PaintByNumber.get_first_pixel(picture, color_count)
# => 19
Implemente a função PaintByNumber.drop_first_pixel/2. Ela deve receber dois argumentos: uma bitstring com a imagem da qual estamos removendo um pixel e a quantidade de cores da paleta. Ela deve retornar a imagem sem o primeiro pixel. Quando receber uma imagem vazia, deve retornar uma imagem vazia.
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>>)
Implemente a função PaintByNumber.concat_pictures/2. Ela deve receber dois argumentos, duas bitstrings. Ela deve retornar uma bitstring que é o resultado de adicionar o primeiro argumento ao início do segundo argumento.
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>>)
Crie sua conta no Exercism para aprender e dominar Elixir com 58 conceitos168 exercícios e mentoria humana de verdade, tudo de graça.