Треки
/
Ruby
Ruby
/
Вправи
/
Машиніст локомотива
Машиніст локомотива

Машиніст локомотива

Навчальна вправа

Вступ

Декомпозиція полягає у вилученні елементів колекції, як-от Array або Hash. Декомпозовані значення потім можна присвоїти змінним у межах однієї інструкції.

Множинне присвоєння дає змогу присвоїти кілька змінних, щоб декомпозувати значення в межах однієї інструкції. Завдяки цьому код стає лаконічнішим і зрозумілішим; змінні, яким присвоюються значення, розділяють комою, як-от first, second, third = [1, 2, 3].

Оператор splat (*) і подвійний оператор splat (**) часто використовують у контексті декомпозиції.

Caution

*<variable_name> і **<variable_name> не варто плутати з * і **. * і ** використовують для множення та піднесення до степеня відповідно, а *<variable_name> і **<variable_name> — як оператори композиції та декомпозиції.

Множинне присвоєння

Множинне присвоєння дає змогу присвоїти кілька змінних в одному рядку. Щоб розділити значення, використаймо кому ,:

>> a, b = 1, 2
=> [1, 2]
>> a
=> 1

Множинне присвоєння не обмежується одним типом даних:

>> x, y, z = 1, "Hello", true
=> [1, "Hello", true]
>> x
=> 1
>> y
=> 'Hello'
>> z
=> true

Множинне присвоєння можна використати, щоб обміняти місцями елементи масиву. Цей прийом досить поширений в алгоритмах сортування. Наприклад:

>> numbers = [1, 2]
=> [1, 2]
>> numbers[0], numbers[1] = numbers[1], numbers[0]
=> [2, 1]
>> numbers
=> [2, 1]
Note

Це також називають «паралельним присвоєнням»; його можна використати, щоб уникнути тимчасової змінної.

Якщо змінних більше, ніж значень, зайвим змінним буде присвоєно nil:

>> a, b, c = 1, 2
=> [1, 2]
>> b
=> 2
>> c
=> nil

Декомпозиція

У Ruby можна декомпозувати елементи масивів/хешів в окремі змінні. Оскільки значення розташовані в масивах у порядку індексів, вони розпаковуються у змінні в тому самому порядку:

>> fruits = ["apple", "banana", "cherry"]
>> x, y, z = fruits
>> x
=> "apple"

Якщо якісь значення не потрібні, можна використати _, щоб позначити «зібрано, але не використано»:

>> fruits = ["apple", "banana", "cherry"]
>> _, _, z = fruits
>> z
=> "cherry"

Глибока декомпозиція

Декомпозиція й присвоєння значень із масивів усередині масиву (який ще називають вкладеним масивом) відбувається так само, як і поверхнева декомпозиція, але потребує виразу декомпозиції з обмежувачами (()), щоб уточнити контекст або позицію значень:

>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>> (a, b), (c, d) = fruits_vegetables
>> a
=> "apple"
>> d
=> "potato"

Можна також глибоко розпакувати лише частину вкладеного масиву:

>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>> a, (c, d) = fruits_vegetables
>> a
=> ["apple", "banana"]
>> c
=> "carrot"

Якщо у декомпозиції змінні розташовані неправильно та/або кількість значень неправильна, ми отримаємо помилку синтаксису:

fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]

(a, b), (d) = fruits_vegetables
# syntax error, unexpected '=', expecting '.' or &. or :: or '['

((a, b), (d)) = fruits_vegetables
# syntax error, unexpected ')', expecting '.' or &. or :: or '['

Поекспериментуймо тут, і ми помітимо, що саме перший зразок визначає розподіл, а не наявні значення з правого боку. Помилка синтаксису не повʼязана зі структурою даних.

Декомпозиція масиву за допомогою одинарного оператора splat (*)

Коли ми декомпозуємо масив, можна використати оператор splat (*), щоб захопити «зайві» значення. Це зрозуміліше, ніж зріз масиву (який у деяких ситуаціях менш читабельний). Наприклад, ми можемо вилучити перший елемент, а потім присвоїти решту значень новому масиву без першого елемента:

>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]
>> x, *last = fruits
>> x
=> "apple"
>> last
=> ["banana", "cherry", "orange", "kiwi", "melon", "mango"]

Можна також вилучити значення на початку й у кінці масиву, згрупувавши всі значення посередині:

>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]
>> x, *middle, y, z = fruits
>> y
=> "melon"
>> middle
=> ["banana", "cherry", "orange", "kiwi"]

Оператор * можна використати і в глибокій декомпозиції:

>> fruits_vegetables = [["apple", "banana", "melon"], ["carrot", "potato", "tomato"]]
>> (a, *rest), b = fruits_vegetables
>> a
=> "apple"
>> rest
=> ["banana", "melon"]

Декомпозиція Hash

Декомпозиція хеша дещо відрізняється від декомпозиції масиву. Щоб розпакувати хеш, спершу треба перетворити його на масив. Інакше декомпозиції не станеться:

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> x, y, z = fruits_inventory
>> x
=> {:apple=>6, :banana=>2, :cherry=>3}
>> y
=> nil

Щоб привести Hash до масиву, можна використати метод to_a:

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> fruits_inventory.to_a
=> [[:apple, 6], [:banana, 2], [:cherry, 3]]
>> x, y, z = fruits_inventory.to_a
>> x
=> [:apple, 6]

Якщо потрібно розпакувати ключі, можна використати метод keys:

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> x, y, z = fruits_inventory.keys
>> x
=> :apple

Якщо потрібно розпакувати значення, можна використати метод values:

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> x, y, z = fruits_inventory.values
>> x
=> 6

Композиція

Компонування дає змогу згрупувати кілька значень в один масив, який присвоюється змінній. Це стає в пригоді, коли ми хочемо декомпозувати значення, щось у них змінити, а потім скомпонувати результат назад у змінну. Це також дозволяє обʼєднувати два або більше масивів/хешів.

Композиція масиву за допомогою оператора splat (*)

Скомпонувати масив можна за допомогою оператора splat (*). Він спакує всі значення в один масив.

>> fruits = ["apple", "banana", "cherry"]
>> more_fruits = ["orange", "kiwi", "melon", "mango"]

# fruits and more_fruits are unpacked and then their elements are packed into combined_fruits
>> combined_fruits = *fruits, *more_fruits

>> combined_fruits
=> ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]

Композиція хеша за допомогою подвійного оператора splat (**)

Компонування хеша виконують за допомогою подвійного оператора splat (**). Він пакує всі пари ключ/значення з одного хеша в інший хеш або обʼєднує два хеші разом.

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> more_fruits_inventory = {orange: 4, kiwi: 1, melon: 2, mango: 3}

# fruits_inventory and more_fruits_inventory are unpacked into key-values pairs and combined.
>> combined_fruits_inventory = {**fruits_inventory, **more_fruits_inventory}

# then the pairs are packed into combined_fruits_inventory
>> combined_fruits_inventory
=> {:apple=>6, :banana=>2, :cherry=>3, :orange=>4, :kiwi=>1, :melon=>2, :mango=>3}

Використання оператора splat (*) і подвійного оператора splat (**) з методами

Композиція з параметрами методу

Коли ми створюємо метод, який приймає довільну кількість аргументів, у визначенні методу можна використати *arguments або **keyword_arguments. *arguments використовують, щоб спакувати довільну кількість позиційних (неключових) аргументів, а **keyword_arguments — довільну кількість ключових аргументів.

Використання *arguments:

# This method is defined to take any number of positional arguments
# (Using the single line form of the definition of a method.)

>> def my_method(*arguments)= arguments

# Arguments given to the method are packed into an array

>> my_method(1, 2, 3)
=> [1, 2, 3]

>> my_method("Hello")
=> ["Hello"]

>> my_method(1, 2, 3, "Hello", "Mars")
=> [1, 2, 3, "Hello", "Mars"]

Використання **keyword_arguments:

# This method is defined to take any number of keyword arguments

>> def my_method(**keyword_arguments)= keyword_arguments

# Arguments given to the method are packed into a dictionary

>> my_method(a: 1, b: 2, c: 3)
=> {:a => 1, :b => 2, :c => 3}

Якщо у визначеному методі немає параметрів для ключових аргументів (**keyword_arguments або <key_word>: <value>), то ключові аргументи буде спаковано в хеш і присвоєно останньому параметру.

>> def my_method(a)= a

>> my_method(a: 1, b: 2, c: 3)
=> {:a => 1, :b => 2, :c => 3}

*arguments і **keyword_arguments можна також використовувати разом:

def my_method(*arguments, **keyword_arguments)
  p arguments.sum
  for (key, value) in keyword_arguments.to_a
    p key.to_s + " = " + value.to_s
  end
end


my_method(1, 2, 3, a: 1, b: 2, c: 3)
6
"a = 1"
"b = 2"
"c = 3"

Можна також писати аргументи до й після *arguments, щоб задати конкретні позиційні аргументи. Це працює так само, як декомпозиція масиву.

Caution

Аргументи потрібно впорядкувати в певній послідовності:

def my_method(<positional_arguments>, *arguments, <positional_arguments>, <keyword_arguments>, **keyword_arguments)

Якщо не дотриматися цього порядку, виникне помилка.

def my_method(a, b, *arguments)
  p a
  p b
  p arguments
end

my_method(1, 2, 3, 4, 5)
1
2
[3, 4, 5]

Позиційні аргументи можна писати до й після *arguments:

>> def my_method(a, *middle, b)= middle

>> my_method(1, 2, 3, 4, 5)
=> [2, 3, 4]

Можна також поєднувати позиційні аргументи, *arguments, ключові аргументи та **keyword_arguments:

>> def my_method(first, *many, last, a:, **keyword_arguments)
     p first
     p many
     p last
     p a
     p keyword_arguments
     end

>> my_method(1, 2, 3, 4, 5, a: 6, b: 7, c: 8)
1
[2, 3, 4]
5
6
{:b => 7, :c => 8}

Якщо записати аргументи в неправильному порядку, виникне помилка:

def my_method(a:, **keyword_arguments, first, *arguments, last)
  arguments
end

my_method(1, 2, 3, 4, a: 5)

syntax error, unexpected local variable or method, expecting & or '&'
... my_method(a:, **keyword_arguments, first, *arguments, last)

Декомпозиція у викликах методів

Оператор splat (*) можна використати, щоб розпакувати масив аргументів у виклик методу:

def my_method(a, b, c)
  p c
  p b
  p a
end

numbers = [1, 2, 3]
my_method(*numbers)
3
2
1

Подвійний оператор splat (**) можна також використати, щоб розпакувати хеш аргументів у виклик методу:

def my_method(a:, b:, c:)
  p c
  p b
  p a
end

numbers = {a: 1, b: 2, c: 3}
my_method(**numbers)
3
2
1

Вказівки

Наш друг Лінус працює машиністом локомотива й водить вантажні потяги між містами. Хоч він чудово дає раду потягам, із логістикою та компʼютерами в нього не так добре. Він хотів би, щоб ми допомогли йому з програмуванням: упорядкували дані про потяги та виправили помилки в даних маршрутів.

Note

Цю вправу можна було б легко розвʼязати за допомогою зрізів, індексації та різних методів hash. Однак, розвʼязуючи кожне із завдань нижче, ми хочемо попрактикувати пакування, розпакування та множинне присвоєння.

1. Створити масив усіх вагонів

Наш друг веде облік ідентифікаторів (ID) кожного вагона, але ніколи не знає наперед, скільки вагонів системі доведеться обробити. Решті логістичної програми було б набагато простіше, якби ці дані були запаковані в один масив.

Реалізуйте метод generate_list_of_wagons(), який приймає довільну кількість ID вагонів. Кожен ID - додатне ціле число. Далі метод має повернути (return) передані ID як один масив.

LocomotiveEngineer.generate_list_of_wagons(1, 7, 12, 3, 14, 8, 5)
# => [1, 7, 12, 3, 14, 8, 5]

2. Виправити масив вагонів

На цьому етапі ми вже потроху розуміємо дані та те, як вони використовуються в логістичній програмі. Система ID завжди присвоює локомотиву ID 1, а решті вагонів у потязі - випадково вибраний ID, більший за 1.

Наш друг причепив до потяга два нові вагони й забув оновити систему! Тепер перші два вагони в масиві потяга треба перемістити в кінець, інакше порядок порушиться.

А щоб було ще складніше, наш друг щойно знайшов другий масив, який, схоже, містить зниклі ID вагонів. Він памʼятає лише, що після переміщення нових вагонів ID із цього другого масиву треба поставити одразу після локомотива.

Лінус буде дуже вдячний нам за виправлення його помилок і впорядкування даних.

Реалізуйте метод fix_list_of_wagons(), який приймає два масиви з ID вагонів. Він має перемістити перші два елементи першого масиву в кінець і вставити значення з другого масиву позаду (праворуч від) ID локомотива (1). Далі метод має повернути (return) масив із цими змінами.

LocomotiveEngineer.fix_list_of_wagons([2, 5, 1, 7, 4, 12, 6, 3, 13], [3, 17, 6, 15])
# => [1, 3, 17, 6, 15, 7, 4, 12, 6, 3, 13, 2, 5]

3. Додати відсутні зупинки

Тепер, коли всі дані про вагони правильні, Лінус хотів би, щоб ми оновили інформацію про маршрути системи. На транспортному маршруті потяг може зупинятися на кількох різних станціях, щоб узяти й/або залишити вантаж. У кожної подорожі може бути різна кількість таких проміжних точок доставки. Наш друг хотів би, щоб ми оновили хеш маршрутизації системи, додавши будь-яку відсутню або додаткову інформацію про доставку.

Реалізуйте метод add_missing_stops(), який приймає хеш маршрутизації, а далі довільну кількість іменованих аргументів. Ці аргументи можуть бути подані як хеш з однією чи кількома зупинками або як будь-яка кількість пар іменованих аргументів stop_<number>: "city". Далі метод має повернути хеш маршрутизації, доповнений додатковим ключем, який містить масив усіх доданих зупинок у порядку.

LocomotiveEngineer.add_missing_stops({from: "New York", to: "Miami"},
                    stop_1: "Washington, DC", stop_2: "Charlotte", stop_3: "Atlanta",
                    stop_4: "Jacksonville", stop_5: "Orlando")
# => {from: "New York", to: "Miami", stops: ["Washington, DC", "Charlotte", "Atlanta", "Jacksonville", "Orlando"]}
Note

Зупинки завжди передаються в правильному порядку, тож про їхнє сортування дбати не потрібно.

4. Розширити інформацію про маршрути

Лінус працював над програмою маршрутизації й помітив, що в деяких маршрутах бракує важливих деталей. Початкову інформацію про маршрут побудовано як хеш, і наш друг хотів би, щоб ми доповнили цей хеш усім, чого бракує. Кожен маршрут у системі потребує трохи інших деталей, тож Лінус дуже хотів би мати універсальне рішення.

Реалізуйте метод extend_route_information(), який приймає два хеші. Перший хеш містить міста відправлення й прибуття, між якими пролягає маршрут потяга.

Другий хеш містить інші деталі маршруту, як-от швидкість, довжину чи температуру. Метод має повернути зведений хеш з усією інформацією про маршрут.

Note

Другий хеш може містити інші або додаткові властивості, ніж ті, що показані в прикладі.

LocomotiveEngineer.extend_route_information({"from": "Berlin", "to": "Hamburg"}, {"length": "100", "speed": "50"})
# => {"from": "Berlin", "to": "Hamburg", "length": "100", "speed": "50"}
Редагувати через GitHub Посилання відкривається в новому вікні або вкладці
Ruby Exercism

Час розпочати Машиніст локомотива?

Зареєструйтеся на Exercism, щоб вивчати й опановувати Ruby, а також 21 концепція121 вправа та справжнє наставництво від людей, і все це безкоштовно.