Trabalhar com dados binários é um conceito importante em qualquer linguagem, e o Elixir oferece uma sintaxe elegante para escrever, corresponder e construir dados binários.
No Elixir, os dados binários são designados por tipo bitstring. O tipo de dados binários (que não deves confundir com dados binários em geral) é uma forma específica de bitstring, que vamos abordar num exercício mais adiante.
Os literais de bitstring definem-se com a forma especial de bitstring <<>>. Quando defines um literal de bitstring, este é 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 nesse caso assume um valor inteiro de 8 bits por predefinição.
# 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). Tens de usar a sintaxe mais longa se o tamanho em bits vier de uma variável.
Ao escrever literais de inteiros binários, podemos escrevê-los diretamente em notação de base 2, colocando o prefixo 0b antes do literal. Repara que, de qualquer forma, serão apresentados como números decimais quando forem impressos nos resultados dos testes ou quando usas o iex.
<<0b1011::4>> == <<11::4>>
# => true
Se o valor do segmento exceder a capacidade do tipo do segmento, é truncado a partir da esquerda.
<<0b1011::3>> == <<0b0011::3>>
# => true
Podes acrescentar tanto ao início como ao fim de um bitstring existente usando a forma especial. O tipo ::bitstring tem de ser usado no bitstring existente se este tiver um tamanho desconhecido.
value = <<0b110::3, 0b001::3>>
new_value = <<0b011::3, value::bitstring, 0b000::3>>
# => <<120, 8::size(4)>>
Podemos concatenar bitstrings guardados em variáveis usando a forma especial. O tipo ::bitstring tem de ser usado quando concatenamos dois bitstrings de tamanhos desconhecidos.
first = <<0b110::3>>
second = <<0b001::3>>
concatenated = <<first::bitstring, second::bitstring>>
# => <<49::size(6)>>
A correspondência de padrões também pode ser usada para obter valores a partir da forma especial. Tens de saber o número de bits de cada fragmento que queres capturar, com uma exceção: o tipo ::bitstring pode ser usado para fazer correspondência de padrões num bitstring de tamanho desconhecido, mas só pode ser usado no último fragmento.
<<value::4, rest::bitstring>> = <<0b01101001::8>>
value == 0b0110
# => true
Os bitstrings podem ser impressos (pelo test runner ou no iex) num formato diferente do formato que foi usado para os criar. Isto causa frequentemente confusão quando estás a aprender bitstrings.
Por predefinição, os bitstrings são apresentados em fragmentos de 8 bits (um byte), mesmo que os tenhas criado com fragmentos de um tamanho diferente.
<<2011::11>>
# => <<251, 3::size(3)>>
Se criares um bitstring que representa uma string imprimível codificada em UTF-8, é apresentado como uma string.
<<>>
# => ""
<<65, 66, 67>>
# => "ABC"
Pintar por números (também conhecido como colorir por números) são imagens a preto e branco feitas para colorir. As diferentes áreas da imagem estão anotadas com números diferentes, e esses números correspondem a cores específicas numa paleta de cores predefinida. O objetivo é preencher as áreas com as cores certas, revelando no final uma bela imagem colorida. É uma atividade relaxante tanto para crianças como para adultos.
A tua tarefa é escrever uma aplicação de pintar por números em Elixir. Queres que a tua aplicação consiga importar e exportar imagens num formato de dados personalizado. Decidiste usar ficheiros binários para guardar os dados das tuas imagens.
Este exercício assume que estás familiarizado com números binários e que compreendes 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.
Imagina que tens uma imagem de um smiley, como a que é mostrada abaixo. A imagem tem um fundo branco. O smiley tem um contorno preto e uma cor de preenchimento amarela.
Esta imagem usa 3 cores. Digamos que atribuímos índices a essas cores:
0 (binário: 0b00) para o branco,1 (binário: 0b01) para o preto,2 (binário: 0b10) para o amarelo.Podemos agora usar esses índices de cor para representar a cor de cada pixel.
| Smiley | Smiley com índices de cor |
|---|---|
![]() |
![]() |
Implementa a função PaintByNumber.palette_bit_size/1. Deve receber o número de cores da paleta e devolver quantos bits são necessários para representar essa quantidade de índices de cor como números binários. Os índices de cor começam sempre em 0 e são números inteiros consecutivos e crescentes.
Por exemplo, representar 13 cores diferentes requer 4 bits. 4 bits conseguem guardar até 16 índices de cor (2^4). 3 bits não seriam suficientes porque 3 bits só conseguem guardar até 8 índices de cor (2^3).
PaintByNumber.palette_bit_size(13)
# => 4
Nota: não existe nenhuma função log2 na biblioteca padrão do Elixir. Mais tarde vais aprender a usar bibliotecas do Erlang a partir do Elixir, onde podes encontrar esta função. Por agora, resolve esta tarefa com recursão e com o operador de potência **.
Implementa a função PaintByNumber.empty_picture/0. Deve devolver um bitstring vazio.
Uma imagem de teste predefinida será usada para testes manuais das funcionalidades básicas da tua aplicação. A imagem de teste é composta por 4 píxeis com 4 cores diferentes.
Implementa a função PaintByNumber.test_picture/0. Deve devolver um bitstring composto por 4 segmentos.
Cada segmento deve ter um tamanho de 2 bits. Os segmentos devem ter os valores 0, 1, 2 e 3.
Implementa a função PaintByNumber.prepend_pixel/3. Deve receber três argumentos: um bitstring com a imagem à qual estamos a adicionar ao início, o número de cores da paleta e o índice da cor do novo pixel. Deve devolver um bitstring com a imagem, com 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>>)
Implementa a função PaintByNumber.get_first_pixel/2. Deve receber dois argumentos: um bitstring com a imagem da qual estamos a ler e o número de cores da paleta. Deve devolver o índice de cor do primeiro pixel da imagem dada. Quando receber uma imagem vazia, deve devolver nil.
picture = <<19::5, 2::5, 18::5>>
color_count = 20
PaintByNumber.get_first_pixel(picture, color_count)
# => 19
Implementa a função PaintByNumber.drop_first_pixel/2. Deve receber dois argumentos: um bitstring com a imagem da qual estamos a remover um pixel e o número de cores da paleta. Deve devolver a imagem sem o primeiro pixel. Quando receber uma imagem vazia, deve devolver 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>>)
Implementa a função PaintByNumber.concat_pictures/2. Deve receber dois argumentos, dois bitstrings. Deve devolver um 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>>)
Inscreve-te no Exercism para aprenderes e dominares Elixir com 58 conceitos168 exercícios, e mentoria humana real, tudo grátis.