diff --git a/README.md b/README.md index 5056cf6..9dd1f34 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # xiter + A Generic Library for Go with the Power of Iterators [![codecov](https://codecov.io/gh/dashjay/xiter/graph/badge.svg?token=GTTJNP1MHT)](https://codecov.io/gh/dashjay/xiter) @@ -9,7 +10,7 @@ A Generic Library for Go with the Power of Iterators TL;DR: The Go team ultimately did not implement the iterator utility due to various considerations, which led to the creation of this package. ----- +---- About two years ago, rsc (Russ Cox) proposed [x/exp/xiter: new package with iterator adapters](https://github.com/golang/go/issues/61898), which defined a new package but was ultimately declined. @@ -26,7 +27,45 @@ The reason can be summarized as below: The Go team recommended this functionality be implemented through third-party packages, which is why this project exists. +## Philosophy + +After some exploration, xiter found its true north: **lazy evaluation with `iter.Seq`**. + +The core principle: **only put things in xiter that benefit from lazy evaluation**. Functions like `Sum`, `Mean`, `Uniq` might *work* with Seq, but they don't benefit from it — a plain slice version is more intuitive and faster. Those belong in companion packages like `xslice`/`xmap`. + +**xiter shines where lazy evaluation matters:** + +- **Sources** — `FromSlice`, `FromChan`, `Range`, `Cycle`, `Generate` +- **Lazy transformers** — `Map`, `Filter`, `Chunk`, `WithIndex` (no intermediate allocations) +- **Short-circuit consumers** — `All`, `Any`, `Find`, `First` (don't read the whole sequence) +- **Combinators** — `Concat`, `Zip`, `Merge`, `Equal` +- **I/O streams** — `Lines`, `ReadFileByChunk` (process files without loading into memory) +- **Concurrent processing** — `ParallelMap`, `FanIn` (orchestrate goroutines) +- **Time-based** — `Ticker`, `Throttle`, `Debounce` (event streams as sequences) +- **Combinatorial** — `Combinations`, `Permutations`, `Product` (explosive output, lazy iteration) +- **Windowing & grouping** — `Window`, `SortedGroupBy` (streaming over batches) + +## Sub-packages + +| Package | Description | Requires Go 1.23+ | +|---------|-------------|-------------------| +| `xiter` | Core iterator utilities (Map, Filter, Concat, Zip, etc.) | Yes | +| `xiter/io` | Lazy I/O — `Lines`, `ReadDir`, `ReadFileByChunk` | Yes | +| `xiter/stream` | Concurrent stream processing — `ParallelMap`, `FanIn`, `Batch` | Yes | +| `xiter/rt` | Time-based sequences — `Ticker`, `Throttle`, `Debounce` | Yes | +| `xiter/collect` | Windowing & grouping — `Window`, `GroupBy`, `SortedGroupBy` | Yes | +| `xiter/combin` | Combinatorial generators — `Combinations`, `Permutations`, `Product` | Yes | +| `xsync` | Concurrency-safe wrappers for sync.Map, sync.Pool, sync.Mutex | No | +| `xslice` | Slice utilities (CountBy, KeyBy, Partition, Chunk, etc.) | No | +| `xmap` | Map utilities (Merge, Filter, Difference, etc.) | No | +| `xstl/list` | Doubly-linked list (generics port of container/list) | No | +| `xstl/lockedmap` | Concurrency-safe map with sync.RWMutex | No | +| `optional` | Optional value type | No | +| `xcmp` | Comparison utilities | No | +| `union` | Algebraic union types | No | + ## Target + This package is designed to implement iterator utilities which are defined in [proposal: x/exp/xiter: new package with iterator adapters](https://github.com/golang/go/issues/61898). And this package will also provide some generic utilities with iter. @@ -39,15 +78,14 @@ This package considers Go versions before 1.23, starting from Go 1.18+ Docs For Packages: -- [cmp](./pkg/cmp/README.md) -- [optional](./pkg/optional/README.md) -- [union](./pkg/union/README.md) -- [xiter](./pkg/xiter/README.md) -- [xslice](./pkg/xslice/README.md) -- [xmap](./pkg/xmap/README.md) +- [xcmp](./xcmp/README.md) +- [optional](./optional/README.md) +- [union](./union/README.md) +- [xiter](./xiter/README.md) +- [xslice](./xslice/README.md) +- [xmap](./xmap/README.md) ## Contribution - Make an issue to tell us what you want. - Create pull request and link to issue. - diff --git a/xiter/README.md b/xiter/README.md index 22951ba..1a02fc9 100644 --- a/xiter/README.md +++ b/xiter/README.md @@ -64,7 +64,11 @@ WARNING: golang 1.23 has higher performance on iterating Seq/Seq2 which boost by - [func Compact\[T comparable\]\(in Seq\[T\]\) Seq\[T\]](<#Compact>) - [func Concat\[V any\]\(seqs ...Seq\[V\]\) Seq\[V\]](<#Concat>) - [func Cycle\[T any\]\(seq Seq\[T\]\) Seq\[T\]](<#Cycle>) + - [func DistinctBy\[V any, K comparable\]\(f func\(V\) K, seq Seq\[V\]\) Seq\[V\]](<#DistinctBy>) + - [func DropWhile\[V any\]\(f func\(V\) bool, seq Seq\[V\]\) Seq\[V\]](<#DropWhile>) - [func Filter\[V any\]\(f func\(V\) bool, seq Seq\[V\]\) Seq\[V\]](<#Filter>) + - [func FlatMap\[In, Out any\]\(f func\(In\) Seq\[Out\], seq Seq\[In\]\) Seq\[Out\]](<#FlatMap>) + - [func Flatten\[T any\]\(seq Seq\[Seq\[T\]\]\) Seq\[T\]](<#Flatten>) - [func FromChan\[T any\]\(in \<\-chan T\) Seq\[T\]](<#FromChan>) - [func FromMapKeys\[K comparable, V any\]\(m map\[K\]V\) Seq\[K\]](<#FromMapKeys>) - [func FromMapValues\[K comparable, V any\]\(m map\[K\]V\) Seq\[V\]](<#FromMapValues>) @@ -73,6 +77,7 @@ WARNING: golang 1.23 has higher performance on iterating Seq/Seq2 which boost by - [func FromSliceShuffle\[T any\]\(in \[\]T\) Seq\[T\]](<#FromSliceShuffle>) - [func Generate\[T any\]\(fn func\(\) T\) Seq\[T\]](<#Generate>) - [func Intersect\[T comparable\]\(left Seq\[T\], right Seq\[T\]\) Seq\[T\]](<#Intersect>) + - [func Intersperse\[V any\]\(sep V, seq Seq\[V\]\) Seq\[V\]](<#Intersperse>) - [func Limit\[V any\]\(seq Seq\[V\], n int\) Seq\[V\]](<#Limit>) - [func Map\[In, Out any\]\(f func\(In\) Out, seq Seq\[In\]\) Seq\[Out\]](<#Map>) - [func Merge\[V xcmp.Ordered\]\(x, y Seq\[V\]\) Seq\[V\]](<#Merge>) @@ -86,6 +91,10 @@ WARNING: golang 1.23 has higher performance on iterating Seq/Seq2 which boost by - [func Seq2ToSeqUnion\[K, V any\]\(seq Seq2\[K, V\]\) Seq\[union.U2\[K, V\]\]](<#Seq2ToSeqUnion>) - [func Seq2ValueToSeq\[K, V any\]\(in Seq2\[K, V\]\) Seq\[V\]](<#Seq2ValueToSeq>) - [func Skip\[T any\]\(seq Seq\[T\], n int\) Seq\[T\]](<#Skip>) + - [func SortBy\[V any, K constraints.Ordered\]\(seq Seq\[V\], f func\(V\) K\) Seq\[V\]](<#SortBy>) + - [func Sorted\[V constraints.Ordered\]\(seq Seq\[V\]\) Seq\[V\]](<#Sorted>) + - [func Split\[V any\]\(f func\(V\) bool, seq Seq\[V\]\) \(tru, fls Seq\[V\]\)](<#Split>) + - [func TakeWhile\[V any\]\(f func\(V\) bool, seq Seq\[V\]\) Seq\[V\]](<#TakeWhile>) - [func Union\[T comparable\]\(left, right Seq\[T\]\) Seq\[T\]](<#Union>) - [func Uniq\[T comparable\]\(seq Seq\[T\]\) Seq\[T\]](<#Uniq>) - [func Zip\[V1, V2 any\]\(x Seq\[V1\], y Seq\[V2\]\) Seq\[Zipped\[V1, V2\]\]](<#Zip>) @@ -107,7 +116,7 @@ WARNING: golang 1.23 has higher performance on iterating Seq/Seq2 which boost by -## func [AllFromSeq]() +## func [AllFromSeq]() ```go func AllFromSeq[T any](seq Seq[T], f func(T) bool) bool @@ -116,7 +125,7 @@ func AllFromSeq[T any](seq Seq[T], f func(T) bool) bool AllFromSeq return true if all elements from seq satisfy the condition evaluated by f. -## func [AnyFromSeq]() +## func [AnyFromSeq]() ```go func AnyFromSeq[T any](seq Seq[T], f func(T) bool) bool @@ -134,7 +143,7 @@ func At[T any](seq Seq[T], index int) optional.O[T] At return the element at index from seq. -## func [AvgByFromSeq]() +## func [AvgByFromSeq]() ```go func AvgByFromSeq[V any, T constraints.Number](seq Seq[V], f func(V) T) float64 @@ -143,7 +152,7 @@ func AvgByFromSeq[V any, T constraints.Number](seq Seq[V], f func(V) T) float64 AvgByFromSeq return the average value of all elements from seq, evaluated by f. -## func [AvgFromSeq]() +## func [AvgFromSeq]() ```go func AvgFromSeq[T constraints.Number](seq Seq[T]) float64 @@ -152,7 +161,7 @@ func AvgFromSeq[T constraints.Number](seq Seq[T]) float64 AvgFromSeq return the average value of all elements from seq. -## func [Contains]() +## func [Contains]() ```go func Contains[T comparable](seq Seq[T], in T) bool @@ -161,7 +170,7 @@ func Contains[T comparable](seq Seq[T], in T) bool Contains return true if v is in seq. -## func [ContainsAll]() +## func [ContainsAll]() ```go func ContainsAll[T comparable](seq Seq[T], in []T) bool @@ -170,7 +179,7 @@ func ContainsAll[T comparable](seq Seq[T], in []T) bool ContainsAll return true if all elements from seq is in vs. -## func [ContainsAny]() +## func [ContainsAny]() ```go func ContainsAny[T comparable](seq Seq[T], in []T) bool @@ -179,7 +188,7 @@ func ContainsAny[T comparable](seq Seq[T], in []T) bool ContainsAny return true if any element from seq is in vs. -## func [ContainsBy]() +## func [ContainsBy]() ```go func ContainsBy[T any](seq Seq[T], f func(T) bool) bool @@ -188,7 +197,7 @@ func ContainsBy[T any](seq Seq[T], f func(T) bool) bool ContainsBy return true if any element from seq satisfies the condition evaluated by f. -## func [Count]() +## func [Count]() ```go func Count[T any](seq Seq[T]) int @@ -216,7 +225,7 @@ onlyLeft, onlyRight := Difference(FromSlice(left), FromSlice(right)) ``` -## func [Equal]() +## func [Equal]() ```go func Equal[V comparable](x, y Seq[V]) bool @@ -271,7 +280,7 @@ false -## func [Equal2]() +## func [Equal2]() ```go func Equal2[K, V comparable](x, y Seq2[K, V]) bool @@ -280,7 +289,7 @@ func Equal2[K, V comparable](x, y Seq2[K, V]) bool Equal2 returns whether the two Seq2 are equal. Like Equal but run with Seq2 -## func [EqualFunc]() +## func [EqualFunc]() ```go func EqualFunc[V1, V2 any](x Seq[V1], y Seq[V2], f func(V1, V2) bool) bool @@ -340,7 +349,7 @@ false -## func [EqualFunc2]() +## func [EqualFunc2]() ```go func EqualFunc2[K1, V1, K2, V2 any](x Seq2[K1, V1], y Seq2[K2, V2], f func(K1, V1, K2, V2) bool) bool @@ -349,7 +358,7 @@ func EqualFunc2[K1, V1, K2, V2 any](x Seq2[K1, V1], y Seq2[K2, V2], f func(K1, V EqualFunc2 returns whether the two sequences are equal according to the function f. Like EqualFunc but run with Seq2 -## func [Find]() +## func [Find]() ```go func Find[T any](seq Seq[T], f func(T) bool) (val T, found bool) @@ -358,7 +367,7 @@ func Find[T any](seq Seq[T], f func(T) bool) (val T, found bool) Find return the first element from seq that satisfies the condition evaluated by f with a boolean representing whether it exists. -## func [FindO]() +## func [FindO]() ```go func FindO[T any](seq Seq[T], f func(T) bool) optional.O[T] @@ -367,7 +376,7 @@ func FindO[T any](seq Seq[T], f func(T) bool) optional.O[T] FindO return the first element from seq that satisfies the condition evaluated by f. -## func [First]() +## func [First]() ```go func First[T any](in Seq[T]) (T, bool) @@ -382,7 +391,7 @@ first, ok := First(seq) ``` -## func [FirstO]() +## func [FirstO]() ```go func FirstO[T any](in Seq[T]) optional.O[T] @@ -397,7 +406,7 @@ first, ok := FirstO(seq) ``` -## func [ForEach]() +## func [ForEach]() ```go func ForEach[T any](seq Seq[T], f func(T) bool) @@ -406,7 +415,7 @@ func ForEach[T any](seq Seq[T], f func(T) bool) ForEach execute f for each element in seq. -## func [ForEachIdx]() +## func [ForEachIdx]() ```go func ForEachIdx[T any](seq Seq[T], f func(idx int, v T) bool) @@ -415,7 +424,7 @@ func ForEachIdx[T any](seq Seq[T], f func(idx int, v T) bool) ForEachIdx execute f for each element in seq with its index. -## func [Head]() +## func [Head]() ```go func Head[T any](seq Seq[T]) (v T, hasOne bool) @@ -424,7 +433,7 @@ func Head[T any](seq Seq[T]) (v T, hasOne bool) Head return the first element from seq with a boolean representing whether it is at least one element in seq. -## func [HeadO]() +## func [HeadO]() ```go func HeadO[T any](seq Seq[T]) optional.O[T] @@ -433,7 +442,7 @@ func HeadO[T any](seq Seq[T]) optional.O[T] HeadO return the first element from seq. -## func [Index]() +## func [Index]() ```go func Index[T comparable](seq Seq[T], v T) int @@ -450,7 +459,7 @@ idx := xiter.Index(seq, 3) ``` -## func [Join]() +## func [Join]() ```go func Join[T ~string](seq Seq[T], sep T) T @@ -459,7 +468,7 @@ func Join[T ~string](seq Seq[T], sep T) T Join return the concatenation of all elements in seq with sep. -## func [Last]() +## func [Last]() ```go func Last[T any](in Seq[T]) (T, bool) @@ -474,7 +483,7 @@ last, ok := Last(seq) ``` -## func [LastO]() +## func [LastO]() ```go func LastO[T any](in Seq[T]) optional.O[T] @@ -489,7 +498,7 @@ last, ok := LastO(seq) ``` -## func [Max]() +## func [Max]() ```go func Max[T constraints.Ordered](seq Seq[T]) (r optional.O[T]) @@ -498,7 +507,7 @@ func Max[T constraints.Ordered](seq Seq[T]) (r optional.O[T]) Max returns the maximum element in seq. -## func [MaxBy]() +## func [MaxBy]() ```go func MaxBy[T any](seq Seq[T], less func(T, T) bool) (r optional.O[T]) @@ -541,7 +550,7 @@ mean := MeanBy(FromSlice([]int{1, 2, 3, 4, 5}), func(v int) int { ``` -## func [Min]() +## func [Min]() ```go func Min[T constraints.Ordered](seq Seq[T]) (r optional.O[T]) @@ -550,7 +559,7 @@ func Min[T constraints.Ordered](seq Seq[T]) (r optional.O[T]) Min return the minimum element in seq. -## func [MinBy]() +## func [MinBy]() ```go func MinBy[T any](seq Seq[T], less func(T, T) bool) (r optional.O[T]) @@ -591,7 +600,7 @@ moderate := ModerateO(FromSlice([]int{1, 2, 3, 4, 5, 5, 5, 6, 6, 6, 6})) ``` -## func [Pull]() +## func [Pull]() ```go func Pull[V any](seq Seq[V]) (next func() (V, bool), stop func()) @@ -642,7 +651,7 @@ func main() { -## func [Pull2]() +## func [Pull2]() ```go func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func()) @@ -651,7 +660,7 @@ func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func()) -## func [Reduce]() +## func [Reduce]() ```go func Reduce[Sum, V any](f func(Sum, V) Sum, sum Sum, seq Seq[V]) Sum @@ -710,7 +719,7 @@ func main() { -## func [Reduce2]() +## func [Reduce2]() ```go func Reduce2[Sum, K, V any](f func(Sum, K, V) Sum, sum Sum, seq Seq2[K, V]) Sum @@ -719,7 +728,7 @@ func Reduce2[Sum, K, V any](f func(Sum, K, V) Sum, sum Sum, seq Seq2[K, V]) Sum Reduce2 combines the values in seq using f. For each pair k, v in seq, it updates sum = f\(sum, k, v\) and then returns the final sum. For example, if iterating over seq yields \(k1, v1\), \(k2, v2\), \(k3, v3\) Reduce returns f\(f\(f\(sum, k1, v1\), k2, v2\), k3, v3\). -## func [Sum]() +## func [Sum]() ```go func Sum[T constraints.Number](seq Seq[T]) T @@ -755,7 +764,7 @@ for v := range ch { ``` -## func [ToMap]() +## func [ToMap]() ```go func ToMap[K comparable, V any](seq Seq2[K, V]) (out map[K]V) @@ -764,7 +773,7 @@ func ToMap[K comparable, V any](seq Seq2[K, V]) (out map[K]V) -## func [ToMapFromSeq]() +## func [ToMapFromSeq]() ```go func ToMapFromSeq[K comparable, V any](seq Seq[K], fn func(k K) V) (out map[K]V) @@ -773,7 +782,7 @@ func ToMapFromSeq[K comparable, V any](seq Seq[K], fn func(k K) V) (out map[K]V) -## func [ToSlice]() +## func [ToSlice]() ```go func ToSlice[T any](seq Seq[T]) (out []T) @@ -782,7 +791,7 @@ func ToSlice[T any](seq Seq[T]) (out []T) ToSlice returns the elements in seq as a slice. -## func [ToSliceN]() +## func [ToSliceN]() ```go func ToSliceN[T any](seq Seq[T], n int) (out []T) @@ -791,7 +800,7 @@ func ToSliceN[T any](seq Seq[T], n int) (out []T) ToSliceN pull out n elements from seq. -## func [ToSliceSeq2Key]() +## func [ToSliceSeq2Key]() ```go func ToSliceSeq2Key[K, V any](seq Seq2[K, V]) (out []K) @@ -808,7 +817,7 @@ keys := ToSliceSeq2Key(seq) ``` -## func [ToSliceSeq2Value]() +## func [ToSliceSeq2Value]() ```go func ToSliceSeq2Value[K, V any](seq Seq2[K, V]) (out []V) @@ -825,7 +834,7 @@ values := ToSliceSeq2Value(seq) ``` -## type [Seq]() +## type [Seq]() Seq is a sequence of elements provided by an iterator\-like function. We made this alias Seq to iter.Seq for providing a compatible interface in lower go versions. @@ -834,7 +843,7 @@ type Seq[V any] iter.Seq[V] ``` -### func [Chunk]() +### func [Chunk]() ```go func Chunk[T any](seq Seq[T], n int) Seq[[]T] @@ -851,7 +860,7 @@ chunkedSeq := xiter.Chunk(seq, 2) ``` -### func [Compact]() +### func [Compact]() ```go func Compact[T comparable](in Seq[T]) Seq[T] @@ -866,7 +875,7 @@ Compact([]int{0, 1, 2, 3, 4}) 👉 [1 2 3 4] ``` -### func [Concat]() +### func [Concat]() ```go func Concat[V any](seqs ...Seq[V]) Seq[V] @@ -934,8 +943,47 @@ seq := xiter.Cycle(xiter.FromSlice([]int{1, 2, 3})) // seq will yield: 1, 2, 3, 1, 2, 3, 1, 2, 3, ... ``` + +### func [DistinctBy]() + +```go +func DistinctBy[V any, K comparable](f func(V) K, seq Seq[V]) Seq[V] +``` + +DistinctBy returns a Seq that removes duplicate elements based on a key function. Unlike Uniq, which requires directly comparable elements, DistinctBy lets you define how to derive a comparable key from each element. + +Example: + +``` +type Person struct{ Name string; Age int } +people := xiter.FromSlice([]Person{{"alice", 30}, {"bob", 30}, {"alice", 25}}) +uniq := xiter.DistinctBy(people, func(p Person) string { return p.Name }) +fmt.Println(xiter.ToSlice(uniq)) +// output: +// [{alice 30} {bob 30}] +``` + + +### func [DropWhile]() + +```go +func DropWhile[V any](f func(V) bool, seq Seq[V]) Seq[V] +``` + +DropWhile skips elements from seq while f\(v\) returns true, then yields the remaining elements. + +Example: + +``` +seq := xiter.FromSlice([]int{1, 3, 5, 7, 2, 4}) +dropped := xiter.DropWhile(func(v int) bool { return v%2 == 1 }, seq) +fmt.Println(xiter.ToSlice(dropped)) +// output: +// [2 4] +``` + -### func [Filter]() +### func [Filter]() ```go func Filter[V any](f func(V) bool, seq Seq[V]) Seq[V] @@ -981,6 +1029,49 @@ func main() {

+ +### func [FlatMap]() + +```go +func FlatMap[In, Out any](f func(In) Seq[Out], seq Seq[In]) Seq[Out] +``` + +FlatMap maps each element in seq to a sequence using f, then flattens all resulting sequences into a single sequence. + +Example: + +``` +seq := xiter.FromSlice([]int{1, 2, 3}) +flatMapped := xiter.FlatMap(func(v int) xiter.Seq[int] { + return xiter.FromSlice([]int{v, v * 10}) +}, seq) +fmt.Println(xiter.ToSlice(flatMapped)) +// output: +// [1 10 2 20 3 30] +``` + + +### func [Flatten]() + +```go +func Flatten[T any](seq Seq[Seq[T]]) Seq[T] +``` + +Flatten flattens a sequence of sequences into a single sequence. + +Example: + +``` +seq := xiter.FromSlice([]xiter.Seq[int]{ + xiter.FromSlice([]int{1, 2}), + xiter.FromSlice([]int{3, 4, 5}), +}) +flat := xiter.Flatten(seq) +fmt.Println(xiter.ToSlice(flat)) +// output: +// [1 2 3 4 5] +``` + ### func [FromChan]() @@ -1005,7 +1096,7 @@ _ = ToSlice(seq) // Returns []int{1, 2} ``` -### func [FromMapKeys]() +### func [FromMapKeys]() ```go func FromMapKeys[K comparable, V any](m map[K]V) Seq[K] @@ -1014,7 +1105,7 @@ func FromMapKeys[K comparable, V any](m map[K]V) Seq[K] -### func [FromMapValues]() +### func [FromMapValues]() ```go func FromMapValues[K comparable, V any](m map[K]V) Seq[V] @@ -1041,7 +1132,7 @@ func FromSliceReverse[T any, Slice ~[]T](in Slice) Seq[T] -### func [FromSliceShuffle]() +### func [FromSliceShuffle]() ```go func FromSliceShuffle[T any](in []T) Seq[T] @@ -1092,8 +1183,27 @@ intersect := Intersect(FromSlice(left), FromSlice(right)) // intersect 👉 [3 4] ``` + +### func [Intersperse]() + +```go +func Intersperse[V any](sep V, seq Seq[V]) Seq[V] +``` + +Intersperse places sep between consecutive elements of seq. + +Example: + +``` +seq := xiter.FromSlice([]int{1, 2, 3}) +withSep := xiter.Intersperse(0, seq) +fmt.Println(xiter.ToSlice(withSep)) +// output: +// [1 0 2 0 3] +``` + -### func [Limit]() +### func [Limit]() ```go func Limit[V any](seq Seq[V], n int) Seq[V] @@ -1142,7 +1252,7 @@ func main() { -### func [Map]() +### func [Map]() ```go func Map[In, Out any](f func(In) Out, seq Seq[In]) Seq[Out] @@ -1191,7 +1301,7 @@ func main() { -### func [Merge]() +### func [Merge]() ```go func Merge[V xcmp.Ordered](x, y Seq[V]) Seq[V] @@ -1233,7 +1343,7 @@ func main() { -### func [MergeFunc]() +### func [MergeFunc]() ```go func MergeFunc[V any](x, y Seq[V], f func(V, V) int) Seq[V] @@ -1270,7 +1380,7 @@ func Repeat[T any](seq Seq[T], count int) Seq[T] Repeat return a seq that repeat seq for count times. -### func [Replace]() +### func [Replace]() ```go func Replace[T comparable](seq Seq[T], from, to T, n int) Seq[T] @@ -1287,7 +1397,7 @@ replacedSeq := Replace(seq, 2, 99, -1) // Replace all 2s with 99 ``` -### func [ReplaceAll]() +### func [ReplaceAll]() ```go func ReplaceAll[T comparable](seq Seq[T], from, to T) Seq[T] @@ -1313,7 +1423,7 @@ func Reverse[T any](seq Seq[T]) Seq[T] Reverse return a reversed seq. -### func [Seq2KeyToSeq]() +### func [Seq2KeyToSeq]() ```go func Seq2KeyToSeq[K, V any](in Seq2[K, V]) Seq[K] @@ -1322,7 +1432,7 @@ func Seq2KeyToSeq[K, V any](in Seq2[K, V]) Seq[K] Seq2KeyToSeq return a seq that only contain keys in seq2. -### func [Seq2ToSeqUnion]() +### func [Seq2ToSeqUnion]() ```go func Seq2ToSeqUnion[K, V any](seq Seq2[K, V]) Seq[union.U2[K, V]] @@ -1340,7 +1450,7 @@ for v := range Seq2ToSeqUnion(seq2) { ``` -### func [Seq2ValueToSeq]() +### func [Seq2ValueToSeq]() ```go func Seq2ValueToSeq[K, V any](in Seq2[K, V]) Seq[V] @@ -1349,7 +1459,7 @@ func Seq2ValueToSeq[K, V any](in Seq2[K, V]) Seq[V] Seq2ValueToSeq return a seq that only contain values in seq2. -### func [Skip]() +### func [Skip]() ```go func Skip[T any](seq Seq[T], n int) Seq[T] @@ -1357,6 +1467,83 @@ func Skip[T any](seq Seq[T], n int) Seq[T] Skip return a seq that skip n elements from seq. + +### func [SortBy]() + +```go +func SortBy[V any, K constraints.Ordered](seq Seq[V], f func(V) K) Seq[V] +``` + +SortBy returns a Seq that yields elements from seq sorted by the key extracted by function f. All elements are materialized for sorting. + +Example: + +``` +type Person struct{ Name string; Age int } +people := xiter.FromSlice([]Person{{"alice", 30}, {"bob", 25}}) +sorted := xiter.SortBy(people, func(p Person) int { return p.Age }) +fmt.Println(xiter.ToSlice(sorted)) +// output: +// [{bob 25} {alice 30}] +``` + + +### func [Sorted]() + +```go +func Sorted[V constraints.Ordered](seq Seq[V]) Seq[V] +``` + +Sorted returns a Seq that yields elements from seq in ascending order. All elements are materialized for sorting. + +Example: + +``` +sorted := xiter.Sorted(xiter.FromSlice([]int{3, 1, 4, 1, 5})) +fmt.Println(xiter.ToSlice(sorted)) +// output: +// [1 1 3 4 5] +``` + + +### func [Split]() + +```go +func Split[V any](f func(V) bool, seq Seq[V]) (tru, fls Seq[V]) +``` + +Split splits seq into two sequences based on predicate f. The first sequence contains elements where f\(v\) is true, the second contains elements where f\(v\) is false. Both sequences are materialized eagerly. + +Example: + +``` +tru, fls := xiter.Split(func(v int) bool { return v%2 == 0 }, xiter.FromSlice([]int{1, 2, 3, 4, 5})) +fmt.Println(xiter.ToSlice(tru)) +fmt.Println(xiter.ToSlice(fls)) +// output: +// [2 4] +// [1 3 5] +``` + + +### func [TakeWhile]() + +```go +func TakeWhile[V any](f func(V) bool, seq Seq[V]) Seq[V] +``` + +TakeWhile yields elements from seq as long as f\(v\) returns true, then stops. + +Example: + +``` +seq := xiter.FromSlice([]int{1, 3, 5, 7, 2}) +taken := xiter.TakeWhile(func(v int) bool { return v%2 == 1 }, seq) +fmt.Println(xiter.ToSlice(taken)) +// output: +// [1 3 5 7] +``` + ### func [Union]() @@ -1376,7 +1563,7 @@ union := Union(FromSlice(left), FromSlice(right)) ``` -### func [Uniq]() +### func [Uniq]() ```go func Uniq[T comparable](seq Seq[T]) Seq[T] @@ -1393,7 +1580,7 @@ uniqSeq := xiter.Uniq(seq) ``` -### func [Zip]() +### func [Zip]() ```go func Zip[V1, V2 any](x Seq[V1], y Seq[V2]) Seq[Zipped[V1, V2]] @@ -1451,7 +1638,7 @@ func main() { -### func [Zip2]() +### func [Zip2]() ```go func Zip2[K1, V1, K2, V2 any](x Seq2[K1, V1], y Seq2[K2, V2]) Seq[Zipped2[K1, V1, K2, V2]] @@ -1473,7 +1660,7 @@ func Equal2[K, V comparable](x, y Seq2[K, V]) bool { ``` -## type [Seq2]() +## type [Seq2]() Seq2 is a sequence of key/value pair provided by an iterator\-like function. We made this alias Seq2 to iter.Seq2 for providing a compatible interface in lower go versions. @@ -1482,7 +1669,7 @@ type Seq2[K, V any] iter.Seq2[K, V] ``` -### func [Concat2]() +### func [Concat2]() ```go func Concat2[K, V any](seqs ...Seq2[K, V]) Seq2[K, V] @@ -1491,7 +1678,7 @@ func Concat2[K, V any](seqs ...Seq2[K, V]) Seq2[K, V] Concat2 returns an Seq2 over the concatenation of the given Seq2s. Like Concat but run with Seq2 -### func [Filter2]() +### func [Filter2]() ```go func Filter2[K, V any](f func(K, V) bool, seq Seq2[K, V]) Seq2[K, V] @@ -1500,7 +1687,7 @@ func Filter2[K, V any](f func(K, V) bool, seq Seq2[K, V]) Seq2[K, V] Filter2 returns an Seq over seq that only includes the key\-value pairs k, v for which f\(k, v\) is true. Like Filter but run with Seq2 -### func [FromMapKeyAndValues]() +### func [FromMapKeyAndValues]() ```go func FromMapKeyAndValues[K comparable, V any](m map[K]V) Seq2[K, V] @@ -1518,7 +1705,7 @@ func FromSliceIdx[T any](in []T) Seq2[int, T] FromSliceIdx received a slice and returned a Seq2 for this slice, key is index. -### func [Limit2]() +### func [Limit2]() ```go func Limit2[K, V any](seq Seq2[K, V], n int) Seq2[K, V] @@ -1527,7 +1714,7 @@ func Limit2[K, V any](seq Seq2[K, V], n int) Seq2[K, V] Limit2 returns a Seq over Seq2 that stops after n key\-value pairs. Like Limit but run with Seq2 -### func [Map2]() +### func [Map2]() ```go func Map2[KIn, VIn, KOut, VOut any](f func(KIn, VIn) (KOut, VOut), seq Seq2[KIn, VIn]) Seq2[KOut, VOut] @@ -1536,7 +1723,7 @@ func Map2[KIn, VIn, KOut, VOut any](f func(KIn, VIn) (KOut, VOut), seq Seq2[KIn, Map2 returns a Seq2 over the results of applying f to each key\-value pair in seq. Like Map but run with Seq2 -### func [MapToSeq2]() +### func [MapToSeq2]() ```go func MapToSeq2[T any, K comparable](in Seq[T], mapFn func(ele T) K) Seq2[K, T] @@ -1557,7 +1744,7 @@ fmt.Println(ToMap(lenMap)) ``` -### func [MapToSeq2Value]() +### func [MapToSeq2Value]() ```go func MapToSeq2Value[T any, K comparable, V any](in Seq[T], mapFn func(ele T) (K, V)) Seq2[K, V] @@ -1577,7 +1764,7 @@ fmt.Println(ToMap(transformed)) ``` -### func [Merge2]() +### func [Merge2]() ```go func Merge2[K xcmp.Ordered, V any](x, y Seq2[K, V]) Seq2[K, V] @@ -1588,7 +1775,7 @@ Merge2 merges two sequences of key\-value pairs ordered by their keys. Pairs app Merge2 is equivalent to calling MergeFunc2 with cmp.Compare\[K\] as the ordering function. -### func [MergeFunc2]() +### func [MergeFunc2]() ```go func MergeFunc2[K, V any](x, y Seq2[K, V], f func(K, K) int) Seq2[K, V] @@ -1648,4 +1835,377 @@ type Zipped2[K1, V1, K2, V2 any] struct { } ``` +# collect + +```go +import "github.com/dashjay/xiter/xiter/collect" +``` + +Package collect provides lazy collection and windowing operations for iter.Seq. + +Functions in this package materialize or buffer elements as needed, then yield results as a sequence for downstream processing. + +## Index + +- [func GroupBy\[T any, K comparable\]\(seq xiter.Seq\[T\], keyFn func\(T\) K\) xiter.Seq\[Group\[K, T\]\]](<#GroupBy>) +- [func SortedGroupBy\[T any, K comparable\]\(seq xiter.Seq\[T\], keyFn func\(T\) K\) xiter.Seq\[Group\[K, T\]\]](<#SortedGroupBy>) +- [func Window\[T any\]\(seq xiter.Seq\[T\], n int\) xiter.Seq\[\[\]T\]](<#Window>) +- [type Group](<#Group>) + + + +## func [GroupBy]() + +```go +func GroupBy[T any, K comparable](seq xiter.Seq[T], keyFn func(T) K) xiter.Seq[Group[K, T]] +``` + +GroupBy groups elements from seq by the key returned by keyFn. The entire sequence is materialized to build groups, then the groups are yielded as a Seq\[Group\[K, V\]\]. + +Example: + +``` +words := xiter.FromSlice([]string{"apple", "banana", "avocado", "blueberry"}) +groups := collect.GroupBy(words, func(s string) string { + return string(s[0]) // group by first letter +}) +for g := range groups { + fmt.Printf("%s: %v\n", g.Key, g.Items) +} +``` + + +## func [SortedGroupBy]() + +```go +func SortedGroupBy[T any, K comparable](seq xiter.Seq[T], keyFn func(T) K) xiter.Seq[Group[K, T]] +``` + +SortedGroupBy returns groups from a pre\-sorted sequence. The input seq must already be sorted by the key returned by keyFn. Unlike GroupBy, this function uses O\(1\) memory per group and yields each group as soon as the key changes. + +If the input is not sorted by key, groups will be split incorrectly \(the same key may appear in multiple groups\). + +Example: + +``` +words := xiter.FromSlice([]string{"apple", "avocado", "banana", "blueberry"}) +groups := collect.SortedGroupBy(words, func(s string) string { + return string(s[0]) // input must be sorted by first letter +}) +``` + + +## func [Window]() + +```go +func Window[T any](seq xiter.Seq[T], n int) xiter.Seq[[]T] +``` + +Window yields sliding windows of n elements from seq. Each window is a newly allocated slice, overlapping by n\-1 elements. If seq has fewer than n elements, no windows are yielded. + +Example: + +``` +seq := xiter.FromSlice([]int{1, 2, 3, 4, 5, 6}) +for w := range collect.Window(seq, 3) { + fmt.Println(w) // [1 2 3] [2 3 4] [3 4 5] [4 5 6] +} +``` + + +## type [Group]() + +Group represents a group of values sharing the same key, produced by GroupBy or SortedGroupBy. + +```go +type Group[K comparable, V any] struct { + Key K + Items []V +} +``` + +# combin + +```go +import "github.com/dashjay/xiter/xiter/combin" +``` + +Package combin provides combinatorial generators for iter.Seq. + +These functions generate combinations, permutations, and cartesian products as lazy sequences. Input sequences are materialized internally since combinatorial operations require random access to elements. + +## Index + +- [func Combinations\[T any\]\(seq xiter.Seq\[T\], k int\) xiter.Seq\[\[\]T\]](<#Combinations>) +- [func Permutations\[T any\]\(seq xiter.Seq\[T\], k int\) xiter.Seq\[\[\]T\]](<#Permutations>) +- [func Product\[T any\]\(seqs ...xiter.Seq\[T\]\) xiter.Seq\[\[\]T\]](<#Product>) + + + +## func [Combinations]() + +```go +func Combinations[T any](seq xiter.Seq[T], k int) xiter.Seq[[]T] +``` + +Combinations yields all k\-length combinations of elements from seq. Results are yielded in lexicographic order of indices. If seq has fewer than k elements, an empty sequence is returned. + +Example: + +``` +seq := xiter.FromSlice([]string{"a", "b", "c"}) +for comb := range combin.Combinations(seq, 2) { + fmt.Println(comb) // [a b] [a c] [b c] +} +``` + + +## func [Permutations]() + +```go +func Permutations[T any](seq xiter.Seq[T], k int) xiter.Seq[[]T] +``` + +Permutations yields all k\-length permutations of elements from seq. Results are yielded in lexicographic order of indices. If seq has fewer than k elements, an empty sequence is returned. + +Example: + +``` +seq := xiter.FromSlice([]string{"a", "b", "c"}) +for perm := range combin.Permutations(seq, 2) { + fmt.Println(perm) // [a b] [a c] [b a] [b c] [c a] [c b] +} +``` + + +## func [Product]() + +```go +func Product[T any](seqs ...xiter.Seq[T]) xiter.Seq[[]T] +``` + +Product yields the cartesian product of the input sequences. All input sequences are materialized internally. If any input sequence is empty, an empty sequence is yielded. + +Example: + +``` +colors := xiter.FromSlice([]string{"red", "blue"}) +sizes := xiter.FromSlice([]string{"S", "M", "L"}) +for p := range combin.Product(colors, sizes) { + fmt.Println(p) // [red S] [red M] [red L] [blue S] [blue M] [blue L] +} +``` + +# io + +```go +import "github.com/dashjay/xiter/xiter/io" +``` + +Package io provides lazy I/O operations that produce iter.Seq sequences. + +Unlike standard I/O functions that read entire files into memory, these functions yield elements on demand, enabling processing of large or streaming data with bounded memory. + +## Index + +- [func Lines\(r io.Reader\) xiter.Seq\[string\]](<#Lines>) +- [func ReadDir\(dir string\) xiter.Seq\[fs.DirEntry\]](<#ReadDir>) +- [func ReadFileByChunk\(filename string, size int\) xiter.Seq\[\[\]byte\]](<#ReadFileByChunk>) + + + +## func [Lines]() + +```go +func Lines(r io.Reader) xiter.Seq[string] +``` + +Lines reads from r line by line, yielding each line as a string. The iteration stops when the reader is exhausted or the consumer stops iterating. + +Example: + +``` +f, _ := os.Open("file.txt") +defer f.Close() +for line := range iox.Lines(f) { + fmt.Println(line) +} +``` + + +## func [ReadDir]() + +```go +func ReadDir(dir string) xiter.Seq[fs.DirEntry] +``` + +ReadDir returns a Seq of directory entries for the specified directory. If the directory cannot be read, an empty sequence is returned. + +Example: + +``` +for entry := range iox.ReadDir(".") { + fmt.Println(entry.Name()) +} +``` + + +## func [ReadFileByChunk]() + +```go +func ReadFileByChunk(filename string, size int) xiter.Seq[[]byte] +``` + +ReadFileByChunk reads the file at filename in chunks of the specified size. Each chunk is a newly allocated slice of bytes. If the file cannot be opened, an empty sequence is returned. + +Example: + +``` +for chunk := range iox.ReadFileByChunk("large.bin", 4096) { + process(chunk) +} +``` + +# rt + +```go +import "github.com/dashjay/xiter/xiter/rt" +``` + +Package rt provides real\-time event stream utilities built on iter.Seq. + +These functions enable time\-based operations on sequences, such as periodic ticks, rate\-limiting, and debouncing. + +## Index + +- [func Debounce\[T any\]\(seq xiter.Seq\[T\], d time.Duration\) xiter.Seq\[T\]](<#Debounce>) +- [func Throttle\[T any\]\(seq xiter.Seq\[T\], d time.Duration\) xiter.Seq\[T\]](<#Throttle>) +- [func Ticker\(d time.Duration\) xiter.Seq\[time.Time\]](<#Ticker>) + + + +## func [Debounce]() + +```go +func Debounce[T any](seq xiter.Seq[T], d time.Duration) xiter.Seq[T] +``` + +Debounce yields values from seq only after a quiet period of duration d has elapsed since the last value was received. If new values arrive before the quiet period elapses, the timer resets. The final value is always flushed when the input sequence is exhausted. + +Example: + +``` +for v := range rt.Debounce(eventStream, 100*time.Millisecond) { + fmt.Println("debounced:", v) +} +``` + + +## func [Throttle]() + +```go +func Throttle[T any](seq xiter.Seq[T], d time.Duration) xiter.Seq[T] +``` + +Throttle limits the rate of values from seq, yielding at most one value per duration d. + +Example: + +``` +seq := xiter.FromSlice([]int{1, 2, 3, 4, 5}) +for v := range rt.Throttle(seq, 100*time.Millisecond) { + fmt.Println(v) // printed at most every 100ms +} +``` + + +## func [Ticker]() + +```go +func Ticker(d time.Duration) xiter.Seq[time.Time] +``` + +Ticker returns a sequence that yields the current time at the specified interval. The sequence is unbounded; use with Limit, TakeWhile, etc. to constrain. + +Example: + +``` +for t := range rt.Ticker(time.Second) { + fmt.Println("tick at", t) +} +``` + +# stream + +```go +import "github.com/dashjay/xiter/xiter/stream" +``` + +Package stream provides concurrent stream processing utilities for iter.Seq. + +These functions enable parallel and batched processing of sequences, useful for CPU\-bound or I/O\-bound workloads where goroutines can improve throughput. + +## Index + +- [func Batch\[T any\]\(seq xiter.Seq\[T\], n int\) xiter.Seq\[\[\]T\]](<#Batch>) +- [func FanIn\[T any\]\(seqs ...xiter.Seq\[T\]\) xiter.Seq\[T\]](<#FanIn>) +- [func ParallelMap\[T, R any\]\(seq xiter.Seq\[T\], fn func\(T\) R, n int\) xiter.Seq\[R\]](<#ParallelMap>) + + + +## func [Batch]() + +```go +func Batch[T any](seq xiter.Seq[T], n int) xiter.Seq[[]T] +``` + +Batch groups elements from seq into slices of at most n elements. The last batch may contain fewer than n elements. + +Example: + +``` +seq := xiter.FromSlice([]int{1, 2, 3, 4, 5}) +for batch := range stream.Batch(seq, 2) { + fmt.Println(batch) // [1 2] [3 4] [5] +} +``` + + +## func [FanIn]() + +```go +func FanIn[T any](seqs ...xiter.Seq[T]) xiter.Seq[T] +``` + +FanIn merges multiple sequences into a single sequence. Values from all input sequences are interleaved as they arrive. The returned sequence ends when all input sequences are exhausted. + +Example: + +``` +seq1 := xiter.FromSlice([]int{1, 2, 3}) +seq2 := xiter.FromSlice([]int{4, 5, 6}) +for v := range stream.FanIn(seq1, seq2) { + fmt.Println(v) // may print 1,4,2,5,3,6 in any interleaving +} +``` + + +## func [ParallelMap]() + +```go +func ParallelMap[T, R any](seq xiter.Seq[T], fn func(T) R, n int) xiter.Seq[R] +``` + +ParallelMap applies fn to each element in seq concurrently using n worker goroutines. Results are yielded in non\-deterministic order as workers complete. + +Example: + +``` +results := stream.ParallelMap( + xiter.FromSlice([]int{1, 2, 3, 4, 5}), + func(v int) int { return v * 2 }, + 3, +) +``` + Generated by [gomarkdoc]() diff --git a/xiter/collect/collect.go b/xiter/collect/collect.go new file mode 100644 index 0000000..4531dae --- /dev/null +++ b/xiter/collect/collect.go @@ -0,0 +1,113 @@ +//go:build go1.23 + +// Package collect provides lazy collection and windowing operations for iter.Seq. +// +// Functions in this package materialize or buffer elements as needed, then +// yield results as a sequence for downstream processing. +package collect + +import "github.com/dashjay/xiter/xiter" + +// Group represents a group of values sharing the same key, produced by +// GroupBy or SortedGroupBy. +type Group[K comparable, V any] struct { + Key K + Items []V +} + +// GroupBy groups elements from seq by the key returned by keyFn. +// The entire sequence is materialized to build groups, then the groups +// are yielded as a Seq[Group[K, V]]. +// +// Example: +// +// words := xiter.FromSlice([]string{"apple", "banana", "avocado", "blueberry"}) +// groups := collect.GroupBy(words, func(s string) string { +// return string(s[0]) // group by first letter +// }) +// for g := range groups { +// fmt.Printf("%s: %v\n", g.Key, g.Items) +// } +func GroupBy[T any, K comparable](seq xiter.Seq[T], keyFn func(T) K) xiter.Seq[Group[K, T]] { + return func(yield func(Group[K, T]) bool) { + groups := make(map[K][]T) + var keys []K + for v := range seq { + k := keyFn(v) + if _, ok := groups[k]; !ok { + keys = append(keys, k) + } + groups[k] = append(groups[k], v) + } + for _, k := range keys { + if !yield(Group[K, T]{Key: k, Items: groups[k]}) { + return + } + } + } +} + +// SortedGroupBy returns groups from a pre-sorted sequence. +// The input seq must already be sorted by the key returned by keyFn. +// Unlike GroupBy, this function uses O(1) memory per group and yields +// each group as soon as the key changes. +// +// If the input is not sorted by key, groups will be split incorrectly +// (the same key may appear in multiple groups). +// +// Example: +// +// words := xiter.FromSlice([]string{"apple", "avocado", "banana", "blueberry"}) +// groups := collect.SortedGroupBy(words, func(s string) string { +// return string(s[0]) // input must be sorted by first letter +// }) +func SortedGroupBy[T any, K comparable](seq xiter.Seq[T], keyFn func(T) K) xiter.Seq[Group[K, T]] { + return func(yield func(Group[K, T]) bool) { + var current *Group[K, T] + for v := range seq { + k := keyFn(v) + if current == nil || current.Key != k { + if current != nil { + if !yield(*current) { + return + } + } + current = &Group[K, T]{Key: k} + } + current.Items = append(current.Items, v) + } + if current != nil && len(current.Items) > 0 { + yield(*current) + } + } +} + +// Window yields sliding windows of n elements from seq. +// Each window is a newly allocated slice, overlapping by n-1 elements. +// If seq has fewer than n elements, no windows are yielded. +// +// Example: +// +// seq := xiter.FromSlice([]int{1, 2, 3, 4, 5, 6}) +// for w := range collect.Window(seq, 3) { +// fmt.Println(w) // [1 2 3] [2 3 4] [3 4 5] [4 5 6] +// } +func Window[T any](seq xiter.Seq[T], n int) xiter.Seq[[]T] { + return func(yield func([]T) bool) { + if n <= 0 { + return + } + window := make([]T, 0, n) + for v := range seq { + window = append(window, v) + if len(window) == n { + w := make([]T, n) + copy(w, window) + if !yield(w) { + return + } + window = window[1:] + } + } + } +} diff --git a/xiter/collect/collect_test.go b/xiter/collect/collect_test.go new file mode 100644 index 0000000..6fc3648 --- /dev/null +++ b/xiter/collect/collect_test.go @@ -0,0 +1,211 @@ +//go:build go1.23 + +package collect_test + +import ( + "testing" + + "github.com/dashjay/xiter/xiter" + "github.com/dashjay/xiter/xiter/collect" +) + +func toSlice[T any](seq xiter.Seq[T]) []T { + var out []T + for v := range seq { + out = append(out, v) + } + return out +} + +func TestGroupBy(t *testing.T) { + t.Run("basic", func(t *testing.T) { + seq := xiter.FromSlice([]string{"apple", "avocado", "banana", "blueberry", "cherry"}) + groups := toSlice(collect.GroupBy(seq, func(s string) string { + return string(s[0]) + })) + + if len(groups) != 3 { + t.Fatalf("GroupBy: got %d groups, want 3", len(groups)) + } + + for _, g := range groups { + switch g.Key { + case "a": + if len(g.Items) != 2 { + t.Fatalf("GroupBy 'a': got %v, want 2 items", g.Items) + } + case "b": + if len(g.Items) != 2 { + t.Fatalf("GroupBy 'b': got %v, want 2 items", g.Items) + } + case "c": + if len(g.Items) != 1 || g.Items[0] != "cherry" { + t.Fatalf("GroupBy 'c': got %v, want [cherry]", g.Items) + } + default: + t.Fatalf("GroupBy: unexpected key %q", g.Key) + } + } + }) + + t.Run("empty", func(t *testing.T) { + groups := toSlice(collect.GroupBy(xiter.FromSlice([]int{}), func(v int) int { + return v % 2 + })) + if len(groups) != 0 { + t.Fatalf("GroupBy empty: got %d groups, want 0", len(groups)) + } + }) + + t.Run("single element", func(t *testing.T) { + groups := toSlice(collect.GroupBy(xiter.FromSlice([]int{42}), func(v int) int { + return v % 2 + })) + if len(groups) != 1 || groups[0].Key != 0 || len(groups[0].Items) != 1 || groups[0].Items[0] != 42 { + t.Fatalf("GroupBy single: got %v", groups) + } + }) + + t.Run("all same key", func(t *testing.T) { + seq := xiter.FromSlice([]int{1, 2, 3, 4, 5}) + groups := toSlice(collect.GroupBy(seq, func(v int) int { + return 1 + })) + if len(groups) != 1 || len(groups[0].Items) != 5 { + t.Fatalf("GroupBy same: got %d groups with %d items", len(groups), len(groups[0].Items)) + } + }) +} + +func TestSortedGroupBy(t *testing.T) { + t.Run("basic", func(t *testing.T) { + seq := xiter.FromSlice([]string{"apple", "avocado", "banana", "blueberry"}) + groups := toSlice(collect.SortedGroupBy(seq, func(s string) string { + return string(s[0]) + })) + + if len(groups) != 2 { + t.Fatalf("SortedGroupBy: got %d groups, want 2", len(groups)) + } + if groups[0].Key != "a" || len(groups[0].Items) != 2 { + t.Fatalf("SortedGroupBy[0]: key=%q items=%v", groups[0].Key, groups[0].Items) + } + if groups[1].Key != "b" || len(groups[1].Items) != 2 { + t.Fatalf("SortedGroupBy[1]: key=%q items=%v", groups[1].Key, groups[1].Items) + } + }) + + t.Run("empty", func(t *testing.T) { + groups := toSlice(collect.SortedGroupBy(xiter.FromSlice([]string{}), func(s string) string { + return s + })) + if len(groups) != 0 { + t.Fatalf("SortedGroupBy empty: got %d, want 0", len(groups)) + } + }) + + t.Run("single element", func(t *testing.T) { + groups := toSlice(collect.SortedGroupBy(xiter.FromSlice([]string{"hello"}), func(s string) string { + return string(s[0]) + })) + if len(groups) != 1 || groups[0].Key != "h" || len(groups[0].Items) != 1 { + t.Fatalf("SortedGroupBy single: got %v", groups) + } + }) + + t.Run("unsorted input splits keys", func(t *testing.T) { + seq := xiter.FromSlice([]string{"apple", "banana", "avocado"}) + groups := toSlice(collect.SortedGroupBy(seq, func(s string) string { + return string(s[0]) + })) + if len(groups) != 3 { + t.Fatalf("SortedGroupBy unsorted: got %d groups, want 3 (a, b, a)", len(groups)) + } + // apple (a) -> group 0 + // banana (b) -> group 1 + // avocado (a) -> group 2 (different from group 0!) + if groups[2].Key != "a" || groups[2].Items[0] != "avocado" { + t.Fatalf("SortedGroupBy unsorted: last group should be (a, [avocado])") + } + }) +} + +func TestWindow(t *testing.T) { + t.Run("basic", func(t *testing.T) { + seq := xiter.FromSlice([]int{1, 2, 3, 4, 5, 6}) + windows := toSlice(collect.Window(seq, 3)) + if len(windows) != 4 { + t.Fatalf("Window: got %d windows, want 4", len(windows)) + } + expected := [][]int{{1, 2, 3}, {2, 3, 4}, {3, 4, 5}, {4, 5, 6}} + for i, w := range windows { + for j, v := range w { + if v != expected[i][j] { + t.Fatalf("Window[%d]: got %v, want %v", i, windows[i], expected[i]) + } + } + } + }) + + t.Run("window equals seq length", func(t *testing.T) { + seq := xiter.FromSlice([]int{1, 2, 3}) + windows := toSlice(collect.Window(seq, 3)) + if len(windows) != 1 || windows[0][0] != 1 || windows[0][1] != 2 || windows[0][2] != 3 { + t.Fatalf("Window full: got %v, want [[1 2 3]]", windows) + } + }) + + t.Run("window larger than seq", func(t *testing.T) { + seq := xiter.FromSlice([]int{1, 2}) + windows := toSlice(collect.Window(seq, 3)) + if len(windows) != 0 { + t.Fatalf("Window larger: got %d windows, want 0", len(windows)) + } + }) + + t.Run("empty", func(t *testing.T) { + windows := toSlice(collect.Window(xiter.FromSlice([]int{}), 3)) + if len(windows) != 0 { + t.Fatalf("Window empty: got %d, want 0", len(windows)) + } + }) + + t.Run("window size 1", func(t *testing.T) { + seq := xiter.FromSlice([]int{1, 2, 3}) + windows := toSlice(collect.Window(seq, 1)) + if len(windows) != 3 { + t.Fatalf("Window size 1: got %d windows, want 3", len(windows)) + } + for i, w := range windows { + if len(w) != 1 || w[0] != i+1 { + t.Fatalf("Window[%d]: got %v, want [%d]", i, w, i+1) + } + } + }) + + t.Run("early stop", func(t *testing.T) { + seq := xiter.FromSlice([]int{1, 2, 3, 4, 5, 6, 7, 8}) + count := 0 + for range collect.Window(seq, 3) { + count++ + if count >= 2 { + break + } + } + if count != 2 { + t.Fatalf("Window early stop: count = %d, want 2", count) + } + }) + + t.Run("zero or negative size", func(t *testing.T) { + seq := xiter.FromSlice([]int{1, 2, 3}) + windows := toSlice(collect.Window(seq, 0)) + if len(windows) != 0 { + t.Fatalf("Window zero: got %d, want 0", len(windows)) + } + windows = toSlice(collect.Window(seq, -1)) + if len(windows) != 0 { + t.Fatalf("Window neg: got %d, want 0", len(windows)) + } + }) +} diff --git a/xiter/combin/combin.go b/xiter/combin/combin.go new file mode 100644 index 0000000..08dac64 --- /dev/null +++ b/xiter/combin/combin.go @@ -0,0 +1,167 @@ +//go:build go1.23 + +// Package combin provides combinatorial generators for iter.Seq. +// +// These functions generate combinations, permutations, and cartesian products +// as lazy sequences. Input sequences are materialized internally since +// combinatorial operations require random access to elements. +package combin + +import "github.com/dashjay/xiter/xiter" + +// Combinations yields all k-length combinations of elements from seq. +// Results are yielded in lexicographic order of indices. +// If seq has fewer than k elements, an empty sequence is returned. +// +// Example: +// +// seq := xiter.FromSlice([]string{"a", "b", "c"}) +// for comb := range combin.Combinations(seq, 2) { +// fmt.Println(comb) // [a b] [a c] [b c] +// } +func Combinations[T any](seq xiter.Seq[T], k int) xiter.Seq[[]T] { + return func(yield func([]T) bool) { + elements := toSlice(seq) + n := len(elements) + if k <= 0 || k > n { + return + } + indices := make([]int, k) + for i := range indices { + indices[i] = i + } + // First combination + comb := make([]T, k) + for i, idx := range indices { + comb[i] = elements[idx] + } + if !yield(comb) { + return + } + for { + i := k - 1 + for i >= 0 && indices[i] == i+n-k { + i-- + } + if i < 0 { + return + } + indices[i]++ + for j := i + 1; j < k; j++ { + indices[j] = indices[j-1] + 1 + } + comb := make([]T, k) + for j, idx := range indices { + comb[j] = elements[idx] + } + if !yield(comb) { + return + } + } + } +} + +// Permutations yields all k-length permutations of elements from seq. +// Results are yielded in lexicographic order of indices. +// If seq has fewer than k elements, an empty sequence is returned. +// +// Example: +// +// seq := xiter.FromSlice([]string{"a", "b", "c"}) +// for perm := range combin.Permutations(seq, 2) { +// fmt.Println(perm) // [a b] [a c] [b a] [b c] [c a] [c b] +// } +func Permutations[T any](seq xiter.Seq[T], k int) xiter.Seq[[]T] { + return func(yield func([]T) bool) { + elements := toSlice(seq) + n := len(elements) + if k <= 0 || k > n { + return + } + selected := make([]int, k) + used := make([]bool, n) + var backtrack func(pos int) bool + backtrack = func(pos int) bool { + if pos == k { + p := make([]T, k) + for i, idx := range selected { + p[i] = elements[idx] + } + return yield(p) + } + for i := 0; i < n; i++ { + if used[i] { + continue + } + used[i] = true + selected[pos] = i + if !backtrack(pos + 1) { + return false + } + used[i] = false + } + return true + } + backtrack(0) + } +} + +// Product yields the cartesian product of the input sequences. +// All input sequences are materialized internally. +// If any input sequence is empty, an empty sequence is yielded. +// +// Example: +// +// colors := xiter.FromSlice([]string{"red", "blue"}) +// sizes := xiter.FromSlice([]string{"S", "M", "L"}) +// for p := range combin.Product(colors, sizes) { +// fmt.Println(p) // [red S] [red M] [red L] [blue S] [blue M] [blue L] +// } +func Product[T any](seqs ...xiter.Seq[T]) xiter.Seq[[]T] { + return func(yield func([]T) bool) { + if len(seqs) == 0 { + return + } + materialized := make([][]T, len(seqs)) + for i, seq := range seqs { + for v := range seq { + materialized[i] = append(materialized[i], v) + } + if len(materialized[i]) == 0 { + return + } + } + indices := make([]int, len(seqs)) + n := len(seqs) + for { + p := make([]T, n) + for i, idx := range indices { + p[i] = materialized[i][idx] + } + if !yield(p) { + return + } + i := n - 1 + for i >= 0 { + indices[i]++ + if indices[i] < len(materialized[i]) { + break + } + indices[i] = 0 + i-- + } + if i < 0 { + return + } + } + } +} + +//nolint:prealloc // size unknown upfront for sequences +func toSlice[T any](seq xiter.Seq[T]) []T { + var result []T + for v := range seq { + result = append(result, v) + } + return result +} diff --git a/xiter/combin/combin_test.go b/xiter/combin/combin_test.go new file mode 100644 index 0000000..3978c1e --- /dev/null +++ b/xiter/combin/combin_test.go @@ -0,0 +1,253 @@ +//go:build go1.23 + +package combin_test + +import ( + "testing" + + "github.com/dashjay/xiter/xiter" + "github.com/dashjay/xiter/xiter/combin" +) + +func toSlice[T any](seq xiter.Seq[T]) []T { + var out []T + for v := range seq { + out = append(out, v) + } + return out +} + +func TestCombinations(t *testing.T) { + t.Run("nCk basic", func(t *testing.T) { + seq := xiter.FromSlice([]string{"a", "b", "c"}) + combs := toSlice(combin.Combinations(seq, 2)) + if len(combs) != 3 { + t.Fatalf("Combinations 3C2: got %d, want 3", len(combs)) + } + expected := [][]string{{"a", "b"}, {"a", "c"}, {"b", "c"}} + for i, c := range combs { + for j, v := range c { + if v != expected[i][j] { + t.Fatalf("Combinations[%d]: got %v, want %v", i, combs, expected) + } + } + } + }) + + t.Run("k=0", func(t *testing.T) { + combs := toSlice(combin.Combinations(xiter.FromSlice([]int{1, 2, 3}), 0)) + if len(combs) != 0 { + t.Fatalf("Combinations k=0: got %d, want 0", len(combs)) + } + }) + + t.Run("k=n", func(t *testing.T) { + combs := toSlice(combin.Combinations(xiter.FromSlice([]int{1, 2, 3}), 3)) + if len(combs) != 1 || len(combs[0]) != 3 { + t.Fatalf("Combinations 3C3: got %v, want [[1 2 3]]", combs) + } + }) + + t.Run("k > n", func(t *testing.T) { + combs := toSlice(combin.Combinations(xiter.FromSlice([]int{1, 2}), 5)) + if len(combs) != 0 { + t.Fatalf("Combinations k>n: got %d, want 0", len(combs)) + } + }) + + t.Run("empty", func(t *testing.T) { + combs := toSlice(combin.Combinations(xiter.FromSlice([]int{}), 2)) + if len(combs) != 0 { + t.Fatalf("Combinations empty: got %d, want 0", len(combs)) + } + }) + + t.Run("k=1", func(t *testing.T) { + combs := toSlice(combin.Combinations(xiter.FromSlice([]int{10, 20, 30}), 1)) + if len(combs) != 3 { + t.Fatalf("Combinations k=1: got %d, want 3", len(combs)) + } + }) + + t.Run("negative k", func(t *testing.T) { + combs := toSlice(combin.Combinations(xiter.FromSlice([]int{1, 2, 3}), -1)) + if len(combs) != 0 { + t.Fatalf("Combinations k=-1: got %d, want 0", len(combs)) + } + }) + + t.Run("5C3 count", func(t *testing.T) { + combs := toSlice(combin.Combinations(xiter.FromSlice([]int{1, 2, 3, 4, 5}), 3)) + if len(combs) != 10 { + t.Fatalf("Combinations 5C3: got %d, want 10", len(combs)) + } + }) +} + +func TestPermutations(t *testing.T) { + t.Run("3P2 basic", func(t *testing.T) { + seq := xiter.FromSlice([]string{"a", "b", "c"}) + perms := toSlice(combin.Permutations(seq, 2)) + if len(perms) != 6 { + t.Fatalf("Permutations 3P2: got %d, want 6", len(perms)) + } + // Verify all permutations are unique + seen := make(map[string]bool) + for _, p := range perms { + key := p[0] + p[1] + if seen[key] { + t.Fatalf("Permutations: duplicate %v", p) + } + seen[key] = true + } + // Verify expected set + expected := []string{"ab", "ac", "ba", "bc", "ca", "cb"} + for _, e := range expected { + if !seen[e] { + t.Fatalf("Permutations: missing %s", e) + } + } + }) + + t.Run("k=0", func(t *testing.T) { + perms := toSlice(combin.Permutations(xiter.FromSlice([]int{1, 2, 3}), 0)) + if len(perms) != 0 { + t.Fatalf("Permutations k=0: got %d, want 0", len(perms)) + } + }) + + t.Run("k=n", func(t *testing.T) { + perms := toSlice(combin.Permutations(xiter.FromSlice([]int{1, 2, 3}), 3)) + if len(perms) != 6 { + t.Fatalf("Permutations 3P3: got %d, want 6", len(perms)) + } + }) + + t.Run("k > n", func(t *testing.T) { + perms := toSlice(combin.Permutations(xiter.FromSlice([]int{1, 2}), 5)) + if len(perms) != 0 { + t.Fatalf("Permutations k>n: got %d, want 0", len(perms)) + } + }) + + t.Run("empty", func(t *testing.T) { + perms := toSlice(combin.Permutations(xiter.FromSlice([]int{}), 2)) + if len(perms) != 0 { + t.Fatalf("Permutations empty: got %d, want 0", len(perms)) + } + }) + + t.Run("single element", func(t *testing.T) { + perms := toSlice(combin.Permutations(xiter.FromSlice([]int{42}), 1)) + if len(perms) != 1 || perms[0][0] != 42 { + t.Fatalf("Permutations single: got %v, want [[42]]", perms) + } + }) + + t.Run("4P3 count", func(t *testing.T) { + perms := toSlice(combin.Permutations(xiter.FromSlice([]int{1, 2, 3, 4}), 3)) + if len(perms) != 24 { + t.Fatalf("Permutations 4P3: got %d, want 24", len(perms)) + } + }) +} + +func TestProduct(t *testing.T) { + t.Run("2x3 basic", func(t *testing.T) { + colors := xiter.FromSlice([]string{"red", "blue"}) + sizes := xiter.FromSlice([]string{"S", "M", "L"}) + products := toSlice(combin.Product(colors, sizes)) + if len(products) != 6 { + t.Fatalf("Product 2x3: got %d, want 6", len(products)) + } + expected := [][]string{ + {"red", "S"}, {"red", "M"}, {"red", "L"}, + {"blue", "S"}, {"blue", "M"}, {"blue", "L"}, + } + for i, p := range products { + for j, v := range p { + if v != expected[i][j] { + t.Fatalf("Product[%d]: got %v, want %v", i, products, expected) + } + } + } + }) + + t.Run("single seq", func(t *testing.T) { + products := toSlice(combin.Product(xiter.FromSlice([]int{1, 2, 3}))) + if len(products) != 3 { + t.Fatalf("Product single: got %d, want 3", len(products)) + } + }) + + t.Run("no seqs", func(t *testing.T) { + products := toSlice(combin.Product[int]()) + if len(products) != 0 { + t.Fatalf("Product none: got %d, want 0", len(products)) + } + }) + + t.Run("empty seq", func(t *testing.T) { + products := toSlice(combin.Product( + xiter.FromSlice([]int{1, 2}), + xiter.FromSlice([]int{}), + )) + if len(products) != 0 { + t.Fatalf("Product empty: got %d, want 0", len(products)) + } + }) + + t.Run("single element seqs", func(t *testing.T) { + products := toSlice(combin.Product( + xiter.FromSlice([]int{1}), + xiter.FromSlice([]int{2}), + xiter.FromSlice([]int{3}), + )) + if len(products) != 1 || len(products[0]) != 3 { + t.Fatalf("Product single elements: got %v, want [[1 2 3]]", products) + } + }) +} + +func TestCombinationsEarlyStop(t *testing.T) { + seq := xiter.FromSlice([]int{1, 2, 3, 4}) + count := 0 + for range combin.Combinations(seq, 2) { + count++ + if count >= 2 { + break + } + } + if count != 2 { + t.Fatalf("Combinations early stop: count = %d, want 2", count) + } +} + +func TestPermutationsEarlyStop(t *testing.T) { + seq := xiter.FromSlice([]int{1, 2, 3}) + count := 0 + for range combin.Permutations(seq, 3) { + count++ + if count >= 2 { + break + } + } + if count != 2 { + t.Fatalf("Permutations early stop: count = %d, want 2", count) + } +} + +func TestProductEarlyStop(t *testing.T) { + colors := xiter.FromSlice([]string{"red", "blue", "green"}) + sizes := xiter.FromSlice([]string{"S", "M", "L"}) + count := 0 + for range combin.Product(colors, sizes) { + count++ + if count >= 3 { + break + } + } + if count != 3 { + t.Fatalf("Product early stop: count = %d, want 3", count) + } +} diff --git a/xiter/io/io.go b/xiter/io/io.go new file mode 100644 index 0000000..a15fc70 --- /dev/null +++ b/xiter/io/io.go @@ -0,0 +1,95 @@ +//go:build go1.23 + +// Package io provides lazy I/O operations that produce iter.Seq sequences. +// +// Unlike standard I/O functions that read entire files into memory, +// these functions yield elements on demand, enabling processing of +// large or streaming data with bounded memory. +package io + +import ( + "bufio" + "io" + "io/fs" + "os" + + "github.com/dashjay/xiter/xiter" +) + +// Lines reads from r line by line, yielding each line as a string. +// The iteration stops when the reader is exhausted or the consumer +// stops iterating. +// +// Example: +// +// f, _ := os.Open("file.txt") +// defer f.Close() +// for line := range iox.Lines(f) { +// fmt.Println(line) +// } +func Lines(r io.Reader) xiter.Seq[string] { + return func(yield func(string) bool) { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + if !yield(scanner.Text()) { + return + } + } + } +} + +// ReadDir returns a Seq of directory entries for the specified directory. +// If the directory cannot be read, an empty sequence is returned. +// +// Example: +// +// for entry := range iox.ReadDir(".") { +// fmt.Println(entry.Name()) +// } +func ReadDir(dir string) xiter.Seq[fs.DirEntry] { + return func(yield func(fs.DirEntry) bool) { + entries, err := os.ReadDir(dir) + if err != nil { + return + } + for _, entry := range entries { + if !yield(entry) { + return + } + } + } +} + +// ReadFileByChunk reads the file at filename in chunks of the specified size. +// Each chunk is a newly allocated slice of bytes. +// If the file cannot be opened, an empty sequence is returned. +// +// Example: +// +// for chunk := range iox.ReadFileByChunk("large.bin", 4096) { +// process(chunk) +// } +//nolint:gosec // G304: file path from caller is intended API +func ReadFileByChunk(filename string, size int) xiter.Seq[[]byte] { + return func(yield func([]byte) bool) { + f, err := os.Open(filename) + if err != nil { + return + } + defer func() { _ = f.Close() }() + buf := make([]byte, size) + for { + n, err := f.Read(buf) + if n > 0 { + chunk := make([]byte, n) + copy(chunk, buf[:n]) + if !yield(chunk) { + return + } + } + if err != nil { + break + } + } + } +} diff --git a/xiter/io/io_test.go b/xiter/io/io_test.go new file mode 100644 index 0000000..a3b17fa --- /dev/null +++ b/xiter/io/io_test.go @@ -0,0 +1,215 @@ +//go:build go1.23 + +package io_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/dashjay/xiter/xiter" + iox "github.com/dashjay/xiter/xiter/io" +) + +func TestLines(t *testing.T) { + t.Run("basic", func(t *testing.T) { + r := strings.NewReader("a\nb\nc\n") + lines := xiter.ToSlice(iox.Lines(r)) + if len(lines) != 3 || lines[0] != "a" || lines[1] != "b" || lines[2] != "c" { + t.Fatalf("Lines: got %v, want [a b c]", lines) + } + }) + + t.Run("empty", func(t *testing.T) { + r := strings.NewReader("") + lines := xiter.ToSlice(iox.Lines(r)) + if len(lines) != 0 { + t.Fatalf("Lines empty: got %v, want []", lines) + } + }) + + t.Run("trailing newline", func(t *testing.T) { + r := strings.NewReader("hello\n") + lines := xiter.ToSlice(iox.Lines(r)) + if len(lines) != 1 || lines[0] != "hello" { + t.Fatalf("Lines trailing: got %v, want [hello]", lines) + } + }) + + t.Run("no trailing newline", func(t *testing.T) { + r := strings.NewReader("hello") + lines := xiter.ToSlice(iox.Lines(r)) + if len(lines) != 1 || lines[0] != "hello" { + t.Fatalf("Lines no trailing: got %v, want [hello]", lines) + } + }) + + t.Run("multiple lines no trailing", func(t *testing.T) { + r := strings.NewReader("a\nb\nc") + lines := xiter.ToSlice(iox.Lines(r)) + if len(lines) != 3 || lines[0] != "a" || lines[1] != "b" || lines[2] != "c" { + t.Fatalf("Lines multi: got %v, want [a b c]", lines) + } + }) + + t.Run("early stop", func(t *testing.T) { + r := strings.NewReader("a\nb\nc\nd\ne\n") + count := 0 + for range iox.Lines(r) { + count++ + if count >= 2 { + break + } + } + if count != 2 { + t.Fatalf("Lines early stop: count = %d, want 2", count) + } + }) +} + +func TestReadDir(t *testing.T) { + t.Run("basic", func(t *testing.T) { + entries := xiter.ToSlice(iox.ReadDir(".")) + if len(entries) == 0 { + t.Fatal("ReadDir: expected at least one entry") + } + found := false + for _, e := range entries { + if e.Name() == "io.go" { + found = true + break + } + } + if !found { + t.Fatal("ReadDir: should contain io.go") + } + }) + + t.Run("nonexistent dir", func(t *testing.T) { + entries := xiter.ToSlice(iox.ReadDir("/nonexistent/path")) + if len(entries) != 0 { + t.Fatalf("ReadDir nonexistent: got %d entries, want 0", len(entries)) + } + }) + + t.Run("early stop", func(t *testing.T) { + count := 0 + for range iox.ReadDir(".") { + count++ + if count >= 1 { + break + } + } + if count != 1 { + t.Fatalf("ReadDir early stop: count = %d, want 1", count) + } + }) +} + +func TestReadFileByChunk(t *testing.T) { + t.Run("basic", func(t *testing.T) { + content := "hello world this is a test file" + dir := t.TempDir() + fpath := filepath.Join(dir, "test.txt") + if err := os.WriteFile(fpath, []byte(content), 0644); err != nil { + t.Fatal(err) + } + + var result []byte + for chunk := range iox.ReadFileByChunk(fpath, 5) { + result = append(result, chunk...) + } + if string(result) != content { + t.Fatalf("ReadFileByChunk: got %q, want %q", string(result), content) + } + }) + + t.Run("nonexistent file", func(t *testing.T) { + chunks := xiter.ToSlice(iox.ReadFileByChunk("/nonexistent/file", 1024)) + if len(chunks) != 0 { + t.Fatalf("ReadFileByChunk nonexistent: got %d chunks, want 0", len(chunks)) + } + }) + + t.Run("empty file", func(t *testing.T) { + dir := t.TempDir() + fpath := filepath.Join(dir, "empty.txt") + if err := os.WriteFile(fpath, []byte{}, 0644); err != nil { + t.Fatal(err) + } + chunks := xiter.ToSlice(iox.ReadFileByChunk(fpath, 1024)) + if len(chunks) != 0 { + t.Fatalf("ReadFileByChunk empty: got %d chunks, want 0", len(chunks)) + } + }) + + t.Run("exact chunk size", func(t *testing.T) { + dir := t.TempDir() + fpath := filepath.Join(dir, "exact.txt") + content := "12345" + if err := os.WriteFile(fpath, []byte(content), 0644); err != nil { + t.Fatal(err) + } + chunks := xiter.ToSlice(iox.ReadFileByChunk(fpath, 5)) + if len(chunks) != 1 { + t.Fatalf("ReadFileByChunk exact: got %d chunks, want 1", len(chunks)) + } + }) + + t.Run("early stop", func(t *testing.T) { + dir := t.TempDir() + fpath := filepath.Join(dir, "early.txt") + content := make([]byte, 100) + for i := range content { + content[i] = byte(i) + } + if err := os.WriteFile(fpath, content, 0644); err != nil { + t.Fatal(err) + } + count := 0 + for range iox.ReadFileByChunk(fpath, 10) { + count++ + if count >= 2 { + break + } + } + if count != 2 { + t.Fatalf("ReadFileByChunk early stop: count = %d, want 2", count) + } + }) + + t.Run("file not closed on early stop", func(t *testing.T) { + // Verify early stop does not leak file handles by creating many files + dir := t.TempDir() + for i := 0; i < 50; i++ { + fpath := filepath.Join(dir, "f.txt") + if err := os.WriteFile(fpath, []byte("data"), 0644); err != nil { + t.Fatal(err) + } + for range iox.ReadFileByChunk(fpath, 1) { + break + } + } + // If we get here without hitting ulimit, file handles are being cleaned up + // (finalizer or GC may handle it; on most systems 50 handles is fine) + }) +} + +func TestLinesFuzzLike(t *testing.T) { + inputs := []string{ + "\n", + "\n\n", + "a\n\nb", + "\na\nb\n", + } + for _, input := range inputs { + lines := xiter.ToSlice(iox.Lines(strings.NewReader(input))) + for _, line := range lines { + if line == "" { + // bufio.Scanner treats consecutive newlines as empty lines; + // this is expected behavior + } + } + } +} diff --git a/xiter/rt/rt.go b/xiter/rt/rt.go new file mode 100644 index 0000000..233ed9f --- /dev/null +++ b/xiter/rt/rt.go @@ -0,0 +1,130 @@ +//go:build go1.23 + +// Package rt provides real-time event stream utilities built on iter.Seq. +// +// These functions enable time-based operations on sequences, such as +// periodic ticks, rate-limiting, and debouncing. +package rt + +import ( + "time" + + "github.com/dashjay/xiter/xiter" +) + +// Ticker returns a sequence that yields the current time at the specified interval. +// The sequence is unbounded; use with Limit, TakeWhile, etc. to constrain. +// +// Example: +// +// for t := range rt.Ticker(time.Second) { +// fmt.Println("tick at", t) +// } +func Ticker(d time.Duration) xiter.Seq[time.Time] { + return func(yield func(time.Time) bool) { + ticker := time.NewTicker(d) + defer ticker.Stop() + for t := range ticker.C { + if !yield(t) { + return + } + } + } +} + +// Throttle limits the rate of values from seq, yielding at most one value +// per duration d. +// +// Example: +// +// seq := xiter.FromSlice([]int{1, 2, 3, 4, 5}) +// for v := range rt.Throttle(seq, 100*time.Millisecond) { +// fmt.Println(v) // printed at most every 100ms +// } +func Throttle[T any](seq xiter.Seq[T], d time.Duration) xiter.Seq[T] { + return func(yield func(T) bool) { + ticker := time.NewTicker(d) + defer ticker.Stop() + for v := range seq { + <-ticker.C + if !yield(v) { + return + } + } + } +} + +// Debounce yields values from seq only after a quiet period of duration d +// has elapsed since the last value was received. If new values arrive before +// the quiet period elapses, the timer resets. The final value is always +// flushed when the input sequence is exhausted. +// +// Example: +// +// for v := range rt.Debounce(eventStream, 100*time.Millisecond) { +// fmt.Println("debounced:", v) +// } +func Debounce[T any](seq xiter.Seq[T], d time.Duration) xiter.Seq[T] { + return func(yield func(T) bool) { + done := make(chan struct{}) + defer close(done) + + in := make(chan T, 1) + go func() { + defer close(in) + for v := range seq { + select { + case in <- v: + case <-done: + return + } + } + }() + + timer := time.NewTimer(d) + if !timer.Stop() { + <-timer.C + } + defer timer.Stop() + + var lastValue T + hasPending := false + + for { + timerC := timer.C + if !hasPending { + timerC = nil + } + + select { + case v, ok := <-in: + if !ok { + if hasPending { + timer.Stop() + if !yield(lastValue) { + return + } + } + return + } + lastValue = v + hasPending = true + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + timer.Reset(d) + + case <-timerC: + if hasPending { + if !yield(lastValue) { + return + } + hasPending = false + } + } + } + } +} diff --git a/xiter/rt/rt_test.go b/xiter/rt/rt_test.go new file mode 100644 index 0000000..5bcc28f --- /dev/null +++ b/xiter/rt/rt_test.go @@ -0,0 +1,131 @@ +//go:build go1.23 + +package rt_test + +import ( + "testing" + "time" + + "github.com/dashjay/xiter/xiter" + "github.com/dashjay/xiter/xiter/rt" +) + +func TestTicker(t *testing.T) { + t.Run("basic", func(t *testing.T) { + count := 0 + for range rt.Ticker(time.Millisecond) { + count++ + if count >= 3 { + break + } + } + if count != 3 { + t.Fatalf("Ticker: got %d ticks, want 3", count) + } + }) + + + t.Run("early stop", func(t *testing.T) { + start := time.Now() + count := 0 + for range rt.Ticker(time.Millisecond) { + count++ + if count >= 2 { + break + } + } + elapsed := time.Since(start) + if count != 2 { + t.Fatalf("Ticker early stop: got %d ticks, want 2", count) + } + if elapsed > 100*time.Millisecond { + t.Fatalf("Ticker early stop took too long: %v", elapsed) + } + }) +} + +func TestThrottle(t *testing.T) { + t.Run("basic", func(t *testing.T) { + seq := xiter.FromSlice([]int{1, 2, 3}) + start := time.Now() + results := xiter.ToSlice(rt.Throttle(seq, time.Millisecond)) + elapsed := time.Since(start) + if len(results) != 3 { + t.Fatalf("Throttle: got %d results, want 3", len(results)) + } + if elapsed < 2*time.Millisecond { + t.Fatalf("Throttle: too fast, elapsed = %v", elapsed) + } + }) + + t.Run("empty", func(t *testing.T) { + results := xiter.ToSlice(rt.Throttle(xiter.FromSlice([]int{}), time.Millisecond)) + if len(results) != 0 { + t.Fatalf("Throttle empty: got %d, want 0", len(results)) + } + }) + + t.Run("single element", func(t *testing.T) { + results := xiter.ToSlice(rt.Throttle(xiter.FromSlice([]int{42}), time.Millisecond)) + if len(results) != 1 || results[0] != 42 { + t.Fatalf("Throttle single: got %v, want [42]", results) + } + }) + + t.Run("early stop", func(t *testing.T) { + seq := xiter.FromSlice([]int{1, 2, 3, 4, 5}) + count := 0 + for range rt.Throttle(seq, time.Millisecond) { + count++ + if count >= 2 { + break + } + } + if count != 2 { + t.Fatalf("Throttle early stop: count = %d, want 2", count) + } + }) +} + +func TestDebounce(t *testing.T) { + t.Run("single value", func(t *testing.T) { + seq := xiter.FromSlice([]int{1}) + results := xiter.ToSlice(rt.Debounce(seq, time.Microsecond)) + if len(results) != 1 || results[0] != 1 { + t.Fatalf("Debounce single: got %v, want [1]", results) + } + }) + + t.Run("rapid values only emit last", func(t *testing.T) { + // Values from a slice arrive faster than the debounce period, + // and verify only the last one is emitted (after the debounce period) + results := xiter.ToSlice(rt.Debounce( + xiter.FromSlice([]int{1, 2, 3, 4, 5}), + 50*time.Millisecond, + )) + if len(results) != 1 || results[0] != 5 { + t.Fatalf("Debounce rapid: got %v, want [5] (len=%d)", results, len(results)) + } + }) + + t.Run("empty", func(t *testing.T) { + results := xiter.ToSlice(rt.Debounce(xiter.FromSlice([]int{}), time.Microsecond)) + if len(results) != 0 { + t.Fatalf("Debounce empty: got %d, want 0", len(results)) + } + }) + + t.Run("early stop", func(t *testing.T) { + seq := xiter.FromSlice([]int{1, 2, 3}) + count := 0 + for range rt.Debounce(seq, time.Microsecond) { + count++ + if count >= 1 { + break + } + } + if count != 1 { + t.Fatalf("Debounce early stop: count = %d, want 1", count) + } + }) +} diff --git a/xiter/stream/stream.go b/xiter/stream/stream.go new file mode 100644 index 0000000..0ccf515 --- /dev/null +++ b/xiter/stream/stream.go @@ -0,0 +1,162 @@ +//go:build go1.23 + +// Package stream provides concurrent stream processing utilities for iter.Seq. +// +// These functions enable parallel and batched processing of sequences, +// useful for CPU-bound or I/O-bound workloads where goroutines can +// improve throughput. +package stream + +import ( + "sync" + + "github.com/dashjay/xiter/xiter" +) + +// ParallelMap applies fn to each element in seq concurrently using n worker +// goroutines. Results are yielded in non-deterministic order as workers +// complete. +// +// Example: +// +// results := stream.ParallelMap( +// xiter.FromSlice([]int{1, 2, 3, 4, 5}), +// func(v int) int { return v * 2 }, +// 3, +// ) +func ParallelMap[T, R any](seq xiter.Seq[T], fn func(T) R, n int) xiter.Seq[R] { + return func(yield func(R) bool) { + if n <= 0 { + n = 1 + } + + done := make(chan struct{}) + defer close(done) + + in := make(chan T) + out := make(chan R) + + // Feed input sequence into channel + go func() { + defer close(in) + for v := range seq { + select { + case in <- v: + case <-done: + return + } + } + }() + + // Start worker pool + var wg sync.WaitGroup + for i := 0; i < n; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for v := range in { + r := fn(v) + select { + case out <- r: + case <-done: + return + } + } + }() + } + + // Close output channel when all workers finish + go func() { + wg.Wait() + close(out) + }() + + // Yield results + for r := range out { + if !yield(r) { + return + } + } + } +} + +// FanIn merges multiple sequences into a single sequence. +// Values from all input sequences are interleaved as they arrive. +// The returned sequence ends when all input sequences are exhausted. +// +// Example: +// +// seq1 := xiter.FromSlice([]int{1, 2, 3}) +// seq2 := xiter.FromSlice([]int{4, 5, 6}) +// for v := range stream.FanIn(seq1, seq2) { +// fmt.Println(v) // may print 1,4,2,5,3,6 in any interleaving +// } +func FanIn[T any](seqs ...xiter.Seq[T]) xiter.Seq[T] { + return func(yield func(T) bool) { + if len(seqs) == 0 { + return + } + + done := make(chan struct{}) + defer close(done) + + out := make(chan T) + var wg sync.WaitGroup + + // Start a goroutine for each input sequence + for _, seq := range seqs { + wg.Add(1) + go func(s xiter.Seq[T]) { + defer wg.Done() + for v := range s { + select { + case out <- v: + case <-done: + return + } + } + }(seq) + } + + go func() { + wg.Wait() + close(out) + }() + + for v := range out { + if !yield(v) { + return + } + } + } +} + +// Batch groups elements from seq into slices of at most n elements. +// The last batch may contain fewer than n elements. +// +// Example: +// +// seq := xiter.FromSlice([]int{1, 2, 3, 4, 5}) +// for batch := range stream.Batch(seq, 2) { +// fmt.Println(batch) // [1 2] [3 4] [5] +// } +func Batch[T any](seq xiter.Seq[T], n int) xiter.Seq[[]T] { + return func(yield func([]T) bool) { + if n <= 0 { + return + } + batch := make([]T, 0, n) + for v := range seq { + batch = append(batch, v) + if len(batch) == n { + if !yield(batch) { + return + } + batch = make([]T, 0, n) + } + } + if len(batch) > 0 { + yield(batch) + } + } +} diff --git a/xiter/stream/stream_test.go b/xiter/stream/stream_test.go new file mode 100644 index 0000000..f68a3d9 --- /dev/null +++ b/xiter/stream/stream_test.go @@ -0,0 +1,200 @@ +//go:build go1.23 + +package stream_test + +import ( + "sync/atomic" + "testing" + + "github.com/dashjay/xiter/xiter" + "github.com/dashjay/xiter/xiter/stream" +) + +func refRange(a, b int) []int { + var res []int + for i := a; i < b; i++ { + res = append(res, i) + } + return res +} + +func toSlice[T any](seq xiter.Seq[T]) []T { + var out []T + for v := range seq { + out = append(out, v) + } + return out +} + +func TestParallelMap(t *testing.T) { + t.Run("basic", func(t *testing.T) { + in := refRange(0, 100) + seq := xiter.FromSlice(in) + results := toSlice(stream.ParallelMap(seq, func(v int) int { + return v * 2 + }, 4)) + + if len(results) != 100 { + t.Fatalf("ParallelMap: got %d results, want 100", len(results)) + } + + // Results come in non-deterministic order, but all values should be present + seen := make(map[int]bool) + for _, v := range results { + if v%2 != 0 { + t.Fatalf("ParallelMap: unexpected value %d (should be even)", v) + } + seen[v] = true + } + for i := 0; i < 100; i++ { + if !seen[i*2] { + t.Fatalf("ParallelMap: missing result %d", i*2) + } + } + }) + + t.Run("empty", func(t *testing.T) { + results := toSlice(stream.ParallelMap(xiter.FromSlice([]int{}), func(v int) int { + return v + }, 2)) + if len(results) != 0 { + t.Fatalf("ParallelMap empty: got %d, want 0", len(results)) + } + }) + + t.Run("single worker", func(t *testing.T) { + in := refRange(0, 10) + results := toSlice(stream.ParallelMap(xiter.FromSlice(in), func(v int) int { + return v + 1 + }, 1)) + if len(results) != 10 { + t.Fatalf("ParallelMap single: got %d, want 10", len(results)) + } + }) + + t.Run("zero workers defaults to one", func(t *testing.T) { + in := refRange(0, 10) + results := toSlice(stream.ParallelMap(xiter.FromSlice(in), func(v int) int { + return v + }, 0)) + if len(results) != 10 { + t.Fatalf("ParallelMap zero: got %d, want 10", len(results)) + } + }) + + t.Run("early stop", func(t *testing.T) { + in := refRange(0, 10000) + var count atomic.Int32 + stopAfter := int32(50) + for range stream.ParallelMap(xiter.FromSlice(in), func(v int) int { + count.Add(1) + return v + }, 4) { + if count.Load() >= stopAfter { + break + } + } + // We stopped early; verify seq iteration stopped + // Note: some extra items may have been processed by workers + // due to buffering, but the key point is we don't iterate forever + }) +} + +func TestFanIn(t *testing.T) { + t.Run("basic", func(t *testing.T) { + seq1 := xiter.FromSlice([]int{1, 2, 3}) + seq2 := xiter.FromSlice([]int{4, 5, 6}) + results := toSlice(stream.FanIn(seq1, seq2)) + if len(results) != 6 { + t.Fatalf("FanIn: got %d results, want 6", len(results)) + } + }) + + t.Run("empty seqs", func(t *testing.T) { + results := toSlice(stream.FanIn[int]()) + if len(results) != 0 { + t.Fatalf("FanIn empty: got %d, want 0", len(results)) + } + }) + + t.Run("single seq", func(t *testing.T) { + results := toSlice(stream.FanIn(xiter.FromSlice([]int{1, 2, 3}))) + if len(results) != 3 { + t.Fatalf("FanIn single: got %d, want 3", len(results)) + } + }) + + t.Run("empty and non-empty", func(t *testing.T) { + seq1 := xiter.FromSlice([]int{}) + seq2 := xiter.FromSlice([]int{1, 2}) + results := toSlice(stream.FanIn(seq1, seq2)) + if len(results) != 2 { + t.Fatalf("FanIn mixed: got %d, want 2", len(results)) + } + }) + + t.Run("early stop", func(t *testing.T) { + seq1 := xiter.FromSlice(refRange(0, 1000)) + seq2 := xiter.FromSlice(refRange(1000, 2000)) + count := 0 + for range stream.FanIn(seq1, seq2) { + count++ + if count >= 5 { + break + } + } + if count != 5 { + t.Fatalf("FanIn early stop: count = %d, want 5", count) + } + }) +} + +func TestBatch(t *testing.T) { + t.Run("basic", func(t *testing.T) { + seq := xiter.FromSlice(refRange(0, 5)) + batches := toSlice(stream.Batch(seq, 2)) + if len(batches) != 3 { + t.Fatalf("Batch: got %d batches, want 3", len(batches)) + } + if len(batches[0]) != 2 || batches[0][0] != 0 || batches[0][1] != 1 { + t.Fatalf("Batch[0]: got %v, want [0 1]", batches[0]) + } + if len(batches[1]) != 2 || batches[1][0] != 2 || batches[1][1] != 3 { + t.Fatalf("Batch[1]: got %v, want [2 3]", batches[1]) + } + if len(batches[2]) != 1 || batches[2][0] != 4 { + t.Fatalf("Batch[2]: got %v, want [4]", batches[2]) + } + }) + + t.Run("exact", func(t *testing.T) { + seq := xiter.FromSlice([]int{1, 2, 3, 4}) + batches := toSlice(stream.Batch(seq, 2)) + if len(batches) != 2 { + t.Fatalf("Batch exact: got %d batches, want 2", len(batches)) + } + }) + + t.Run("single element", func(t *testing.T) { + seq := xiter.FromSlice([]int{1}) + batches := toSlice(stream.Batch(seq, 5)) + if len(batches) != 1 || batches[0][0] != 1 { + t.Fatalf("Batch single: got %v, want [[1]]", batches) + } + }) + + t.Run("empty", func(t *testing.T) { + batches := toSlice(stream.Batch(xiter.FromSlice([]int{}), 2)) + if len(batches) != 0 { + t.Fatalf("Batch empty: got %d, want 0", len(batches)) + } + }) + + t.Run("zero or negative size", func(t *testing.T) { + seq := xiter.FromSlice(refRange(0, 5)) + batches := toSlice(stream.Batch(seq, 0)) + if len(batches) != 0 { + t.Fatalf("Batch zero: got %d, want 0", len(batches)) + } + }) +} diff --git a/xiter/xiter.go b/xiter/xiter.go index c0ad3b1..f4cc6cf 100644 --- a/xiter/xiter.go +++ b/xiter/xiter.go @@ -7,6 +7,7 @@ import ( "iter" "maps" "math/rand" + "sort" "strings" "github.com/dashjay/xiter/internal/constraints" @@ -1116,3 +1117,201 @@ func Compact[T comparable](in Seq[T]) Seq[T] { } } } + +// FlatMap maps each element in seq to a sequence using f, then flattens +// all resulting sequences into a single sequence. +// +// Example: +// +// seq := xiter.FromSlice([]int{1, 2, 3}) +// flatMapped := xiter.FlatMap(func(v int) xiter.Seq[int] { +// return xiter.FromSlice([]int{v, v * 10}) +// }, seq) +// fmt.Println(xiter.ToSlice(flatMapped)) +// // output: +// // [1 10 2 20 3 30] +func FlatMap[In, Out any](f func(In) Seq[Out], seq Seq[In]) Seq[Out] { + return func(yield func(Out) bool) { + for in := range seq { + for out := range f(in) { + if !yield(out) { + return + } + } + } + } +} + +// Flatten flattens a sequence of sequences into a single sequence. +// +// Example: +// +// seq := xiter.FromSlice([]xiter.Seq[int]{ +// xiter.FromSlice([]int{1, 2}), +// xiter.FromSlice([]int{3, 4, 5}), +// }) +// flat := xiter.Flatten(seq) +// fmt.Println(xiter.ToSlice(flat)) +// // output: +// // [1 2 3 4 5] +func Flatten[T any](seq Seq[Seq[T]]) Seq[T] { + return FlatMap(func(inner Seq[T]) Seq[T] { return inner }, seq) +} + +// TakeWhile yields elements from seq as long as f(v) returns true, then stops. +// +// Example: +// +// seq := xiter.FromSlice([]int{1, 3, 5, 7, 2}) +// taken := xiter.TakeWhile(func(v int) bool { return v%2 == 1 }, seq) +// fmt.Println(xiter.ToSlice(taken)) +// // output: +// // [1 3 5 7] +func TakeWhile[V any](f func(V) bool, seq Seq[V]) Seq[V] { + return func(yield func(V) bool) { + for v := range seq { + if !f(v) || !yield(v) { + return + } + } + } +} + +// DropWhile skips elements from seq while f(v) returns true, +// then yields the remaining elements. +// +// Example: +// +// seq := xiter.FromSlice([]int{1, 3, 5, 7, 2, 4}) +// dropped := xiter.DropWhile(func(v int) bool { return v%2 == 1 }, seq) +// fmt.Println(xiter.ToSlice(dropped)) +// // output: +// // [2 4] +func DropWhile[V any](f func(V) bool, seq Seq[V]) Seq[V] { + return func(yield func(V) bool) { + dropping := true + for v := range seq { + if dropping { + dropping = f(v) + if dropping { + continue + } + } + if !yield(v) { + return + } + } + } +} + +// DistinctBy returns a Seq that removes duplicate elements based on a key function. +// Unlike Uniq, which requires directly comparable elements, DistinctBy lets you +// define how to derive a comparable key from each element. +// +// Example: +// +// type Person struct{ Name string; Age int } +// people := xiter.FromSlice([]Person{{"alice", 30}, {"bob", 30}, {"alice", 25}}) +// uniq := xiter.DistinctBy(people, func(p Person) string { return p.Name }) +// fmt.Println(xiter.ToSlice(uniq)) +// // output: +// // [{alice 30} {bob 30}] +func DistinctBy[V any, K comparable](f func(V) K, seq Seq[V]) Seq[V] { + return func(yield func(V) bool) { + seen := make(map[K]struct{}) + for v := range seq { + k := f(v) + if _, ok := seen[k]; !ok { + seen[k] = struct{}{} + if !yield(v) { + return + } + } + } + } +} + +// Intersperse places sep between consecutive elements of seq. +// +// Example: +// +// seq := xiter.FromSlice([]int{1, 2, 3}) +// withSep := xiter.Intersperse(0, seq) +// fmt.Println(xiter.ToSlice(withSep)) +// // output: +// // [1 0 2 0 3] +func Intersperse[V any](sep V, seq Seq[V]) Seq[V] { + return func(yield func(V) bool) { + first := true + for v := range seq { + if !first && !yield(sep) { + return + } + first = false + if !yield(v) { + return + } + } + } +} + +// Split splits seq into two sequences based on predicate f. +// The first sequence contains elements where f(v) is true, +// the second contains elements where f(v) is false. +// Both sequences are materialized eagerly. +// +// Example: +// +// tru, fls := xiter.Split(func(v int) bool { return v%2 == 0 }, xiter.FromSlice([]int{1, 2, 3, 4, 5})) +// fmt.Println(xiter.ToSlice(tru)) +// fmt.Println(xiter.ToSlice(fls)) +// // output: +// // [2 4] +// // [1 3 5] +func Split[V any](f func(V) bool, seq Seq[V]) (tru, fls Seq[V]) { + var trueVals, falseVals []V + for v := range seq { + if f(v) { + trueVals = append(trueVals, v) + } else { + falseVals = append(falseVals, v) + } + } + return FromSlice(trueVals), FromSlice(falseVals) +} + +// Sorted returns a Seq that yields elements from seq in ascending order. +// All elements are materialized for sorting. +// +// Example: +// +// sorted := xiter.Sorted(xiter.FromSlice([]int{3, 1, 4, 1, 5})) +// fmt.Println(xiter.ToSlice(sorted)) +// // output: +// // [1 1 3 4 5] +func Sorted[V constraints.Ordered](seq Seq[V]) Seq[V] { + return SortBy(seq, func(v V) V { return v }) +} + +// SortBy returns a Seq that yields elements from seq sorted by the key +// extracted by function f. All elements are materialized for sorting. +// +// Example: +// +// type Person struct{ Name string; Age int } +// people := xiter.FromSlice([]Person{{"alice", 30}, {"bob", 25}}) +// sorted := xiter.SortBy(people, func(p Person) int { return p.Age }) +// fmt.Println(xiter.ToSlice(sorted)) +// // output: +// // [{bob 25} {alice 30}] +func SortBy[V any, K constraints.Ordered](seq Seq[V], f func(V) K) Seq[V] { + return func(yield func(V) bool) { + all := ToSlice(seq) + sort.Slice(all, func(i, j int) bool { return f(all[i]) < f(all[j]) }) + for _, v := range all { + if !yield(v) { + return + } + } + } +} diff --git a/xiter/xiter_old.go b/xiter/xiter_old.go index 88e5102..e50dddd 100644 --- a/xiter/xiter_old.go +++ b/xiter/xiter_old.go @@ -6,6 +6,7 @@ package xiter import ( "math/rand" "runtime" + "sort" "strconv" "strings" @@ -982,3 +983,103 @@ func Compact[T comparable](in Seq[T]) Seq[T] { }) } } + +func FlatMap[In, Out any](f func(In) Seq[Out], seq Seq[In]) Seq[Out] { + return func(yield func(Out) bool) { + seq(func(in In) bool { + cont := true + f(in)(func(out Out) bool { + if !yield(out) { + cont = false + return false + } + return true + }) + return cont + }) + } +} + +func Flatten[T any](seq Seq[Seq[T]]) Seq[T] { + return FlatMap(func(inner Seq[T]) Seq[T] { return inner }, seq) +} + +func TakeWhile[V any](f func(V) bool, seq Seq[V]) Seq[V] { + return func(yield func(V) bool) { + seq(func(v V) bool { + if !f(v) { + return false + } + return yield(v) + }) + } +} + +func DropWhile[V any](f func(V) bool, seq Seq[V]) Seq[V] { + return func(yield func(V) bool) { + dropping := true + seq(func(v V) bool { + if dropping && f(v) { + return true + } + dropping = false + return yield(v) + }) + } +} + +func DistinctBy[V any, K comparable](f func(V) K, seq Seq[V]) Seq[V] { + return func(yield func(V) bool) { + seen := make(map[K]struct{}) + seq(func(v V) bool { + k := f(v) + if _, ok := seen[k]; !ok { + seen[k] = struct{}{} + return yield(v) + } + return true + }) + } +} + +func Intersperse[V any](sep V, seq Seq[V]) Seq[V] { + return func(yield func(V) bool) { + first := true + seq(func(v V) bool { + if !first && !yield(sep) { + return false + } + first = false + return yield(v) + }) + } +} + +func Split[V any](f func(V) bool, seq Seq[V]) (tru, fls Seq[V]) { + var trueVals, falseVals []V + seq(func(v V) bool { + if f(v) { + trueVals = append(trueVals, v) + } else { + falseVals = append(falseVals, v) + } + return true + }) + return FromSlice(trueVals), FromSlice(falseVals) +} + +func Sorted[V constraints.Ordered](seq Seq[V]) Seq[V] { + return SortBy(seq, func(v V) V { return v }) +} + +func SortBy[V any, K constraints.Ordered](seq Seq[V], f func(V) K) Seq[V] { + return func(yield func(V) bool) { + all := ToSlice(seq) + sort.Slice(all, func(i, j int) bool { return f(all[i]) < f(all[j]) }) + for _, v := range all { + if !yield(v) { + return + } + } + } +} diff --git a/xiter/xiter_test.go b/xiter/xiter_test.go index 259bf08..d80cf1e 100644 --- a/xiter/xiter_test.go +++ b/xiter/xiter_test.go @@ -419,6 +419,93 @@ func TestXIter(t *testing.T) { testLimit(t, seq, 1) assert.Equal(t, []int{1, 2, 3, 4}, xiter.ToSlice(seq)) }) + + t.Run("flat_map", func(t *testing.T) { + seq := xiter.FlatMap(func(v int) xiter.Seq[int] { + return xiter.FromSlice([]int{v, v * 10}) + }, xiter.FromSlice([]int{1, 2, 3})) + assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, xiter.ToSlice(seq)) + testLimit(t, seq, 1) + }) + + t.Run("flatten", func(t *testing.T) { + seq := xiter.Flatten(xiter.FromSlice([]xiter.Seq[int]{ + xiter.FromSlice([]int{1, 2}), + xiter.FromSlice([]int{3, 4, 5}), + })) + assert.Equal(t, []int{1, 2, 3, 4, 5}, xiter.ToSlice(seq)) + testLimit(t, seq, 1) + }) + + t.Run("take_while", func(t *testing.T) { + seq := xiter.TakeWhile(func(v int) bool { return v < 5 }, xiter.FromSlice([]int{1, 3, 5, 7})) + assert.Equal(t, []int{1, 3}, xiter.ToSlice(seq)) + testLimit(t, seq, 1) + }) + + t.Run("drop_while", func(t *testing.T) { + seq := xiter.DropWhile(func(v int) bool { return v < 5 }, xiter.FromSlice([]int{1, 3, 5, 7, 2})) + assert.Equal(t, []int{5, 7, 2}, xiter.ToSlice(seq)) + testLimit(t, seq, 1) + }) + + t.Run("distinct_by", func(t *testing.T) { + seq := xiter.DistinctBy(func(v int) int { return v % 2 }, xiter.FromSlice([]int{1, 3, 2, 4, 5})) + assert.Equal(t, []int{1, 2}, xiter.ToSlice(seq)) + testLimit(t, seq, 1) + }) + + t.Run("intersperse", func(t *testing.T) { + seq := xiter.Intersperse(0, xiter.FromSlice([]int{1, 2, 3})) + assert.Equal(t, []int{1, 0, 2, 0, 3}, xiter.ToSlice(seq)) + testLimit(t, seq, 1) + + single := xiter.Intersperse(0, xiter.FromSlice([]int{1})) + assert.Equal(t, []int{1}, xiter.ToSlice(single)) + + empty := xiter.Intersperse(0, xiter.FromSlice([]int{})) + assert.Nil(t, xiter.ToSlice(empty)) + }) + + t.Run("split", func(t *testing.T) { + tru, fls := xiter.Split(func(v int) bool { return v%2 == 0 }, xiter.FromSlice([]int{1, 2, 3, 4, 5})) + assert.Equal(t, []int{2, 4}, xiter.ToSlice(tru)) + assert.Equal(t, []int{1, 3, 5}, xiter.ToSlice(fls)) + + allTru, allFls := xiter.Split(func(v int) bool { return true }, xiter.FromSlice([]int{1, 2, 3})) + assert.Equal(t, []int{1, 2, 3}, xiter.ToSlice(allTru)) + assert.Nil(t, xiter.ToSlice(allFls)) + + noneTru, noneFls := xiter.Split(func(v int) bool { return false }, xiter.FromSlice([]int{1, 2, 3})) + assert.Nil(t, xiter.ToSlice(noneTru)) + assert.Equal(t, []int{1, 2, 3}, xiter.ToSlice(noneFls)) + + emptyTru, emptyFls := xiter.Split(func(v int) bool { return true }, xiter.FromSlice([]int{})) + assert.Nil(t, xiter.ToSlice(emptyTru)) + assert.Nil(t, xiter.ToSlice(emptyFls)) + }) + + t.Run("sorted", func(t *testing.T) { + sorted := xiter.Sorted(xiter.FromSlice([]int{3, 1, 4, 1, 5, 9})) + assert.Equal(t, []int{1, 1, 3, 4, 5, 9}, xiter.ToSlice(sorted)) + }) + + t.Run("sort_by", func(t *testing.T) { + type person struct { + Name string + Age int + } + people := xiter.FromSlice([]person{ + {"charlie", 35}, + {"alice", 30}, + {"bob", 25}, + }) + sorted := xiter.SortBy(people, func(p person) int { return p.Age }) + result := xiter.ToSlice(sorted) + assert.Equal(t, 25, result[0].Age) + assert.Equal(t, 30, result[1].Age) + assert.Equal(t, 35, result[2].Age) + }) } func TestXIter61898(t *testing.T) { diff --git a/xstl/README.md b/xstl/README.md index cd22610..799e8e9 100644 --- a/xstl/README.md +++ b/xstl/README.md @@ -269,4 +269,162 @@ func (l *List[V]) Remove(e *Element[V]) V Remove removes e from l if e is an element of list l. It returns the element value e.Value. The element must not be nil. +# lockedmap + +```go +import "github.com/dashjay/xiter/xstl/lockedmap" +``` + +Package lockedmap provides a concurrency\-safe generic map backed by sync.RWMutex and a regular Go map. + +Unlike xsync.SyncMap \(which wraps sync.Map\), LockedMap ensures that all methods including Len, ToMap, and Range provide consistent snapshots under concurrent access. + +## Index + +- [type LockedMap](<#LockedMap>) + - [func NewLockedMap\[K comparable, V any\]\(\) \*LockedMap\[K, V\]](<#NewLockedMap>) + - [func \(m \*LockedMap\[K, V\]\) Clear\(\)](<#LockedMap[K, V].Clear>) + - [func \(m \*LockedMap\[K, V\]\) CompareAndDelete\(key K, old V\) bool](<#LockedMap[K, V].CompareAndDelete>) + - [func \(m \*LockedMap\[K, V\]\) CompareAndSwap\(key K, old, new V\) bool](<#LockedMap[K, V].CompareAndSwap>) + - [func \(m \*LockedMap\[K, V\]\) Delete\(key K\)](<#LockedMap[K, V].Delete>) + - [func \(m \*LockedMap\[K, V\]\) Len\(\) int](<#LockedMap[K, V].Len>) + - [func \(m \*LockedMap\[K, V\]\) Load\(key K\) \(value V, ok bool\)](<#LockedMap[K, V].Load>) + - [func \(m \*LockedMap\[K, V\]\) LoadAndDelete\(key K\) \(value V, loaded bool\)](<#LockedMap[K, V].LoadAndDelete>) + - [func \(m \*LockedMap\[K, V\]\) LoadOrStore\(key K, value V\) \(actual V, loaded bool\)](<#LockedMap[K, V].LoadOrStore>) + - [func \(m \*LockedMap\[K, V\]\) Range\(f func\(key K, value V\) bool\)](<#LockedMap[K, V].Range>) + - [func \(m \*LockedMap\[K, V\]\) Store\(key K, value V\)](<#LockedMap[K, V].Store>) + - [func \(m \*LockedMap\[K, V\]\) Swap\(key K, value V\) \(previous V, loaded bool\)](<#LockedMap[K, V].Swap>) + - [func \(m \*LockedMap\[K, V\]\) ToMap\(\) map\[K\]V](<#LockedMap[K, V].ToMap>) + + + +## type [LockedMap]() + +LockedMap is a concurrency\-safe generic map backed by sync.RWMutex. The zero value is not ready to use; use NewLockedMap to create one. + +```go +type LockedMap[K comparable, V any] struct { + // contains filtered or unexported fields +} +``` + + +### func [NewLockedMap]() + +```go +func NewLockedMap[K comparable, V any]() *LockedMap[K, V] +``` + +NewLockedMap creates a new empty LockedMap. + + +### func \(\*LockedMap\[K, V\]\) [Clear]() + +```go +func (m *LockedMap[K, V]) Clear() +``` + +Clear deletes all entries, resulting in an empty map. + + +### func \(\*LockedMap\[K, V\]\) [CompareAndDelete]() + +```go +func (m *LockedMap[K, V]) CompareAndDelete(key K, old V) bool +``` + +CompareAndDelete deletes the entry for a key if the stored value equals old. Comparison uses reflect.DeepEqual. + + +### func \(\*LockedMap\[K, V\]\) [CompareAndSwap]() + +```go +func (m *LockedMap[K, V]) CompareAndSwap(key K, old, new V) bool +``` + +CompareAndSwap swaps the value for a key if the stored value equals old. Comparison uses reflect.DeepEqual. + + +### func \(\*LockedMap\[K, V\]\) [Delete]() + +```go +func (m *LockedMap[K, V]) Delete(key K) +``` + +Delete deletes the value for a key. + + +### func \(\*LockedMap\[K, V\]\) [Len]() + +```go +func (m *LockedMap[K, V]) Len() int +``` + +Len returns the number of elements in the map. O\(1\) complexity. + + +### func \(\*LockedMap\[K, V\]\) [Load]() + +```go +func (m *LockedMap[K, V]) Load(key K) (value V, ok bool) +``` + +Load returns the value stored for the key, or false if no value exists. + + +### func \(\*LockedMap\[K, V\]\) [LoadAndDelete]() + +```go +func (m *LockedMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) +``` + +LoadAndDelete deletes the value for a key, returning the previous value if any. + + +### func \(\*LockedMap\[K, V\]\) [LoadOrStore]() + +```go +func (m *LockedMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) +``` + +LoadOrStore returns the existing value for the key if present. Otherwise, it stores and returns the given value. + + +### func \(\*LockedMap\[K, V\]\) [Range]() + +```go +func (m *LockedMap[K, V]) Range(f func(key K, value V) bool) +``` + +Range calls f sequentially for each key and value present in the map. If f returns false, range stops the iteration. + +NOTE: Do not call Store, Delete, or other mutating methods on m from within f, or a deadlock will occur. + + +### func \(\*LockedMap\[K, V\]\) [Store]() + +```go +func (m *LockedMap[K, V]) Store(key K, value V) +``` + +Store sets the value for a key. + + +### func \(\*LockedMap\[K, V\]\) [Swap]() + +```go +func (m *LockedMap[K, V]) Swap(key K, value V) (previous V, loaded bool) +``` + +Swap swaps the value for a key and returns the previous value if any. + + +### func \(\*LockedMap\[K, V\]\) [ToMap]() + +```go +func (m *LockedMap[K, V]) ToMap() map[K]V +``` + +ToMap returns a copy of the map as a regular map. The returned map is a consistent snapshot at the time of the call. + Generated by [gomarkdoc]() diff --git a/xstl/lockedmap/locked_map.go b/xstl/lockedmap/locked_map.go new file mode 100644 index 0000000..03d26f5 --- /dev/null +++ b/xstl/lockedmap/locked_map.go @@ -0,0 +1,165 @@ +// MIT License +// +// Copyright (c) 2025 DashJay +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Package lockedmap provides a concurrency-safe generic map backed by +// sync.RWMutex and a regular Go map. +// +// Unlike xsync.SyncMap (which wraps sync.Map), LockedMap ensures that all +// methods including Len, ToMap, and Range provide consistent snapshots +// under concurrent access. +package lockedmap + +import ( + "reflect" + "sync" +) + +// LockedMap is a concurrency-safe generic map backed by sync.RWMutex. +// The zero value is not ready to use; use NewLockedMap to create one. +type LockedMap[K comparable, V any] struct { + mu sync.RWMutex + m map[K]V +} + +// NewLockedMap creates a new empty LockedMap. +func NewLockedMap[K comparable, V any]() *LockedMap[K, V] { + return &LockedMap[K, V]{m: make(map[K]V)} +} + +// Load returns the value stored for the key, or false if no value exists. +func (m *LockedMap[K, V]) Load(key K) (value V, ok bool) { + m.mu.RLock() + defer m.mu.RUnlock() + value, ok = m.m[key] + return +} + +// Store sets the value for a key. +func (m *LockedMap[K, V]) Store(key K, value V) { + m.mu.Lock() + defer m.mu.Unlock() + m.m[key] = value +} + +// LoadOrStore returns the existing value for the key if present. +// Otherwise, it stores and returns the given value. +func (m *LockedMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { + m.mu.Lock() + defer m.mu.Unlock() + if v, ok := m.m[key]; ok { + return v, true + } + m.m[key] = value + return value, false +} + +// LoadAndDelete deletes the value for a key, returning the previous value if any. +func (m *LockedMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) { + m.mu.Lock() + defer m.mu.Unlock() + if v, ok := m.m[key]; ok { + delete(m.m, key) + return v, true + } + return value, false +} + +// Delete deletes the value for a key. +func (m *LockedMap[K, V]) Delete(key K) { + m.mu.Lock() + defer m.mu.Unlock() + delete(m.m, key) +} + +// Range calls f sequentially for each key and value present in the map. +// If f returns false, range stops the iteration. +// +// NOTE: Do not call Store, Delete, or other mutating methods on m from within f, +// or a deadlock will occur. +func (m *LockedMap[K, V]) Range(f func(key K, value V) bool) { + m.mu.RLock() + defer m.mu.RUnlock() + for k, v := range m.m { + if !f(k, v) { + break + } + } +} + +// Len returns the number of elements in the map. O(1) complexity. +func (m *LockedMap[K, V]) Len() int { + m.mu.RLock() + defer m.mu.RUnlock() + return len(m.m) +} + +// ToMap returns a copy of the map as a regular map. +// The returned map is a consistent snapshot at the time of the call. +func (m *LockedMap[K, V]) ToMap() map[K]V { + m.mu.RLock() + defer m.mu.RUnlock() + out := make(map[K]V, len(m.m)) + for k, v := range m.m { + out[k] = v + } + return out +} + +// Swap swaps the value for a key and returns the previous value if any. +func (m *LockedMap[K, V]) Swap(key K, value V) (previous V, loaded bool) { + m.mu.Lock() + defer m.mu.Unlock() + previous, loaded = m.m[key] + m.m[key] = value + return +} + +// CompareAndSwap swaps the value for a key if the stored value equals old. +// Comparison uses reflect.DeepEqual. +func (m *LockedMap[K, V]) CompareAndSwap(key K, old, new V) bool { + m.mu.Lock() + defer m.mu.Unlock() + if v, ok := m.m[key]; ok && reflect.DeepEqual(v, old) { + m.m[key] = new + return true + } + return false +} + +// CompareAndDelete deletes the entry for a key if the stored value equals old. +// Comparison uses reflect.DeepEqual. +func (m *LockedMap[K, V]) CompareAndDelete(key K, old V) bool { + m.mu.Lock() + defer m.mu.Unlock() + if v, ok := m.m[key]; ok && reflect.DeepEqual(v, old) { + delete(m.m, key) + return true + } + return false +} + +// Clear deletes all entries, resulting in an empty map. +func (m *LockedMap[K, V]) Clear() { + m.mu.Lock() + m.m = make(map[K]V) + m.mu.Unlock() +} diff --git a/xstl/lockedmap/locked_map_test.go b/xstl/lockedmap/locked_map_test.go new file mode 100644 index 0000000..fcaefdc --- /dev/null +++ b/xstl/lockedmap/locked_map_test.go @@ -0,0 +1,311 @@ +// MIT License +// +// Copyright (c) 2025 DashJay +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package lockedmap + +import ( + "sync" + "testing" +) + +func TestLockedMap(t *testing.T) { + t.Run("load missing", func(t *testing.T) { + m := NewLockedMap[string, int]() + v, ok := m.Load("a") + if ok || v != 0 { + t.Errorf("Load missing key: got (%d, %t), want (0, false)", v, ok) + } + }) + + t.Run("store and load", func(t *testing.T) { + m := NewLockedMap[string, int]() + m.Store("a", 1) + v, ok := m.Load("a") + if !ok || v != 1 { + t.Errorf("Store+Load: got (%d, %t), want (1, true)", v, ok) + } + }) + + t.Run("load or store existing", func(t *testing.T) { + m := NewLockedMap[string, int]() + m.Store("a", 1) + v, loaded := m.LoadOrStore("a", 2) + if !loaded || v != 1 { + t.Errorf("LoadOrStore existing: got (%d, %t), want (1, true)", v, loaded) + } + }) + + t.Run("load or store missing", func(t *testing.T) { + m := NewLockedMap[string, int]() + v, loaded := m.LoadOrStore("a", 1) + if loaded || v != 1 { + t.Errorf("LoadOrStore missing: got (%d, %t), want (1, false)", v, loaded) + } + }) + + t.Run("load and delete", func(t *testing.T) { + m := NewLockedMap[string, int]() + m.Store("a", 1) + v, loaded := m.LoadAndDelete("a") + if !loaded || v != 1 { + t.Errorf("LoadAndDelete: got (%d, %t), want (1, true)", v, loaded) + } + if _, ok := m.Load("a"); ok { + t.Error("LoadAndDelete: key still exists") + } + }) + + t.Run("delete", func(t *testing.T) { + m := NewLockedMap[string, int]() + m.Store("a", 1) + m.Delete("a") + if _, ok := m.Load("a"); ok { + t.Error("Delete: key still exists") + } + }) + + t.Run("len", func(t *testing.T) { + m := NewLockedMap[string, int]() + if n := m.Len(); n != 0 { + t.Errorf("Len empty: got %d, want 0", n) + } + m.Store("a", 1) + m.Store("b", 2) + if n := m.Len(); n != 2 { + t.Errorf("Len: got %d, want 2", n) + } + m.Delete("a") + if n := m.Len(); n != 1 { + t.Errorf("Len after delete: got %d, want 1", n) + } + }) + + t.Run("to map", func(t *testing.T) { + m := NewLockedMap[string, int]() + m.Store("a", 1) + m.Store("b", 2) + out := m.ToMap() + if len(out) != 2 { + t.Fatalf("ToMap len: got %d, want 2", len(out)) + } + if out["a"] != 1 || out["b"] != 2 { + t.Errorf("ToMap content: got %v", out) + } + // verify it's a copy + out["c"] = 3 + if m.Len() != 2 { + t.Error("ToMap: modification of returned map affected original") + } + }) + + t.Run("range", func(t *testing.T) { + m := NewLockedMap[int, int]() + for i := 0; i < 10; i++ { + m.Store(i, i*10) + } + seen := make(map[int]bool) + m.Range(func(k int, v int) bool { + if v != k*10 { + t.Errorf("Range: unexpected value %d for key %d", v, k) + } + seen[k] = true + return true + }) + if len(seen) != 10 { + t.Errorf("Range visited %d keys, want 10", len(seen)) + } + }) + + t.Run("range early stop", func(t *testing.T) { + m := NewLockedMap[int, int]() + for i := 0; i < 10; i++ { + m.Store(i, i) + } + count := 0 + m.Range(func(k int, v int) bool { + count++ + return count < 3 + }) + if count != 3 { + t.Errorf("Range early stop: visited %d, want 3", count) + } + }) + + t.Run("swap", func(t *testing.T) { + m := NewLockedMap[string, int]() + prev, loaded := m.Swap("a", 1) + if loaded || prev != 0 { + t.Errorf("Swap missing: got (%d, %t), want (0, false)", prev, loaded) + } + v, _ := m.Load("a") + if v != 1 { + t.Errorf("Swap missing: stored value = %d, want 1", v) + } + + prev, loaded = m.Swap("a", 2) + if !loaded || prev != 1 { + t.Errorf("Swap existing: got (%d, %t), want (1, true)", prev, loaded) + } + v, _ = m.Load("a") + if v != 2 { + t.Errorf("Swap existing: stored value = %d, want 2", v) + } + }) + + t.Run("compare and swap", func(t *testing.T) { + m := NewLockedMap[string, int]() + m.Store("a", 1) + + if m.CompareAndSwap("a", 99, 2) { + t.Error("CompareAndSwap with wrong old value: should have failed") + } + if !m.CompareAndSwap("a", 1, 2) { + t.Error("CompareAndSwap with correct old value: should have succeeded") + } + v, _ := m.Load("a") + if v != 2 { + t.Errorf("CompareAndSwap: stored value = %d, want 2", v) + } + }) + + t.Run("compare and delete", func(t *testing.T) { + m := NewLockedMap[string, int]() + m.Store("a", 1) + + if m.CompareAndDelete("a", 99) { + t.Error("CompareAndDelete with wrong old value: should have failed") + } + if _, ok := m.Load("a"); !ok { + t.Error("CompareAndDelete: key should still exist") + } + if !m.CompareAndDelete("a", 1) { + t.Error("CompareAndDelete with correct old value: should have succeeded") + } + if _, ok := m.Load("a"); ok { + t.Error("CompareAndDelete: key should have been deleted") + } + }) + + t.Run("clear", func(t *testing.T) { + m := NewLockedMap[string, int]() + m.Store("a", 1) + m.Store("b", 2) + m.Clear() + if n := m.Len(); n != 0 { + t.Errorf("Clear: len = %d, want 0", n) + } + if _, ok := m.Load("a"); ok { + t.Error("Clear: key 'a' still exists") + } + }) + + t.Run("concurrent store and load", func(t *testing.T) { + m := NewLockedMap[int, int]() + var wg sync.WaitGroup + n := 1000 + for i := 0; i < n; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + m.Store(i, i) + }(i) + } + wg.Wait() + + if m.Len() != n { + t.Errorf("Concurrent store: len = %d, want %d", m.Len(), n) + } + }) + + t.Run("concurrent store and len", func(t *testing.T) { + m := NewLockedMap[int, int]() + var wg sync.WaitGroup + // writer + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 100; i++ { + m.Store(i, i) + } + }() + // reader calling Len concurrently + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 100; i++ { + _ = m.Len() + } + }() + wg.Wait() + }) + + t.Run("concurrent store and to map", func(t *testing.T) { + m := NewLockedMap[int, int]() + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 100; i++ { + m.Store(i, i) + } + }() + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 100; i++ { + _ = m.ToMap() + } + }() + wg.Wait() + }) + + t.Run("concurrent store and range", func(t *testing.T) { + m := NewLockedMap[int, int]() + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 100; i++ { + m.Store(i, i) + } + }() + wg.Add(1) + go func() { + defer wg.Done() + for i := 0; i < 100; i++ { + m.Range(func(k int, v int) bool { return true }) + } + }() + wg.Wait() + }) +} + +func TestLockedMapZeroValue(t *testing.T) { + // Zero value should panic; the only way to use it is via NewLockedMap + defer func() { + if r := recover(); r == nil { + t.Error("Using zero-value LockedMap should panic (nil map)") + } + }() + var m LockedMap[string, int] + m.Store("a", 1) // should panic: assignment to entry in nil map +}