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