Skip to content

Commit

Permalink
CLJS-454: Allow nanoseconds in fractions part of instant literals
Browse files Browse the repository at this point in the history
This patch mimics the clojure reader which truncates the fraction part to 9 digits (i.e. nanoseconds). This patch truncates to
milliseconds (3 digits). Note that this patch also fixes three other bugs:

* CLJS-564
* The clojure reader reads "#inst \"2003-12-10T00:00:00.4\"" as #inst "2003-12-10T00:00:00.400" but the clojurescript reader (prior to
this patch) reads the same string as #inst "2003-12-10T00:00:00.004"
* #inst tests generated bad test data
  • Loading branch information
jonase authored and swannodette committed Aug 3, 2013
1 parent 9d26766 commit a749ab6
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 30 deletions.
67 changes: 39 additions & 28 deletions src/cljs/cljs/reader.cljs
Expand Up @@ -440,12 +440,12 @@ nil if the end of stream has been reached")

;; read instances

(defn ^:private zero-fill-right [s width]
(defn ^:private zero-fill-right-and-truncate [s width]
(cond (= width (count s)) s
(< width (count s)) (.substring s 0 width)
(< width (count s)) (subs s 0 width)
:else (loop [b (gstring/StringBuffer. s)]
(if (< (.getLength b) width)
(recur (.append b \0))
(recur (.append b "0"))
(.toString b)))))

(defn ^:private divisible?
Expand All @@ -468,31 +468,42 @@ nil if the end of stream has been reached")
(fn [month leap-year?]
(get (if leap-year? dim-leap dim-norm) month))))

(def ^:private parse-and-validate-timestamp
(let [timestamp #"(\d\d\d\d)(?:-(\d\d)(?:-(\d\d)(?:[T](\d\d)(?::(\d\d)(?::(\d\d)(?:[.](\d+))?)?)?)?)?)?(?:[Z]|([-+])(\d\d):(\d\d))?"
check (fn [low n high msg]
(assert (<= low n high) (str msg " Failed: " low "<=" n "<=" high))
n)]
(fn [ts]
(when-let [[[_ years months days hours minutes seconds milliseconds] [_ _ _] :as V]
(->> ts
(re-matches timestamp)
(split-at 8)
(map vec))]
(let [[[_ y mo d h m s ms] [offset-sign offset-hours offset-minutes]]
(->> V
(map #(update-in %2 [0] %)
[(constantly nil) #(if (= % "-") "-1" "1")])
(map (fn [v] (map #(js/parseInt % 10) v))))
offset (* offset-sign (+ (* offset-hours 60) offset-minutes))]
[(if-not years 1970 y)
(if-not months 1 (check 1 mo 12 "timestamp month field must be in range 1..12"))
(if-not days 1 (check 1 d (days-in-month mo (leap-year? y)) "timestamp day field must be in range 1..last day in month"))
(if-not hours 0 (check 0 h 23 "timestamp hour field must be in range 0..23"))
(if-not minutes 0 (check 0 m 59 "timestamp minute field must be in range 0..59"))
(if-not seconds 0 (check 0 s (if (= m 59) 60 59) "timestamp second field must be in range 0..60"))
(if-not milliseconds 0 (check 0 ms 999 "timestamp millisecond field must be in range 0..999"))
offset])))))
(def ^:private timestamp-regex #"(\d\d\d\d)(?:-(\d\d)(?:-(\d\d)(?:[T](\d\d)(?::(\d\d)(?::(\d\d)(?:[.](\d+))?)?)?)?)?)?(?:[Z]|([-+])(\d\d):(\d\d))?")

(defn ^:private parse-int [s]
(let [n (js/parseInt s)]
(if-not (js/isNaN n)
n)))

(defn ^:private check [low n high msg]
(when-not (<= low n high)
(reader-error nil (str msg " Failed: " low "<=" n "<=" high)))
n)

(defn parse-and-validate-timestamp [s]
(let [[_ years months days hours minutes seconds fraction offset-sign offset-hours offset-minutes :as v]
(re-matches timestamp-regex s)]
(if-not v
(reader-error nil (str "Unrecognized date/time syntax: " s))
(let [years (parse-int years)
months (or (parse-int months) 1)
days (or (parse-int days) 1)
hours (or (parse-int hours) 0)
minutes (or (parse-int minutes) 0)
seconds (or (parse-int seconds) 0)
fraction (or (parse-int (zero-fill-right-and-truncate fraction 3)) 0)
offset-sign (if (= offset-sign "-") -1 1)
offset-hours (or (parse-int offset-hours) 0)
offset-minutes (or (parse-int offset-minutes) 0)
offset (* offset-sign (+ (* offset-hours 60) offset-minutes))]
[years
(check 1 months 12 "timestamp month field must be in range 1..12")
(check 1 days (days-in-month months (leap-year? years)) "timestamp day field must be in range 1..last day in month")
(check 0 hours 23 "timestamp hour field must be in range 0..23")
(check 0 minutes 59 "timestamp minute field must be in range 0..59")
(check 0 seconds (if (= minutes 59) 60 59) "timestamp second field must be in range 0..60")
(check 0 fraction 999 "timestamp millisecond field must be in range 0..999")
offset]))))

(defn parse-timestamp
[ts]
Expand Down
17 changes: 15 additions & 2 deletions test/cljs/cljs/reader_test.cljs
Expand Up @@ -43,7 +43,11 @@

;; inst
(let [est-inst (reader/read-string "#inst \"2010-11-12T13:14:15.666-05:00\"")
utc-inst (reader/read-string "#inst \"2010-11-12T18:14:15.666-00:00\"")]
utc-inst (reader/read-string "#inst \"2010-11-12T18:14:15.666-00:00\"")
pad (fn [n]
(if (< n 10)
(str "0" n)
n))]

(assert (= (.valueOf (js/Date. "2010-11-12T13:14:15.666-05:00"))
(.valueOf est-inst)))
Expand All @@ -55,10 +59,19 @@
(.valueOf utc-inst)))

(doseq [month (range 1 13) day (range 1 29) hour (range 1 23)]
(let [s (str "#inst \"2010-" month "-" day "T" hour ":14:15.666-06:00\"")]
(let [s (str "#inst \"2010-" (pad month) "-" (pad day) "T" (pad hour) ":14:15.666-06:00\"")]
(assert (= (-> s reader/read-string .valueOf)
(-> s reader/read-string pr-str reader/read-string .valueOf))))))

(let [insts [(reader/read-string "#inst \"2012\"")
(reader/read-string "#inst \"2012-01\"")
(reader/read-string "#inst \"2012-01-01\"")
(reader/read-string "#inst \"2012-01-01T00\"")
(reader/read-string "#inst \"2012-01-01T00:00:00.000\"")
(reader/read-string "#inst \"2012-01-01T00:00:00.000123456\"")
(reader/read-string "#inst \"2012-01-01T00:00:00.000123456789+00:00\"")]]
(assert (apply = (map #(.valueOf %) insts))))

;; uuid literals
(let [u (reader/read-string "#uuid \"550e8400-e29b-41d4-a716-446655440000\"")]
(assert (= u (reader/read-string "#uuid \"550e8400-e29b-41d4-a716-446655440000\"")))
Expand Down

0 comments on commit a749ab6

Please sign in to comment.