Journal Portage de TapTempo en Clojure (v2)

Posté par (page perso) . Licence CC by-sa.
Tags :
13
14
mar.
2018

Salut à tous. Voici une 2ème version en Clojure de TapTempo.
Je ne mets pas de référence vers le premier jet tellement il était à côté de la plaque (la version en Forth était beaucoup trop simpliste aussi). Désolé pour le bruit.

Dans celle-ci, nous avons :

  • Le lissage avec une vraie moyenne sur sample-size.

  • la gestion des arguments en ligne de commande avec validation des entrées :

    • nombre de samples
    • précision de l'affichage
    • reset après un certain temps
    • version
    • usage
  • L'internationalisation avec la fonction _ qui est un raccourci de la fonction gettext. Je n'ai fais que l'anglais et le français.

Les traductions se trouve dans une table de hachage dans le répertoire translations.

  • Des tests unitaires (pour la validation et la non régression en cas de changement).

Techniquement, la définition des arguments est faite via une table de hachage cli-options avec comme clés :default, :parse-fn et :validate et la fonction parse-opts.

Le calcul de la moyenne du tempo utilise des fonctions d'ordre supérieur (reduce, map, partition…).

Comme particularité, on peut trouver de la compréhension de liste (destructuring bind) sur des tableaux ou des tables de hachages :

(defn print-tempo [[tempo cnt] prec] ...)
ou
(defn do-loop [{:keys [precision sample-size reset-time] :as options}] ...)

Je mets ici la version complète en un seul fichier mais vous pouvez trouver une version mieux organisée sur github.

(ns taptempo.core
  (:require [clojure.tools.cli :refer [parse-opts]]
            [clojure.string :as str]
            [gettext.core :refer [_]]
            [trptcolin.versioneer.core :as version])
  (:import jline.console.ConsoleReader)
  (:gen-class))


(defn now []
  (System/currentTimeMillis))

(defn read-char []
  (->> (ConsoleReader.) (.readCharacter) char))

(defn parse-int [x]
  (Integer/parseInt x))

(def cli-options
  [["-p", "--precision PREC" (_ "change the number of decimal for the tempo. The default is 0 decimal places, the max is 5 decimals")
    :default 0
    :parse-fn parse-int
    :validate [#(<= 0 % 5) (_ "Must be a number between 0 and 5")]]
   ["-r", "--reset-time T" (_ "change the time in seconds to reset the calculation. The default is 5 seconds")
    :default 5
    :parse-fn parse-int
    :validate [pos? (_ "Must be a positive number")]]
   ["-s", "--sample-size N" (_ "change the number of samples needed to calculate the tempo. The default is 5 samples")
    :default 5
    :parse-fn parse-int
    :validate [#(<= 2 %) (_ "Must be a number greater than 1")]]
   ["-v", "--version" (_ "print the version number")]
   ["-h", "--help"]])

(defn calc-tempo
  "Compute tempo in bpm from samples"
  [samples]
  (let [cnt (count samples)]
    [(when (> cnt 1)
       (* 60 1000 (/ (dec cnt)
                     (reduce +
                             (map (fn [[end start]] (- end start))
                                  (partition 2 1 samples))))))
     cnt]))

(defn print-tempo [[tempo cnt] prec]
  (println (format (str "Tempo: %." prec "f bpm (%d " (_ "samples") ")") (float (or tempo 0)) cnt)))

(defn compute-next
  "Append now to samples. Keep at most sample-size.
   Reset samples if more than reset-time has elapsed since last call"
  [now samples {:keys [sample-size reset-time]}]
  (if (< (- now (first samples)) (* 1000 reset-time))
    (conj (take (dec sample-size) samples) now)
    [now]))

(defn do-loop [{:keys [precision sample-size reset-time] :as options}]
  (println (_ "Press the enter key in cadence (q to quit)"))
  (loop [samples [(now)]]
    (when-not (= (read-char) \q)
      (print-tempo (calc-tempo samples) precision)
      (recur (compute-next (now) samples options))))
  (println (_ "Bye!")))

(defn usage [options-summary]
  (->> ["Usage: TapTempo [options]"
        ""
        "Options:"
        options-summary]
       (str/join \newline)))

(defn version []
  (version/get-version "taptempo" "taptempo"))

(defn error-msg [errors]
  (str (_ "The following errors occurred while parsing your command") ":\n\n"
       (str/join \newline errors)))

(defn validate-args [args]
  (let [{:keys [options arguments errors summary]} (parse-opts args cli-options)]
    (cond
      (:help options)    {:exit-message (usage summary) :ok? true}
      (:version options) {:exit-message (version) :ok? true}
      errors             {:exit-message (error-msg errors)}
      :else              {:options options})))

(defn exit [status msg]
  (println msg)
  (System/exit status))

(defn -main [& args]
  (let [{:keys [options exit-message ok?]} (validate-args args)]
    (if exit-message
      (exit (if ok? 0 1) exit-message)
      (do-loop options))))
  • # Tests

    Posté par (page perso) . Évalué à 4.

    Bravo pour cette nouvelle version, surtout pour les tests unitaires !

    J'ai regardé les fichiers du dossier test, et j'ai du mal à comprendre ce qui est fait. Par exemple :

    (deftest calc-tempo-test
      (testing "calc-tempo from 0"
        (is (= (calc-tempo [1]) [nil 1]))
        (is (= (calc-tempo [60000 0]) [1N 2]))
        (is (= (calc-tempo [120000 60000 0]) [1N 3]))
        (is (= (calc-tempo [180000 120000 60000 0]) [1N 4]))
        (is (= (calc-tempo [240000 180000 120000 60000 0]) [1N 5]))
        (is (= (calc-tempo [300000 240000 180000 120000 60000 0]) [1N 6]))
        (is (= (calc-tempo [360000 300000 240000 180000 120000 60000 0]) [1N 7])))
      (testing "calc-tempo from 0 quicker"
        (is (= (calc-tempo [36000 30000 24000 18000 12000 6000 0]) [10N 7])))
      (testing "calcl-tempo from 1000"
        (is (= (calc-tempo [361000 301000 241000 181000 121000 61000 1000]) [1N 7])))
      (testing "calc-tempo from 1000 quiker"
        (is (= (calc-tempo [37000 31000 25000 19000 13000 7000 1000]) [10N 7])))
      (testing "calc-tempo for some validated entries"
        (is (= (calc-tempo [1521057817004]) [nil 1]))
        (is (= (calc-tempo [1521057820183 1521057817004]) [60000/3179 2]))
        (is (= (calc-tempo [1521057820602 1521057820183 1521057817004]) [60000/1799 3]))
        (is (= (calc-tempo [1521057821046 1521057820602 1521057820183 1521057817004]) [90000/2021 4]))
        (is (= (calc-tempo [1521057821469 1521057821046 1521057820602 1521057820183 1521057817004]) [48000/893 5]))
        (is (= (calc-tempo [1521057821914 1521057821469 1521057821046 1521057820602 1521057820183 1521057817004]) [30000/491 6]))
        (is (= (calc-tempo [1521057822350 1521057821914 1521057821469 1521057821046 1521057820602 1521057820183]) [300000/2167 6]))
        (is (= (calc-tempo [1521057822807 1521057822350 1521057821914 1521057821469 1521057821046 1521057820602]) [20000/147 6]))
        (is (= (calc-tempo [1521057823243 1521057822807 1521057822350 1521057821914 1521057821469 1521057821046]) [300000/2197 6]))
        (is (= (calc-tempo [1521057823700 1521057823243 1521057822807 1521057822350 1521057821914 1521057821469]) [300000/2231 6]))
        (is (= (calc-tempo [1521057824167 1521057823700 1521057823243 1521057822807 1521057822350 1521057821914]) [100000/751 6]))
        (is (= (calc-tempo [1521057824633 1521057824167 1521057823700 1521057823243 1521057822807 1521057822350]) [100000/761 6]))))
    

    Tu passes les timestamps en entrée du programme ?

    • [^] # Re: Tests

      Posté par (page perso) . Évalué à 5.

      Oui, la version précédente était vraiment trop simpliste.

      Les test unitaires sont exécutés à part avec la ligne de commande lein test (il faut que je le rajoute dans le README).
      Pour le fonctionnement, la fonction is attend que tous les tests en argument soient vrais.

      Donc par exemple pour :

      (is (= (calc-tempo [60000 0]) [1N 2]))

      L'appel à (calc-tempo [60000 0]) doit être égal à [1N 2]. C'est à dire 1 bpm avec 2 samples en entrées.
      Si ce n'est pas le cas, le test ne passe pas.

      Les premiers tests sont ceux attendu théoriquement. Les suivants sont de vrais échantillons (tu dois pouvoir voir l'heure à laquelle ils ont été pris :-) ).

  • # Ton du journal

    Posté par (page perso) . Évalué à 6.

    Pour ceux qui n'aurait pas suivit et qui aurait tiqué sur le ton du début de journal : je suis l'auteur de la 1ère version de TapTempo en Clojure et de celle en Forth.
    Je ne me permettrais pas de dénigrer de cette manière du code autre que le mien. Mais ces versions ne respectaient en rien le cahier des charges du journal initial (méa culpa). Ça m'apprendra à ne pas bien lire les specs… La version 2 me semble plus cohérente dans ce sens. On ne pouvait pas en rester à la 1ere version.

    Je trouve génial toutes les versions qui ont fleuries sur github. C'est vraiment sympa d'avoir un programme un peu plus évolué qu'un 'hello world' avec toutes les fonctionnalités attendu.
    Du coup un simple git clone et les explications pour compiler et on a un template pour un programme connu dans un langage qu'on ne maîtrise pas.

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.