Skip to content

Commit 53cf14d

Browse files
authored
feat: add PopN method to remove multiple arbitrary items from set (#166)
1 parent ded19ef commit 53cf14d

File tree

4 files changed

+145
-0
lines changed

4 files changed

+145
-0
lines changed

set.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ type Set[T comparable] interface {
191191
// Pop removes and returns an arbitrary item from the set.
192192
Pop() (T, bool)
193193

194+
// PopN removes and returns up to n arbitrary items from the set.
195+
// It returns a slice of the removed items and the actual number of items removed.
196+
// If the set is empty or n is less than or equal to 0s, it returns an empty slice and 0.
197+
// If n is greater than the set's size, all items are
198+
PopN(n int) ([]T, int)
199+
194200
// ToSlice returns the members of the set as a slice.
195201
ToSlice() []T
196202

set_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,6 +1242,118 @@ func Test_PopUnsafe(t *testing.T) {
12421242
}
12431243
}
12441244

1245+
func Test_PopNSafe(t *testing.T) {
1246+
a := NewSet[string]()
1247+
a.Add("a")
1248+
a.Add("b")
1249+
a.Add("c")
1250+
a.Add("d")
1251+
1252+
// Test pop with n <= 0
1253+
items, count := a.PopN(0)
1254+
if count != 0 {
1255+
t.Errorf("expected 0 items popped, got %d", count)
1256+
}
1257+
items, count = a.PopN(-1)
1258+
if count != 0 {
1259+
t.Errorf("expected 0 items popped, got %d", count)
1260+
}
1261+
1262+
captureSet := NewSet[string]()
1263+
1264+
// Test pop 2 items
1265+
items, count = a.PopN(2)
1266+
if count != 2 {
1267+
t.Errorf("expected 2 items popped, got %d", count)
1268+
}
1269+
if len(items) != 2 {
1270+
t.Errorf("expected 2 items in slice, got %d", len(items))
1271+
}
1272+
if a.Cardinality() != 2 {
1273+
t.Errorf("expected 2 items remaining, got %d", a.Cardinality())
1274+
}
1275+
captureSet.Append(items...)
1276+
1277+
// Test pop more than remaining
1278+
items, count = a.PopN(3)
1279+
if count != 2 {
1280+
t.Errorf("expected 2 items popped, got %d", count)
1281+
}
1282+
if a.Cardinality() != 0 {
1283+
t.Errorf("expected 0 items remaining, got %d", a.Cardinality())
1284+
}
1285+
captureSet.Append(items...)
1286+
1287+
// Test pop from empty set
1288+
items, count = a.PopN(1)
1289+
if count != 0 {
1290+
t.Errorf("expected 0 items popped, got %d", count)
1291+
}
1292+
if len(items) != 0 {
1293+
t.Errorf("expected empty slice, got %d items", len(items))
1294+
}
1295+
1296+
if !captureSet.Contains("c", "a", "d", "b") {
1297+
t.Error("unexpected result set; should be a,b,c,d (any order is fine")
1298+
}
1299+
}
1300+
1301+
func Test_PopNUnsafe(t *testing.T) {
1302+
a := NewThreadUnsafeSet[string]()
1303+
a.Add("a")
1304+
a.Add("b")
1305+
a.Add("c")
1306+
a.Add("d")
1307+
1308+
// Test pop with n <= 0
1309+
items, count := a.PopN(0)
1310+
if count != 0 {
1311+
t.Errorf("expected 0 items popped, got %d", count)
1312+
}
1313+
items, count = a.PopN(-1)
1314+
if count != 0 {
1315+
t.Errorf("expected 0 items popped, got %d", count)
1316+
}
1317+
1318+
captureSet := NewThreadUnsafeSet[string]()
1319+
1320+
// Test pop 2 items
1321+
items, count = a.PopN(2)
1322+
if count != 2 {
1323+
t.Errorf("expected 2 items popped, got %d", count)
1324+
}
1325+
if len(items) != 2 {
1326+
t.Errorf("expected 2 items in slice, got %d", len(items))
1327+
}
1328+
if a.Cardinality() != 2 {
1329+
t.Errorf("expected 2 items remaining, got %d", a.Cardinality())
1330+
}
1331+
captureSet.Append(items...)
1332+
1333+
// Test pop more than remaining
1334+
items, count = a.PopN(3)
1335+
if count != 2 {
1336+
t.Errorf("expected 2 items popped, got %d", count)
1337+
}
1338+
if a.Cardinality() != 0 {
1339+
t.Errorf("expected 0 items remaining, got %d", a.Cardinality())
1340+
}
1341+
captureSet.Append(items...)
1342+
1343+
// Test pop from empty set
1344+
items, count = a.PopN(1)
1345+
if count != 0 {
1346+
t.Errorf("expected 0 items popped, got %d", count)
1347+
}
1348+
if len(items) != 0 {
1349+
t.Errorf("expected empty slice, got %d items", len(items))
1350+
}
1351+
1352+
if !captureSet.Contains("c", "a", "d", "b") {
1353+
t.Error("unexpected result set; should be a,b,c,d (any order is fine")
1354+
}
1355+
}
1356+
12451357
func Test_EmptySetProperties(t *testing.T) {
12461358
empty := NewSet[string]()
12471359

threadsafe.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,12 @@ func (t *threadSafeSet[T]) Pop() (T, bool) {
289289
return t.uss.Pop()
290290
}
291291

292+
func (t *threadSafeSet[T]) PopN(n int) ([]T, int) {
293+
t.Lock()
294+
defer t.Unlock()
295+
return t.uss.PopN(n)
296+
}
297+
292298
func (t *threadSafeSet[T]) ToSlice() []T {
293299
t.RLock()
294300
l := len(*t.uss)

threadunsafe.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,27 @@ func (s *threadUnsafeSet[T]) Pop() (v T, ok bool) {
262262
return v, false
263263
}
264264

265+
func (s *threadUnsafeSet[T]) PopN(n int) (items []T, count int) {
266+
if n <= 0 || len(*s) == 0 {
267+
return make([]T, 0), 0
268+
}
269+
sn := s.Cardinality()
270+
if n > sn {
271+
n = sn
272+
}
273+
274+
items = make([]T, 0, sn)
275+
for item := range *s {
276+
if count >= n {
277+
break
278+
}
279+
delete(*s, item)
280+
items = append(items, item)
281+
count++
282+
}
283+
return items, count
284+
}
285+
265286
func (s threadUnsafeSet[T]) Remove(v T) {
266287
delete(s, v)
267288
}

0 commit comments

Comments
 (0)