Merge tag 'sparse-array-0.1.0'
This commit is contained in:
commit
a60dabbef1
23
README.md
23
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.
|
Arbitrary numbers of dimensions are supported, up to limits imposed by the JVM stack.
|
||||||
|
|
||||||
|
[](https://clojars.org/sparse-array)
|
||||||
|
|
||||||
## Conventions:
|
## Conventions:
|
||||||
|
|
||||||
### Sparse arrays
|
### 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
|
### Dense arrays
|
||||||
|
|
||||||
|
|
25
project.clj
25
project.clj
|
@ -1,6 +1,27 @@
|
||||||
(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/)."
|
: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"
|
:url "http://example.com/FIXME"
|
||||||
:license {:name "Eclipse Public License"
|
:license {:name "Eclipse Public License"
|
||||||
:url "http://www.eclipse.org/legal/epl-v10.html"}
|
:url "http://www.eclipse.org/legal/epl-v10.html"}
|
||||||
:dependencies [[org.clojure/clojure "1.8.0"]])
|
:dependencies [[org.clojure/clojure "1.8.0"]]
|
||||||
|
|
||||||
|
:plugins [[lein-codox "0.10.4"]
|
||||||
|
[lein-release "1.0.5"]]
|
||||||
|
|
||||||
|
|
||||||
|
;; `lein release` doesn't play nice with `git flow release`. Run `lein release` in the
|
||||||
|
;; `develop` branch, then merge the release tag into the `master` branch.
|
||||||
|
|
||||||
|
:release-tasks [["vcs" "assert-committed"]
|
||||||
|
["clean"]
|
||||||
|
["test"]
|
||||||
|
["codox"]
|
||||||
|
["change" "version" "leiningen.release/bump-version" "release"]
|
||||||
|
["vcs" "commit"]
|
||||||
|
;; ["vcs" "tag"] -- not working, problems with secret key
|
||||||
|
["uberjar"]
|
||||||
|
["install"]
|
||||||
|
["deploy" "clojars"]
|
||||||
|
["change" "version" "leiningen.release/bump-version"]
|
||||||
|
["vcs" "commit"]])
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,18 @@
|
||||||
(ns sparse-array.core)
|
(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)
|
||||||
|
|
||||||
|
(defn- unsafe-sparse-operations?
|
||||||
|
"returns `true` if `*safe-sparse-operations*` is `false`, and vice versa."
|
||||||
|
[]
|
||||||
|
(not (true? *safe-sparse-operations*)))
|
||||||
|
|
||||||
(defn make-sparse-array
|
(defn make-sparse-array
|
||||||
"Make a sparse array with these `dimensions`. Every member of `dimensions`
|
"Make a sparse array with these `dimensions`. Every member of `dimensions`
|
||||||
must be a keyword; otherwise, `nil` will be returned."
|
must be a keyword; otherwise, `nil` will be returned."
|
||||||
|
@ -14,26 +27,63 @@
|
||||||
:data
|
:data
|
||||||
(rest dimensions))}))
|
(rest dimensions))}))
|
||||||
|
|
||||||
|
(defn- safe-test-or-throw
|
||||||
|
"If `v` is truthy or `*safe-sparse-operations*` is false, return `v`;
|
||||||
|
otherwise, throw an `ExceptionInfo` with this `message` and the map `m`."
|
||||||
|
[v message m]
|
||||||
|
(if-not
|
||||||
|
v
|
||||||
|
(if
|
||||||
|
*safe-sparse-operations*
|
||||||
|
(throw (ex-info message m))
|
||||||
|
v)
|
||||||
|
v))
|
||||||
|
|
||||||
(defn sparse-array?
|
(defn sparse-array?
|
||||||
"`true` if `x` is a sparse array conforming to the conventions established
|
"`true` if `x` is a sparse array conforming to the conventions established
|
||||||
by this library, else `false`."
|
by this library, else `false`."
|
||||||
([x]
|
([x]
|
||||||
(and
|
(apply
|
||||||
(map? x)
|
sparse-array?
|
||||||
(pos? (:dimensions x))
|
(cons
|
||||||
(keyword? (:coord x))
|
x
|
||||||
|
(cons
|
||||||
|
(:coord x)
|
||||||
(if
|
(if
|
||||||
(coll? (:content x))
|
(coll? (:content x))
|
||||||
|
(:content x))))))
|
||||||
|
([x & axes]
|
||||||
|
(and
|
||||||
|
(safe-test-or-throw
|
||||||
|
(map? x)
|
||||||
|
"Array must be a map" {:array x})
|
||||||
|
(safe-test-or-throw
|
||||||
|
(and (integer? (:dimensions x)) (pos? (:dimensions x)))
|
||||||
|
(str "The value of `:dimensions` must be a positive integer, not " (:dimensions x))
|
||||||
|
{:array x})
|
||||||
|
(safe-test-or-throw
|
||||||
|
(keyword? (:coord x))
|
||||||
|
(str "The value of `:coord` must be a keyword, not " (:coord x))
|
||||||
|
{:array x})
|
||||||
|
(safe-test-or-throw
|
||||||
|
(= (:coord x) (first axes))
|
||||||
|
(str "The value of `:coord` must be " (first axes) ", not " (:coord x))
|
||||||
|
{:array x})
|
||||||
|
(if
|
||||||
|
(empty? (rest axes))
|
||||||
|
(safe-test-or-throw
|
||||||
|
(= (:content x) :data)
|
||||||
|
"If there are no further axes the value of `:content` must be `:data`"
|
||||||
|
{:array x})
|
||||||
|
(and
|
||||||
|
(= (:content x) (rest axes))
|
||||||
(every?
|
(every?
|
||||||
sparse-array?
|
sparse-array?
|
||||||
(map #(x %) (filter integer? (keys x))))
|
(map #(x %) (filter integer? (keys x)))))))))
|
||||||
(= (:content x) :data)))))
|
|
||||||
|
|
||||||
(defn put
|
(defn- unsafe-put
|
||||||
"Return a sparse array like this `array` but with this `value` at these
|
[array value coordinates]
|
||||||
`coordinates`. Returns `nil` if any coordinate is invalid."
|
(cond
|
||||||
[array value & coordinates]
|
|
||||||
(if
|
|
||||||
(every?
|
(every?
|
||||||
#(and (integer? %) (or (zero? %) (pos? %)))
|
#(and (integer? %) (or (zero? %) (pos? %)))
|
||||||
coordinates)
|
coordinates)
|
||||||
|
@ -51,14 +101,75 @@
|
||||||
(apply make-sparse-array (:content array)))
|
(apply make-sparse-array (:content array)))
|
||||||
(cons value (rest coordinates))))))))
|
(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)}))
|
||||||
|
: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
|
(defn get
|
||||||
"Return the value in this sparse `array` at these `coordinates`."
|
"Return the value in this sparse `array` at these `coordinates`."
|
||||||
;; TODO: I am CERTAIN there is a more elegant solution to this.
|
|
||||||
[array & coordinates]
|
[array & coordinates]
|
||||||
(if
|
(cond
|
||||||
(= :data (:content array))
|
(unsafe-sparse-operations?)
|
||||||
(array (first coordinates))
|
(unsafe-get array coordinates)
|
||||||
(apply get (cons (array (first coordinates)) (rest 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
|
||||||
|
(unsafe-get array coordinates)))
|
||||||
|
|
||||||
(defn merge-sparse-arrays
|
(defn merge-sparse-arrays
|
||||||
"Return a sparse array taking values from sparse arrays `a1` and `a2`,
|
"Return a sparse array taking values from sparse arrays `a1` and `a2`,
|
||||||
|
@ -74,7 +185,7 @@
|
||||||
nil
|
nil
|
||||||
(= :data (:content a1))
|
(= :data (:content a1))
|
||||||
(merge a1 a2)
|
(merge a1 a2)
|
||||||
:else
|
(or (unsafe-sparse-operations?) (and (sparse-array? a1) (sparse-array? a2)))
|
||||||
(reduce
|
(reduce
|
||||||
merge
|
merge
|
||||||
a2
|
a2
|
||||||
|
|
|
@ -4,7 +4,94 @@
|
||||||
|
|
||||||
(deftest creation-and-testing
|
(deftest creation-and-testing
|
||||||
(testing "Creation and testing."
|
(testing "Creation and testing."
|
||||||
(is (sparse-array? (make-sparse-array :x :y :z)))))
|
(is (sparse-array? (make-sparse-array :x :y :z)))
|
||||||
|
(is (sparse-array? {:dimensions 2,
|
||||||
|
:coord :x,
|
||||||
|
:content '(:y),
|
||||||
|
3 {:dimensions 1,
|
||||||
|
:coord :y,
|
||||||
|
:content :data,
|
||||||
|
4 "hello"},
|
||||||
|
4 {:dimensions 1,
|
||||||
|
:coord :y,
|
||||||
|
:content :data,
|
||||||
|
3 "goodbye"}}))
|
||||||
|
(is (= (sparse-array? []) false))
|
||||||
|
(is (= (sparse-array? "hello") false))
|
||||||
|
(is (=
|
||||||
|
(sparse-array?
|
||||||
|
(dissoc (make-sparse-array :x :y :z) :dimensions))
|
||||||
|
false)
|
||||||
|
"All mandatory keywords must be present: dimensions")
|
||||||
|
(is (=
|
||||||
|
(sparse-array?
|
||||||
|
(dissoc (make-sparse-array :x :y :z) :coord))
|
||||||
|
false)
|
||||||
|
"All mandatory keywords must be present: coord")
|
||||||
|
(is (=
|
||||||
|
(sparse-array?
|
||||||
|
(dissoc (make-sparse-array :x :y :z) :content))
|
||||||
|
false)
|
||||||
|
"All mandatory keywords must be present: content")
|
||||||
|
(is (=
|
||||||
|
(sparse-array? {:dimensions 2,
|
||||||
|
:coord :x,
|
||||||
|
:content '(:y),
|
||||||
|
3 {:dimensions 1,
|
||||||
|
:coord :y,
|
||||||
|
:content :data,
|
||||||
|
4 "hello"},
|
||||||
|
4 {:dimensions 1,
|
||||||
|
:coord :y,
|
||||||
|
:content :data,
|
||||||
|
3 "goodbye"}
|
||||||
|
5 :foo})
|
||||||
|
false)
|
||||||
|
"Can't have data in a non-data layer")
|
||||||
|
))
|
||||||
|
|
||||||
|
(deftest testing-safe
|
||||||
|
(testing "Checking that correct exceptions are thrown when `*safe-sparse-operations*` is true"
|
||||||
|
(binding [*safe-sparse-operations* true]
|
||||||
|
(is
|
||||||
|
(thrown-with-msg?
|
||||||
|
clojure.lang.ExceptionInfo
|
||||||
|
#"Array must be a map"
|
||||||
|
(sparse-array? [])))
|
||||||
|
(is
|
||||||
|
(thrown-with-msg?
|
||||||
|
clojure.lang.ExceptionInfo
|
||||||
|
#"The value of `:dimensions` must be a positive integer, not .*"
|
||||||
|
(sparse-array?
|
||||||
|
(dissoc (make-sparse-array :x :y :z) :dimensions)))
|
||||||
|
"All mandatory keywords must be present: dimensions")
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
clojure.lang.ExceptionInfo
|
||||||
|
#"The value of `:coord` must be a keyword, not .*"
|
||||||
|
(sparse-array?
|
||||||
|
(dissoc (make-sparse-array :x :y :z) :coord))))
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
clojure.lang.ExceptionInfo
|
||||||
|
#"If there are no further axes the value of `:content` must be `:data`"
|
||||||
|
(sparse-array?
|
||||||
|
(dissoc (make-sparse-array :x :y :z) :content)))
|
||||||
|
"All mandatory keywords must be present: content")
|
||||||
|
(is (thrown-with-msg?
|
||||||
|
clojure.lang.ExceptionInfo
|
||||||
|
#"Array must be a map"
|
||||||
|
(sparse-array? {:dimensions 2,
|
||||||
|
:coord :x,
|
||||||
|
:content '(:y),
|
||||||
|
3 {:dimensions 1,
|
||||||
|
:coord :y,
|
||||||
|
:content :data,
|
||||||
|
4 "hello"},
|
||||||
|
4 {:dimensions 1,
|
||||||
|
:coord :y,
|
||||||
|
:content :data,
|
||||||
|
3 "goodbye"}
|
||||||
|
5 :foo}))
|
||||||
|
"Can't have data in a non-data layer"))))
|
||||||
|
|
||||||
(deftest put-and-get
|
(deftest put-and-get
|
||||||
(testing "get"
|
(testing "get"
|
||||||
|
@ -14,6 +101,13 @@
|
||||||
:content '(:y)
|
:content '(:y)
|
||||||
3 {:dimensions 1 :coord :y :content :data 4 "hello"}}
|
3 {:dimensions 1 :coord :y :content :data 4 "hello"}}
|
||||||
actual (get array 3 4)]
|
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))))
|
(is (= actual expected))))
|
||||||
(testing "put"
|
(testing "put"
|
||||||
(let [expected "hello"
|
(let [expected "hello"
|
||||||
|
@ -26,12 +120,24 @@
|
||||||
(let
|
(let
|
||||||
[expected "hello"
|
[expected "hello"
|
||||||
actual (get (put (make-sparse-array :x) expected 3) 3)]
|
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"
|
(testing "round trip, two dimensions"
|
||||||
(let
|
(let
|
||||||
[expected "hello"
|
[expected "hello"
|
||||||
actual (get (put (make-sparse-array :x :y) expected 3 4) 3 4)]
|
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"
|
(testing "round trip, three dimensions"
|
||||||
(let
|
(let
|
||||||
[expected "hello"
|
[expected "hello"
|
||||||
|
@ -41,7 +147,29 @@
|
||||||
(let
|
(let
|
||||||
[expected "hello"
|
[expected "hello"
|
||||||
actual (get (put (make-sparse-array :p :q :r :s) expected 3 4 5 6) 3 4 5 6)]
|
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
|
(deftest merge-test
|
||||||
(testing "merge, one dimension"
|
(testing "merge, one dimension"
|
||||||
|
@ -84,9 +212,17 @@
|
||||||
[nil nil nil nil nil]
|
[nil nil nil nil nil]
|
||||||
[nil nil nil nil "hello"]
|
[nil nil nil nil "hello"]
|
||||||
[nil nil nil "goodbye" nil]]
|
[nil nil nil "goodbye" nil]]
|
||||||
actual (sparse-to-dense (put
|
actual (sparse-to-dense {:dimensions 2,
|
||||||
(put
|
:coord :x,
|
||||||
(make-sparse-array :x :y)
|
:content '(:y),
|
||||||
"hello" 3 4)
|
3 {:dimensions 1,
|
||||||
"goodbye" 4 3))]
|
:coord :y,
|
||||||
|
:content :data,
|
||||||
|
4 "hello"},
|
||||||
|
4 {:dimensions 1,
|
||||||
|
:coord :y,
|
||||||
|
:content :data,
|
||||||
|
3 "goodbye"}})]
|
||||||
(is (= actual expected)))))
|
(is (= actual expected)))))
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue