From bc1af9cdb0f81f75e5bd0d0d6c1f69594dd8d43e Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Wed, 26 Jun 2019 12:08:30 +0100 Subject: [PATCH] lein-release plugin: preparing 0.1.0 release --- README.md | 23 ++++++++- project.clj | 2 +- src/sparse_array/core.clj | 92 +++++++++++++++++++++++---------- test/sparse_array/core_test.clj | 47 +++++++++++++++-- 4 files changed, 132 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index d7e0b74..a6d6be5 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ A Clojure library designed to manipulate sparse *arrays* - multi-dimensional spa Arbitrary numbers of dimensions are supported, up to limits imposed by the JVM stack. +[![Clojars Project](https://img.shields.io/clojars/v/sparse-array.svg)](https://clojars.org/sparse-array) + ## Conventions: ### Sparse arrays @@ -32,7 +34,26 @@ Thus an array with a single value 'hello' at coordinates x = 3, y = 4, z = 5 wou } ``` -At the present stage of development, where the expectations of an operation are violated, `nil` is returned and no exception is thrown. However, it's probable that later there will be at least the option of thowing specific exceptions, as otherwise debugging could be tricky. +### Errors and error-reporting + +A dynamic variable, `*safe-sparse-operations*`, is provided to handle behaviour in error conditions. If this is `false`, bad data will generally not cause an exception to be thrown, and corrupt structures may be returned, thus: + +```clojure +(put (make-sparse-array :x :y :z) "hello" 3) ;; insufficient coordinates specified + +=> {:dimensions 3, :coord :x, :content (:y :z), 3 {:dimensions 2, :coord :y, :content (:z), nil {:dimensions 1, :coord :z, :content :data, nil nil}}} +``` + +However, if `*safe-sparse-operations*` is bound to `true`, exceptions will be thrown instead: + +```clojure +(binding [*safe-sparse-operations* true] + (put (make-sparse-array :x :y :z) "hello" 3)) + +ExceptionInfo Expected 3 coordinates; found 1 clojure.core/ex-info (core.clj:4617) +``` + +Sanity checking data is potentially expensive; for this reason `*safe-sparse-operations*` defaults to `false`, but you make wish to bind it to `true` especially while debugging. ### Dense arrays diff --git a/project.clj b/project.clj index fb40d19..8130d19 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject sparse-array "0.1.0-SNAPSHOT" +(defproject sparse-array "0.1.0" :description "A Clojure library designed to manipulate sparse *arrays* - multi-dimensional spaces accessed by indices, but containing arbitrary values rather than just numbers. For sparse spaces which contain numbers only, you're better to use a *sparse matrix* library, for example [clojure.core.matrix](https://mikera.github.io/core.matrix/)." :url "http://example.com/FIXME" :license {:name "Eclipse Public License" diff --git a/src/sparse_array/core.clj b/src/sparse_array/core.clj index ca98565..c984580 100644 --- a/src/sparse_array/core.clj +++ b/src/sparse_array/core.clj @@ -1,12 +1,14 @@ (ns sparse-array.core) +(declare put get) + (def ^:dynamic *safe-sparse-operations* "Whether spase array operations should be conducted safely, with careful checking of data conventions and exceptions thrown if expectations are not met. Normally `false`." false) -(defmacro unsafe-sparse-operations? +(defn- unsafe-sparse-operations? "returns `true` if `*safe-sparse-operations*` is `false`, and vice versa." [] (not (true? *safe-sparse-operations*))) @@ -79,13 +81,9 @@ sparse-array? (map #(x %) (filter integer? (keys x))))))))) -(defn put - "Return a sparse array like this `array` but with this `value` at these - `coordinates`. Returns `nil` if any coordinate is invalid." - [array value & coordinates] +(defn- unsafe-put + [array value coordinates] (cond - (and *safe-sparse-operations* (not (sparse-array? array))) - (throw (ex-info "Sparse array expected" {:array array})) (every? #(and (integer? %) (or (zero? %) (pos? %))) coordinates) @@ -101,37 +99,77 @@ (or (array (first coordinates)) (apply make-sparse-array (:content array))) - (cons value (rest coordinates)))))) - *safe-sparse-operations* + (cons value (rest coordinates)))))))) + +(defn put + "Return a sparse array like this `array` but with this `value` at these + `coordinates`. Returns `nil` if any coordinate is invalid." + [array value & coordinates] + (cond + (nil? value) + nil + (unsafe-sparse-operations?) + (unsafe-put array value coordinates) + (not (sparse-array? array)) + (throw (ex-info "Sparse array expected" {:array array})) + (not= (:dimensions array) (count coordinates)) + (throw + (ex-info + (str "Expected " (:dimensions array) " coordinates; found " (count coordinates)) + {:array array + :coordinates coordinates})) + (not + (every? + #(and (integer? %) (or (zero? %) (pos? %))) + coordinates)) (throw (ex-info "Coordinates must be zero or positive integers" {:array array :coordinates coordinates - :invalid (remove #(and (pos? %) (integer? %)) coordinates)})))) + :invalid (remove #(and (pos? %) (integer? %)) coordinates)})) + :else + (unsafe-put array value coordinates) + value + *safe-sparse-operations*)) + +(defn- unsafe-get + ;; TODO: I am CERTAIN there is a more elegant solution to this. + [array coordinates] + (let [v (array (first coordinates))] + (cond + (= :data (:content array)) + v + (nil? v) + nil + :else + (apply get (cons v (rest coordinates)))))) (defn get "Return the value in this sparse `array` at these `coordinates`." - ;; TODO: I am CERTAIN there is a more elegant solution to this. [array & coordinates] (cond - *safe-sparse-operations* - (cond - (not (sparse-array? array)) - (throw (ex-info "Sparse array expected" {:array array})) - (not (every? - #(and (integer? %) (or (zero? %) (pos? %))) - coordinates)) - (throw - (ex-info - "Coordinates must be zero or positive integers" - {:array array - :coordinates coordinates - :invalid (remove #(and (pos? %) (integer? %)) coordinates)}))) - (= :data (:content array)) - (array (first coordinates)) + (unsafe-sparse-operations?) + (unsafe-get array coordinates) + (not (sparse-array? array)) + (throw (ex-info "Sparse array expected" {:array array})) + (not (every? + #(and (integer? %) (or (zero? %) (pos? %))) + coordinates)) + (throw + (ex-info + "Coordinates must be zero or positive integers" + {:array array + :coordinates coordinates + :invalid (remove #(and (pos? %) (integer? %)) coordinates)})) + (not (= (:dimensions array) (count coordinates))) + (throw + (ex-info + (str "Expected " (:dimensions array) " coordinates; found " (count coordinates)) + {:array array + :coordinates coordinates})) :else - (apply get (cons (array (first coordinates)) (rest coordinates))))) + (unsafe-get array coordinates))) (defn merge-sparse-arrays "Return a sparse array taking values from sparse arrays `a1` and `a2`, diff --git a/test/sparse_array/core_test.clj b/test/sparse_array/core_test.clj index 5ec9c85..b06f2ad 100644 --- a/test/sparse_array/core_test.clj +++ b/test/sparse_array/core_test.clj @@ -101,6 +101,13 @@ :content '(:y) 3 {:dimensions 1 :coord :y :content :data 4 "hello"}} actual (get array 3 4)] + (is (= actual expected))) + (let [expected nil + array {:dimensions 2, + :coord :x, + :content '(:y) + 3 {:dimensions 1 :coord :y :content :data 4 "hello"}} + actual (get array 4 3)] (is (= actual expected)))) (testing "put" (let [expected "hello" @@ -113,12 +120,24 @@ (let [expected "hello" actual (get (put (make-sparse-array :x) expected 3) 3)] - (is (= actual expected)))) + (is (= actual expected))) + (binding [*safe-sparse-operations* true] + ;; enabling error handling shouldn't make any difference + (let + [expected "hello" + actual (get (put (make-sparse-array :x) expected 3) 3)] + (is (= actual expected))))) (testing "round trip, two dimensions" (let [expected "hello" actual (get (put (make-sparse-array :x :y) expected 3 4) 3 4)] - (is (= actual expected)))) + (is (= actual expected))) + (binding [*safe-sparse-operations* true] + ;; enabling error handling shouldn't make any difference + (let + [expected "hello" + actual (get (put (make-sparse-array :x :y) expected 3 4) 3 4)] + (is (= actual expected))))) (testing "round trip, three dimensions" (let [expected "hello" @@ -128,7 +147,29 @@ (let [expected "hello" actual (get (put (make-sparse-array :p :q :r :s) expected 3 4 5 6) 3 4 5 6)] - (is (= actual expected))))) + (is (= actual expected)))) + (testing "Error handling, number of dimensions" + (binding [*safe-sparse-operations* true] + (is + (thrown-with-msg? + clojure.lang.ExceptionInfo + #"Expected 3 coordinates; found 2" + (put (make-sparse-array :x :y :z) "hello" 3 4))) + (is + (thrown-with-msg? + clojure.lang.ExceptionInfo + #"Expected 3 coordinates; found 4" + (put (make-sparse-array :x :y :z) "hello" 3 4 5 6))) + (is + (thrown-with-msg? + clojure.lang.ExceptionInfo + #"Expected 3 coordinates; found 2" + (get (make-sparse-array :x :y :z) 3 4))) + (is + (thrown-with-msg? + clojure.lang.ExceptionInfo + #"Expected 3 coordinates; found 4" + (get (make-sparse-array :x :y :z) 3 4 5 6)))))) (deftest merge-test (testing "merge, one dimension"