From 1aa3173dc99d9f6d49998b943f95302887e80f5b Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Fri, 28 Jun 2019 09:45:05 +0100 Subject: [PATCH] merge dense with sparse now works Not yet sure I'm right about the semantics of sparse-with-dense: are nils in a dense array first class values? --- README.md | 2 +- src/sparse_array/core.clj | 99 +++++++++++++++++++++++++-------- test/sparse_array/core_test.clj | 20 +++++-- 3 files changed, 93 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 0a62f82..42dea28 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ e.g. ### extract-from-dense -Note that the above example returns the default axis sequence {i0, i1, i2...}; +Note that the above example returns the default axis sequence `{i0, i1, i2...}`; extracting from a sparse array will always retain the axes of the array extracted from. Dense arrays, obviously, do not have explicit axes. diff --git a/src/sparse_array/core.clj b/src/sparse_array/core.clj index 1b796e6..5b42495 100644 --- a/src/sparse_array/core.clj +++ b/src/sparse_array/core.clj @@ -42,6 +42,10 @@ (defn sparse-array? "`true` if `x` is a sparse array conforming to the conventions established by this library, else `false`." + ;; TODO: sparse-array? should not throw exceptions even when + ;; *safe-sparse-operations* is true, since we may use to test + ;; whether an object is a sparse array. The place to throw the exceptions + ;; (if required) is after it has failed. ([x] (apply sparse-array? @@ -171,6 +175,31 @@ :else (unsafe-get array coordinates))) + +(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))) + ;; `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)) + +(defn dense-array? + "Basically, any vector can be considered as a dense array of one dimension. + If we're seeking a dense array of more than one dimension, the number of + dimensions should be specified as `d`." + ([x] + (vector? x)) + ([x d] + (and (vector? x) (< d (dense-dimensions x))))) + (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. `a1` and `a2` @@ -198,29 +227,55 @@ (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))) - ;; `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)) +(defn merge-dense-with-sparse + "Merge this dense array `d` with this sparse array `s`, returning a new + dense array with the same arity as `d`, preferring values from `s` where + there is conflict" + [d s] + (apply + vector + (map + #(cond + (= :data (:content s)) + (or (s %2) %1) + (nil? (s %2)) + %1 + :else + (merge-dense-with-sparse %1 (s %2))) + d + (range)))) -(defn dense-array? - "Basically, any vector can be considered as a dense array of one dimension. - If we're seeking a dense array of more than one dimension, the number of - dimensions should be specified as `d`." - ([x] - (vector? x)) - ([x d] - (and (vector? x) (< d (dense-dimensions x))))) +(defn merge-arrays + "Merge two arrays `a1`, `a2`, which may be either dense or sparse but which + should have the same number of axes and compatible dimensions, and return a + new dense array preferring values from `a2`." + [a1 a2] + (cond + (dense-array? a2) + a2 ;; if a2 is dense, no values from a1 will be returned + (sparse-array? a1) + (cond + (sparse-array? a2) + (merge-sparse-arrays a1 a2) + *safe-sparse-operations* + (throw + (ex-info + "Object passed as array is neither dense not sparse" + {:array a2}))) + (dense-array? a1) + (cond + (sparse-array? a2) + (merge-dense-with-sparse a1 a2) + *safe-sparse-operations* + (throw + (ex-info + "Object passed as array is neither dense not sparse" + {:array a2}))) + *safe-sparse-operations* + (throw + (ex-info + "Object passed as array is neither dense not sparse" + {:array a1})))) (defn dense-to-sparse "Return a sparse array representing the content of the dense array `x`, diff --git a/test/sparse_array/core_test.clj b/test/sparse_array/core_test.clj index b06f2ad..3ec986b 100644 --- a/test/sparse_array/core_test.clj +++ b/test/sparse_array/core_test.clj @@ -172,20 +172,30 @@ (get (make-sparse-array :x :y :z) 3 4 5 6)))))) (deftest merge-test - (testing "merge, one dimension" + (testing "merge, sparse arrays, one dimension" (let - [merged (merge-sparse-arrays + [merged (merge-arrays (put (make-sparse-array :x) "hello" 3) (put (make-sparse-array :x) "goodbye" 4))] (is (= "hello" (get merged 3))) (is (= "goodbye" (get merged 4))))) - (testing "merge, two dimensions" + (testing "merge, sparse arrays, two dimensions" (let - [merged (merge-sparse-arrays + [merged (merge-arrays (put (make-sparse-array :x :y) "hello" 3 4) (put (make-sparse-array :x :y) "goodbye" 4 3))] (is (= "hello" (get merged 3 4))) - (is (= "goodbye" (get merged 4 3)))))) + (is (= "goodbye" (get merged 4 3))))) + (testing "merge, dense with sparse, two dimensions") + (let [dense [[[nil nil nil][nil nil nil][nil nil nil]] + [[nil nil nil][nil nil nil][nil nil nil]] + [[nil nil nil][nil nil nil][nil nil nil]]] + sparse (put (put (make-sparse-array :x :y :z) "hello" 0 0 0) "goodbye" 2 2 2) + expected [[["hello" nil nil] [nil nil nil] [nil nil nil]] + [[nil nil nil] [nil nil nil] [nil nil nil]] + [[nil nil nil] [nil nil nil] [nil nil "goodbye"]]] + actual (merge-arrays dense sparse)] + (is (= actual expected)))) (deftest dense-to-sparse-tests (testing "dense-to-sparse, one dimension"