Skip to content

Commit 98a32e8

Browse files
authored
Add a set library to Basilisp (#261)
* Add a set library to Basilisp * basilisp.set/index function * Index function * Partial join implementation * Not implemented join arity * Add gitattributes * Fix test runner reporting errors as failures * Fix set lib
1 parent ce4346a commit 98a32e8

File tree

5 files changed

+308
-2
lines changed

5 files changed

+308
-2
lines changed

src/basilisp/lang/set.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,18 @@ def __contains__(self, item):
3434
def __eq__(self, other):
3535
return self._inner == other
3636

37+
def __ge__(self, other):
38+
return self._inner >= other
39+
40+
def __gt__(self, other):
41+
return self._inner > other
42+
43+
def __le__(self, other):
44+
return self._inner <= other
45+
46+
def __lt__(self, other):
47+
return self._inner < other
48+
3749
def __hash__(self):
3850
return hash(self._inner)
3951

@@ -43,6 +55,39 @@ def __iter__(self):
4355
def __len__(self):
4456
return len(self._inner)
4557

58+
def difference(self, *others):
59+
e = self._inner
60+
for other in others:
61+
e = e.difference(other)
62+
return Set(e)
63+
64+
def intersection(self, *others):
65+
e = self._inner
66+
for other in others:
67+
e = e.intersection(other)
68+
return Set(e)
69+
70+
def symmetric_difference(self, *others):
71+
e = self._inner
72+
for other in others:
73+
e = e.symmetric_difference(other)
74+
return Set(e)
75+
76+
def union(self, *others):
77+
e = self._inner
78+
for other in others:
79+
e = e.union(other)
80+
return Set(e)
81+
82+
def isdisjoint(self, s):
83+
return self._inner.isdisjoint(s)
84+
85+
def issuperset(self, other):
86+
return self._inner >= other
87+
88+
def issubset(self, other):
89+
return self._inner <= other
90+
4691
@property
4792
def meta(self) -> Optional[Map]:
4893
return self._meta

src/basilisp/set.lpy

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
(ns basilisp.set)
2+
3+
(defn difference
4+
"Return a new set with elements from s1 not in the others.
5+
If no others are provided, return s."
6+
([s] s)
7+
([s1 & others]
8+
(apply-method s1 difference others)))
9+
10+
(defn disjoint?
11+
"Return true if s1 shares no common elements with s2.
12+
13+
Sets are considered disjoint if and only if their intersection
14+
is the empty set."
15+
[s1 s2]
16+
(.isdisjoint s1 s2))
17+
18+
(defn index
19+
"Given a set of maps, return a mapping of unique selections of keys
20+
from ks to the set of values which have those mappings."
21+
[rel ks]
22+
(if-not (seq rel)
23+
{}
24+
(reduce (fn [m v]
25+
(let [index-val (select-keys v ks)]
26+
(if (contains? m index-val)
27+
(update m index-val conj v)
28+
(assoc m index-val #{v}))))
29+
{}
30+
rel)))
31+
32+
(defn intersection
33+
"Return a new set with only elements in s1 that are in all
34+
the others. If no others are provided, return s."
35+
([s] s)
36+
([s1 & others]
37+
(apply-method s1 intersection others)))
38+
39+
(defn map-invert
40+
"Return a map whose vals and keys are swapped.
41+
42+
Duplicate values used as keys will overwrite each other as
43+
iteration order is non-deterministic."
44+
[m]
45+
(if-not (seq m)
46+
m
47+
(reduce (fn [m entry]
48+
(assoc m (val entry) (key entry)))
49+
{}
50+
m)))
51+
52+
(defn project
53+
"Given a set of maps, return a set of those maps with only the keys in ks."
54+
[rel ks]
55+
(set (map #(select-keys % ks) rel)))
56+
57+
(defn rename-keys
58+
"Return m with any keys appearing in kmap replaced with the value
59+
in kmap."
60+
[m kmap]
61+
(if-not (seq m)
62+
m
63+
(reduce (fn [m entry]
64+
(let [orig-k (key entry)
65+
new-k (val entry)]
66+
(if (contains? m orig-k)
67+
(let [v (get m orig-k)
68+
clean-map (dissoc m orig-k)]
69+
(assoc clean-map new-k v))
70+
m)))
71+
m
72+
kmap)))
73+
74+
(defn rename
75+
"Given a set of maps, return a set whose maps have had any keys in kmap
76+
renamed to the value in kmap."
77+
[rel kmap]
78+
(set (map #(rename-keys % kmap) rel)))
79+
80+
(defn select
81+
"Return a set of values for which pred is true."
82+
[pred xset]
83+
(set (filter pred xset)))
84+
85+
(defn symmetric-difference
86+
"Return a new set with elements which are not shared by any of
87+
the s1 and others. If no others are provided, return s."
88+
([s] s)
89+
([s1 & others]
90+
(apply-method s1 symmetric-difference others)))
91+
92+
(defn subset?
93+
"Return true if every element in s1 is also in s2."
94+
[s1 s2]
95+
(.issubset s1 s2))
96+
97+
(defn superset?
98+
"Return true if every element in s2 is also in s1."
99+
[s1 s2]
100+
(.issuperset s1 s2))
101+
102+
(defn union
103+
"Return a new set with elements in s1 that are in the others.
104+
If no others are provided, return s."
105+
([s] s)
106+
([s1 & others]
107+
(apply-method s1 union others)))
108+
109+
(defn ^:private product
110+
"Generate the Cartesian product of sets l and r, calling (f left right)
111+
on each relation. Returns a set of all returned elements."
112+
[f l r]
113+
(set
114+
(mapcat (fn [l-elem]
115+
(map #(f l-elem %) r))
116+
l)))
117+
118+
(defn join
119+
"Perform a natural join on the maps in lrel and rrel using keys shared
120+
by elements of each relation. If ks is specified, join only on the given
121+
keys."
122+
([lrel rrel]
123+
(let [shared (vec
124+
(intersection
125+
(apply intersection (map (comp set keys) lrel))
126+
(apply intersection (map (comp set keys) rrel))))
127+
128+
lindex (index lrel shared)
129+
rindex (index rrel shared)]
130+
(if-not (seq lindex)
131+
#{}
132+
(reduce (fn [s [k l-indexed]]
133+
(if (contains? rindex k)
134+
(apply conj s (product merge l-indexed (get rindex k)))
135+
s))
136+
#{}
137+
lindex))))
138+
([lrel rrel keymap]
139+
(let [[f s ks] (if (<= (count lrel) (count rrel))
140+
[lrel rrel (map-invert keymap)]
141+
[rrel lrel keymap])
142+
idx (index f (vals ks))]
143+
(if-not (seq s)
144+
#{}
145+
(reduce (fn [ret x]
146+
(let [match (idx (rename-keys (select-keys x (keys ks)) ks))]
147+
(if match
148+
(reduce (fn [s v]
149+
(conj s (merge v x)))
150+
ret
151+
match)
152+
ret)))
153+
#{}
154+
s)))))

tests/basilisp/test_multifn.lpy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
(ns basilisp.multifn-test
1+
(ns basilisp.test-multifn
22
(:require
33
[basilisp.test :refer [deftest is testing]]))
44

tests/basilisp/test_set.lpy

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
(ns basilisp.test-set
2+
(:require
3+
[basilisp.set :as set]
4+
[basilisp.test :refer [deftest is testing]]))
5+
6+
(deftest difference-test
7+
(is (= #{:a :b :c} (set/difference #{:a :b :c})))
8+
(is (= #{:a :b} (set/difference #{:a :b :c} #{:c :d :e})))
9+
(is (= #{:a} (set/difference #{:a :b :c} #{:c :d} #{:b "z" 3}))))
10+
11+
(deftest disjoint?-test
12+
(is (not (set/disjoint? #{:a :b :c} #{:c :d :e})))
13+
(is (set/disjoint? #{:a :b :c} #{:d :e :f})))
14+
15+
(deftest index-test
16+
(is (= {} (set/index #{} [:weight])))
17+
(is (= {{:weight 100} #{{:name "Gary" :weight 100}
18+
{:name "Darla" :weight 100}}
19+
{:weight 50} #{{:name "Karl" :weight 50}}}
20+
(set/index #{{:name "Gary" :weight 100}
21+
{:name "Darla" :weight 100}
22+
{:name "Karl" :weight 50}}
23+
[:weight]))))
24+
25+
(deftest intersection-test
26+
(is (= #{:a :b :c} (set/intersection #{:a :b :c})))
27+
(is (= #{:c} (set/intersection #{:a :b :c} #{:c :d :e})))
28+
(is (= #{} (set/intersection #{:a :b :c} #{:c :d} #{:b "z" 3}))))
29+
30+
(deftest join-test
31+
(testing "natural join"
32+
(is (= #{{:b 1 :a 2}
33+
{:b 2 :a 1}
34+
{:b 2 :a 2}
35+
{:b 1 :a 1}}
36+
(set/join #{{:a 1} {:a 2}}
37+
#{{:b 1} {:b 2}})))
38+
(is (= #{{:owner "Jeff" :manufacturer "Apple" :kind "laptop" :use "travel"}
39+
{:owner "Anna" :manufacturer "Dell", :kind "laptop" :use "travel"}
40+
{:owner "Dinesh" :manufacturer "HP", :kind "desktop" :use "workstation"}}
41+
(set/join #{{:manufacturer "Dell" :owner "Anna" :kind "laptop"}
42+
{:manufacturer "HP" :owner "Dinesh" :kind "desktop"}
43+
{:manufacturer "Apple" :owner "Jeff" :kind "laptop"}}
44+
#{{:kind "desktop" :use "workstation"}
45+
{:kind "laptop" :use "travel"}}))))
46+
47+
(testing "keymap join"
48+
(is (= #{{:owner "Jeff" :manufacturer "Apple" :kind "laptop" :type "laptop" :use "travel"}
49+
{:owner "Anna" :manufacturer "Dell", :kind "laptop" :type "laptop" :use "travel"}
50+
{:owner "Dinesh" :manufacturer "HP", :kind "desktop" :type "desktop" :use "workstation"}}
51+
(set/join #{{:manufacturer "Dell" :owner "Anna" :kind "laptop"}
52+
{:manufacturer "HP" :owner "Dinesh" :kind "desktop"}
53+
{:manufacturer "Apple" :owner "Jeff" :kind "laptop"}}
54+
#{{:type "desktop" :use "workstation"}
55+
{:type "laptop" :use "travel"}}
56+
{:kind :type})))))
57+
58+
(deftest map-invert-test
59+
(is (= {} (set/map-invert {})))
60+
(is (= {"a" :a} (set/map-invert {:a "a"})))
61+
(is (= {3 :b 's :a} (set/map-invert {:a 's :b 3}))))
62+
63+
(deftest project-test
64+
(is (= #{} (set/project #{} [:a :b :c])))
65+
(is (= #{{:a "a"} {:a "A"}}
66+
(set/project #{{:a "a" :d "e"} {:a "A" :z "p"}}
67+
[:a :b :c]))))
68+
69+
(deftest rename-keys-test
70+
(is (= {} (set/rename-keys {} {:a :new-a})))
71+
(is (= {:new-a "a"} (set/rename-keys {:a "a"} {:a :new-a})))
72+
(is (= {:a "a"} (set/rename-keys {:a "a"} {:b :new-b})))
73+
(is (= {:a "a" :new-b "b"} (set/rename-keys {:a "a" :b "b"} {:b :new-b}))))
74+
75+
(deftest rename-test
76+
(is (= #{} (set/rename #{} {:a :new-a})))
77+
(is (= #{{:new-a "a"}} (set/rename #{{:a "a"}} {:a :new-a})))
78+
(is (= #{{:a "a"}} (set/rename #{{:a "a"}} {:b :new-b})))
79+
(is (= #{{:a "a" :new-b "b"}} (set/rename #{{:a "a" :b "b"}} {:b :new-b})))
80+
(is (= #{{:a "a" :new-b "b"} {:a "A" :new-b "B"}}
81+
(set/rename #{{:a "a" :b "b"} {:a "A" :b "B"}}
82+
{:b :new-b}))))
83+
84+
(deftest select-test
85+
(is (= #{} (set/select odd? #{})))
86+
(is (= #{1 3} (set/select odd? #{1 2 3})))
87+
(is (= #{2} (set/select even? #{1 2 3})))
88+
(is (= #{1 2 3} (set/select identity #{1 2 3}))))
89+
90+
(deftest symmetric-difference-test
91+
(is (= #{:a :b :c} (set/symmetric-difference #{:a :b :c})))
92+
(is (= #{:a :b :d :e} (set/symmetric-difference #{:a :b :c} #{:c :d :e})))
93+
(is (= #{:a :d "z" 3} (set/symmetric-difference #{:a :b :c} #{:c :d} #{:b "z" 3}))))
94+
95+
(deftest subset?-test
96+
(is (not (set/subset? #{:a :b :c} #{:c :d :e})))
97+
(is (set/subset? #{:a :b :c} #{:a :b :c :d :e :f})))
98+
99+
(deftest superset?-test
100+
(is (set/superset? #{:a :b :c :d :e :f} #{:a :d :f}))
101+
(is (not (set/superset? #{:a :b :c} #{:c :d :e})))
102+
(is (not (set/superset? #{:a :b :c} #{:a :b :c :d :e :f}))))
103+
104+
(deftest union-test
105+
(is (= #{:a :b :c} (set/union #{:a :b :c})))
106+
(is (= #{:a :b :c :d :e} (set/union #{:a :b :c} #{:c :d :e})))
107+
(is (= #{:a :b :c :d "z" 3} (set/union #{:a :b :c} #{:c :d} #{:b "z" 3}))))

tests/basilisp/test_string.lpy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
(ns basilisp.string-test
1+
(ns basilisp.test-string
22
(:require
33
[basilisp.string :as str]
44
[basilisp.test :refer [deftest is]]))

0 commit comments

Comments
 (0)