web-dev-qa-db-fra.com

Quelle est la façon la plus simple d'analyser les nombres en clojure?

J'utilise Java pour analyser les nombres, par exemple.

(. Integer parseInt  numberString)

Existe-t-il une méthode plus clojuriffique qui traiterait à la fois les nombres entiers et les flottants, et retournerait les nombres de clojure? Je ne suis pas particulièrement préoccupé par les performances ici, je veux juste traiter un tas de nombres délimités par des espaces blancs dans un fichier et faire quelque chose avec eux, de la manière la plus simple possible.

Un fichier peut donc avoir des lignes comme:

5  10  0.0002
4  12  0.003

Et j'aimerais pouvoir transformer les lignes en vecteurs de nombres.

53
Rob Lachlan

Vous pouvez utiliser le lecteur edn pour analyser les nombres. Cela a l'avantage de vous donner des flotteurs ou des bignums en cas de besoin aussi.

user> (require '[clojure.edn :as edn])
nil
user> (edn/read-string "0.002")
0.0020

Si vous voulez un énorme vecteur de nombres, vous pouvez tricher et faire ceci:

user> (let [input "5  10  0.002\n4  12  0.003"]
        (read-string (str "[" input "]")))
[5 10 0.0020 4 12 0.0030]

Un peu hacky cependant. Ou il y a re-seq:

user> (let [input "5  10  0.002\n4  12  0.003"]
        (map read-string (re-seq #"[\d.]+" input)))
(5 10 0.0020 4 12 0.0030)

Ou un vecteur par ligne:

user> (let [input "5  10  0.002\n4  12  0.003"]
        (for [line (line-seq (Java.io.BufferedReader.
                              (Java.io.StringReader. input)))]
             (vec (map read-string (re-seq #"[\d.]+" line)))))
([5 10 0.0020] [4 12 0.0030])

Je suis sûr qu'il existe d'autres moyens.

61
Brian Carper

Je ne sais pas si c'est "le moyen le plus simple", mais je pensais que c'était assez amusant, alors ... Avec un hack de réflexion, vous pouvez accéder uniquement à la partie de lecture des numéros du lecteur de Clojure:

(let [m (.getDeclaredMethod clojure.lang.LispReader
                            "matchNumber"
                            (into-array [String]))]
  (.setAccessible m true)
  (defn parse-number [s]
    (.invoke m clojure.lang.LispReader (into-array [s]))))

Ensuite, utilisez comme ceci:

user> (parse-number "123")
123
user> (parse-number "123.5")
123.5
user> (parse-number "123/2")
123/2
user> (class (parse-number "123"))
Java.lang.Integer
user> (class (parse-number "123.5"))
Java.lang.Double
user> (class (parse-number "123/2"))
clojure.lang.Ratio
user> (class (parse-number "123123451451245"))
Java.lang.Long
user> (class (parse-number "123123451451245123514236146"))
Java.math.BigInteger
user> (parse-number "0x12312345145124")
5120577133367588
user> (parse-number "12312345142as36146") ; note the "as" in the middle
nil

Remarquez comment cela ne lance pas l'habituel NumberFormatException si quelque chose ne va pas; vous pouvez ajouter un chèque pour nil et le jeter vous-même si vous le souhaitez.

En ce qui concerne les performances, nous allons avoir un microbenchmark non scientifique (les deux fonctions ont été "réchauffées"; les exécutions initiales ont été plus lentes comme d'habitude):

user> (time (dotimes [_ 10000] (parse-number "1234123512435")))
"Elapsed time: 564.58196 msecs"
nil
user> (time (dotimes [_ 10000] (read-string "1234123512435")))
"Elapsed time: 561.425967 msecs"
nil

L'avertissement évident: clojure.lang.LispReader.matchNumber est une méthode statique privée de clojure.lang.LispReader et peut être modifié ou supprimé à tout moment.

25
Michał Marczyk

Si vous voulez être plus sûr, vous pouvez utiliser Float/parseFloat

user=> (map #(Float/parseFloat (% 0)) (re-seq #"\d+(\.\d+)?" "1 2.2 3.5"))
(1.0 2.2 3.5)
user=> 
24
lazy1

À mon avis, le meilleur moyen/le plus sûr qui fonctionne lorsque vous le souhaitez pour n'importe quel numéro et échoue lorsqu'il n'est pas un numéro est le suivant:

(defn parse-number
  "Reads a number from a string. Returns nil if not a number."
  [s]
  (if (re-find #"^-?\d+\.?\d*$" s)
    (read-string s)))

par exemple.

(parse-number "43") ;=> 43
(parse-number "72.02") ;=> 72.02
(parse-number "009.0008") ;=> 9.008
(parse-number "-92837482734982347.00789") ;=> -9.2837482734982352E16
(parse-number "89blah") ;=> nil
(parse-number "z29") ;=> nil
(parse-number "(exploit-me)") ;=> nil

Fonctionne pour les entiers, les flottants/doubles, les bignums, etc. Si vous souhaitez ajouter un support pour la lecture d'autres notations, augmentez simplement l'expression régulière.

19
solussd

L'approche suggérée par Brian Carper (en utilisant une chaîne de lecture) fonctionne bien, mais seulement jusqu'à ce que vous essayiez d'analyser des nombres remplis de zéro comme "010". Observer:

user=> (read-string "010")
8
user=> (read-string "090")
Java.lang.RuntimeException: Java.lang.NumberFormatException: Invalid number: 090 (NO_SOURCE_FILE:0)

C'est parce que clojure essaie d'analyser "090" comme octal, et 090 n'est pas un octal valide!

15
Stathis Sideris

La réponse de Brian Carper est presque correcte. Au lieu d'utiliser la chaîne de lecture directement à partir du noyau de clojure. Utilisez clojure.edn/read-string. Il est sûr et analysera tout ce que vous lui lancerez.

(ns edn-example.core
    (require [clojure.edn :as edn]))

(edn/read-string "2.7"); float 2.7
(edn/read-string "2"); int 2

simple, facile et sûr d'exécution;)

14
carocad

Utilisez bigint et bigdec

(bigint "1")
(bigint "010") ; returns 10N as expected
(bigint "111111111111111111111111111111111111111111111111111")
(bigdec "11111.000000000000000000000000000000000000000000001")

bigintde Clojure utilisera les primitives si possible , tout en évitant les regexps, le problème des littéraux octaux ou la taille limitée des autres types numériques, provoquant (Integer. "10000000000") échouer.

(Cette dernière chose m'est arrivée et c'était assez déroutant: je l'ai enveloppé dans un parse-int fonction, et ensuite supposé que parse-int signifiait "analyser un entier naturel" et non "analyser un entier 32 bits")

7
berdario

Je trouve que la réponse de solussd fonctionne très bien pour mon code. Sur cette base, voici une amélioration prenant en charge la notation scientifique. En outre, (.trim s) est ajouté afin que l'espace supplémentaire puisse être toléré.

(defn parse-number
  "Reads a number from a string. Returns nil if not a number."
  [s]
  (if (re-find #"^-?\d+\.?\d*([Ee]\+\d+|[Ee]-\d+|[Ee]\d+)?$" (.trim s))
    (read-string s)))

par exemple.

(parse-number "  4.841192E-002  ")    ;=> 0.04841192
(parse-number "  4.841192e2 ")    ;=> 484.1192
(parse-number "  4.841192E+003 ")    ;=> 4841.192
(parse-number "  4.841192e.2 ")  ;=> nil
(parse-number "  4.841192E ")  ;=> nil
7
Kevin Zhu

Ce sont les deux meilleures approches et correctes:

Utilisation de Java interop:

(Long/parseLong "333")
(Float/parseFloat "333.33")
(Double/parseDouble "333.3333333333332")
(Integer/parseInt "-333")
(Integer/parseUnsignedInt "333")
(BigInteger. "3333333333333333333333333332")
(BigDecimal. "3.3333333333333333333333333332")
(Short/parseShort "400")
(Byte/parseByte "120")

Cela vous permet de contrôler avec précision le type dans lequel vous souhaitez analyser le nombre, lorsque cela est important pour votre cas d'utilisation.

Utilisation du lecteur Clojure EDN:

(require '[clojure.edn :as edn])
(edn/read-string "333")

Contrairement à l'utilisation de read-string de clojure.core qui n'est pas sûr à utiliser sur des entrées non fiables, edn/read-string est sûr à exécuter sur des entrées non fiables telles que les entrées utilisateur.

C'est souvent plus pratique que l'interop Java si vous n'avez pas besoin d'avoir un contrôle spécifique des types. Il peut analyser n'importe quel littéral numérique que Clojure peut analyser, comme:

;; Ratios
(edn/read-string "22/7")
;; Hexadecimal
(edn/read-string "0xff")

Liste complète ici: https://www.rubberducking.com/2019/05/05/clojure-for-non-clojure-programmers.html#numbers

0
Didier A.