← Cours 4 : Parseurs Cours 6 : Mini jQuery →

Mini Excel

quand le CSV rencontre le tableur

1. Introduction

Dans le cours précédent, on a construit un parseur CSV et un parseur XML. On a vu comment transformer des données d'un format à l'autre. Il est temps de passer à la vitesse supérieure : construire une mini application tableur.

L'idée : on charge un CSV, on l'affiche, on peut le filtrer, trier, faire des calculs sur les colonnes, et même générer des documents personnalisés par publipostage (mail merge). Le tout en OCaml pur, avec les monades et les ADT qu'on connaît.

Application réelle : Ce qu'on construit ici est un mini moteur de tableur. Ajoutez une interface graphique (GTK, Tk, ou web) et vous avez un vrai Excel-like en OCaml. Des bibliothèques comme ocaml-excel ou table existent, mais construire la vôtre vous fait tout comprendre.

Les pré-requis : cours 2 (ADT, listes, map/filter/fold), cours 3 (monades, notamment la monade liste), cours 4 (parseur CSV).

2. Modèle de données

Une feuille de calcul, c'est une grille : des lignes et des colonnes. Chaque cellule peut contenir une valeur (texte, nombre) ou une formule.

OCaml
---------- Modèle de données ----------

// Type d'une cellule *)
type cell =
  | Text  of string           // Texte brut *)
  | Num   of float            // Nombre *)
  | Empty                      // Cellule vide *)

// Une ligne est une liste de cellules *)
type row = cell list

// Une table : des en-têtes (noms de colonnes) et des lignes *)
type table = {
  headers: string list;
  rows:    row list;
}

Pour charger un CSV dans notre table, on utilise le parseur du cours 4 et on convertit les chaînes en cellules :

OCaml
---------- Chargement CSV ----------

// Convertir une chaîne en cellule (nombre si possible, sinon texte) *)
let parse_cell s =
  try Num (float_of_string s)
  with Failure _ ->
    if s = "" then Empty
    else Text s

// Charger un CSV dans une table *)
let load_csv data =
  match run csv_parser data with
  | None      -> None
  | Some (raw, _) ->
      Some {
        headers = raw.headers;
        rows    = List.map
                     (fun r -> List.map parse_cell r)
                     raw.rows;
      }

On a aussi besoin d'afficher la table proprement :

OCaml
---------- Affichage ----------

let show_cell c =
  match c with
  | Empty    -> ""
  | Text s  -> s
  | Num n   -> string_of_float n

let show_row r =
  String.concat " | " (List.map show_cell r)

let show_table t =
  String.concat "\n"
    ((String.concat " | " t.headers)
     :: List.map show_row t.rows)
Un tableur sans affichage, c'est comme une calculatrice sans écran : ça marche dans votre tête, mais personne ne vous croit. L'affichage en pipe ASCII, c'est le minimum syndical. On verra plus tard comment générer du HTML (cours 6) pour faire un vrai affichage web.

4. Calculs

Le cœur d'un tableur, ce sont les calculs. On peut calculer des statistiques sur une colonne, ou évaluer des formules.

4.1 Statistiques

OCaml
---------- Fonctions statistiques ----------

// Extraire les valeurs numériques d'une colonne *)
let nums t name =
  column t name
  |> List.filter_map (fun c ->
       match c with
       | Num n -> Some n
       | _      -> None)

// Somme d'une colonne numérique *)
let sum t name = List.fold_left (+.) 0. (nums t name)

// Moyenne d'une colonne numérique *)
let avg t name =
  let vals = nums t name in
  if vals = [] then 0.
  else List.fold_left (+.) 0. vals /. float_of_int (List.length vals)

// Minimum, Maximum *)
let min t name =
  match nums t name with
  | []    -> None
  | lst   -> Some (List.fold_left min_float max_float lst)

let max t name =
  match nums t name with
  | []    -> None
  | lst   -> Some (List.fold_left max_float min_float lst)

// Compter les cellules non vides d'une colonne *)
let count t name =
  List.length (column t name)
sum, avg, min, max, count : les cinq doigts de la main statistique. Avec ça, vous pouvez analyser n'importe quel tableau de données, que ce soit des ventes, des températures, ou le nombre de tasses de café bues par vos collègues.

4.2 Formules

Allons plus loin : des formules permettant de calculer une nouvelle colonne à partir des existantes.

OCaml
---------- Formules ----------

// Type d'une formule *)
type formula =
  | Ref     of string         // référence à une colonne : "Nom" *)
  | ConstF  of float          // constante numérique *)
  | Add     of formula * formula
  | Sub     of formula * formula
  | Mul     of formula * formula
  | Div     of formula * formula

// Évaluer une formule pour une ligne donnée *)
let rec eval_formula t row_idx f =
  match f with
  | ConstF n -> Num n
  | Ref name ->
      (match cell_at t row_idx name with
      | None -> Empty
      | Some c -> c)
  | Add (a, b) ->
      (match (eval_formula t row_idx a,
              eval_formula t row_idx b) with
      | (Num x, Num y) -> Num (x +. y)
      | _              -> Empty)
  | Sub (a, b) -> ((* similaire *))
  | Mul (a, b) -> ((* similaire *))
  | Div (a, b) -> ((* similaire, avec division par zéro → Empty *))

// Ajouter une colonne calculée à la table *)
let add_column t name f =
  let new_col =
    List.mapi (fun i _ -> eval_formula t i f) t.rows
  in
  let add_cell row cell = row @ [cell] in
  { headers = t.headers @ [name];
    rows    = List.map2 add_cell t.rows new_col;
  }

Exemple d'utilisation :

OCaml
// Ajouter une colonne "TTC" = "Prix" * 1.20 *)
let table_ttc =
  add_column table "TTC"
    (Mul (Ref "Prix", ConstF 1.20))
Application : Dans un vrai tableur, les formules sont stockées sous forme de chaînes (comme "=A1*B2") et parsées. Notre ADT formula est l'AST de ces formules. On pourrait ajouter un parseur de formules (en utilisant Parsec, cours 4) pour passer de "=Prix * 1.20" à notre AST.

5. Filtres & tris

Un tableur permet de filtrer les lignes selon des critères et de les trier. Avec la monade Liste et List.sort, c'est immédiat :

OCaml
---------- Filtres ----------

// Type d'une condition de filtre *)
type condition =
  | Eq  of string * string  // colonne = valeur *)
  | Gt  of string * string  // colonne > valeur *)
  | Lt  of string * string  // colonne < valeur *)
  | And of condition * condition
  | Or  of condition * condition

// Évaluer une condition sur une ligne *)
let rec eval_cond t row cond =
  match cond with
  | Eq (col, val_) ->
      (match col_index t col with
      | None -> false
      | Some i -> show_cell (List.nth row i) = val_)
  | Gt (col, val_) ->
      (match col_index t col with
      | None -> false
      | Some i ->
          (match List.nth row i with
          | Num n -> n >. float_of_string val_
          | _      -> false))
  | And (a, b) -> eval_cond t row a && eval_cond t row b
  | Or  (a, b) -> eval_cond t row a || eval_cond t row b

// Filtrer la table *)
let filter_rows t cond =
  { t with rows = List.filter (eval_cond t) t.rows }

---------- Tris ----------

// Trier la table selon une colonne *)
let sort_by t col asc =
  match col_index t col with
  | None -> t
  | Some i ->
      let cmp r1 r2 =
        compare (show_cell (List.nth r1 i))
                  (show_cell (List.nth r2 i))
      in
      let sorted = List.sort cmp t.rows in
      { t with rows = if asc then sorted else List.rev sorted }

Avec la monade Option et List.nth_opt, on peut sécuriser tout ça. Mais l'essentiel est là.

Trier un CSV avec OCaml, c'est comme ranger sa bibliothèque par ordre alphabétique : satisfaisant, mais ça dépend de ce qu'on met dans la colonne "Auteur". Avec List.sort, au moins, on est sûr que le tri est fait. Pas comme mon étagère du bas.

6. Publipostage (Mail Merge)

Le publipostage (mail merge) permet de générer des documents personnalisés à partir d'un modèle et d'une table de données. Chaque ligne de la table remplit les "trous" du modèle pour produire un document unique.

C'est ce qui permet d'envoyer 10 000 lettres personnalisées sans les écrire à la main. Les politiques adorent. Les entreprises aussi.

OCaml
---------- Publipostage ----------

// Type d'un modèle : texte avec des trous {{colonne}} *)
type template =
  | Lit  of string          // Texte littéral *)
  | Hole of string          // Trou : nom de colonne *)
  | Cat  of template * template  // Concaténation *)

// Parser un modèle "Bonjour {{Nom}}, vous avez {{Âge}} ans" *)
let parse_template s =
  let rec parse i =
    if i >= String.length s then []
    else if s.[i] = '{' && i + 1 < String.length s
            && s.[i+1] = '{'
    then
      let close = String.index_from s (i+2) '}'
          (* en pratique, gérer l'erreur *)
      in
      let name = String.sub s (i+2) (close - i - 2) in
      Hole name :: parse (close + 2)
    else
      let end_lit =
        try String.index_from s i '{'
        with Not_found -> String.length s
      in
      let text = String.sub s i (end_lit - i) in
      Lit text :: parse end_lit
  in
  let parts = parse 0 in
  List.fold_right (fun a b -> Cat (a, b)) parts (Lit "")

// Remplir un modèle avec une ligne de la table *)
let rec fill_template t row tmpl =
  match tmpl with
  | Lit s      -> s
  | Hole col   ->
      (match col_index t col with
      | None -> "{{" ^ col ^ "}}"
      | Some i -> show_cell (List.nth row i))
  | Cat (a, b)  -> fill_template t row a ^ fill_template t row b

// Générer tous les documents (un par ligne) *)
let mail_merge t tmpl =
  List.map (fill_template t) t.rows

Exemple :

OCaml
let modele = parse_template "Bonjour {{Nom}}, vous êtes de {{Ville}} !"

// Avec notre table du cours 4 : *)
// | Nom   | Âge | Ville  | *)
// | Alice | 30  | Paris  | *)
// | Bob   | 25  | Lyon   | *)

mail_merge table modele
(* = ["Bonjour Alice, vous êtes de Paris !"; *)
(*    "Bonjour Bob, vous êtes de Lyon !"] *)
Application réelle : Le publipostage est utilisé pour les factures, les lettres de motivation, les certificats personnalisés, les étiquettes d'envoi. En combinant avec l'export HTML (section suivante), on peut générer des centaines de pages web personnalisées à partir d'un seul fichier CSV.

7. Export & rapports

Une table, c'est bien. Mais une table qu'on peut exporter en HTML, c'est mieux. On peut générer des rapports, des tableaux de bord, des pages web.

OCaml
---------- Export HTML ----------

let escape_html s =
  s
  |> Str.global_replace (Str.regexp "&") "&amp;"
  |> Str.global_replace (Str.regexp "<")  "&lt;"
  |> Str.global_replace (Str.regexp ">")  "&gt;"

let export_html t title =
  Printf.sprintf {|<!DOCTYPE html>
<html><head><meta charset="UTF-8">
<title>%s</title>
<style>table { border-collapse: collapse; width: 100%%; }
th, td { border: 1px solid #ccc; padding: 6px; text-align: left; }
th { background: #0E8A7A; color: #fff; }
tr:nth-child(even) { background: #f5f5f5; }
</style></head><body>
<h1>%s</h1>
<table>
<thead><tr>%s</tr></thead>
<tbody>%s</tbody>
</table></body></html>|}
  title
  title
  (String.concat ""
    (List.map (fun h -> Printf.sprintf "<th>%s</th>" (escape_html h))
      t.headers))
  (String.concat ""
    (List.map (fun row ->
      Printf.sprintf "<tr>%s</tr>"
        (String.concat ""
          (List.map (fun c ->
            Printf.sprintf "<td>%s</td>" (escape_html (show_cell c)))
          row)))
    t.rows))

Et on peut aussi exporter en CSV après transformation, pour sauvegarder ou échanger :

OCaml
---------- Export CSV ----------

let escape_csv s =
  if String.contains s ',' || String.contains s '"'
  then "\"" ^ Str.global_replace (Str.regexp "\"") "\"" s ^ "\""
  else s

let export_csv t =
  String.concat "\n"
    ((String.concat "," (List.map escape_csv t.headers))
     :: List.map (fun r ->
          String.concat "," (List.map (escape_csv % show_cell) r))
        t.rows)
Pipeline complet : Load CSV |> Filter |> Add column |> Sort |> Export HTML. C'est un programme ETL (Extract, Transform, Load) complet en moins de 200 lignes d'OCaml.

8. Conclusion

Ce cours a montré comment combiner toutes les techniques des cours précédents pour construire une mini application tableur :

BriqueSourceUsage dans Mini Excel
ADTCours 2Type cell, table, formula, condition
Map / Filter / FoldCours 2List.map sur les lignes/colonnes, filter des lignes
Monade OptionCours 3safe_cell avec Option.bind
Monade ListeCours 3Requêtes "SQL-like" sur les lignes
Parseur CSVCours 4Chargement des données load_csv
Génération HTMLCours 4export_html, rapport

Ce qu'il faut retenir

  • Un tableur, c'est une table + des formules + des filtres + de l'export.
  • Les formules sont des ADT (AST) qu'on évalue avec une fonction récursive.
  • Le publipostage sépare le modèle des données — le principe MVC avant l'heure.
  • Tout ça tient en ~200 lignes d'OCaml pur, grâce aux combinateurs des cours précédents.

Dans le prochain cours, on utilisera le parseur HTML pour construire une bibliothèque de manipulation de documents à la jQuery : sélecteurs CSS, traversée du DOM, modifications, génération de pages.

← Cours 4 : Parseurs Cours 6 : Mini jQuery →

Fait avec ❤️, des SUM et des VLOOKUP artisanaux

© 2026 — laurent.thiry@uha.fr