Journal Kotaten : un Tap Tempo en Kotlin

Posté par  (site web personnel, Mastodon) .
Étiquettes :
16
9
mar.
2018

J'avais besoin d'un exemple quelconque pour tester différentes choses en Kotlin, dont :

  • L'internationalisation
  • La surcharge des opérateurs
  • L'utilisation de dépendances et de libs Java

Et comme c'est la mode de faire des clones de Tap Tempo, ça me paraissait être un bon candidat. Donc voici le mien – en Kotlin, donc. Il me parait être plus dans l'esprit de la version d'origine que la version Java (qui elle aussi tourne sur une JVM).

Pour vous éviter de fouiller, voici le code source du seul fichier avec de la logique dedans :

package fr.spacefox.kotaten

import com.beust.jcommander.*
import com.beust.jcommander.validators.PositiveInteger
import com.google.common.collect.EvictingQueue
import java.text.DecimalFormat
import java.text.MessageFormat
import java.util.*
import java.util.ResourceBundle

private const val NS_PER_SECOND = 1_000_000_000L
private const val PRECISION_DEFAULT = 0
private const val PRECISION_MIN = 0
private const val PRECISION_MAX = 0
private const val RESET_TIMER_DEFAULT = 5L
private const val SAMPLE_SIZE_DEFAULT = 5

private fun Double.toBpm(): Double {
    return 60.0 * 1_000_000_000 / this
}

private operator fun ResourceBundle.get(key: String): String {
    return this.getString(key)
}

@Parameters(resourceBundle = "messages")
class Settings {
    @Parameter(names = ["-h", "--help"], descriptionKey = "options.help", help = true)
    var help = false

    @Parameter(
            names = ["-p", "--precision"],
            descriptionKey = "options.precision",
            validateWith = [PrecisionValidator::class])
    var precision = PRECISION_DEFAULT

    @Parameter(
            names = ["-r", "--reset-time"],
            descriptionKey = "options.resettime",
            validateWith = [PositiveInteger::class])
    var resetTime = RESET_TIMER_DEFAULT

    @Parameter(
            names = ["-s", "--sample-size"],
            descriptionKey = "options.samplesize",
            validateWith = [PositiveInteger::class])
    var sampleSize = SAMPLE_SIZE_DEFAULT

    @Parameter(names = ["-v", "--version"], descriptionKey = "options.version")
    var version = false

    class PrecisionValidator: IParameterValidator {
        override fun validate(name: String?, value: String?) {
            val precision = Integer.parseInt(value)
            if (precision < PRECISION_MIN || precision > PRECISION_MAX) {
                throw ParameterException(MessageFormat.format(
                        ResourceBundle.getBundle("messages")["options.precision.outofscope"],
                        PRECISION_MIN,
                        PRECISION_MAX,
                        precision))
            }
        }
    }
}

class Kotaten(settings: Settings) {

    private val input = Scanner(System.`in`)
    private val samples: EvictingQueue<Long> = EvictingQueue.create<Long>(settings.sampleSize)
    private val messages = ResourceBundle.getBundle("messages")
    private val formatter = DecimalFormat()
    private val resetTime = settings.resetTime * NS_PER_SECOND

    init {
        input.useDelimiter("")
        formatter.minimumFractionDigits = settings.precision
        formatter.maximumFractionDigits = settings.precision
    }

    fun run() {
        println(messages["instructions"])
        var key: Char
        var start: Long? = null
        var end: Long
        while (input.hasNext()) {
            key = input.next()[0]
            if (key == 'q')
                break

            end = System.nanoTime()
            if (start == null || (end - start) > resetTime) {
                samples.clear()
                println(messages["typeagain"])
            } else {
                samples.add(end - start)
            }
            start = end

            if (samples.size > 0)
                print(MessageFormat.format(
                        messages["tempo"],
                        formatter.format(samples.average().toBpm())))
        }
        println(messages.getString("bye"))
    }
}

fun main(args: Array<String>) {
    val settings = Settings()
    val jc = JCommander.newBuilder()
            .addObject(settings)
            .build()
    jc.parse(*args)

    if (settings.help) {
        jc.usage()
        return
    }
    if (settings.version) {
        println(ResourceBundle.getBundle("config")["version"])
        return
    }
    Kotaten(settings).run()
}
  • # Vraie app

    Posté par  . Évalué à 3.

    J'adore. Plus qu'à ajouter une commande du flash pour faire clignoter le flash au rythme du tempo. C'est la fête!

    ⚓ À g'Auch TOUTE! http://afdgauch.online.fr

  • # Java ?

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

    J'ai du mal à comprendre la relation du Kotlin avec le Java. Le langage est interprété par une JVM ? et on peut utiliser directement d'autres modules java ?
    Si oui, quel est son intérêt ? moins verbeux ?

    (et merci pour cette version !)

    • [^] # Re: Java ?

      Posté par  . Évalué à 4.

      Le langage est compilé directement en bytecode, donc exécuté par la jvm comme du java, ou tout autre langage jvm (clojure, scala etc).
      100% compatible avec java (y compris les annotations par exemple).
      L’interet C’est des feature plus moderne (first class support des optional), et vachement vachement moins verbeux.

      C’est en gros comme java, mais largement modernisé.

      Linuxfr, le portail francais du logiciel libre et du neo nazisme.

      • [^] # Re: Java ?

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

        groumly a bien résumé. En gros, Kotlin c'est le résultat des développeurs d'IntelliJ qui se sont dit « Il nous faut un langage compatible avec Java sur la JVM, mais plus simple, plus moderne, et avec des améliorations orientées pragmatiquement pour les développeurs ».

        Plus d'infos :

        La connaissance libre : https://zestedesavoir.com

  • # Conversion automatique de code

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

    À noter que Kotlin (enfin, au moins IntelliJ) permet de convertir du code Java en Kotlin. Ça fonctionne, mais nécessite un peu de nettoyage et de « Kotlinisation » du code.

    Par exemple, si je convertis le TapTempo Java directement, j'obtiens (après avoir remis les fonctions directement dans le fichier, sans passer par un objet inutile) :

    package com.i2bp.taptempo
    
    import org.apache.commons.cli.CommandLine
    import org.apache.commons.cli.CommandLineParser
    import org.apache.commons.cli.DefaultParser
    import org.apache.commons.cli.HelpFormatter
    import org.apache.commons.cli.Option
    import org.apache.commons.cli.Options
    import org.apache.commons.cli.ParseException
    
    import java.text.DecimalFormat
    import java.util.Scanner
    import java.util.ArrayDeque
    import java.util.Date
    import java.util.Deque
    
    fun computeBPM(currentTime: Long, lastTime: Long, occurenceCount: Int): Double {
        var occurenceCount = occurenceCount
        if (occurenceCount == 0) {
            occurenceCount = 1
        }
    
        val elapsedTime = (currentTime - lastTime).toDouble()
        val meanTime = elapsedTime / occurenceCount
    
        return 60.0 * 1000 / meanTime
    }
    
    fun main(args: Array<String>) {
    
        var precision = 0
        var resetTime = 5
        var sampleSize = 5
        val options = Options()
        val hitTimePoints = ArrayDeque<Long>()
    
        val optHelp = Option("h", "help", false, "Display this help message.")
        optHelp.isRequired = false
        options.addOption(optHelp)
    
        val optPrecision = Option("p", "precision", true, "Set the decimal precision of the tempo display. Default is 0 digits, max is 5 digits.")
        optPrecision.isRequired = false
        options.addOption(optPrecision)
    
        val optResetTime = Option("r", "reset-time", true, "Set the time in second to reset the computation. Default is 5 seconds.")
        optResetTime.isRequired = false
        options.addOption(optResetTime)
    
        val optSampleSize = Option("s", "sample-size", true, "Set the number of samples needed to compute the tempo. Default is 5 samples.")
        optSampleSize.isRequired = false
        options.addOption(optSampleSize)
    
        val optVersion = Option("v", "version", false, "Display the version.")
        optVersion.isRequired = false
        options.addOption(optVersion)
    
        val parser = DefaultParser()
        val formatter = HelpFormatter()
        var cmd: CommandLine? = 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 > 5) {
                    precision = 5
                }
            }
            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 (e: ParseException) {
            println(e.javaClass.toString() + ": " + e.message)
            formatter.printHelp("TempoTap", options)
            System.exit(1)
        } catch (e: NumberFormatException) {
            println(e.javaClass.toString() + ": " + e.message)
            formatter.printHelp("TempoTap", options)
            System.exit(1)
        }
    
        if (cmd!!.hasOption('h') || cmd.hasOption('v')) {
            if (cmd.hasOption('h')) {
                formatter.printHelp("TempoTap", options)
            }
            if (cmd.hasOption('v')) {
                println("Version: 1.0")
            }
            System.exit(0)
        }
    
        val df = DecimalFormat()
        df.maximumFractionDigits = precision
        df.minimumFractionDigits = precision
    
        println("Hit enter key for each beat (q to quit).\n")
    
        val keyboard = Scanner(System.`in`)
        keyboard.useDelimiter("")
    
        var shouldContinue = true
        while (shouldContinue) {
    
            var c: Char
            do {
                c = keyboard.next()[0]
                if (c == 'q') {
                    shouldContinue = false
                    println("Bye Bye!\n")
                    break
                }
            } while (c.toInt() != 10)
    
            if (shouldContinue) {
                val currentTime = System.currentTimeMillis()
    
                // Reset if the hit diff is too big.
                if (!hitTimePoints.isEmpty() && currentTime - hitTimePoints.last > resetTime * 1000) {
                    // Clear the history.
                    hitTimePoints.clear()
                }
    
                hitTimePoints.add(currentTime)
                if (hitTimePoints.size > 1) {
                    val bpm = computeBPM(hitTimePoints.last, hitTimePoints.first, hitTimePoints.size - 1)
    
                    val bpmRepresentation = df.format(bpm)
                    println("Tempo: $bpmRepresentation bpm")
                } else {
                    println("[Hit enter key one more time to start bpm computation...]")
                }
    
                while (hitTimePoints.size > sampleSize) {
                    hitTimePoints.pop()
                }
            }
        }
    }

    Bref, c'est surtout utile pour :

    • Avoir une base de code cohérente.
    • Vérifier comment un concept Java est censé se transcrire en Kotlin.

    La connaissance libre : https://zestedesavoir.com

Suivre le flux des commentaires

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