From b3f6591e4b3c3e0885373c390aefb66bb1c0a6cc Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Mon, 24 Jun 2019 11:58:23 +0100 Subject: [PATCH 1/4] Added conversion functions; wrote documentation. --- README.md | 109 +++++++++++++++++++++++++++++++- project.clj | 2 +- src/sparse_array/core.clj | 88 ++++++++++++++++++++++++-- test/sparse_array/core_test.clj | 25 ++++++++ 4 files changed, 214 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c2ac9a9..d7e0b74 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ # sparse-array -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 *sparce matrix* library, for example [clojure.core.matrix](https://mikera.github.io/core.matrix/). +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/). + +Arbitrary numbers of dimensions are supported, up to limits imposed by the JVM stack. ## Conventions: +### Sparse arrays + For the purposes of this library, a sparse array shall be implemented as a map, such that all keys are non-negative members of the set of integers, except for the following keyword keys, all of which are expected to be present: 1. `:dimensions` The number of dimensions in this array, counting the present one (value expected to be a real number); @@ -30,9 +34,110 @@ 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. +### Dense arrays + +For the purposes of conversion, a **dense array** is assumed to be a vector; a two dimensional dense array a vector of vectors; a three dimensional dense array a vector of vectors of vectors, and so on. For any depth `N`, all vectors at depth `N` must have the same arity. If these conventions are not respected conversion may fail. + ## Usage -FIXME +### make-sparse-array +`sparse-array.core/make-sparse-array ([& dimensions])` + +Make a sparse array with these `dimensions`. Every member of `dimensions` must be a keyword; otherwise, `nil` will be returned. + +e.g. + +```clojure +(make-sparse-array :x :y :z) + +=> {:dimensions 3, :coord :x, :content (:y :z)} + +``` + +### sparse-array? + +`sparse-array.core/sparse-array? ([x])` + +`true` if `x` is a sparse array conforming to the conventions established by this library, else `false`. + +### put + +`sparse-array.core/put ([array value & coordinates])` + +Return a sparse array like this `array` but with this `value` at these `coordinates`. Returns `nil` if any coordinate is invalid. + +e.g. + +```clojure +(put (put (make-sparse-array :x :y) "hello" 3 4) "goodbye" 4 3) + +=> {:dimensions 2, + :coord :x, + :content (:y), + 3 {:dimensions 1, :coord :y, :content :data, 4 "hello"}, + 4 {:dimensions 1, :coord :y, :content :data, 3 "goodbye"}} +``` + +### get + +`sparse-array.core/get ([array & coordinates])` + +Return the value in this sparse `array` at these `coordinates`. + +### merge-sparse-arrays + +`sparse-array.core/merge-sparse-arrays ([a1 a2])` + +Return a sparse array taking values from sparse arrays `a1` and `a2`, but preferring values from `a2` where there is a conflict. `a1` and `a2` must have the **same** dimensions in the **same** order, or `nil` will be returned. + +e.g. + +```clojure +(merge-sparse-arrays + (put (make-sparse-array :x) "hello" 3) + (put (make-sparse-array :x) "goodbye" 4))) + +=> {:dimensions 1, :coord :x, :content :data, 3 "hello", 4 "goodbye"} +``` + +### dense-to-sparse + +`sparse-array.core/dense-to-sparse ([x] [x coordinates])` + +Return a sparse array representing the content of the dense array `x`, assuming these `coordinates` if specified. *NOTE THAT* if insufficient values of `coordinates` are specified, the resulting sparse array will be malformed. + +e.g. + +```clojure +(dense-to-sparse [nil nil nil "hello" nil "goodbye"]) + +=> {:dimensions 1, :coord :i0, :content :data, 3 "hello", 5 "goodbye"} +``` + +### sparse-to-dense + +`sparse-array.core/sparse-to-dense ([x] [x arity])` + +Return a dense array representing the content of the sparse array `x`. + +**NOTE THAT** this has the potential to consume very large amounts of memory. + +e.g. + +```clojure +(sparse-to-dense + (put + (put + (make-sparse-array :x :y) + "hello" 3 4) + "goodbye" 4 3)) + +=> [[nil nil nil nil nil] + [nil nil nil nil nil] + [nil nil nil nil nil] + [nil nil nil nil "hello"] + [nil nil nil "goodbye" nil]] +``` ## License diff --git a/project.clj b/project.clj index 7c8bf54..d9c47b7 100644 --- a/project.clj +++ b/project.clj @@ -1,5 +1,5 @@ (defproject sparse-array "0.1.0-SNAPSHOT" - :description "FIXME: write description" + :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" :url "http://www.eclipse.org/legal/epl-v10.html"} diff --git a/src/sparse_array/core.clj b/src/sparse_array/core.clj index a8b55b2..254b426 100644 --- a/src/sparse_array/core.clj +++ b/src/sparse_array/core.clj @@ -63,7 +63,9 @@ (defn merge-sparse-arrays "Return a sparse array taking values from sparse arrays `a1` and `a2`, - but preferring values from `a2` where there is a conflict." + but preferring values from `a2` where there is a conflict. `a1` and `a2` + must have the **same** dimensions in the **same** order, or `nil` will + be returned." [a1 a2] (cond (nil? a1) a2 @@ -86,16 +88,88 @@ (keys a1) (keys a2)))))))) +(defn dense-dimensions + "How many usable dimensions (represented as vectors) does the dense array + `x` have?" + [x] + (if + (vector? x) + (if + (every? vector? x) + (inc (apply min (map dense-dimensions x))) + 1) + 0)) + (defn dense-to-sparse "Return a sparse array representing the content of the dense array `x`, - assuming these `coordinates` if specified." + assuming these `coordinates` if specified. *NOTE THAT* if insufficient + values of `coordinates` are specified, the resulting sparse array will + be malformed." ([x] - :TODO) + (dense-to-sparse x (map #(keyword (str "i" %)) (range)))) ([x coordinates] - :TODO)) + (let + [dimensions (dense-dimensions x)] + (reduce + merge + (apply make-sparse-array (take dimensions coordinates)) + (map + (fn [i v] (if (nil? v) nil (hash-map i v))) + (range) + (if + (> dimensions 1) + (map #(dense-to-sparse % (rest coordinates)) x) + x)))))) + +(defn arity + "Return the arity of the sparse array `x`." + [x] + (inc (apply max (filter integer? (keys x))))) + +(defn child-arity + "Return the largest arity among the arities of the next dimension layer of + the sparse array `x`." + [x] + (apply + max + (cons + -1 ;; if no children are sparse arrays, we should return 0ß + (map + arity + (filter sparse-array? (vals x)))))) (defn sparse-to-dense "Return a dense array representing the content of the sparse array `x`. - If this blows up out of memory, that's strictly your problem." - [x] - :TODO) + + **NOTE THAT** this has the potential to consume very large amounts of memory." ([x] + (sparse-to-dense x (arity x))) + ([x arity] + (if + (map? x) + (let [a (child-arity x)] + (apply + vector + (map + #(let [v (x %)] + (if + (= :data (:content x)) + v + (sparse-to-dense v a))) + (range arity)))) + (apply vector (repeat arity nil))))) + + +(sparse-to-dense (put (make-sparse-array :x) "hello" 4)) + +(def x + (put + (put + (make-sparse-array :x :y) + "hello" 3 4) + "goodbye" 4 3)) + +(child-arity x) + +(sparse-to-dense (x 1) 4) +(sparse-to-dense x) + diff --git a/test/sparse_array/core_test.clj b/test/sparse_array/core_test.clj index 4c7e214..815618f 100644 --- a/test/sparse_array/core_test.clj +++ b/test/sparse_array/core_test.clj @@ -65,3 +65,28 @@ sparse (dense-to-sparse dense)] (is (= "hello" (get sparse 3))) (is (= "goodbye" (get sparse 5)))))) + +(deftest dense-dimensions-tests + (testing "dense-dimensions" + (is (= 0 (dense-dimensions 1))) + (is (= 1 (dense-dimensions [1 2 3]))) + (is (= 2 (dense-dimensions [[1 2 3][2 4 6][3 6 9]]))) + (is (= 3 + (dense-dimensions + [[[1 2 3][2 4 6][3 6 9]] + [[2 4 6][4 16 36][6 12 18]] + [[3 6 9][18 96 (* 6 36 3)][100 200 300]]]))))) + +(deftest sparse-to-dense-tests + (testing "Conversion from sparse to dense arrays" + (let [expected [[nil nil nil nil nil] + [nil nil nil nil nil] + [nil nil nil nil nil] + [nil nil nil nil "hello"] + [nil nil nil "goodbye" nil]] + actual (sparse-to-dense (put + (put + (make-sparse-array :x :y) + "hello" 3 4) + "goodbye" 4 3))] + (is (= actual expected))))) From b91dfbb90fbdf45f8987dea55d923e3f8ef51263 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Mon, 24 Jun 2019 12:03:42 +0100 Subject: [PATCH 2/4] Formatting! --- src/sparse_array/core.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sparse_array/core.clj b/src/sparse_array/core.clj index 254b426..45d8aea 100644 --- a/src/sparse_array/core.clj +++ b/src/sparse_array/core.clj @@ -141,7 +141,8 @@ (defn sparse-to-dense "Return a dense array representing the content of the sparse array `x`. - **NOTE THAT** this has the potential to consume very large amounts of memory." ([x] + **NOTE THAT** this has the potential to consume very large amounts of memory." + ([x] (sparse-to-dense x (arity x))) ([x arity] (if From a1e10d9cea4ab1e8239e5139a9a7437d200deb5b Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Mon, 24 Jun 2019 12:22:10 +0100 Subject: [PATCH 3/4] Minor clean-up --- src/sparse_array/core.clj | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/sparse_array/core.clj b/src/sparse_array/core.clj index 45d8aea..db7d804 100644 --- a/src/sparse_array/core.clj +++ b/src/sparse_array/core.clj @@ -1,6 +1,5 @@ (ns sparse-array.core) - (defn make-sparse-array "Make a sparse array with these `dimensions`. Every member of `dimensions` must be a keyword; otherwise, `nil` will be returned." @@ -96,7 +95,7 @@ (vector? x) (if (every? vector? x) - (inc (apply min (map dense-dimensions x))) + (inc (apply max (map dense-dimensions x))) 1) 0)) @@ -160,17 +159,3 @@ (apply vector (repeat arity nil))))) -(sparse-to-dense (put (make-sparse-array :x) "hello" 4)) - -(def x - (put - (put - (make-sparse-array :x :y) - "hello" 3 4) - "goodbye" 4 3)) - -(child-arity x) - -(sparse-to-dense (x 1) 4) -(sparse-to-dense x) - From d430937c569bedf188ce876487879fb14352d21e Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Mon, 24 Jun 2019 12:25:16 +0100 Subject: [PATCH 4/4] Fix a regression! --- src/sparse_array/core.clj | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sparse_array/core.clj b/src/sparse_array/core.clj index db7d804..b169df8 100644 --- a/src/sparse_array/core.clj +++ b/src/sparse_array/core.clj @@ -95,7 +95,10 @@ (vector? x) (if (every? vector? x) - (inc (apply max (map dense-dimensions x))) + (inc (apply min (map dense-dimensions x))) + ;; `min` is right here, not `max`, because otherwise + ;; we will get malformed arrays. Be liberal with what you + ;; consume, conservative with what you return! 1) 0))