Ecriture d’une fonction de calcul: json2sql
Ecriture d’une fonction de calcul: json2sql
10 novembre 2024 - Auteur : - Catégories : Blog, FileMaker, Technique

Ecriture d’une fonction de calcul: json2sql

Après avoir passé deux jours sur l’écriture d’une fonction de calcul, je me suis dit que je n’étais plus à un jour près et que raconter l’histoire de cette écriture pourrait être intéressant pour d’autres développeurs, non pas tellement pour cette fonction en elle-même, mais pour capter l’état d’esprit et les idées générales qui permettent d’aborder une fonction complexe.

En plus, Claris m’ayant fait l’honneur de me programmer à la conférence Engage en mars prochain à Austin, avec un sujet aussi peu banal et prétentieux que « comment, moi, Fabrice Nordmann, j’aborde une problématique à résoudre avec la plateforme FileMaker », il faut que je commence sérieusement à faire de l’introspection pour comprendre moi-même comment je fonctionne afin de l’expliquer. En tant que lecteur vous me servez donc de cobaye, ce dont je vous remercie.

Tout d’abord, expliquons ce que fait la fonction.

La fonction s’appelle json2sql et permet de formuler une requête SQL pour FileMaker en utilisant un paramètre JSON.

En effet, la fonction d’écriture de JSON, JSONSetElement, est devenue franchement agréable à utiliser avec la version 21 de FileMaker, et on prend vite l’habitude, alors qu’écrire du SQL est vraiment pénible, surtout si on veut l’écrire bien, sans dépendance au nom des champs, afin que la requête ne s’arrête pas de fonctionner si on renomme une rubrique.

L’idée est donc de passer un paramètre JSON à cette fonction, qu’elle le traduise en requête SQL et l’exécute (ça c’est l’idée avec laquelle j’ai commencé, mais ça s’est bien compliqué ensuite, vous allez voir).

Nous avions déjà une fonction que nous utilisons beaucoup, sql.match ( _requestedField ; _matchField ; _match ), qui permet de retrouver les valeurs d’une colonne (_requestedField) dans les enregistrements correspondant à un critère dans une requête du type.

SELECT _requestedField WHERE _matchField = _match

Elle nous permet, de réaliser une grosse proportion des requêtes dont nous avons besoin quand on code en FileMaker.

Mais évidemment, c’est très limité. Si on veut plusieurs colonnes, des fonctions, plusieurs comparaisons (WHERE)… ça ne suffit pas.

Nous avons aussi un ensemble de fonctions qui nous aident à écrire proprement du SQL, comme sql.table, sql.col, sql.in.clause… mais rien d’aussi cool que le JSON.

Et puis aussi, il y avait de la vaisselle sale dans l’évier, donc vraiment c’était le moment de trouver une fonction à écrire.

 

Donc c’est parti, « yapuka »

D’abord, quelle doit être « l’empreinte » de cette fonction ? Quel nom ? quels paramètres ?

J’aime faire simple, alors :

json2sql ( _json )

Ben oui, JSON, c’est déjà fait pour structurer l’information, donc je vais tout indiquer dans le JSON, c’est joli. (Spoiler : ça ne finira pas comme ça).

Je réfléchis donc à la structure du JSON, en ayant bien en tête que la fonction doit, in fine, appeler la fonction native

ExecuteSQL ( sqlQuery ; fieldSeparator ; rowSeparator { ; arguments... } )

Si vous n’êtes pas familier avec la fonction ExecuteSQL, rappelons que sqlQuery est une requête SQL de type SELECT uniquement, que l’on peut utiliser des ? en lieu et place des valeurs de comparaison et que ces ? vont être remplacés par FileMaker par des éléments dans la liste d’arguments passés en fin de fonction, dans l’ordre. L’avantage de cela est que FileMaker gère tout seul les types de données (transforme les dates au format SQL, ajoute les guillemets autour des textes mais pas des nombres, gère le séparateur décimal…)

Donc par exemple si j’écris :

ExecuteSQL ( "SELECT primaryKey FROM invoices WHERE clientName > ? AND invoiceDate = ? AND totalAmount > ?" ; "" ; "" ; "L" ; Date ( 6 ; 1 ; 2024 ) ; round ( 100/3 ;2 ) )

Alors FileMaker va comprendre tout seul et exécuter la requête:

SELECT primaryKey FROM invoices WHERE clientName > 'L' AND invoiceDate = '2024-06-01' AND totalAmount > 333.33

(ajout des guillemets simples, transformation de la date, et utilisation du point comme séparateur décimal, même si mon fichier utilise la virgule, car SQL comprend le point).

Voilà pour ce rappel concernant ExecutesSQL, revenons maintenant à l’écriture de la fonction.

On aura donc un JSON écrit avec une fonction du type:

JSONSetElement ( ""
   ; [ "query" ; <un objet JSON compliqué auquel je réfléchirai dans le détail après> ; JSONObject ]
   ; [ "fieldSeparator" ; "" ; JSONString ]
   ; [ "rawSeparator" ; "" ; JSONString ]
   ; [ "arguments" ; <un array JSON puisque les arguments doivent être passés dans l'ordre> ; JSONArray ]
)

Notons aussi que je souhaite écrire

  • "{table}"."{column}" et nom simplement {column}
  • "{table}" et non simplement {table}

Ceci afin d’éviter les problèmes de noms réservés de SQL ainsi que les nommages bizarre des champs avec par exemple une espace. Il est très important selon moi d’être dans cet état d’esprit : tout le monde doit pouvoir utiliser cette fonction, dans n’importe quel fichier FileMaker. Je ne prends donc ni pour acquis que les rubriques seront nommées « correctement » ou que le séparateur décimal d’un fichier sera forcément le mien, ou la langue le français (même si j’utilise la version anglaise de FileMaker). Donc toujours penser « ubiquité » : la fonction doit marcher partout, et si elle ne le peut pas, cela doit être conscient, assumé et documenté.

La structure « algorithmique » de la requête est donc :

"SELECT" & [ boucle sur une liste ordonnée de noms de colonnes ] & " FROM " & [ table, qui est une information que je peux extraire de la même liste si je qualifie pleinement les références aux rubriques avec la notation table::rubrique ]

Suivi d’une partie facultative (on n’est pas obligé d’avoir des critères) :

& " WHERE " & [ boucle sur un tableau de critères composés d'un opérateur logique (sauf pour le premier), d'une colonne, d'un opérateur de comparaison, et d'une valeur ]

Je me lance donc enfin dans l’écriture d’un prototype de JSON pour le paramètre query.

J’ouvre donc le DataViewer et…

JSONSetElement ( ""
   ; [ "query.columns[+]" ; GetFieldName ( invoice::invoiceNumber ) ; JSONString ]
   ; [ "query.columns[+]" ; GetFieldName ( invoice::invoiceDate ) ; JSONString ]
   ; [ "query.columns[+]" ; GetFieldName ( invoice::amount ) ; JSONString ]
)

Voici donc un premier tableau, celui du nom des colonnes (il s’agit bien du nom et non de la valeur, c’est pourquoi le type est toujours String).

Sauf que… je réalise que j’ai oublié les fonctions. Je sais que je ne veux pas les gérer tout de suite, mais il faut que ma structure permette l’évolution. J’ai aussi en tête que si un jour je veux gérer les jointures, il va me falloir un peu plus d’information sur les colonnes, mais ça, c’est pour plus tard. Je reprends :

JSONSetElement ( ""
   ; [ "query.columns[+].name" ; GetFieldName ( invoice::invoiceNumber ) ; JSONString ]
   ; [ "query.columns[:].function" ; "Sum" ; JSONString ]
   ; [ "query.columns[+].name" ; GetFieldName ( invoice::invoiceDate ) ; JSONString ]
   ; [ "query.columns[+].name" ; GetFieldName ( invoice::amount ) ; JSONString ]
)

Bien, maintenant j’ai un tableau qui représente la liste des colonnes, je dois maintenant en faire quelque chose.

Avant tout, il me faut un environnement pour ça. J’écris donc dans mon Visualiseur de données :

Let ([
   _json = JSONSetElement ( ""
         ; [ "query.columns[+].name" ; GetFieldName ( invoice::invoiceNumber ) ; JSONString ]
         ; [ "query.columns[:].function" ; "Sum" ; JSONString ]
         ; [ "query.columns[+].name" ; GetFieldName ( invoice::invoiceDate ) ; JSONString ]
         ; [ "query.columns[+].name" ; GetFieldName ( invoice::amount ) ; JSONString ]
      );
   _query = ""
];
   _query
)

Je peux maintenant attaquer le paramètre query, et observer le résultat.

Je ne vous remets pas tout le contenu du Visualiseur de données, juste le paramètre _query de la fonction Let (Definir en français)

"SELECT " & While ([
        j = _json ;
	          c = ValueCount ( JSONListKeys ( j ; "columns" )) ;
	          i = 0 ;
	          r = "" ;
	          v = "" ;
	          to = "" ;
	        toq = "" 
	     ];
	        i < c ;
	     [
		        v = Substitute ( JSONGetElement ( j ; "columns[" & i & "].name" ) ; "::" ; ¶ ) ;
		        function = JSONGetElement ( j ; "columns[" & i & "].function" ) ;
		        // as we are selecting from 1 table only, let's extract the table occurrence name only during the first iteration
		        to = Case ( i = 0 ; GetValue ( v ; 1 ) ; to ) ;
		        // quoted version, to avoid issue with reserved words or blanks
		        toq = Case ( i = 0 ; Quote ( to ) ; toq ) ;
		        column = GetValue ( v ; 2 ) ;
		        colq = Quote ( column ) ; // quoted version
		        // append to the list
		        r = List ( r ; Case ( IsEmpty ( function ) ; colq ; function & "(" & colq & ")" )) ;
		        i = i+1
	     ];
		        Substitute ( r ; ¶ ; ", " ) & " FROM " & to 
   )
)

D’abord, rassurez-vous, je n’ai pas écrit ça d’un seul coup, mais je ne peux pas décortiquer chaque étape, sans quoi vous allez vous ennuyer et la vaisselle dans l’évier va commencer à sentir…

Notez tout de même que :

  • je ne me suis pas occupé des fonctions tout de suite
  • j’utilise pour mes fonctions While (TantQue) un certain nombre de conventions qui me font aller assez vite :
    • l’itérateur est toujours i (sauf dans le cas d’une boucle imbriquée (While dans While, auquel cas c’est j)
    • je commence par c (le nombre d’itérations), et par initialiser i (l’itérateur), r (le résultat), et en général v (la valeur traitée)
    • la condition est presque toujours i < c
    • l’incrémentation de l’itérateur (i = i+1) intervient en premier paramètre de la partie logique, ou en dernier quand on travaille sur un array JSON (0-based)
  • je n’ai ajouté les commentaires que plus tard

Vous pourriez aussi vous demander pourquoi avoir to et toq, column et colq, et pas seulement la version q (entre guillemets). Vous avez raison, à ce stade ça n’est pas nécessaire. Je les ai laissés ici pour que la comparaison avec les versions suivantes soient facilitées (je vous mâche le travail !)

Clause WHERE

Je peux donc passer aux critères.

Je commence à écrire le tableau (array), sauf que là, il me faut quelques données, ou du moins, à ce stade, une table et des rubriques.

Je demande à ChatGPT de me fournir un échantillon de données en csv, en faisant bien attention d’avoir différents types (texte (dont au moins une colonne contenant des textes multi-lignes), nombre, date) et des doublons (pour pouvoir faire des regroupements). Je m’assure aussi d’avoir des colonnes dont les noms comportent des espaces ou des caractères accentués. Je glisse mon CSV sur l’icone de FileMaker qui le convertit en fichier .fmp12 et hop, on peut travailler « pour de vrai ». En fait, pas tout à fait encore. Je dois d’abord convertir les données de type date qui sont au format YYYY-MM-DD au format FileMaker, qui dans mon fichier est DD/MM/YYYY. Cela aurait dû me mettre la puce à l’oreille, mais… vous allez voir.

Retour au Visualiseur de données.

JSONSetElement ( "" 
   ; [ "criteria[+].column" ; GetFieldName ( invocies::total ) ; JSONString ]
   ; [ "criteria[:].operator" ; "<" ; JSONString ]
   ; [ "criteria[:].value" ; 100.23 ; JSONNumber ]
   ; [ "criteria[+].column" ; GetFieldName ( invoices::invoiceNumber ) ; JSONString ]
   ; [ "criteria[:].operator" ; "LIKE" ; JSONString ]
   ; [ "criteria[:].logicalOperator" ; "OR" ; JSONString ]
   ; [ "criteria[:].value" ; "NC%" ; JSONString ]
   ; [ "criteria[+].column" ; GetFieldName ( invoices::clientVatNumber ) ; JSONString ]
   ; [ "criteria[:].operator" ; "=" ; JSONString ]
   ; [ "criteria[:].value" ; "*" ; JSONString ]
   ; [ "criteria[+].column" ; GetFieldName ( invoices::taxTotal ) ; JSONString ]
   ; [ "criteria[:].operator" ; "=" ; JSONString ]
   ; [ "criteria[:].value" ; "=" ; JSONString ]
   ; [ "criteria[+].column" ; GetFieldName ( invoices::date ) ; JSONString ]
   ; [ "criteria[:].operator" ; ">" ; JSONString ]
   ; [ "criteria[:].value" ; Date ( 12 ; 1 ; 2015 ) ; JSONString ]
)

Comme vous le voyez, avec cette nouvelle notation [+] et [:] de la version 21, il est vraiment très aisé d’écrire un array de manière naturelle. Avec [+], je commence une nouvelle rangée, avec [:] j’ajoute une « colonne » (« attribut ») en restant dans la même rangée.

Je peux donc écrire presque naturellement, et donc au cours de l’écriture me viennent des idées. Par exemple l’idée d’utiliser des opérateurs de recherche classiques et bien pratiques en FileMaker tel que « = » (vide) et « * » (non vide). Je prévois donc ces cas dans le prototype en pensant que je les traiterais pour des opérateurs IS NULL et IS NOT NULL en SQL. L’important ici est que je n’y avais pas pensé avant de commencer, et que je juge au fil de l’eau que ce sera assez facile à gérer et que donc je vais l’intégrer dès la première version.

Premier écueil

En revanche, en écrivant ce prototype je me rends compte d’une chose à laquelle je n’avais pas pensé (j’aurais dû, mais je dois bien me rendre à l’évidence, au risque de décevoir ma mère : je suis imparfait)
Note: pour ma grand mère, dont je vous parlais ici, c’est tout noir ou tout blanc, elle est très booléenne.

Et donc, ce problème, c’est qu’en arrivant à la comparaison de date, je me rends compte que si je passe comme je le voulais les arguments sous forme de ? pour qu’ils soient substitués pas les valeurs (voir plus haut sur la syntaxe de ExecuteSQL), le type sera forcément perdu par un passage par JSON, JSON étant dépourvu de type date, et donc FileMaker sera incapable de passer le bon type de donnée (il n’y verra que du texte ou du nombre).

Cela s’ajoute à ce que j’avais déjà en tête, à savoir que pour passer un nombre variable de paramètres à une fonction (ce qu’indiquent les {} (facultatif) et les … (nombre variable) dans l’empreinte de la fonction

ExecuteSQL ( sqlQuery ; fieldSeparator ; rowSeparator { ; arguments... } )

Et ça, je sais que ça va m’obliger à utiliser l’indirection et la fonction Evaluate… et je n’ai pas très envie à ce stade, en particulier quand on est dans un contexte où on joue avec l’indépendance des noms des champs, avec des guillemets, des conversions de type… Bref, le truc pénible qui va compliquer l’écriture de cette fonction, sa maintenance, et éventuellement sa qualité. On verra plus tard qu’on y arrivera, à l’indirection (Evaluate), mais pour d’autres raisons.

Donc je laisse tomber l’idée de passer les arguments avec des ?, et je dois donc insérer directement les valeurs correctement dans le JSON à l’aide d’une fonction que j’utilisais déjà pour écrire les requêtes SQL : sql.getAsSqlData ( _data ; _type ). C’est vrai pour les données de types date, heure et horodatage. Les nombres sont bien gérés par JSON.

D’ailleurs, en écrivant cet article, je me rends compte que le nom même de la fonction json2sql n’est pas forcément le bon. Dans notre bibliothèque de fonction, le premier mot nous permet de regrouper alphabétiquement les fonctions selon ce qu’elles traitent. Nous avons une ribambelle de text.<quelque chose>, de json.<quelque chose>… or ici, il me semble que le sujet est plus le SQL que le JSON. Donc je devrais probablement renommer la fonction sql.fromJson… je vais devoir encore réfléchir un peu à ça.

Cette mésaventure me pousse à reconsidérer l’empreinte que j’avais choisie, avec un simple paramètre en JSON.

Sans que ce soit évident (je pourrais très bien continuer dans cette voie, je me dis que le fait de passer les paramètres fieldSeparator et rowSeparator dans le JSON ne mettra pas en valeur la différence avec ExecuteSQL et la manière de passer les arguments. Je décide de revenir sur cette décision et de passer à l’empreinte suivante :

json2sql ( _jsonQuery ; _fieldSeparator ; _rowSeparator )

qui ressemble plus à l’empreinte de la fonction native :

ExecuteSQL ( sqlQuery ; fieldSeparator ; rowSeparator { ; arguments... } )

et cela allège un peu la documentation que l’utilisateur (développeur) devra lire pour utiliser la fonction.

Cela aussi, ça fait partie de « l’état d’esprit » dans lequel je me mets pour écrire une fonction personnalisée : j’essaie de respecter les usages de la plateforme FileMaker, et d’être aussi cohérent que possible. Je ne renonce pas pour autant aux _ (underscores) devant les paramètres : d’abord ils rappellent qu’il s’agit d’une fonction personnalisée, et ensuite c’est une convention que je trouve assez importante dans mon code, mais on ne va pas discuter conventions de nommage…

J’ajoute donc à ma fonction la partie qui s’occupe des critères (clause WHERE) :

<the first part of the function here>
& Case ( not IsEmpty ( JSONListKeys ( j ; "criteria" )) ; 
   " WHERE " &
   While ([
      i = 0 ;
      c = ValueCount ( JSONListKeys ( j ; "criteria" )) ;
      v = "" ;
      r = ""
   ];
      i < c ;
   [
      f = GetValue ( Substitute ( JSONGetElement ( j ; "criteria[" & i & "].column" ) ; "::" ; ¶ ) ; 2 ) ;
      o = JSONGetElement ( j ; "criteria[" & i & "].operator" ) ;
      o = Case ( IsEmpty ( o ) ; "=" ; o ) ; // default comparison operator is =
      lo = JSONGetElement ( j ; "criteria[" & i & "].logicalOperator" ) ; 
      lo = Case ( IsEmpty ( lo ) ; "AND" ; lo ) ; // default logical operator is AND
      value = JSONGetElement ( j ; "criteria[" & i & "].value" ) ; 
      type = JSONGetElementType (  j ; "criteria[" & i & "].value" ) ;
      // this ugly things converts the data to JSON then parses it as a string to handle decimal separators and single quotes.
      v = Case ( type = JSONNumber or type = JSONBoolean ; Substitute ( JSONSetElement ( "" ; "v" ; value ; JSONNumber ) ; [ "{\"v\":" ; "" ] ; [ "}" ; "" ] ) ; "'" & value & "'" ) ;
      r = r & Case ( i ; " " & lo & " " ) // the logical operator is omitted for the first criterion
          & Quote ( f ) & " " & Case ( o = "=" and ( IsEmpty ( value ) or value = "=" ) ; "IS NULL" ; o = "=" and value = "*" ; "IS NOT NULL" ;  o & " " &  v ) ;
      i = i+1 
   ] ;
      r
   )
)

Quelques commentaires :

  • cette partie est conditionnée au fait qu’on trouve bien un tableau (array) « criteria » dans le JSON, j’aurais pu inclure le  » WHERE  » dans le résultat du While, mais je trouve que c’est plus lisible ainsi. On voit tout de suite qu’il s’agit de la clause WHERE de la requête.
  • pour l’opérateur de comparaison (o), on voit que s’il est vide, j’utilise « = », valeur par défaut. Penser à des valeurs par défaut permet de faciliter l’usage de la fonction.
  • pour l’opérateur logique, c’est AND par défaut
  • comme prévu, le traitement de * et de = est très simple (voir la définition de r)

Je passe rapidement sur les autres clauses GROUP BY, ORDER BY, OFFSET et FETCH FIRST. Elles sont assez facilement compréhensibles dans le code de la fonction.

Notons simplement que j’ai volontairement exclus le OFFSET par pourcentage, parce que ça introduisait une ambiguité au niveau du paramètre JSON : dois-je envoyer un nombre (5) ou un texte (« 5% »). Et puis je vous avoue que je n’ai jamais utilisé ce pourcentage, donc pas envie d’ajouter de la complexité pour rien, en tout cas pas dans la première version.

// GROUP BY
& Case ( not IsEmpty ( JSONListKeys ( j ; "group" )) ; " GROUP BY " &

   While ([
      i = 0 ;
      c = ValueCount ( JSONListKeys ( j ; "group" )) ;
      f = "" ;
      r = ""
   ];
      i < c ;
   [
      f = GetValue ( Substitute ( JSONGetElement ( j ; "group[" & i & "].column" ) ; "::" ; ¶ ) ; 2 ) ;
      r = List ( r ; Quote ( f )) ;
      i = i+1 
   ] ;
      Substitute ( r ; ¶ ; ", " )
   )
)

// ORDER BY
& Case ( not IsEmpty ( JSONListKeys ( j ; "sort" )) ; " ORDER BY " &
   While ([
      i = 0 ;
      c = ValueCount ( JSONListKeys ( j ; "sort" )) ;
      d = "" ;
      r = ""
   ];
      i < c ;
   [
      f = GetValue ( Substitute ( JSONGetElement ( j ; "sort[" & i & "].column" ) ; "::" ; ¶ ) ; 2 ) ;
      d = JSONGetElement ( j ; "sort[" & i & "].dir" ) ; 
      d = Case ( IsEmpty ( d ) ; "A" ; d ) ;
      r = List ( r ; Quote ( f ) & Case ( Left ( d ; 1 ) = "d" ; " DESC" ; " ASC" )) ;
      i = i+1 
   ] ;
      Substitute ( r ; ¶ ; ", " )
   )
)

// OFFSET and FETCH FIRST
& Case ( JSONGetElement ( j ; "offset" ) ; " OFFSET " & JSONGetElement ( j ; "offset" ) & " ROWS" )
& Case ( JSONGetElement ( j ; "limit" ) ; " FETCH FIRST " & JSONGetElement ( j ; "limit" ) & " ROWS ONLY" )

Exécution du SQL et c’est fini !

Et donc me voilà avec une fonction qui crée un parfaite requête SQL à partir d’un parmètre JSON. Je n’avais plus qu’à exécuter le SQL et retourner le résultat.

Rien de plus simple.

Quelques tests. Ça marche du feu de Dieu ! (pour les plaintes en blasphèmes, adressez-vous au guichet d’à côté). Les requêtes fonctionnent, j’obtiens bien les bons résultats, avec ou sans fonctions, avec ou sans critères…

Et puis… j’ai eu une idée.

Et c’est donc là que les ennuis ont commencé…

… mais je vous les raconterai dans une seconde partie.

Conclusions de la première partie

  • se mettre dans un certain état d’esprit. Ici je sais que je travaille sur une fonction générique, elle doit donc être capable de fonctionner dans n’importe quel contexte, et être facilement utilisable pas n’importe quel développeur. Quand je crée une fonction dans le cadre d’un projet client, je préfixe le nom de la fonction du nom du projet, et en fin de projet, je parcours ces fonctions pour voir si certaines mériteraient d’être rendues génériques et d’entrer dans notre boîte à outil. Mais si certaines se qualifient, ce n’est JAMAIS sans une revue du code pour qu’il soit présenté de manière cohérente avec nos autres fonctions, qu’il prévoie bien des cas dont nous n’avions pas besoin dans ce projet spécifique…
  • dans la mesure du possible, respecter la plateforme FileMaker et écrire les fonctions d’une manière qui les rendra utilisables assez naturellement dans ce cadre.
  • penser à gérer des valeurs par défaut pour ne pas contraindre l’utilisateur (le développeur) à renseigner tous les paramètres.
  • planifier, mais pas trop. J’ai une idée de ce que dois faire la fonction, je diffère d’emblée certaines possibilités (ici les jointures par exemple), mais d’un autre côté je ne m’interdis pas d’avoir des idées en cours de route (le traitement des opérateurs de comparaison = et * dans cet exemple). Comme vous le verrez dans la seconde partie, avoir eu une idée en cours de route m’a coûté très cher en temps, mais d’un autre côté le résultat méritait cette peine.
  • travailler avec des données. Partir du concret et aller vers l’abstrait. Beaucoup plus efficace que de monter toute une construction mentale et d’ensuite la confronter à la réalité et se rendre compte que rien ne va.
  • penser aux types de données, et garder en tête que quand on va boucler (While), les textes multi-lignes sont en soi un type de données.
  • avoir des conventions (ici la structure de la boucle While, entre autres) et s’y tenir.
Article précédent/suivant

Add comment

Ce site est protégé par reCAPTCHA et la Politique de confidentialité, ainsi que les Conditions de service Google s’appliquent.