I want to check whether the string variable s is a number (integer or float) formatted as a string. I thought it could be done using string-to-number
like this
(defun string-number-base-p (s)
(when (or (equal "0" s)
(not (equal 0 (string-to-number s))))
t))
but (as NickD pointed out) that also returns t
for string-numbers with trailing characters like "123zxyz"
.
Note that the built-in function string-to-number
returns 0 instead of nil when it fails to convert a string, but also returns 0 for the string "0" which contains a valid number. Also, string-to-number
considers ,
a non-number character and not a decimal or thousands separator, and will hence ignore everything from ,
when parsing. So (string-to-number "2,3")
returns 2
.
Examples of valid number-strings include "0", "1", "-1", "1.3", and "4.3e10".
Comparison of answers
Based on the answers given, I've run some output and speed comparisons: see the table and code below. Notable is that the pure regex version is not slower than the other versions while being much more flexible: To parse the strings in the header of the table 1 million times, the regex-based method took 13.6s, the read-based method 13.6s, and the imperfect base method 11.2s.
string-number | "0" | "1" | "-0" | "-1" | "+0" | "+1" | "01" | "-01" | "+01" | "2.3" | "-2.3" | "1.00" | "0.00" | "-0.00" | "00.00" | "-00.00" | "02.3" | ".0" | ".1" | "-.1" | "0." | "1." | "10." | "2,3" | ",0" | "0e0" | "2e5" | "2.3e5" | "-2.3e5" | "2.3e-5" | "2.3e03" | "2.3e0" | "0.01e4" | ".1e5" | "-.1e5" | "-.0e0" | "-0.0e10" | "2,3e5" | " " | "." | "-" | "-." | "b1" | "1b" | "2-3" | "3.4.5" | "4.,6" | "1,." | "e" | "-e" | "-.e" | ".e4" | "1e2-2" | "e2.3" | "e2.3.4" | "-e10" | "1e.-" | "10e." | "1\n" | "1\t" | "1 1" | "\n" | "\t" |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
-regex-p | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | t | nil | nil | nil | nil |
-read-p | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | nil | t | t | t | t | t | t | t | t | t | t | t | t | t | nil | nil | nil | nil | nil | nil | nil | nil | t | t | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | t | t | t | nil | nil |
-read2-p | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | nil | t | t | t | t | t | t | t | t | t | t | t | t | t | nil | nil | nil | nil | nil | nil | nil | nil | t | t | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | t | t | t | nil | nil |
-read3-p | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | nil | nil | t | t | t | t | t | t | t | t | t | t | t | t | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | nil | t | t | nil | nil | nil |
-base-p | t | t | nil | t | nil | t | t | t | t | t | t | t | t | t | t | t | t | t | t | t | nil | t | t | t | nil | t | t | t | t | t | t | t | t | t | t | t | t | t | nil | nil | nil | nil | nil | t | t | t | t | t | nil | nil | nil | nil | t | nil | nil | nil | t | t | t | t | t | nil | nil |
(setq l (list "0" "1" "-0" "-1" "+0" "+1" "01" "-01" "+01" "2.3" "-2.3" "1.00"
"0.00" "-0.00" "00.00" "-00.00" "02.3" ".0" ".1" "-.1" "0." "1."
"10." "2,3" ",0" "0e0" "2e5" "2.3e5" "-2.3e5" "2.3e-5" "2.3e03"
"2.3e0" "0.01e4" ".1e5" "-.1e5" "-.0e0" "-0.0e10" "2,3e5" " " "."
"-" "-." "b1" "1b" "2-3" "3.4.5" "4.,6" "1,." "e" "-e" "-.e"
".e4" "1e2-2" "e2.3" "e2.3.4" "-e10" "1e.-" "10e." "1\n" "1\t"
"1 1" "\n" "\t"))
(defconst string-number-regex
(concat "^[+-]?\\(?:[0-9]+\\(?:[.,][0-9]*\\)?\\(?:e[+-]?[0-9]+\\)?"
"\\|[.,][0-9]+\\(?:e[+-]?[0-9]+\\)?\\)$")
"Matches integers and floats with exponent.
This allows for leading and trailing decimal point, leading zeros in base,
leading zeros in exponent, + signs, and , as alternative decimal separator.")
(defun string-number-regex-p (s)
(when (string-match-p
string-number-regex s)
t))
(defun string-number-read-p (s)
(condition-case _invalid-read-syntax
(numberp (read s))
(error nil)))
(defun string-number-read2-p (s)
(when (and (string-match-p "[^ .,\t\n\r]" s)
(numberp (read s)))
t))
(cl-defun string-number-read3-p (str &key (test #'numberp))
"Test whether STR is a string that contains one sexp of a certain type.
The type is identified by the TEST.
The default TEST is `numberp'."
(and (stringp str)
(string-match "\\S-" str) ;; not a whitespace string
(condition-case nil
(with-temp-buffer
(insert str)
(goto-char (point-min))
(and (funcall test (read (current-buffer)))
(looking-at "\\s-*\\'"))) ;; only whitespace up to eob
(error nil))))
(defun string-number-base-p (s)
(when (or (equal "0" s)
(not (equal 0 (string-to-number s))))
t))
(concat
"| string-number | "
(mapconcat (lambda (s) (concat "\"" s "\"")) l " | ")
" |\n|---|"
(mapconcat (lambda (s) "---") l "|")
"|\n| -regex-p | "
(mapconcat (lambda (s) (symbol-name (string-number-regex-p s))) l " | ")
" |\n| -read-p | "
(mapconcat (lambda (s) (symbol-name (string-number-read-p s))) l " | ")
" |\n| -read2-p | "
(mapconcat (lambda (s) (symbol-name (string-number-read2-p s))) l " | ")
" |\n| -base-p | "
(mapconcat (lambda (s) (symbol-name (string-number-base-p s))) l " | ")
" |")
;; => table
(let ((reps 1000000))
(list
(cons 'regex (benchmark-run-compiled reps (mapc #'string-number-regex-p l)))
(cons 'read (benchmark-run-compiled reps (mapc #'string-number-read-p l)))
(cons 'read2 (benchmark-run-compiled reps (mapc #'string-number-read2-p l)))
(cons 'base (benchmark-run-compiled reps (mapc #'string-number-base-p l)))))
;; =>
;; ((regex 13.573386 0 0.0)
;; (read 13.607431 81 4.161939)
;; (read2 17.170425 14 0.739117)
;; (base 11.242897 12 0.591818))
;; note: read3 is around 40 times slower,
;; taking 58s for 100000 repetitions where the others take 1.4s
(let ((reps 1000000))
(list
(cons 'regex (benchmark-run reps (mapc #'string-number-regex-p l)))
(cons 'read (benchmark-run reps (mapc #'string-number-read-p l)))
(cons 'read2 (benchmark-run reps (mapc #'string-number-read2-p l)))
(cons 'base (benchmark-run reps (mapc #'string-number-base-p l)))))
;; =>
;; ((regex 13.58847 0 0.0)
;; (read 15.86764 81 6.213117)
;; (read2 17.26436 14 0.870470)
;; (base 11.18225 12 0.565733))