Added conversion functions; wrote documentation.

This commit is contained in:
Simon Brooke 2019-06-24 11:58:23 +01:00
parent 9c60d925d6
commit b3f6591e4b
4 changed files with 214 additions and 10 deletions

109
README.md
View file

@ -1,9 +1,13 @@
# sparse-array # 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: ## 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: 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); 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. 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 ## 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 ## License

View file

@ -1,5 +1,5 @@
(defproject sparse-array "0.1.0-SNAPSHOT" (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" :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"}

View file

@ -63,7 +63,9 @@
(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`,
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] [a1 a2]
(cond (cond
(nil? a1) a2 (nil? a1) a2
@ -86,16 +88,88 @@
(keys a1) (keys a1)
(keys a2)))))))) (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 (defn dense-to-sparse
"Return a sparse array representing the content of the dense array `x`, "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] ([x]
:TODO) (dense-to-sparse x (map #(keyword (str "i" %)) (range))))
([x coordinates] ([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 (defn sparse-to-dense
"Return a dense array representing the content of the sparse array `x`. "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] **NOTE THAT** this has the potential to consume very large amounts of memory." ([x]
:TODO) (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)

View file

@ -65,3 +65,28 @@
sparse (dense-to-sparse dense)] sparse (dense-to-sparse dense)]
(is (= "hello" (get sparse 3))) (is (= "hello" (get sparse 3)))
(is (= "goodbye" (get sparse 5)))))) (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)))))