Робота з бінарними даними - важлива концепція в будь-якій мові, й Elixir пропонує елегантний синтаксис, який дає змогу записувати, зіставляти зі зразком і будувати бінарні дані.
У Elixir бінарні дані називають бітовим рядком. Тип бінарних даних (не плутати з бінарними даними загалом) - це окремий різновид бітового рядка, який ми розглянемо в одній із наступних вправ.
Літерали бітових рядків задають за допомогою спеціальної форми бітового рядка <<>>. Визначаючи літерал бітового рядка, його описують сегментами. Кожен сегмент має значення і тип, розділені оператором ::. Тип визначає, скільки бітів буде використано для кодування значення. Тип можна взагалі не вказувати, і тоді типовим значенням буде 8-бітне ціле число.
# This defines a bitstring with three segments of a single bit each
<<0::1, 1::1, 0::1>>
Запис ::1 як типу - це скорочення для ::size(1). Довший синтаксис доведеться використати, якщо розмір у бітах береться зі змінної.
Записуючи цілі числа в двійковому вигляді, ми можемо вказати їх безпосередньо в двійковій системі числення, додавши перед літералом 0b. Звернімо увагу: у результатах тестів чи під час роботи з iex вони все одно відображаються як десяткові числа.
<<0b1011::4>> == <<11::4>>
# => true
Якщо значення сегмента перевищує місткість типу цього сегмента, його буде обрізано зліва.
<<0b1011::3>> == <<0b0011::3>>
# => true
До наявного бітового рядка можна додавати як на початок, так і в кінець, використовуючи спеціальну форму. Якщо розмір наявного бітового рядка невідомий, для нього потрібно вказати тип ::bitstring.
value = <<0b110::3, 0b001::3>>
new_value = <<0b011::3, value::bitstring, 0b000::3>>
# => <<120, 8::size(4)>>
Бітові рядки, збережені у змінних, можна зʼєднати за допомогою спеціальної форми. Коли ми зʼєднуємо два бітові рядки невідомого розміру, потрібно вказати тип ::bitstring.
first = <<0b110::3>>
second = <<0b001::3>>
concatenated = <<first::bitstring, second::bitstring>>
# => <<49::size(6)>>
Зіставлення зі зразком також можна застосувати, щоб дістати значення зі спеціальної форми. Для кожного фрагмента, який ми хочемо захопити, потрібно знати кількість бітів, і є лише один виняток: тип ::bitstring можна використати для зіставлення зі зразком бітового рядка невідомого розміру, але лише для останнього фрагмента.
<<value::4, rest::bitstring>> = <<0b01101001::8>>
value == 0b0110
# => true
Бітові рядки можуть друкуватися (під час запуску тестів або в iex) в іншому форматі, ніж той, у якому їх було створено. Це часто збиває з пантелику, коли ми вивчаємо бітові рядки.
Типово бітові рядки відображаються фрагментами по 8 бітів (один байт), навіть якщо ми створили їх із фрагментів іншого розміру.
<<2011::11>>
# => <<251, 3::size(3)>>
Якщо бітовий рядок представляє придатний для друку рядок тексту (англ. string) у кодуванні UTF-8, його відображають як рядок тексту.
<<>>
# => ""
<<65, 66, 67>>
# => "ABC"
Розфарбовування за номерами (також відоме як розфарбовування за кольорами) - це чорно-білі зображення, призначені для розфарбовування. Різні області зображення позначені різними числами, а числа відповідають конкретним кольорам у наперед визначеній палітрі кольорів. Мета - заповнити області правильними кольорами й відкрити красиве барвисте зображення в результаті. Це розслабне заняття і для дітей, і для дорослих.
Наше завдання - написати застосунок для розфарбовування за номерами на Elixir. Ми хочемо, щоб наш застосунок умів імпортувати й експортувати зображення у власному форматі даних. Ми вирішили використовувати бінарні файли, щоб зберігати дані зображення.
Ця вправа припускає, що ми знайомі з двійковими числами і розуміємо принципи перетворення двійкових чисел на десяткові та десяткових чисел на двійкові.
Уявімо, що в нас є зображення смайлика, як те, що показано нижче. Зображення має білий фон. Смайлик має чорний контур і жовтий колір заливки.
У цьому зображенні використано 3 кольори. Припустімо, ми присвоюємо цим кольорам індекси:
0 (двійкове: 0b00) для білого,1 (двійкове: 0b01) для чорного,2 (двійкове: 0b10) для жовтого.Тепер ми можемо використати ці індекси кольорів, щоб представити колір кожного пікселя.
| Смайлик | Смайлик з індексами кольорів |
|---|---|
![]() |
![]() |
Реалізуйте функцію PaintByNumber.palette_bit_size/1. Вона має приймати кількість кольорів у палітрі й повертати, скільки бітів потрібно, щоб представити таку кількість індексів кольорів як двійкові числа. Індекси кольорів завжди починаються з 0 і є неперервними зростаючими цілими числами.
Наприклад, для представлення 13 різних кольорів потрібно 4 біти. 4 біти можуть зберегти до 16 індексів кольорів (2^4). 3 бітів було б недостатньо, бо 3 біти можуть зберегти лише до 8 індексів кольорів (2^3).
PaintByNumber.palette_bit_size(13)
# => 4
Примітка: у стандартній бібліотеці Elixir немає функції log2. Пізніше ми навчимося використовувати бібліотеки Erlang з Elixir, де цю функцію можна знайти. А тепер розвʼяжіть це завдання за допомогою рекурсії та оператора піднесення до степеня **.
Реалізуйте функцію PaintByNumber.empty_picture/0. Вона має повертати порожній бітовий рядок.
Наперед визначене тестове зображення буде використовуватися для ручного тестування базових можливостей нашого застосунку. Тестове зображення складається з 4 пікселів із 4 різними кольорами.
Реалізуйте функцію PaintByNumber.test_picture/0. Вона має повертати бітовий рядок, що складається з 4 сегментів.
Кожен сегмент має мати бітовий розмір 2. Сегменти мають мати значення 0, 1, 2 і 3.
Реалізуйте функцію PaintByNumber.prepend_pixel/3. Вона має приймати три аргументи: бітовий рядок із зображенням, до якого ми додаємо, кількість кольорів у палітрі та індекс кольору для нового пікселя. Вона має повертати бітовий рядок із зображенням, до якого новий піксель додано на початок.
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>>)
Реалізуйте функцію PaintByNumber.get_first_pixel/2. Вона має приймати два аргументи: бітовий рядок із зображенням, з якого ми читаємо, і кількість кольорів у палітрі. Вона має повертати індекс кольору першого пікселя в заданому зображенні. Коли їй передано порожнє зображення, вона має повертати nil.
picture = <<19::5, 2::5, 18::5>>
color_count = 20
PaintByNumber.get_first_pixel(picture, color_count)
# => 19
Реалізуйте функцію PaintByNumber.drop_first_pixel/2. Вона має приймати два аргументи: бітовий рядок із зображенням, з якого ми прибираємо піксель, і кількість кольорів у палітрі. Вона має повертати зображення без першого пікселя. Коли їй передано порожнє зображення, вона має повертати порожнє зображення.
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>>)
Реалізуйте функцію PaintByNumber.concat_pictures/2. Вона має приймати два аргументи, два бітові рядки. Вона має повертати бітовий рядок, що є результатом додавання першого аргументу на початок другого.
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>>)
Зареєструйтеся на Exercism, щоб вивчати й опановувати Elixir, а також 58 концепцій168 вправ та справжнє наставництво від людей, і все це безкоштовно.