Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new(slideThrough, slideThroughCyclic): add a new pair of operators #19

Merged
merged 1 commit into from
Nov 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/operators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ export { join } from './join'
export { endWith } from './end-with'
export { startWith } from './start-with'
export { scan } from './scan'
export { slideThrough, slideThroughCyclic } from './slide-through'
4 changes: 4 additions & 0 deletions src/operators/pairwise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { Operator } from '../core/types'
* Empty iterator and an iterator with a single value will both result in an
* empty iterator.
*
* This is a specialization of {@link slideThrough}.
*
* @returns
* Operator<T, [T, T]>
*
Expand Down Expand Up @@ -68,6 +70,8 @@ export function pairwise<T> (): Operator<T, [T, T]> {
* yielded pair will consist of the last and the first value of the source
* iterator.
*
* This is a specialization of {@link slideThroughCyclic}.
*
* @returns
* Operator<T, [T, T]>
*
Expand Down
212 changes: 212 additions & 0 deletions src/operators/slide-through.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { Operator } from '../core/types'


/**
* @short
* Like `pairwise`, but you *slide through* a custom window size.
*
* @categories
* operator
*
* @description
* Processes an iterable by yielding successive overlapping tuples of a specified
* window size. Each tuple contains `windowSize` consecutive elements from the source
* iterable. As iteration progresses, the window slides forward by one position,
* excluding the first element of the previous window and including the next element
* from the iterable. This operation continues until the source iterable is fully
* consumed.
*
* It follows that the size of the resulting iterable will be the same as the window
* size, unless the size of the source iterable is less than the window size -- in
* that case, the resulting iterable is empty.
*
* This is a generalization of {@link pairwise}.
*
* @since
* 0.0.4
*
* @parameter
* windowSize
* number
* The size of each yielded tuple.
*
* @returns
* Operator<T, Array<T>>
*
* @example
* j.pipe(
* [1, 2, 3, 4, 5],
* j.slideThrough(3),
* )
* // => [[1, 2, 3], [2, 3, 4], [3, 4, 5]]
*
* @example
* j.pipe(
* [1, 2, 3],
* j.slideThrough(4),
* )
* // => []
*/
export function slideThrough<T> (windowSize: 1): Operator<T, [T]>
export function slideThrough<T> (windowSize: 2): Operator<T, [T, T]>
export function slideThrough<T> (windowSize: 3): Operator<T, [T, T, T]>
export function slideThrough<T> (windowSize: 4): Operator<T, [T, T, T, T]>
export function slideThrough<T> (windowSize: 5): Operator<T, [T, T, T, T, T]>
export function slideThrough<T> (windowSize: 6): Operator<T, [T, T, T, T, T, T]>
export function slideThrough<T> (windowSize: 7): Operator<T, [T, T, T, T, T, T, T]>
export function slideThrough<T> (windowSize: 8): Operator<T, [T, T, T, T, T, T, T, T]>
export function slideThrough<T> (windowSize: 9): Operator<T, [T, T, T, T, T, T, T, T, T]>
export function slideThrough<T> (windowSize: 10): Operator<T, [T, T, T, T, T, T, T, T, T, T]>
export function slideThrough<T> (windowSize: number): Operator<T, Array<T>>
export function slideThrough<T> (windowSize: number): Operator<T, Array<T>> {

if (!Number.isInteger(windowSize) || windowSize < 1) {
throw new RangeError(`Window size must be an integer not less than 1; an attempt was made to define the window size as ${windowSize}.`)
}

return function* (iterable: Iterable<T>): IterableIterator<Array<T>> {
const window: Array<T> = []
for (const value of iterable) {
window.push(value)
if (window.length == windowSize) {
yield [...window]
window.shift()
}
}
}

}

/**
* @short
* Like `slideThrough`, but *cyclic*.
*
* @categories
* operator
*
* @description
* Processes an iterable by yielding successive overlapping sub-arrays of a specified
* window size, similar to `slideThrough`. However, upon fully consuming the source
* iterable, this operator wraps around to the beginning, continuing the sliding
* window operation in a cyclic manner. The final sub-arrays combine elements from
* the end of the iterable with those from the beginning, forming a continuous loop.
*
* It follows that the size of the resulting iterable will always be either the size
* of the source iterable or the window size, whichever is smaller.
*
* This is a generalization of {@link pairwiseCyclic}.
*
* @since
* 0.0.4
*
* @parameter
* windowSize
* number
* The size of each yielded tuple.
*
* @returns
* Operator<T, Array<T>>
*
* @example
* j.pipe(
* [1, 2, 3, 4, 5],
* j.slideThrough(3),
* )
* // => [
* // [1, 2, 3],
* // [2, 3, 4],
* // [3, 4, 5],
* // [4, 5, 1],
* // [5, 1, 2],
* // ]
*
* @example
* j.pipe(
* [1, 2, 3],
* j.slideThrough(4),
* )
* // => [
* // [1, 2, 3, 1],
* // [2, 3, 1, 2],
* // [3, 1, 2, 3],
* // ]
*/
export function slideThroughCyclic<T> (windowSize: 1): Operator<T, [T]>
export function slideThroughCyclic<T> (windowSize: 2): Operator<T, [T, T]>
export function slideThroughCyclic<T> (windowSize: 3): Operator<T, [T, T, T]>
export function slideThroughCyclic<T> (windowSize: 4): Operator<T, [T, T, T, T]>
export function slideThroughCyclic<T> (windowSize: 5): Operator<T, [T, T, T, T, T]>
export function slideThroughCyclic<T> (windowSize: 6): Operator<T, [T, T, T, T, T, T]>
export function slideThroughCyclic<T> (windowSize: 7): Operator<T, [T, T, T, T, T, T, T]>
export function slideThroughCyclic<T> (windowSize: 8): Operator<T, [T, T, T, T, T, T, T, T]>
export function slideThroughCyclic<T> (windowSize: 9): Operator<T, [T, T, T, T, T, T, T, T, T]>
export function slideThroughCyclic<T> (windowSize: 10): Operator<T, [T, T, T, T, T, T, T, T, T, T]>
export function slideThroughCyclic<T> (windowSize: number): Operator<T, Array<T>>
export function slideThroughCyclic<T> (windowSize: number): Operator<T, Array<T>> {

if (!Number.isInteger(windowSize) || windowSize < 1) {
throw new RangeError(`Window size must be an integer not less than 1; an attempt was made to define the window size as ${windowSize}.`)
}

return function* (iterable: Iterable<T>): IterableIterator<Array<T>> {

// This will slide across the source iterable.
// We'll be mutating this array, but we'll yield a shallow copy.
const window: Array<T> = []

// We need the head later due to the cyclic nature of the operator.
const head: Array<T> = []

for (const value of iterable) {
if (head.length < windowSize) {
head.push(value)
}
window.push(value)
if (window.length == windowSize) {
yield [...window]
window.shift()
}
}

if (head.length === windowSize) {

// The source iterable is at least as long as the window size, so everything plays out regularly.
// We just need to yield `windowSize - 1` more tuples, and wrap the window to the beginning.
for (let i = 0; i < windowSize - 1; i++) {
window.push(head[i])
yield [...window]
window.shift()
}

} else /* head.length < windowSize */ {

// The source iterable was consumed before the window was fully filled in.
// This means that we didn't yield anything yet, and that both head and window are populated with all
// values yielded from the source observable.

// Before proceeding, we must handle a special edge case here: the source iterable can be empty.
// In that case we're already done, as the correct result is an empty iterable.
if (head.length == 0) return

// We firstly populate the window by iterating over the head (which is in this case the whole source iterable)
// as many times as it's needed to fully fill it in.
let index = head.length
while (window.length < windowSize) {
window.push(head[index++ % head.length])
}

// We're ready for the first tuple immediately.
yield [...window]

// Now we slide for the other tuples.
for (let i = 0; i < head.length - 1; i++) {
window.shift()
window.push(head[index++ % head.length])
yield [...window]
}

}

}

}
Loading