Parse decimal number using Cuerdas in a ClojureScript program

Viewed 253

I am using Cuerdas library in a ClojureScript program to parse floating point numbers. Sometimes the input from a user is missing the integral part, like .1 for 0.1. I need to handle this. But I ran into strange results when comparing the parsed numbers:

(= 0.1 (parse-number "0.1"))
;; => true

(= .1 (parse-number "0.1"))
;; => true

(= 0.1 (parse-number ".1"))
;; => false

(= .1 (parse-number ".1"))
;; => false

The last two results above are surprising to me. When comparing literal number I get the expected result:

(= 0 .0)
;; => true

The strange behavior is also visible when I just parse the decimal number without integer part, like here:

(parse-number ".1")
;; => .1

.1
;; => 0.1

I found out that I can use js/parseFloat to get what I want, but I'm curious what is the reason for this behavior when using Cuerdas? What is this .1 value and how is it different than 0.1?

2 Answers

parse-number source looked like this:

(defn parse-number
  "General purpose function for parse number like
  string to number. It works with both integers
  and floats."
  [s]
  (if (nil? s)
    #?(:cljs js/NaN :clj Double/NaN)
    (if (numeric? s)
      (edn/read-string s)
      #?(:cljs js/NaN :clj Double/NaN))))

Both ".1" and "0.1" are considered to be numeric? (was implemented in cuerdas by a regex check):

cljs.user=> (def re #"^[+-]?([0-9]*\.?[0-9]+|[0-9]+\.?[0-9]*)([eE][+-]?[0-9]+)?$")
#'cljs.user/re
cljs.user=> (boolean (re-matches re "0.1"))
true
cljs.user=> (boolean (re-matches re ".1"))
true

so they will be read by cljs.reader/read-string.

When reading the string "0.1" the return type is a js/Number, but ".1" is of type cljs.core/Symbol:

cljs.user=> (cljs.reader/read-string ".1")
.1
cljs.user=> (cljs.reader/read-string "0.1")
0.1
cljs.user=> (type (cljs.reader/read-string "0.1"))
#object[Number]
cljs.user=> (type (cljs.reader/read-string ".1"))
cljs.core/Symbol

So although it looks like it correctly parsed the .1 it actually turned it into a symbol. The symbol .1 is not equal to the number .1.

Note that parse-number is no longer available in newer versions of cuerdas because it "is a string manipulation library not a numbers parsing library".

I have some numeric parsing functions that work for both CLJ and CLJS. From the unit tests, we have

The headers:

(ns tst.tupelo.parse
  ;---------------------------------------------------------------------------------------------------
  ;   https://code.thheller.com/blog/shadow-cljs/2019/10/12/clojurescript-macros.html
  ;   http://blog.fikesfarm.com/posts/2015-12-18-clojurescript-macro-tower-and-loop.html
  #?(:cljs (:require-macros
             [tupelo.misc]
             [tupelo.testy]))
  (:require
    [clojure.test] ; sometimes this is required - not sure why
    [tupelo.parse :as tpar]
    [tupelo.misc :as misc]
    [tupelo.core :as t :refer [spy spyx spyxx spyx-pretty]]
    [tupelo.testy :refer [deftest testing is dotest isnt is= isnt= is-set= is-nonblank=
                          throws? throws-not? define-fixture]])
  #?(:clj (:import [java.lang Math]))
  )

Parsing integers:

#?(:cljs
   (do
     (dotest
       (is= 0 (tpar/parse-int "0"))
       (is= 15 (tpar/parse-int "15"))
       (is= -5 (tpar/parse-int "-5"))
       (is= 99999 (tpar/parse-int "99999"))

       (throws? (tpar/parse-int ""))
       (throws? (tpar/parse-int "05"))
       (throws? (tpar/parse-int "123xxx"))
       (throws? (tpar/parse-int "12x3"))
       (throws? (tpar/parse-int "12.3"))
       (throws? (tpar/parse-int "xxx123")))

Parsing floats

     (dotest
       (is= 0 (tpar/parse-float "0"))
       (is= 0 (tpar/parse-float "0.0"))
       (is= 12.345 (tpar/parse-float "12.345"))
       (is= -5.1 (tpar/parse-float "-5.1"))
       (is= 42 (tpar/parse-float "42.0"))
       (is= 42 (tpar/parse-float "42"))
       (is= 123.45 (tpar/parse-float "1.2345e2"))

       (throws? (tpar/parse-float ""))
       (throws? (tpar/parse-float "xxx1.23"))
       (throws? (tpar/parse-float "1.23xxx"))
       (throws? (tpar/parse-float "1.2xx34")))
     ))
Related