jeudi 3 décembre 2015

Optimisation VBA-Excel sur des boucles et du remplacement de données

Le document sur lequel nous travaillons est présent sur github.

Cas d'usage

Dans mes activités de PMO (personne n'est parfait) je m'amuse (!) à construire de jolies feuilles de route, ou roadmap pour les intimes du barratin globish. Les meilleurs amis du PMO pour ce type d'activité sont Excel et powerpoint :
  • Excel pour la collecte de données (je suis un IT guy quand même),
  • Powerpoint pour la couche de présentation un peu "glam'".
J'en connais qui n'utilise que l'un ou l'autre. Je préfère les couplages intelligents.
Bon, pour finir l'explication, j'ai donc une très belle feuille de route, avec beaucoup de jalons, sous format excel, un nombre de clients et de domaines applicatifs importants, et un besoin de présenter des visions différenciées à chacun.
Pour cela, rien ne vaut Excel (toujours), beaucoup de mise en forme conditionnelle et quelques macros.

Le problème

Le principe est de récupérer (à la main) une copie de la feuille de route pour la mettre dans un onglet complémentaire, puis de nettoyer les cases sans contenu visible pour que l'affichage soit un peu sympa.
Je partage ce petit bout de code avec vous, qui est la version 0.


Sub makeItClear()

Dim ws As Worksheet, r As Range, c As Range, nbligne As Long
Application.ScreenUpdating = False

Set ws = ThisWorkbook.Sheets("Feuil1")

nbligne = InputBox("nb ligne à traiter", , 20) + 0
Set r = ws.Range("d8:aa" & nbligne)

For Each c In r.Cells
   If c.Text = "" Then c.ClearContents
   If Len(c.Text) > 2 Then c.HorizontalAlignment = xlLeft
Next

Application.ScreenUpdating = True

End Sub


De manière très simple, l'algorithme marche, par contre, c'est long. Pour une plage d'une quarantaine de lignes, il faut compter entre quinze et vingt minutes. Pour la feuille de route complète, pas loin d'une heure.

Le pourquoi

Après beaucoup de recherche, il s'avère que la partie lourde de l'algorithme est la partie écriture/réécriture du classeur, c'est à dire :

   If c.Text = "" Then c.ClearContents

Le problème vient du coût d'accès systématique au classeur pour écrire. Rien de nouveau me direz vous, et vous avez parfaitement raison. Le coût d'accès en écriture est toujours ce qui coûte cher en I/O et casse les performance.

Un peu de craft

Le problème étant l'accès, la première idée qui me vient est donc de ne plus faire un accès simultanée mais un accès par "small batch".


Sub makeItClear()

Dim myCells As String
Dim ws As Worksheet, r As Range, c As Range, nbligne As Long

myCells = "$E$5" 'cellule d'init ou il n'y a rien
Application.ScreenUpdating = False

Set ws = ThisWorkbook.Sheets("Feuil1")

nbligne = InputBox("nb ligne à traiter", , 20) + 0
Set r = ws.Range("f8:ag" & nbligne)



For Each c In r.Cells
  If c = "" Then myCells = myCells + "," + c.Address
  If Len(c.Text) = 1 Then c.HorizontalAlignment = xlCenter
Next

ws.range(mycells).clearcontents

Application.ScreenUpdating = True

End Sub



Pour faire simple, au lieu de faire un accès immédiat, le principe est de stocker la liste de tous les cas positifs et de faire une suppression globale.
L'idée est belle, mais ça ne marche pas du premier coup ! Sur le fond, le soucis est que la plage est trop longue !

Plus de craft !

Alors un peu de tests, il semble que la limite du 'range' de VBA est de 256 caractères. Pourquoi ? Je ne sais pas, dans tous les cas, une chaine de 256 caractères passe le test, une chaine de 512 non.
Pour s'en sortir, il reste donc à éclater la chaine en sous-chaine de maximum 256 caractères. Dans tous les autres langages, j'aurais joué avec des tableaux. En VBA, c'est vraiment pas pratique. Il reste donc à jouer avec des chaines :-/
On va donc faire une modification dans le code et rajouter deux fonctions complémentaires :

Sub makeItClear()

Dim myCells As String, calculMode As Variant
...

myCells = Replace(myCells, "$", "")

While Len(myCells) > 0
  m = cutLongList(myCells, ",", 256)
  ws.Range(m).Value = ""
  myCells = Mid(myCells, Len(m) + 2)
Wend

Application.ScreenUpdating = True

End Sub



Je nettoie donc la chaine de plage des '$' pour réduire sa taille, puis je travaille une boucle de réduction de chaine.
Pour cela, il faut deux petites fonctions :


Private Function cutLongList(ByVal myString As String, ByVal delimiter As String, ByVal maxValue As Long) As String
Dim i As Long
cutLongList = myString

If Len(myString) < maxValue Then Exit Function 

  i = 0 
While isMyNumCarMyCar(myString, maxValue - i, delimiter) = False 
  i = i + 1
Wend 
cutLongList = Mid(myString, 1, maxValue - i - 1)
End Function 

Function isMyNumCarMyCar(ByVal myString As String, ByVal numCar As Long, ByVal myCar As String) As Boolean 
'''Pour vérifier qu'un numéro de caractères correspond à un caractère attendu
  isMyNumCarMyCar = IIf(Mid(myString, numCar, 1) = myCar, True, False) 
End Function

N'ayant pas de meilleures fonctionnalités pour faire du slicing de chaines avec des Arrays, la fonction 'isMyNumCarMyCar' permet de vérifier qu'un numéro de caractère de chaine est bien un caractère passé en paramètre.
Et 'cutLongList' permet juste de faire le retour de substring. Elle serait sans doute à améliorer avec quelques traitements d'erreurs.

Conclusion

Première conclusion, ce changement de code permet de passer d'un temps de traitement de 10 minutes à moins de 30 secondes. Evidemment, ça jette.
Au milieu, je retiens :
  • Que l'accès en écriture dans les classeurs excel est à limiter dans les boucles,
  • Que l'objet range de VBA contient au maximum 256 caractères,
  • Que les objets Arrays de VBA sont vraiment pas top en termes de capacité.
Pour la suite, on attaque les gros projets avec du templating de powerpoint avec une source excel.

mardi 1 décembre 2015

Une loi sur la cigarette en voiture ? Et ma liberté

Ce matin un certain énervement m'a pris, en entendant que nos chers députés allaient examiner une loi sur l'interdiction de fumer dans les voitures en cas de présence d'un enfant de moins de 12 ans. Il raisonna convenablement avec une tribune de Jean-Marc Porquet dans le Canard du 26/11 et s'article en plusieurs temps.

Temps 1 : de l'usage du temps des absents chez les députés

En période de crise, et en période générale, lorsque les medias nous bassinent que les agendas parlementaires sont débordés, qui est la personne ayant positionné un tel texte dans la pile des sujets "urgents", "capitaux" ou "nécessaires" à la survie de notre pays.
Il faut croire qu'en terme de gestion du temps, et des séances parlementaires finissant à des heures indues, il restait un créneau pour que les députés présents - et ils ne sont pas nombreux sauf cas de sujets "capitaux", "urgents", etc. - se saisissent d'un sujet aussi important.

Temps 2 : de la perception de la santé publique et de l'hygiénisme républicain

Mon second désarroi est sans doute celui du mensonge caractérisé par nos hommes et femmes politiques, de tout bord, de tout poil et de tout profil, sur les questions de santé publique depuis une cinquantaine d'année. Il est de notoriété publique que la cigarette est un poison pour la population. Et il est aussi de notoriété publique que les politiques de prévention autour de ce sujet sont d'une efficacité impressionnante.
La raison en est pourtant très simple, et rejoint deux phénomènes distincts :
  • Celui de la vache à lait : la cigarette est une source de taxe et de revenus pour l'Etat avec une couverture du paquet de cigarettes à environ 80% (d'après le figaro du 23/07/15 et tabac liberte en 2013)
  • Celui de la grenouille : en augmentant progressivement le prix du tabac, les fumeurs s'accoutument à payer plus cher, ne sentant pas la température augmenter
Je passe volontairement sous silence l'ensemble des lobbies des cigarettiers d'un côté et des buralistes de l'autre, des médicaments et des substituts nicotiniques, etc.
Au milieu des années 70 JG Padioleau analysait déjà les différences de traitement des politiques publiques sur le tabac, montrant que la France entrait bien (et n'a pas changé) dans une démarche où la volonté n'est pas l'arrêt du tabagisme mais la taxation. Autant pour notre santé.

Temps 3 : Laissez moi tranquille dans ma voiture

Je mets trois choses au clair avant de poursuivre :
  1. J'ai une fille,
  2. Je ne fume plus, ce qui signifie que j'ai fumé, que j'ai arrêté avant la naissance de ma fille,
  3. Je n'ai plus de voiture depuis presque dix ans.
Malgré ces faits, il m'est insupportable d'entendre des hommes politiques vouloir déterminer mes activités dans la sphère privée. Dans ma voiture je suis chez moi. Alors le législateur, s'il n'a vraiment que cela à faire de son temps (et si c'est bien le cas, il serait bon de le mettre à travailler sur des sujets de fond) a plusieurs options :
  • Soit le tabac est vraiment dangereux, c'est un problème de santé publique, et il doit être interdit partout ! Oui, partout, à la maison, dans la voiture, à la vente, à la distribution. C'est un poison (!) il faut l'interdire du fait des risques sanitaires à long terme.
  • Soit les activités limitant l'attention lors de la conduite sont dangereuses, comme fumer, boire, manger, téléphoner, écouter de la musique, parler à son voisin, etc. et elles doivent être interdites car mettant en danger l'ensemble des usagers de la voiture.
Une position intermédiaire visant à expliquer que fumer dans la voiture est nocif pour les enfants, alors que fumer dans la maison l'est moins (?) ne tient pas. A moins que cela ne soit l'étape d'après ? Quelle est la réflexion sous-jacente, la logique étayant le principe ?
A force de légiférer sur tout, nos libertés finissent par disparaître. Ou plus exactement, il se passe la même chose qu'avec les grenouilles : en les mettant dans l'eau froide puis en chauffant progressivement, nous ne nous rendons pas compte que nous finirons ébouillantés.

Temps 4 : Big Brother is f***ing you ou de l'applicatif

Enfin, raisonnons un peu, car nos hommes politiques n'ayant plus que des chauffeurs, doivent oublier quelques éléments de bon sens à force de sortir de grandes écoles et de ne pas sortir de leurs tours d'ivoire :
  • Comment fait-on dans une voiture pour différencier un enfant de 12 ans d'un enfant de 10 ou 14 ? Un vrai plaisir pour les policiers devant apprécier l'age des enfants à bord d'un voiture avec un fumeur à bord.
  • Que feront les policiers ? Ils devront surveiller les véhicules et remarquer 1) qu'une personne à bord fume, 2) que des enfants sont présents à bord 3) que les enfants n'ont pas 12 ans. A partir de là, il faudra arrêter le véhicule, puis verbaliser. Je suis persuadé que les représentants de l'ordre seront ravis de jouer à ce petit jeu là, et que le volume de contraventions distribué sera très élévé. Ceci rejoint un second volet correspondant à l'analyse de la performance des politiques publiques, mais nous sommes loin du sujet.
Que reste-t-il comme option ? Probablement utiliser des systèmes préexistants, par exemple des caméras de surveillance pour analyser le comportement des conducteurs et passagers et vérifier la présence d'enfants pour opérer la verbalisation. De la science fiction me direz-vous. Hélas non, les algorithmes de reconnaissance d'age sont plutôt efficaces aujourd'hui, celui de reconnaissance d'image ou de patterns d'images aussi. Le plus compliqué est de poser suffisamment de caméra, puis de les brancher.

Conclusion : Politique mèle toi de ce qui te regarde

Big Brother or not, ce type d'idées est une aberration totale, une atteinte insoutenable à nos libertés fondamentales, à celle qui introduit notre devise nationale "Liberté, Fraternité, Egalité".
Caché sous des airs de politique de santé publique, nos hommes politiques profitent de moment de recueillement pour faire passer comme des chevaux de Troie des lois liberticides - encore - sur des sujets triviaux, permettant par la suite d'en faire passer plus encore. Il en va des restrictions des libertés comme du pucelage : une fois la voie ouverte, le suivant aura plus de place pour s'introduire.
Est-ce bien ou mal, je ne le sais pas, et ce n'est pas ma question. Ce n'est par contre pas le pacte auquel je souscris en tant que citoyen Français.

Politique, fais ton boulot, réfléchis sur les difficultés structurelles de notre pays et ne me casse pas les nougats avec des mesurettes sans ambition. Et si tu ne sais pas le faire, que tu es emprisonné dans un magma de bordels accumulés depuis 250 ans ou plus, file ta dém' et laisse le peuple se bouger le cul. Parce que franchement, pour sortir des conneries pareil, c'est pas la peine de payer des impôts.