# AP1 · 4 — Lire une chaîne sans perdre ce qui reste

## Notre question

Comment découper un texte en morceaux, puis reconnaître chaque morceau
sans accepter n'importe quoi ? Nous lisons des nombres avec la
bibliothèque, puis nous écrivons notre premier lecteur de tokens.

> **Repères.** Poly, chap. 1, section 1.3 « Les chaînes », chap. 2, sections 2.2.5 et 2.5, et chap. 3, section 3.5.1. Le cours se fait ici, dans le carnet.

> 💡 Durée indicative : 25 min, reprise collective comprise. Ce carnet
> est autonome : aucune cellule d'un autre carnet n'est nécessaire. Je le
> projette au tableau ; écrivez une prédiction avant d'exécuter, puis
> expliquez un résultat qui vous a surpris. **Sauvegardez-le dès maintenant
> sous `AP1/ap1-04-chaines-tokens.livemd`.**

Les cellules `assert` sont des vérifications fournies : si une condition
est fausse, Livebook montre le résultat obtenu et celui attendu.
Évaluez cet import une fois, puis réévaluez votre code avant ses tests
après chaque modification.

```elixir
import ExUnit.Assertions
```

## Utiliser une bibliothèque avant d'écrire un lecteur

Les chaînes Elixir se notent avec des guillemets doubles. Le module
`String` fournit déjà beaucoup d'opérations : `trim/1` enlève les blancs
aux extrémités, `split/1` découpe sur les blancs en ignorant les morceaux
vides, `length/1` compte les graphèmes. Une lettre affichée peut occuper
plusieurs octets : une longueur de chaîne n'est pas une taille mémoire.
Prévoyez les deux listes avant d'évaluer.

```elixir
source = "  12  3   -  "
{String.trim(source), String.split(source), String.split(source, " ")}
```

Nous voulons accepter plusieurs espaces, des tabulations et des retours
à la ligne. Dans la [documentation de `String.split`](https://hexdocs.pm/elixir/String.html#split/1),
retrouvez ce qui distingue `/1` et `/2`. Quelle forme convient à notre
calculatrice ? Lisez l'exemple avant de choisir une fonction.

Pour transformer le texte d'un nombre, `Integer.parse/1` renvoie
`{nombre, reste}` ou `:error`. Prévoyez les résultats ci-dessous. Pourquoi
conserver le reste peut-il être utile à un lecteur de langage ?

```elixir
{Integer.parse("12"), Integer.parse("12x"), Integer.parse("chat"),
 Integer.parse("-12")}
```

Accepter `"12x"` comme le nombre 12 ferait disparaître une faute de frappe.
Notre calculatrice exigera un morceau entièrement lu. `case` permet
d'examiner un résultat avec plusieurs motifs, essayés dans l'ordre.
Comme une fonction, il renvoie la valeur de la branche choisie.

```elixir
parsed = Integer.parse("12x")

case parsed do
  {number, ""} -> {:ok, number}
  {_number, rest} -> {:error, {:trailing_text, rest}}
  :error -> {:error, :expected_integer}
end
```

## Une première lecture de tokens

Un *token* représente un morceau reconnu : `{:number, 12}`, `:plus`,
`:minus` ou `:multiply`. Écrivez `Token.read/1`. Le cas `"+"` est fourni ;
ajoutez `"-"` et `"*"`, puis le cas général avec `Integer.parse/1` et
`case`. Acceptez uniquement un reste vide. Pour toute autre entrée,
renvoyez `{:error, {:invalid_token, texte_original}}` : les deux échecs
du `case` ci-dessus sont fondus en un seul.

```elixir
defmodule Token do
  def read("+"), do: {:ok, :plus}
  # Ajoutez les deux opérateurs, puis la lecture d'un entier complet.
end
```

```elixir
assert Token.read("12") == {:ok, {:number, 12}}
assert Token.read("-12") == {:ok, {:number, -12}}
assert Token.read("-") == {:ok, :minus}
assert Token.read("*") == {:ok, :multiply}
assert Token.read("12x") == {:error, {:invalid_token, "12x"}}
assert Token.read("chat") == {:error, {:invalid_token, "chat"}}
```

## B2 — Reconnaître un préfixe

AP2 devra lire `"12.0+3.0"` même sans espaces. `String.split/1` ne suffit
alors plus. Dans un motif, `"A" <> rest` reconnaît un préfixe fixe et
conserve ce qui suit. On ne peut pas écrire `first <> rest` pour deviner
deux morceaux de longueurs inconnues.

Pour prendre cette habitude, voici le cas A d'un petit lecteur d'ADN.
L'entrée ne contient ici que les lettres ASCII A, C, G et T. Complétez
les trois cas analogues, qui consomment une lettre sans changer le total.

```elixir
defmodule DNA do
  def count_a("", total), do: total
  def count_a("A" <> rest, total), do: count_a(rest, total + 1)
  # Ajoutez C, G et T.
end
```

```elixir
assert DNA.count_a("", 0) == 0
assert DNA.count_a("ACGTA", 0) == 2
assert DNA.count_a("AA", 3) == 5
```

Tracez `DNA.count_a("ACA", 0)` et expliquez pourquoi les appels
s'arrêtent. Pour le lien avec AP2, reconnaissez oralement ce que
donnerait `"+" <> rest = "+3.0"` : quel reste faut-il transmettre au
prochain appel ?

## Garder une trace

Notez la différence entre un nombre lu et une entrée entièrement
acceptée, avec l'exemple `"12x"`. Gardez aussi votre trace du lecteur
d'ADN.

Enregistrez ce carnet sous `AP1/ap1-04-chaines-tokens.livemd` dans votre dépôt privé
`ap-homework`, puis ouvrez `ap1-05-pile-calculatrice.livemd` depuis la page AP1 quand nous
y passons ensemble. Chaque carnet repart d'une session neuve : vos modules
précédents ne sont pas chargés, et ce n'est pas nécessaire.

Un bout de syntaxe oublié ? Les chapitres 1 à 4 du [poly](https://ap-2026.noce.fr/poly/poly-elixir.pdf)
reprennent les formes du langage, chacune avec un exemple et son résultat ;
le cours, lui, se fait dans les carnets.

Pour toute question, vous pouvez me contacter par email :
[aurelien.noce@minesparis.psl.eu](mailto:aurelien.noce@minesparis.psl.eu).
