Journal TapTempo en Java 17

Posté par  . Licence CC By‑SA.
Étiquettes :
19
7
nov.
2021

Cher journal,

Suite à la sortie récente de Java 17, j'ai créer une nouvelle version de TapTempo en en utilisant les dernière évolutions de Java.

La version initiale de TapTempo en Java est ici. Je n'ai pas trouvé le dépot avec les sources, mais un commentaire contenait le code principal. Je me suis basé sur ce commentaire pour faire la version en Java.

J'ai utilisé divers nouveaux mécanisme de Java 17 :
- le type var,
- les records,
- les texts blocs dans les tests (c-a-d les string sur plusieurs lignes),
- le switch expression.
J'ai hésité à ajouter les classe sealed et le pattern matching, mais ça aurais compliqué le code inutilement. Par rapport à la version initiale, j'ai ajouté l'internationalisation pour gérer le français, et j'ai mis quelques tests.

La première version que j'ai faites (version 1.0.0), ne gérait pas l'internationalisation, et les chaines de caractères étaient en texts blocs. On peut voir le code ici. Je trouve ces texts bloc très lisibles. En ajoutant l'internationalisation, j'ai du les enlevé de la classe principale, mais je les ai laissé dans les tests.

J'ai découvert la méthode formatted de la classe String qui permet de formater une chaine de caractère. Elle est apparue en Java 15. Exemple d'utilisation :

    "Valeur: %d".formatted(10)

Voici le code principal :

    package org.github.abarhub.taptempojava;

    import org.apache.commons.cli.*;

    import java.text.DecimalFormat;
    import java.time.Clock;
    import java.time.Duration;
    import java.time.Instant;
    import java.util.ArrayDeque;
    import java.util.Deque;
    import java.util.ResourceBundle;
    import java.util.Scanner;

    public class TapTempo {

        private static Clock clock = Clock.systemUTC();

        record Parameter(int precision, int resetTime, int sampleSize) {
        }

        enum Action {
            END, CALCULATE, OTHER
        }

        static class ExitException extends RuntimeException {
            private final int exitCode;

            public ExitException(int exitCode) {
                this.exitCode = exitCode;
            }

            public int getExitCode() {
                return exitCode;
            }
        }

        private static Parameter parserArguments(String[] args, ResourceBundle messages) {
            var precision = 0;
            var precisionMax = 5;
            var resetTime = 5;
            var sampleSize = 5;
            var options = new Options();

            var optHelp = new Option("h", "help", false,
                    messages.getString("cliHelp"));
            optHelp.setRequired(false);
            options.addOption(optHelp);

            var optPrecision = new Option("p", "precision", true,
                    messages.getString("cliPrecision").formatted(precision, precisionMax));
            optPrecision.setRequired(false);
            options.addOption(optPrecision);

            var optResetTime = new Option("r", "reset-time", true,
                    messages.getString("cliReset").formatted(resetTime));
            optResetTime.setRequired(false);
            options.addOption(optResetTime);

            var optSampleSize = new Option("s", "sample-size", true,
                    messages.getString("cliNbSample").formatted(sampleSize));
            optSampleSize.setRequired(false);
            options.addOption(optSampleSize);

            var optVersion = new Option("v", "version", false,
                    messages.getString("cliVersion"));
            optVersion.setRequired(false);
            options.addOption(optVersion);

            var parser = new DefaultParser();
            var formatter = new HelpFormatter();
            CommandLine cmd = null;

            try {
                cmd = parser.parse(options, args);
                if (cmd.hasOption('p')) {
                    precision = Integer.parseInt(cmd.getOptionValue('p'));
                    if (precision < 0) {
                        precision = 0;
                    } else if (precision > precisionMax) {
                        precision = precisionMax;
                    }
                }
                if (cmd.hasOption('r')) {
                    resetTime = Integer.parseInt(cmd.getOptionValue('r'));
                    if (resetTime < 1) {
                        resetTime = 1;
                    }
                }
                if (cmd.hasOption('s')) {
                    sampleSize = Integer.parseInt(cmd.getOptionValue('s'));
                    if (sampleSize < 1) {
                        sampleSize = 1;
                    }
                }
            } catch (NumberFormatException | ParseException e) {
                System.out.println(e.getClass() + ": " + e.getMessage());
                formatter.printHelp("TempoTap", options);
                throw new ExitException(1);
            }

            if (cmd.hasOption('h') || cmd.hasOption('v')) {
                if (cmd.hasOption('h')) {
                    formatter.printHelp("TempoTap", options);
                }
                if (cmd.hasOption('v')) {
                    System.out.printf((messages.getString("version")), Version.getVersion());
                }
                throw new ExitException(0);
            }

            return new Parameter(precision, resetTime, sampleSize);
        }

        public static double computeBPM(Instant currentTime, Instant lastTime, int occurenceCount) {
            if (occurenceCount == 0) {
                occurenceCount = 1;
            }

            Duration elapsedTime = Duration.between(lastTime, currentTime);
            var meanTime = elapsedTime.dividedBy(occurenceCount);

            return 60.0 * 1000 / meanTime.toMillis();
        }

        public static boolean compareDiff(Instant lastTime, Instant currentTime, long resetTime) {
            return Duration.between(lastTime, currentTime).compareTo(Duration.ofSeconds(resetTime)) > 0;
        }

        public static void setClock(Clock clock) {
            TapTempo.clock = clock;
        }

        public static void run(String[] args) {

            ResourceBundle messages = ResourceBundle.getBundle("Message");
            Deque<Instant> hitTimePoints = new ArrayDeque<>();
            var parameter = parserArguments(args, messages);

            var df = new DecimalFormat();
            df.setMaximumFractionDigits(parameter.precision);
            df.setMinimumFractionDigits(parameter.precision);

            System.out.println(messages.getString("start"));

            var keyboard = new Scanner(System.in);
            keyboard.useDelimiter("");

            boolean shouldContinue = true;
            while (shouldContinue) {

                Action action;
                do {
                    char c = keyboard.next().charAt(0);
                    action = switch (c) {
                        case 'q' -> Action.END;
                        case '\n' -> Action.CALCULATE;
                        default -> Action.OTHER;

                    };
                } while (action == Action.OTHER);

                if (action == Action.END) {
                    shouldContinue = false;
                    System.out.println(messages.getString("quit"));
                } else {
                    var currentTime = Instant.now(clock);


                    // Reset if the hit diff is too big.
                    if (!hitTimePoints.isEmpty() && compareDiff(hitTimePoints.getLast(), currentTime, parameter.resetTime)) {
                        // Clear the history.
                        hitTimePoints.clear();
                    }

                    hitTimePoints.add(currentTime);
                    if (hitTimePoints.size() > 1) {
                        var bpm = computeBPM(hitTimePoints.getLast(), hitTimePoints.getFirst(), hitTimePoints.size() - 1);

                        String bpmRepresentation = df.format(bpm);
                        System.out.printf(messages.getString("tempo"), bpmRepresentation);
                    } else {
                        System.out.println(messages.getString("hitEnter"));
                    }

                    while (hitTimePoints.size() > parameter.sampleSize) {
                        hitTimePoints.pop();
                    }
                }
            }
        }

        public static void main(String[] args) throws Exception {
            try {
                run(args);
            } catch (ExitException e) {
                System.exit(e.exitCode);
            }
        }

    }

Le code compile avec Maven et Java 17. J'ai mis les sources en licence Apache 2. Il devrait fonctionner sur les principaux os (Debian/Raspberry/Windows/MacOS/…).

sources
classe principale
release

  • # C'est du pinaillage mais le ne vaudrait mieux pas utiliser Clock

    Posté par  (site web personnel, Mastodon) . Évalué à 10.

    D'après la documentation, Clock n'a aucune obligation d'être monotone (au contraire, puisqu'elle doit explicitement lisser les secondes intercalaires). Son utilisation peut donc renvoyer des intervalles de temps faux pour une utilisation comme ici.

    Cf ce tuto pour une explication générale du problème, et cet article pour un détail du cas en Java.

    Note que dans le cas de TapTempo tu auras probablement pas de grosses surprises, mais utiliser la bonne horloge t'évitera des bugs impossibles ou presque à reproduire (changement d'horaires, horloge système en train d'être mise à jours par NTP ou un autre utilisateur…)

    La connaissance libre : https://zestedesavoir.com

  • # Fédération TapTempo : nommer de nouveaux administrateurs ?

    Posté par  (site web personnel) . Évalué à 6.

    Alors que la famille TapTempo s'enrichit d'une nouvelle version, serait-il possible de relancer la Fédération TapTempo ?

    Dans un projet GitHub, on peut ajouter des "collaborators" dans la partie "Settings > Manage access". Avoir trois ou quatre personnes permettrait de mettre à jour le site avec les dernières versions TapTempo. Pour l'instant, Fabien Marteau est le seul à avoir l'accès : https://github.com/Martoni

  • # Java cours derrière Scala ?

    Posté par  (site web personnel) . Évalué à 5.

    C'est assez marrant de voir les récentes évolutions de Java, on a l'impression que Java cours derrière Scala (et Kotlin) : on a eu map/fold/filter, le type Option, et maintenant var, les record (des sortes de case class).
    Je pose une question : avez-vous des infos sur les conceptions qu'ont les concepteurs des nouvelles normes Java ? Ce serait en effet intéressant pour comprendre dans quel sens ils veulent emmener le langage

    « Il n’y a pas de choix démocratiques contre les Traités européens » - Jean-Claude Junker

    • [^] # Re: Java cours derrière Scala ?

      Posté par  . Évalué à 4.

      Oui en effet, Java se rapproche de Scala. La prochaine grosse évolution c'est le pattern matching sur les types avec les classes sealed. La vidéo Devoxx présente certaines évolutions. Il va y avoir aussi des coroutines.

      Le problème pour ces évolutions, c'est qu'il faut rester compatible avec les anciens programmes (c-a-d que la compilation des anciens programmes fonctionnent toujours), et que les principaux outils (eclipse, maven, hibernate, etc…) soient compatibles. C'est pour cela que c'est un peu long.

    • [^] # Re: Java cours derrière Scala ?

      Posté par  (site web personnel) . Évalué à 5.

      Scala est le brouillon de Java :-)

      Le post ci-dessus est une grosse connerie, ne le lisez pas sérieusement.

    • [^] # Re: Java cours derrière Scala ?

      Posté par  . Évalué à 6.

      Actuellement, il y a plusieurs gros chantiers au niveau du langage :

      • mettre en place le pattern matching (projet Amber) avec des morceaux qui arrivent au fur et à mesure des versions
      • les threads légers (projet Loom) qui est bientôt prêt, je crois qu'ils en sont aux implémentations pour les architectures autres que x86_64
      • améliorer les génériques et mettre en place des primitives sans référence (projet Valhalla) dont des petits bouts sont déjà arrivés dans la JVM
      • réduire la taille des objets (projet lilliput)

      Des évolutions seront directement visible du développeur au niveau du langages et d'autres concernent la machinerie interne.

      L'inspiration des autres langages, qui ne tourne pas forcément sur la JVM, est clairement assumée. Par rapport à Scala et Kotlin, la grosse différence, à partir du moment où Java implémente une fonctionnalité il y a aussi des modifications de la JVM pour que ça fonctionne bien mieux.

      Petit à petit de nouvelles méthodes sont implémenter pour éviter de se trimballer des null afin de rendre le langage plus sûr.

Suivre le flux des commentaires

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