From d8799dca5c5e5ffaf9298e70da14b2ab5a9dfa0e Mon Sep 17 00:00:00 2001 From: Stuart Sierra Date: Sun, 13 Nov 2011 19:02:28 -0500 Subject: [PATCH 1/2] Add custom data readers; refs CLJ-871 --- src/clj/clojure/core.clj | 19 ++++++++++++++ src/jvm/clojure/lang/LispReader.java | 45 ++++++++++++++++++++++++++++++++++ src/jvm/clojure/lang/RT.java | 2 + test/clojure/test_clojure/reader.clj | 20 +++++++++++++++ 4 files changed, 86 insertions(+), 0 deletions(-) diff --git a/src/clj/clojure/core.clj b/src/clj/clojure/core.clj index b0a25fc..c75932b 100644 --- a/src/clj/clojure/core.clj +++ b/src/clj/clojure/core.clj @@ -5739,6 +5739,25 @@ Defaults to true" {:added "1.0"}) +(add-doc-and-meta *data-readers* + "A map from keywords to data reader functions. A data reader + function takes a single argument and returns a value to use in its + place. + + When the reader encounters #:keyword it will read the next form and + call the data reader function mapped to that keyword. Example: + + (binding [*data-readers* + (assoc *data-readers* :user/reversed (fn [x] (reverse x)))] + (read-string \"#:user/reversed (1 2 3)\")) + ;=> (3 2 1) + + If no function is mapped to the given keyword, the following form + will be returned with the added metadata key :data mapped to the + keyword. If the form does not support IMeta (examples: numbers, + strings) it will be returned as-is." + {:added "1.4"}) + (defn future? "Returns true if x is a future" {:added "1.1" diff --git a/src/jvm/clojure/lang/LispReader.java b/src/jvm/clojure/lang/LispReader.java index dbb59a6..ab49997 100644 --- a/src/jvm/clojure/lang/LispReader.java +++ b/src/jvm/clojure/lang/LispReader.java @@ -109,6 +109,7 @@ static IFn ctorReader = new CtorReader(); dispatchMacros['!'] = new CommentReader(); dispatchMacros['<'] = new UnreadableReader(); dispatchMacros['_'] = new DiscardReader(); + dispatchMacros[':'] = new DataReader(); } static boolean isWhitespace(int ch){ @@ -1199,6 +1200,50 @@ public static class CtorReader extends AFn{ } } + +public static class DataReader extends AFn{ + public Object invoke(Object reader, Object firstChar){ + PushbackReader r = (PushbackReader) reader; + try { r.unread(':'); } + catch (IOException e) { throw new RuntimeException(e); } + + Object meta = read(r, true, null, true); + if(!(meta instanceof Keyword)) + throw new IllegalArgumentException(":data metadata must be Keyword"); + + Object o = read(r, true, null, true); + + ILookup data_readers = (ILookup)RT.DATA_READERS.deref(); + IFn data = (IFn)RT.get(data_readers, meta); + if(data != null) + return data.invoke(o); + + // No data reader, just add :data metadata + meta = RT.map(RT.DATA_KEY, meta); + int line = -1; + if(r instanceof LineNumberingPushbackReader) + line = ((LineNumberingPushbackReader) r).getLineNumber(); + if(o instanceof IMeta) + { + if(line != -1 && o instanceof ISeq) + meta = ((IPersistentMap) meta).assoc(RT.LINE_KEY, line); + if(o instanceof IReference) + { + ((IReference)o).resetMeta((IPersistentMap) meta); + return o; + } + Object ometa = RT.meta(o); + for(ISeq s = RT.seq(meta); s != null; s = s.next()) { + IMapEntry kv = (IMapEntry) s.first(); + ometa = RT.assoc(ometa, kv.getKey(), kv.getValue()); + } + return ((IObj) o).withMeta((IPersistentMap) ometa); + } + else + return o; + } +} + /* public static void main(String[] args) throws Exception{ //RT.init(); diff --git a/src/jvm/clojure/lang/RT.java b/src/jvm/clojure/lang/RT.java index 81ca178..80aeb94 100644 --- a/src/jvm/clojure/lang/RT.java +++ b/src/jvm/clojure/lang/RT.java @@ -179,9 +179,11 @@ final static public Var ERR = Var.intern(CLOJURE_NS, Symbol.intern("*err*"), new PrintWriter(new OutputStreamWriter(System.err), true)).setDynamic(); final static Keyword TAG_KEY = Keyword.intern(null, "tag"); +final static Keyword DATA_KEY = Keyword.intern(null, "data"); final static Keyword CONST_KEY = Keyword.intern(null, "const"); final static public Var AGENT = Var.intern(CLOJURE_NS, Symbol.intern("*agent*"), null).setDynamic(); final static public Var READEVAL = Var.intern(CLOJURE_NS, Symbol.intern("*read-eval*"), T).setDynamic(); +final static public Var DATA_READERS = Var.intern(CLOJURE_NS, Symbol.intern("*data-readers*"), RT.map()).setDynamic(); final static public Var ASSERT = Var.intern(CLOJURE_NS, Symbol.intern("*assert*"), T).setDynamic(); final static public Var MATH_CONTEXT = Var.intern(CLOJURE_NS, Symbol.intern("*math-context*"), null).setDynamic(); static Keyword LINE_KEY = Keyword.intern(null, "line"); diff --git a/test/clojure/test_clojure/reader.clj b/test/clojure/test_clojure/reader.clj index 7a06034..5a6cff0 100644 --- a/test/clojure/test_clojure/reader.clj +++ b/test/clojure/test_clojure/reader.clj @@ -292,6 +292,26 @@ (deftest t-Metadata (is (= (meta '^:static ^:awesome ^{:static false :bar :baz} sym) {:awesome true, :bar :baz, :static true}))) +;; Data readers + +(deftest data-readers-undefined + (let [x (binding [*data-readers* nil] + (read-string "#:clojure.test-clojure.reader/foo [1 2 3]"))] + (is (= [1 2 3] x)) + (is (= :clojure.test-clojure.reader/foo (:data (meta x)))))) + +(deftest data-readers-not-imeta + (let [x (binding [*data-readers* nil] + (read-string "#:clojure.test-clojure.reader/foo \"foo\""))] + (is (= "foo" x)))) + +(deftest data-readers-provided + (let [x (binding [*data-readers* + {::foo set}] + (read-string "#:clojure.test-clojure.reader/foo [1 2 3]"))] + (is (= #{1 2 3} x)) + (is (not (contains? (meta x) :data))))) + ;; Var-quote (#') (deftest t-Var-quote) -- 1.7.4.1 From 4a851285f8bf0f3cc3ebcf93f6f1eccbbfb354cf Mon Sep 17 00:00:00 2001 From: Stuart Sierra Date: Sun, 13 Nov 2011 19:43:18 -0500 Subject: [PATCH 2/2] CLJ-871: data reader for #:instant [year month day hrs min sec ms] --- src/clj/clojure/core.clj | 21 +++++++++++++++++++++ test/clojure/test_clojure/reader.clj | 9 +++++++++ 2 files changed, 30 insertions(+), 0 deletions(-) diff --git a/src/clj/clojure/core.clj b/src/clj/clojure/core.clj index c75932b..6e2db17 100644 --- a/src/clj/clojure/core.clj +++ b/src/clj/clojure/core.clj @@ -6526,3 +6526,24 @@ "Returns true if a value has been produced for a promise, delay, future or lazy sequence." {:added "1.3"} [^clojure.lang.IPending x] (.isRealized x)) + +;;; Data readers + +(defn- parse-date + ([] (parse-date 1970 1 1 0 0 0 0)) + ([year] (parse-date year 1 1 0 0 0 0)) + ([year month] (parse-date year month 1 0 0 0 0)) + ([year month day] (parse-date year month day 0 0 0 0)) + ([year month day hrs] (parse-date year month day hrs 0 0 0)) + ([year month day hrs min] (parse-date year month day hrs min 0 0)) + ([year month day hrs min sec] (parse-date year month day hrs min sec 0)) + ([year month day hrs min sec ms] + (-> (doto (java.util.GregorianCalendar. year (dec month) day hrs min sec) + (.setTimeZone (java.util.TimeZone/getTimeZone "UTC"))) + (.getTime) + (.getTime) + (+ (or ms 0)) + (java.util.Date.)))) + +(alter-var-root #'*data-readers* + assoc :instant (fn [v] (apply parse-date v))) diff --git a/test/clojure/test_clojure/reader.clj b/test/clojure/test_clojure/reader.clj index 5a6cff0..7c8f9df 100644 --- a/test/clojure/test_clojure/reader.clj +++ b/test/clojure/test_clojure/reader.clj @@ -312,6 +312,15 @@ (is (= #{1 2 3} x)) (is (not (contains? (meta x) :data))))) +(deftest default-date-reader + (are [x y] (= x (.toGMTString (read-string y))) + "13 Nov 2011 00:00:00 GMT" "#:instant [2011 11 13]" + "1 Jan 1920 00:00:00 GMT" "#:instant [1920 1 1]" + "31 Dec 2010 03:02:01 GMT" "#:instant [2010 12 31 3 2 1]" + "31 Dec 2010 03:02:01 GMT" "#:instant [2010 12 31 3 2 1 99]") + (is (= 99 (rem (.getTime (read-string "#:instant [2010 12 31 3 2 1 99]")) + 1000)))) + ;; Var-quote (#') (deftest t-Var-quote) -- 1.7.4.1