141
votes

I have various strings, some like "45", some like "45px". How how I convert both of these to the number 45?

13
I am glad someone is not afraid to ask some basic questions.octopusgrabbus
+1 - part of the challenge is that the Clojure docs sometimes don't address these "basic" questions that we take for granted in other languages. (I had the same question 3 years later and found this).Glenn
@octopusgrabbus - I would be interested to know "why" people are afraid of asking basic questions?yazz.com
@Zubair it is supposed basic things are explained somewhere already so you most probably overlooked something and your question will be downvoted for "no research effort".Al.G.
For those coming here from Google looking to convert "9" into 9, this is the best thing that worked for me: (Integer. "9").weltschmerz

13 Answers

86
votes

This will work on 10px or px10

(defn parse-int [s]
   (Integer. (re-find  #"\d+" s )))

it will parse the first continuous digit only so

user=> (parse-int "10not123")
10
user=> (parse-int "abc10def11")
10
85
votes

New answer

I like snrobot's answer better. Using the Java method is simpler and more robust than using read-string for this simple use case. I did make a couple of small changes. Since the author didn't rule out negative numbers, I adjusted it to allow negative numbers. I also made it so it requires the number to start at the beginning of the string.

(defn parse-int [s]
  (Integer/parseInt (re-find #"\A-?\d+" s)))

Additionally I found that Integer/parseInt parses as decimal when no radix is given, even if there are leading zeroes.

Old answer

First, to parse just an integer (since this is a hit on google and it's good background information):

You could use the reader:

(read-string "9") ; => 9

You could check that it's a number after it's read:

(defn str->int [str] (if (number? (read-string str))))

I'm not sure if user input can be trusted by the clojure reader so you could check before it's read as well:

(defn str->int [str] (if (re-matches (re-pattern "\\d+") str) (read-string str)))

I think I prefer the last solution.

And now, to your specific question. To parse something that starts with an integer, like 29px:

(read-string (second (re-matches (re-pattern "(\\d+).*") "29px"))) ; => 29
31
votes
(defn parse-int [s]
  (Integer. (re-find #"[0-9]*" s)))

user> (parse-int "10px")
10
user> (parse-int "10")
10
15
votes

This works in repl for me, much more straight forward.

(read-string "123")

=> 123

10
votes

AFAIK there's no standard solution for your problem. I think something like the following, which uses clojure.contrib.str-utils2/replace, should help:

(defn str2int [txt]
  (Integer/parseInt (replace txt #"[a-zA-Z]" "")))
8
votes

This isn't perfect, but here's something with filter, Character/isDigit and Integer/parseInt. It won't work for floating point numbers and it fails if there is no digit in the input, so you should probably clean it up. I hope there's a nicer way of doing this that doesn't involve so much Java.

user=> (defn strToInt [x] (Integer/parseInt (apply str (filter #(Character/isDigit %) x))))
#'user/strToInt
user=> (strToInt "45px")
45
user=> (strToInt "45")
45
user=> (strToInt "a")
java.lang.NumberFormatException: For input string: "" (NO_SOURCE_FILE:0)
4
votes

I would probably add a few things to the requirements:

  • Has to start with a digit
  • Has to tolerate empty inputs
  • Tolerates being passed any object (toString is standard)

Maybe something like:

(defn parse-int [v] 
   (try 
     (Integer/parseInt (re-find #"^\d+" (.toString v))) 
     (catch NumberFormatException e 0)))

(parse-int "lkjhasd")
; => 0
(parse-int (java.awt.Color. 4 5 6))
; => 0
(parse-int "a5v")
; => 0
(parse-int "50px")
; => 50

and then perhaps bonus points for making this a multi-method that allows for a user-supplied default other than 0.

4
votes

Expanding on snrobot's answer:

(defn string->integer [s] 
  (when-let [d (re-find #"-?\d+" s)] (Integer. d)))

This versions returns nil if there are no digits in the input, rather than raising an exception.

My question is whether it's acceptable to abbreviate the name to "str->int", or if things like this should always be fully specified.

4
votes

For anyone else looking to parse a more normal String literal into a number, that is, a string which doesn't have other non numeric characters. These are the two best approaches:

Using 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")

This lets you precisely control the type you want to parse the number in, when that matters to your use case.

Using the Clojure EDN reader:

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

Unlike using read-string from clojure.core which isn't safe to use on untrusted input, edn/read-string is safe to run on untrusted input such as user input.

This is often more convenient then the Java interop if you don't need to have specific control of the types. It can parse any number literal that Clojure can parse such as:

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

Full list here: https://www.rubberducking.com/2019/05/clojure-for-non-clojure-programmers.html#numbers

3
votes

Also using (re-seq) function can extend the return value to a string containing all the numbers existing in the input string in order:

(defn convert-to-int [s] (->> (re-seq #"\d" s) (apply str) (Integer.)))

(convert-to-int "10not123") => 10123

(type *1) => java.lang.Integer

3
votes

The question asks about parsing a string into a number.

(number? 0.5)
;;=> true

So from the above decimals ought to be parsed as well.

Perhaps not exactly answering the question now, but for general use I think you would want to be strict about whether it is a number or not (so "px" not allowed) and let the caller handle non-numbers by returning nil:

(defn str->number [x]
  (when-let [num (re-matches #"-?\d+\.?\d*" x)]
    (try
      (Float/parseFloat num)
      (catch Exception _
        nil))))

And if Floats are problematic for your domain instead of Float/parseFloat put bigdec or something else.

2
votes

For simple cases you can just use a regex to pull out the first string of digits as mentioned above.

If you have a more complicated situation you may wish to use the InstaParse library:

(ns tst.parse.demo
  (:use tupelo.test)
  (:require
    [clojure.string :as str]
    [instaparse.core :as insta]
    [tupelo.core :as t] ))
(t/refer-tupelo)

(dotest
  (let [abnf-src            "
size-val      = int / int-px
int           = digits          ; ex '123'
int-px        = digits <'px'>   ; ex '123px'
<digits>      = 1*digit         ; 1 or more digits
<digit>       = %x30-39         ; 0-9
"
    tx-map        {:int      (fn fn-int [& args]
                               [:int (Integer/parseInt (str/join args))])
                   :int-px   (fn fn-int-px [& args]
                               [:int-px (Integer/parseInt (str/join args))])
                   :size-val identity
                  }

    parser              (insta/parser abnf-src :input-format :abnf)
    instaparse-failure? (fn [arg] (= (class arg) instaparse.gll.Failure))
    parse-and-transform (fn [text]
                          (let [result (insta/transform tx-map
                                         (parser text))]
                            (if (instaparse-failure? result)
                              (throw (IllegalArgumentException. (str result)))
                              result)))  ]
  (is= [:int 123]     (parse-and-transform "123"))
  (is= [:int-px 123]  (parse-and-transform "123px"))
  (throws?            (parse-and-transform "123xyz"))))
2
votes

How about this one to avoid an exception on certain strings ?

(defn string-to-number [in]
  (let [s (strip-whitespace in)      ;; trim
        f (re-find #"\d+" s)]        ;; search digit else nil
    (if f (Integer/parseInt f) 0)))  ;; if not-nil do cast

(string-to-number "-")
(string-to-number "10")
(string-to-number "px10")
(string-to-number "1200 xr")