Parcours
/
Ruby
Ruby
/
Exercices
/
Conducteur de locomotive
Conducteur de locomotive

Conducteur de locomotive

Exercice d'apprentissage

Introduction

La décomposition désigne l'action d'extraire les éléments d'une collection, comme un Array ou un Hash. Les valeurs décomposées peuvent ensuite être affectées à des variables au sein d'une même instruction.

L'affectation multiple permet d'affecter plusieurs variables pour décomposer des valeurs au sein d'une seule instruction. Le code y gagne en concision et en lisibilité, et il suffit pour cela de séparer les variables à affecter par une virgule, comme dans first, second, third = [1, 2, 3].

L'opérateur splat (*), et l'opérateur double splat (**), sont souvent utilisés dans des contextes de décomposition.

Caution

Il ne faut pas confondre *<variable_name> et **<variable_name> avec * et **. Si * et ** servent respectivement à la multiplication et à l'exponentiation, *<variable_name> et **<variable_name> sont des opérateurs de composition et de décomposition.

Affectation multiple

L'affectation multiple permet d'affecter plusieurs variables sur une seule ligne. Pour séparer les valeurs, utilise une virgule , :

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

L'affectation multiple n'est pas limitée à un seul type de données :

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

L'affectation multiple permet d'échanger des éléments dans des tableaux. Cette pratique est très courante dans les algorithmes de tri. Par exemple :

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

On parle aussi d'« affectation parallèle », ce qui permet d'éviter une variable temporaire.

S'il y a plus de variables que de valeurs, les variables en trop se voient affecter nil :

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

Décomposition

En Ruby, il est possible de décomposer les éléments de tableaux/hash dans des variables distinctes. Comme les valeurs apparaissent dans les tableaux selon l'ordre de leurs indices, elles sont réparties dans les variables dans ce même ordre :

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

Si certaines valeurs ne sont pas nécessaires, tu peux utiliser _ pour indiquer qu'elles sont « récupérées mais non utilisées » :

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

Décomposition profonde

Décomposer et affecter les valeurs de tableaux situés à l'intérieur d'un tableau (aussi appelé tableau imbriqué) fonctionne de la même manière qu'une décomposition superficielle, mais nécessite une expression de décomposition délimitée (()) pour clarifier le contexte ou la position des valeurs :

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

Tu peux aussi décomposer profondément seulement une partie d'un tableau imbriqué :

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

Si la décomposition comporte des variables mal placées ou un nombre incorrect de valeurs, tu obtiendras une erreur de syntaxe :

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 '['

Fais l'expérience ici, et tu remarqueras que c'est le premier motif qui dicte la répartition, et non les valeurs disponibles à droite. L'erreur de syntaxe n'est pas liée à la structure de données.

Décomposition d'un tableau avec l'opérateur splat simple (*)

Quand tu décomposes un tableau, tu peux utiliser l'opérateur splat (*) pour récupérer les valeurs « restantes ». C'est plus clair que de découper le tableau (ce qui, dans certaines situations, est moins lisible). Par exemple, on peut extraire le premier élément, puis affecter les valeurs restantes à un nouveau tableau sans le premier élément :

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

On peut aussi extraire les valeurs au début et à la fin du tableau tout en regroupant toutes les valeurs du milieu :

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

On peut également utiliser * dans une décomposition profonde :

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

Décomposition d'un Hash

Décomposer un hash est un peu différent de décomposer un tableau. Pour pouvoir décomposer un hash, tu dois d'abord le convertir en tableau. Sinon, il n'y aura pas de décomposition :

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

Pour convertir un Hash en tableau, tu peux utiliser la méthode 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]

Si tu veux récupérer les clés, tu peux utiliser la méthode keys :

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

Si tu veux récupérer les valeurs, tu peux utiliser la méthode values :

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

Composition

Composer consiste à regrouper plusieurs valeurs dans un seul tableau que l'on affecte à une variable. C'est utile quand tu veux décomposer des valeurs, les modifier, puis recomposer le résultat dans une variable. Cela permet aussi d'effectuer des fusions sur 2 tableaux/hash ou plus.

Composition d'un tableau avec l'opérateur splat (*)

On peut composer un tableau à l'aide de l'opérateur splat (*). Toutes les valeurs sont alors regroupées dans un tableau.

>> 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"]

Composition d'un hash avec l'opérateur double splat (**)

On compose un hash à l'aide de l'opérateur double splat (**). Toutes les paires clé/valeur d'un hash sont alors regroupées dans un autre hash, ou bien deux hash sont combinés.

>> 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}

Utilisation de l'opérateur splat (*) et de l'opérateur double splat (**) avec les méthodes

Composition avec les paramètres de méthode

Quand tu crées une méthode qui accepte un nombre arbitraire d'arguments, tu peux utiliser *arguments ou **keyword_arguments dans la définition de la méthode. *arguments sert à regrouper un nombre arbitraire d'arguments positionnels (non nommés) et **keyword_arguments sert à regrouper un nombre arbitraire d'arguments nommés.

Utilisation de *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"]

Utilisation de **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}

Si la méthode définie n'a aucun paramètre prévu pour les arguments nommés (**keyword_arguments ou <key_word>: <value>), alors les arguments nommés sont regroupés dans un hash et affectés au dernier paramètre.

>> def my_method(a)= a

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

*arguments et **keyword_arguments peuvent aussi être utilisés en combinaison l'un avec l'autre :

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"

Tu peux aussi écrire des arguments avant et après *arguments pour autoriser des arguments positionnels précis. Cela fonctionne de la même manière que la décomposition d'un tableau.

Caution

Les arguments doivent être structurés dans un ordre précis :

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

Si tu ne respectes pas cet ordre, tu obtiendras une erreur.

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]

Tu peux écrire des arguments positionnels avant et après *arguments :

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

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

Tu peux aussi combiner des arguments positionnels, *arguments, des arguments nommés et **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}

Écrire les arguments dans un ordre incorrect provoquera une erreur :

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)

Décomposition dans les appels de méthode

Tu peux utiliser l'opérateur splat (*) pour répartir un tableau d'arguments dans un appel de méthode :

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

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

Tu peux aussi utiliser l'opérateur double splat (**) pour répartir un hash d'arguments dans un appel de méthode :

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

Instructions

Ton ami Linus est conducteur de locomotive et conduit des trains de marchandises d'une ville à l'autre. C'est un as du pilotage, mais beaucoup moins de la logistique et de l'informatique. Il aimerait faire appel à tes talents de programmeur pour organiser les détails des trains et corriger les erreurs dans les données d'itinéraire.

Note

Cet exercice pourrait facilement se résoudre avec du découpage, de l'indexation et diverses méthodes hash. Cependant, nous aimerions que tu t'entraînes à empaqueter, dépaqueter et faire des affectations multiples pour résoudre chacune des tâches ci-dessous.

1. Crée une liste de tous les wagons

Ton ami a noté l'identifiant de chaque wagon, mais ne sait jamais combien de wagons le système devra traiter à un moment donné. Ce serait bien plus simple pour le reste du programme de logistique que ces données soient regroupées dans un seul array.

Implémente une méthode generate_list_of_wagons() qui prend un nombre arbitraire d'identifiants de wagons. Chaque identifiant est un entier positif. La méthode doit ensuite return les identifiants donnés sous la forme d'un seul array.

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

2. Corrige la liste des wagons

À ce stade, tu commences à bien sentir les données et la manière dont elles sont utilisées dans le programme de logistique. Le système d'identifiants attribue toujours l'identifiant 1 à la locomotive, puis un identifiant supérieur à 1, choisi au hasard, aux autres wagons du train.

Ton ami a dû attacher deux nouveaux wagons au train et a oublié de mettre à jour le système ! Du coup, les deux premiers wagons du array du train doivent être déplacés à la fin, sinon tout sera dans le désordre.

Pour compliquer les choses, ton ami vient de découvrir un second array qui semble contenir des identifiants de wagons manquants. Tout ce dont ton ami se souvient, c'est qu'une fois les nouveaux wagons déplacés, les identifiants de ce second array doivent être placés juste après la locomotive désignée.

Linus te serait vraiment reconnaissant de corriger ses erreurs et de consolider les données.

Implémente une méthode fix_list_of_wagons() qui prend deux arrays contenant des identifiants de wagons. Elle doit déplacer les deux premiers éléments du premier array à la fin, et insérer les valeurs du second array derrière (à droite de) l'identifiant de la locomotive (1). La méthode doit ensuite return un array contenant les modifications.

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. Ajoute les arrêts manquants

Maintenant que toutes les données des wagons sont correctes, Linus aimerait que tu mettes à jour les informations d'itinéraire du système. Sur un trajet, un train peut s'arrêter dans plusieurs gares pour charger ou décharger de la marchandise. Chaque trajet peut comporter un nombre différent de ces points de livraison intermédiaires. Ton ami aimerait que tu complètes le hash d'itinéraire du système avec les informations de livraison manquantes ou supplémentaires.

Implémente une méthode add_missing_stops() qui prend un hash d'itinéraire suivi d'un nombre variable d'arguments nommés. Ces arguments peuvent se présenter sous la forme d'un hash contenant un ou plusieurs arrêts, ou d'un nombre quelconque de paires stop_<number>: "city". Ta méthode doit ensuite renvoyer le hash d'itinéraire complété par une clé supplémentaire contenant un array de tous les arrêts ajoutés, dans l'ordre.

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

Les arrêts seront toujours fournis dans le bon ordre, donc pas besoin de te soucier de les trier.

4. Étends les informations d'itinéraire

Linus travaille sur le programme d'itinéraires et a remarqué que certains trajets ne contiennent pas quelques détails importants. Les informations initiales d'un trajet sont construites sous la forme d'un hash, et ton ami aimerait que tu complètes ce hash avec tout ce qui manque. Chaque trajet du système demande des détails légèrement différents, donc Linus préférerait vraiment une solution générique.

Implémente une méthode extend_route_information() qui prend deux hash. Le premier hash contient les villes de départ et d'arrivée entre lesquelles circule le train.

Le second hash contient d'autres détails d'itinéraire, comme la vitesse, la longueur ou la température du train. La méthode doit renvoyer un hash consolidé contenant toutes les informations d'itinéraire.

Note

Le second hash peut contenir des propriétés différentes ou plus nombreuses que celles montrées dans l'exemple.

LocomotiveEngineer.extend_route_information({"from": "Berlin", "to": "Hamburg"}, {"length": "100", "speed": "50"})
# => {"from": "Berlin", "to": "Hamburg", "length": "100", "speed": "50"}
Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet
Ruby Exercism

Prêt à commencer Conducteur de locomotive ?

Inscris-toi sur Exercism pour apprendre et maîtriser Ruby avec 21 concepts121 exercices, et un vrai mentorat humain, le tout gratuitement.