From dd4df87b3aad753597b1b3a4f501bf5278ea3456 Mon Sep 17 00:00:00 2001 From: x5a17ed <0x5a17ed@tuta.io> Date: Sat, 25 May 2024 14:57:18 +0200 Subject: [PATCH] window: add iterator --- itlib/window.go | 116 +++++++++++++++++++++++++++++++++++++++++++ itlib/window_test.go | 104 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 itlib/window.go create mode 100644 itlib/window_test.go diff --git a/itlib/window.go b/itlib/window.go new file mode 100644 index 0000000..37a17ec --- /dev/null +++ b/itlib/window.go @@ -0,0 +1,116 @@ +// Copyright (c) 2024 individual contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package itlib + +import ( + "github.com/0x5a17ed/itkit" +) + +type windowSubIterator[T any] struct{ parent *WindowIterator[T] } + +func (it windowSubIterator[T]) Next() bool { return it.parent.windowNext() } +func (it windowSubIterator[T]) Value() T { return it.parent.windowValue() } + +// WindowIterator +type WindowIterator[T any] struct { + // Size specifies the window size. + Size uint + + // Steps specifies the number of items the iterator will + // advance for each call of the Next function. + Steps uint + + // FillValue is used to supplement missing items in case the + // window is larger than the iterable can yield items. + FillValue T + + // Source is the original source to yield items from. + Source itkit.Iterator[T] + + window []T + offset uint + index uint + cur T +} + +// Ensure WindowIterator implements the iterator interface. +var _ itkit.Iterator[itkit.Iterator[struct{}]] = &WindowIterator[struct{}]{} + +func (it *WindowIterator[T]) windowNext() (ok bool) { + if ok = it.index < it.Size; ok { + it.cur, it.index = it.window[(it.offset+it.index)%it.Size], it.index+1 + } + return +} + +func (it *WindowIterator[T]) windowValue() T { + return it.cur +} + +func (it *WindowIterator[T]) fillWindow(n uint) bool { + if !it.Source.Next() { + return false + } + + it.window[it.offset] = it.Source.Value() + for i := uint(1); i < n; i++ { + j := (it.offset + i) % it.Size + if it.Source.Next() { + it.window[j] = it.Source.Value() + } else { + it.window[j] = it.FillValue + } + } + + return true +} + +// Next implements the [itkit.Iterator.Next] interface. +func (it *WindowIterator[T]) Next() (ok bool) { + if it.window == nil { + it.window = make([]T, it.Size) + if ok = it.fillWindow(it.Size); !ok { + it.window = nil + } + return + } + + if ok = it.fillWindow(it.Steps); ok { + it.offset, it.index = (it.offset+it.Steps)%it.Size, 0 + } + + return +} + +// Value implements the [itkit.Iterator.Value] interface. +func (it *WindowIterator[T]) Value() itkit.Iterator[T] { + return windowSubIterator[T]{parent: it} +} + +// Iter returns the [WindowIterator] as an [itkit.Iterator] value. +func (it *WindowIterator[T]) Iter() itkit.Iterator[itkit.Iterator[T]] { + return it +} + +// Window returns a new [WindowIterator] value. +func Window[T any](n uint, src itkit.Iterator[T]) itkit.Iterator[itkit.Iterator[T]] { + var zero T + return &WindowIterator[T]{ + Size: n, + Steps: 1, + FillValue: zero, + Source: src, + } +} diff --git a/itlib/window_test.go b/itlib/window_test.go new file mode 100644 index 0000000..eefc9e5 --- /dev/null +++ b/itlib/window_test.go @@ -0,0 +1,104 @@ +// Copyright (c) 2024 individual contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package itlib_test + +import ( + "testing" + + "github.com/0x5a17ed/itkit" + "github.com/0x5a17ed/itkit/iters/rangeit" + "github.com/0x5a17ed/itkit/iters/sliceit" + "github.com/0x5a17ed/itkit/itlib" + "github.com/stretchr/testify/assert" +) + +func TestWindow(t *testing.T) { + p := func(n int) *int { return &n } + + fixture := func(x, y int) itkit.Iterator[*int] { + return itlib.Map(rangeit.RangeFrom(x, y), func(el int) *int { + return &el + }) + } + + type testCase[T any] struct { + name string + it itkit.Iterator[itkit.Iterator[T]] + want [][]*int + } + tt := []testCase[*int]{ + { + name: "empty", + it: itlib.Window(3, itlib.Empty[*int]()), + want: nil, + }, + { + name: "golden", + it: itlib.Window(3, fixture(1, 6)), + want: [][]*int{ + {p(1), p(2), p(3)}, + {p(2), p(3), p(4)}, + {p(3), p(4), p(5)}, + }, + }, + { + name: "multistep", + it: (&itlib.WindowIterator[*int]{ + Size: 3, + Steps: 2, + FillValue: nil, + Source: fixture(1, 6), + }).Iter(), + want: [][]*int{ + {p(1), p(2), p(3)}, + {p(3), p(4), p(5)}, + }, + }, + { + name: "fill-start", + it: (&itlib.WindowIterator[*int]{ + Size: 3, + Steps: 1, + FillValue: p(0), + Source: fixture(1, 3), + }).Iter(), + want: [][]*int{ + {p(1), p(2), p(0)}, + }, + }, + { + name: "fill-end", + it: (&itlib.WindowIterator[*int]{ + Size: 3, + Steps: 2, + FillValue: p(0), + Source: fixture(1, 7), + }).Iter(), + want: [][]*int{ + {p(1), p(2), p(3)}, + {p(3), p(4), p(5)}, + {p(5), p(6), p(0)}, + }, + }, + } + for _, tc := range tt { + tc := tc + t.Run(tc.name, func(t *testing.T) { + got := sliceit.To(itlib.Map(tc.it, sliceit.To[*int])) + + assert.Exactly(t, tc.want, got) + }) + } +}