Added conversion functions; wrote documentation.
This commit is contained in:
parent
9c60d925d6
commit
b3f6591e4b
109
README.md
109
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
|
||||
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)))))
|
||||
|
|
Loading…
Reference in a new issue