├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ └── issue-template.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── github-ci.yml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── all.go ├── all_test.go ├── any.go ├── any_test.go ├── batch.go ├── batch_test.go ├── cartesianproduct.go ├── cartesianproduct_test.go ├── chain.go ├── chain_test.go ├── channel.go ├── channel_test.go ├── collect.go ├── collect_test.go ├── combinations.go ├── combinations_test.go ├── deferloop.go ├── deferloop_test.go ├── doc.go ├── enumerate.go ├── enumerate_test.go ├── equal.go ├── equal_test.go ├── equivalent.go ├── equivalent_test.go ├── example_test.go ├── filter.go ├── filter_test.go ├── fold.go ├── fold_test.go ├── go.mod ├── go.sum ├── img ├── logo.svg ├── logo1398x1048.png └── logo1398x400.png ├── interval.go ├── interval_test.go ├── keys.go ├── keys_test.go ├── keyvalue.go ├── keyvalue_test.go ├── linspace.go ├── linspace_test.go ├── list.go ├── list_test.go ├── map.go ├── map_test.go ├── max.go ├── max_test.go ├── mean.go ├── mean_test.go ├── min.go ├── min_test.go ├── parallelize.go ├── parallelize_test.go ├── permutations.go ├── permutations_test.go ├── product.go ├── product_test.go ├── random.go ├── random_test.go ├── reduce.go ├── reduce_test.go ├── reverse.go ├── reverse_test.go ├── slice.go ├── slice_test.go ├── sort.go ├── sort_test.go ├── string.go ├── string_test.go ├── sum.go ├── sum_test.go ├── transform.go ├── transform_test.go ├── types.go ├── values.go ├── values_test.go ├── window.go ├── window_test.go ├── zip.go └── zip_test.go /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | All contributions are welcome! This guide will help you get started with the contribution process. 4 | 5 | ### Find/Create Issue 6 | 7 | Before you start contributing, you need to find an issue you'd like to resolve. Take a look at [issues](https://github.com/alvii147/gloop/issues) that are currently open and unassigned, and see which one you'd like to tackle. Every PR must have a corresponding being resolved. 8 | 9 | If you want to report a bug or request a feature, you may also [create an issue](https://github.com/alvii147/gloop/issues/new) yourself. 10 | 11 | ### Install Go 12 | 13 | Install [Go](https://go.dev/) if you haven't already. You can check if you already have Go installed by running: 14 | 15 | ```bash 16 | go version 17 | ``` 18 | 19 | ### Install Git 20 | 21 | Install [Git](https://git-scm.com/) if you haven't already. You can check if you already have Git installed by running: 22 | 23 | ```bash 24 | git --version 25 | ``` 26 | 27 | ### Install GNU Make 28 | 29 | Install [GNU Make](https://www.gnu.org/software/make/) if you haven't already. You can check if you already have GNU Make installed by running: 30 | 31 | ```bash 32 | make --version 33 | ``` 34 | 35 | ## Install golangci-lint 36 | 37 | Install [golangci-lint](https://golangci-lint.run/) if you haven't already. You can check if you already have golangci-lint install by running: 38 | 39 | ```bash 40 | golangci-lint version 41 | ``` 42 | 43 | ### Fork this Repository 44 | 45 | Create a [fork](https://github.com/alvii147/gloop/fork) of this repository on your own GitHub account. 46 | 47 | ## Make Changes 48 | 49 | Clone your forked repository: 50 | 51 | ```bash 52 | git clone https://github.com//.git 53 | ``` 54 | 55 | Once you've cloned it locally, make your desired changes. 56 | 57 | ## Update Documentation 58 | 59 | If your changes include additions/updates to code, remember to: 60 | 61 | * Add/update an example of usage in `example_test.go` 62 | * Add/update the [features section](https://github.com/alvii147/gloop?#features) in `README.md` 63 | 64 | ## Running Tests 65 | 66 | To run all tests with coverage, run: 67 | 68 | ```bash 69 | make test 70 | ``` 71 | 72 | For this library, **100% coverage is mandatory.** 73 | 74 | To run a specific test, set the `TESTCASE` variable: 75 | 76 | ``` 77 | make test TESTCASE=TestName 78 | ``` 79 | 80 | ## Running Linter 81 | 82 | To run the linter, run: 83 | 84 | ```bash 85 | make lint 86 | ``` 87 | 88 | ## Open Pull Request 89 | 90 | Open a pull request between your fork and the `main` branch of this repository for your proposed changes. Remember to document the details of your proposal. 91 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue template 3 | about: Report a bug or request a feature 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Issue Type 11 | 12 | I'm submitting a/an ... 13 | 14 | * [ ] Bug Report 15 | * [ ] Feature Request 16 | * [ ] Other 17 | 18 | ## Steps to Reproduce the Problem 19 | 20 | * :one: 21 | * :two: 22 | * :three: 23 | 24 | ## Expected Behavior 25 | 26 | 27 | 28 | ## Actual Behavior 29 | 30 | 31 | 32 | ## Specifications 33 | 34 | * Version: 35 | * Platform: 36 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Resolved Issue(s) 2 | 3 | 8 | * # 9 | 10 | ## Proposed Changes 11 | 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/github-ci.yml: -------------------------------------------------------------------------------- 1 | name: GitHub CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | go-mod: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v4 17 | with: 18 | go-version: '1.23' 19 | 20 | - name: Check go mod is up to date 21 | run: | 22 | go mod tidy 23 | git diff --exit-code 24 | 25 | lint: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - name: Set up Go 31 | uses: actions/setup-go@v4 32 | with: 33 | go-version: '1.23' 34 | 35 | - name: Install golangci-lint 36 | run: | 37 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin 38 | 39 | - name: Run lint check 40 | run: | 41 | make lint 42 | 43 | tests: 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | 48 | - name: Set up Go 49 | uses: actions/setup-go@v4 50 | with: 51 | go-version: '1.23' 52 | 53 | - name: Run tests 54 | run: | 55 | make test 56 | 57 | - name: Show coverage 58 | run: | 59 | make cover 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | bin/ 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | main 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Go workspace file 17 | go.work 18 | 19 | # OS specific 20 | .DS_Store 21 | 22 | # VS code 23 | __debug_bin* 24 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - asasalint 5 | - asciicheck 6 | - bidichk 7 | - bodyclose 8 | - canonicalheader 9 | - contextcheck 10 | - copyloopvar 11 | - decorder 12 | - dogsled 13 | - dupword 14 | - durationcheck 15 | - err113 16 | - errcheck 17 | - errchkjson 18 | - errname 19 | - errorlint 20 | - exhaustive 21 | - exptostd 22 | - fatcontext 23 | - forbidigo 24 | - funcorder 25 | - ginkgolinter 26 | - gocheckcompilerdirectives 27 | - gochecknoglobals 28 | - gochecknoinits 29 | - gochecksumtype 30 | - goconst 31 | - gocritic 32 | - gocyclo 33 | - godot 34 | - godox 35 | - goheader 36 | - gomoddirectives 37 | - gomodguard 38 | - goprintffuncname 39 | - gosec 40 | - gosmopolitan 41 | - govet 42 | - grouper 43 | - iface 44 | - importas 45 | - inamedparam 46 | - ineffassign 47 | - interfacebloat 48 | - intrange 49 | - lll 50 | - loggercheck 51 | - maintidx 52 | - makezero 53 | - mirror 54 | - misspell 55 | - mnd 56 | - musttag 57 | - nakedret 58 | - nestif 59 | - nilerr 60 | - nilnesserr 61 | - nilnil 62 | - nlreturn 63 | - noctx 64 | - nolintlint 65 | - nonamedreturns 66 | - nosprintfhostport 67 | - perfsprint 68 | - prealloc 69 | - predeclared 70 | - promlinter 71 | - protogetter 72 | - reassign 73 | - recvcheck 74 | - revive 75 | - rowserrcheck 76 | - sloglint 77 | - spancheck 78 | - sqlclosecheck 79 | - staticcheck 80 | - tagalign 81 | - tagliatelle 82 | - testableexamples 83 | - testifylint 84 | - testpackage 85 | - thelper 86 | - tparallel 87 | - unconvert 88 | - unparam 89 | - unused 90 | - usestdlibvars 91 | - usetesting 92 | - wastedassign 93 | - whitespace 94 | - wrapcheck 95 | - wsl 96 | - zerologlint 97 | settings: 98 | gosec: 99 | excludes: 100 | - G404 101 | revive: 102 | rules: 103 | - name: unused-parameter 104 | disabled: true 105 | exclusions: 106 | rules: 107 | - path: _test\.go 108 | linters: 109 | - contextcheck 110 | - goconst 111 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## 1.0.0 - 2025-05-24 8 | 9 | This is the first stable release of *gloop*! :tada: 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | alvii147@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Zahin Zaman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO=go 2 | PKG=github.com/alvii147/gloop 3 | COV=coverage.out 4 | 5 | TEST_OPTS=-skip Example.* -coverprofile $(COV) 6 | ifdef TESTCASE 7 | TEST_OPTS=-run $(TESTCASE) 8 | endif 9 | 10 | TEST_OPTS:=$(TEST_OPTS) -v -count=1 11 | 12 | .PHONY: test 13 | test: 14 | $(GO) test $(TEST_OPTS) $(PKG) 15 | 16 | .PHONY: race 17 | race: 18 | $(GO) test -race $(TEST_OPTS) $(PKG) 19 | 20 | .PHONY: cover 21 | cover: 22 | $(GO) tool cover -func $(COV); 23 | 24 | .PHONY: lint 25 | lint: 26 | golangci-lint run 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Genocide Watch](https://hinds-banner.vercel.app/genocide-watch?variant=plum)](https://www.pcrf.net/) 2 | 3 |

4 | gloop logo 5 |

6 | 7 |

8 | gloop is a Go utility library for convenient looping using Go's range-over-func feature. 9 |

10 | 11 |
12 | 13 | [![Go Reference](https://pkg.go.dev/badge/github.com/alvii147/gloop.svg)](https://pkg.go.dev/github.com/alvii147/gloop) [![Tests](https://img.shields.io/github/actions/workflow/status/alvii147/gloop/github-ci.yml?branch=main&label=tests&logo=github)](https://github.com/alvii147/gloop/actions) ![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen) [![Go Report Card](https://goreportcard.com/badge/github.com/alvii147/gloop)](https://goreportcard.com/report/github.com/alvii147/gloop) [![License](https://img.shields.io/github/license/alvii147/gloop)](https://github.com/alvii147/gloop/blob/main/LICENSE) 14 | 15 |
16 | 17 | # Installation 18 | 19 | Install `gloop` using the `go get` command: 20 | 21 | ```bash 22 | go get github.com/alvii147/gloop 23 | ``` 24 | 25 | > [!NOTE] 26 | > Go version 1.23+ required as older versions don't offer the range-over-func feature. 27 | 28 | # Usage 29 | 30 | Once installed, `gloop` can be imported and used directly in your project: 31 | 32 | ```go 33 | package main 34 | 35 | import ( 36 | "fmt" 37 | 38 | "github.com/alvii147/gloop" 39 | ) 40 | 41 | func main() { 42 | for seq := range gloop.Permutations(gloop.String("CAT"), 3) { 43 | perm := gloop.ToString(seq) 44 | fmt.Println(perm) 45 | } 46 | } 47 | ``` 48 | 49 | This ranges over and outputs all permutations of `CAT`: 50 | 51 | ``` 52 | CAT 53 | CTA 54 | ACT 55 | ATC 56 | TCA 57 | TAC 58 | ``` 59 | 60 | See more specific documentation and examples in the [features section](#features) below. 61 | 62 | # Features 63 | 64 | ## Generators 65 | 66 | * [`Interval`](https://pkg.go.dev/github.com/alvii147/gloop#Interval) allows looping over values in a given interval of a given step size. 67 | * [`Linspace`](https://pkg.go.dev/github.com/alvii147/gloop#Linspace) allows looping over evenly spaced values within a given interval. n must be greater than 1. 68 | * [`RandomNormal`](https://pkg.go.dev/github.com/alvii147/gloop#RandomNormal) allows looping over a given number of random values drawn from a Gaussian distribution. The size must not be negative and the standard deviation must be positive. 69 | * [`RandomUniform`](https://pkg.go.dev/github.com/alvii147/gloop#RandomUniform) allows looping over a given number of random values drawn from a uniform distribution. The size must not be negative. 70 | 71 | ## Scalar Iterators 72 | 73 | * [`Chain`](https://pkg.go.dev/github.com/alvii147/gloop#Chain) allows looping over multiple [iter.Seq] sequences. 74 | * [`Chain2`](https://pkg.go.dev/github.com/alvii147/gloop#Chain2) allows looping over multiple [iter.Seq2] sequences. 75 | * [`Channel`](https://pkg.go.dev/github.com/alvii147/gloop#Channel) allows looping over values from a given channel. The values are consumed from the channel. 76 | * [`Collect`](https://pkg.go.dev/github.com/alvii147/gloop#Collect) allows looping over a given set of values. 77 | * [`Enumerate`](https://pkg.go.dev/github.com/alvii147/gloop#Enumerate) allows looping over an [iter.Seq] sequence with an index, converting it to an [iter.Seq2] sequence. 78 | * [`Filter`](https://pkg.go.dev/github.com/alvii147/gloop#Filter) runs a given function on each value from an [iter.Seq] sequence and allows looping over values for which the function returns true. 79 | * [`Filter2`](https://pkg.go.dev/github.com/alvii147/gloop#Filter2) runs a given function on each value from an [iter.Seq2] sequence and allows looping over values for which the function returns true. 80 | * [`Keys`](https://pkg.go.dev/github.com/alvii147/gloop#Keys) allows looping over an [iter.Seq2], converting it to an [iter.Seq] sequence by discarding the value. 81 | * [`KeyValue`](https://pkg.go.dev/github.com/alvii147/gloop#KeyValue) converts an [iter.Seq] sequence of [KeyValuePair] values to an [iter.Seq2] sequence. 82 | * [`KeyValue2`](https://pkg.go.dev/github.com/alvii147/gloop#KeyValue2) converts an [iter.Seq2] sequence to an [iter.Seq] sequence of [KeyValuePair] values. 83 | * [`List`](https://pkg.go.dev/github.com/alvii147/gloop#List) allows looping over a given [container/list.List]. 84 | * [`Map`](https://pkg.go.dev/github.com/alvii147/gloop#Map) allows looping over keys and values in a map. 85 | * [`Reverse`](https://pkg.go.dev/github.com/alvii147/gloop#Reverse) allows looping over an [iter.Seq] sequence in order of descending index. 86 | * [`Reverse2`](https://pkg.go.dev/github.com/alvii147/gloop#Reverse2) allows looping over an [iter.Seq2] sequence in order of descending index. 87 | * [`Slice`](https://pkg.go.dev/github.com/alvii147/gloop#Slice) allows looping over a given slice. 88 | * [`Sort`](https://pkg.go.dev/github.com/alvii147/gloop#Sort) allows looping over an [iter.Seq] sequence in sorted order. 89 | * [`SortByComparison`](https://pkg.go.dev/github.com/alvii147/gloop#SortByComparison) allows looping over an [iter.Seq] sequence in sorted order using a comparison function. 90 | * [`SortByComparison2`](https://pkg.go.dev/github.com/alvii147/gloop#SortByComparison2) allows looping over an [iter.Seq2] sequence in sorted order using a comparison function. 91 | * [`SortByRank`](https://pkg.go.dev/github.com/alvii147/gloop#SortByRank) allows looping over an [iter.Seq] sequence in sorted order using a ranking function. 92 | * [`SortByRank2`](https://pkg.go.dev/github.com/alvii147/gloop#SortByRank2) allows looping over an [iter.Seq2] sequence in sorted order using a ranking function. 93 | * [`String`](https://pkg.go.dev/github.com/alvii147/gloop#String) allows looping over the runes in a given string. 94 | * [`Transform`](https://pkg.go.dev/github.com/alvii147/gloop#Transform) runs a given function on each value over an [iter.Seq] sequence and allows looping over the returned values. 95 | * [`Transform2`](https://pkg.go.dev/github.com/alvii147/gloop#Transform2) runs a given function on each key and value over an [iter.Seq2] sequence and allows looping over the returned values. 96 | * [`Values`](https://pkg.go.dev/github.com/alvii147/gloop#Values) allows looping over an [iter.Seq2] and converting it to an [iter.Seq] sequence by discarding the key. 97 | * [`Zip`](https://pkg.go.dev/github.com/alvii147/gloop#Zip) allows looping over two [iter.Seq] sequences in pairs. 98 | * [`Zip2`](https://pkg.go.dev/github.com/alvii147/gloop#Zip2) allows looping over two [iter.Seq2] sequences in pairs. 99 | 100 | ## Vector Iterators 101 | 102 | * [`Batch`](https://pkg.go.dev/github.com/alvii147/gloop#Batch) allows looping over an [iter.Seq] sequence in batches of a given size. The batch size must be positive. 103 | * [`Batch2`](https://pkg.go.dev/github.com/alvii147/gloop#Batch2) allows looping over an [iter.Seq2] sequence in batches of a given size. The batch size must be positive. 104 | * [`CartesianProduct`](https://pkg.go.dev/github.com/alvii147/gloop#CartesianProduct) allows looping over the Cartesian product of a given size for an [iter.Seq] sequence. The size must be positive. 105 | * [`CartesianProduct2`](https://pkg.go.dev/github.com/alvii147/gloop#CartesianProduct2) allows looping over the Cartesian product of a given size for an [iter.Seq2] sequence. The size must be positive. 106 | * [`Combinations`](https://pkg.go.dev/github.com/alvii147/gloop#Combinations) allows looping over all combinations of a given size for an [iter.Seq] sequence. The size must be positive. 107 | * [`Combinations2`](https://pkg.go.dev/github.com/alvii147/gloop#Combinations2) allows looping over all combinations of a given size for an [iter.Seq2] sequence. The size must be positive. 108 | * [`Permutations`](https://pkg.go.dev/github.com/alvii147/gloop#Permutations) allows looping over all permutations of a given size for an [iter.Seq] sequence. The size must be positive. 109 | * [`Permutations2`](https://pkg.go.dev/github.com/alvii147/gloop#Permutations2) allows looping over all permutations of a given size for an [iter.Seq2] sequence. The size must be positive. 110 | * [`Window`](https://pkg.go.dev/github.com/alvii147/gloop#Window) allows looping over an [iter.Seq] sequence in sliding windows of a given size. 111 | * [`Window2`](https://pkg.go.dev/github.com/alvii147/gloop#Window2) allows looping over an [iter.Seq2] sequence in sliding windows of a given size. 112 | * [`ZipN`](https://pkg.go.dev/github.com/alvii147/gloop#ZipN) allows looping over multiple [iter.Seq] sequences simultaneously. 113 | * [`ZipN2`](https://pkg.go.dev/github.com/alvii147/gloop#ZipN2) allows looping over multiple [iter.Seq2] sequences simultaneously. 114 | 115 | ## Aggregators 116 | 117 | * [`All`](https://pkg.go.dev/github.com/alvii147/gloop#All) computes whether or not all values in an [iter.Seq] sequence are true. 118 | * [`Any`](https://pkg.go.dev/github.com/alvii147/gloop#Any) computes whether or not any value in an [iter.Seq] sequence is true. 119 | * [`Equal`](https://pkg.go.dev/github.com/alvii147/gloop#Equal) checks if two given [iter.Seq] sequences are exactly equal in contents and order. 120 | * [`Equal2`](https://pkg.go.dev/github.com/alvii147/gloop#Equal2) checks if two given [iter.Seq2] sequences are exactly equal in contents and order. 121 | * [`Equivalent`](https://pkg.go.dev/github.com/alvii147/gloop#Equivalent) checks if two given [iter.Seq] sequences are equal in contents, ignoring order. 122 | * [`Equivalent2`](https://pkg.go.dev/github.com/alvii147/gloop#Equivalent2) checks if two given [iter.Seq2] sequences are equal in contents, ignoring order. 123 | * [`Fold`](https://pkg.go.dev/github.com/alvii147/gloop#Fold) runs a given function on each value from an [iter.Seq] sequence and accumulates the result into a single value. 124 | * [`Fold2`](https://pkg.go.dev/github.com/alvii147/gloop#Fold2) runs a given function on each value from an [iter.Seq2] sequence and accumulates the result into a single value. 125 | * [`Max`](https://pkg.go.dev/github.com/alvii147/gloop#Max) computes the maximum value over an [iter.Seq] sequence. 126 | * [`MaxByComparison`](https://pkg.go.dev/github.com/alvii147/gloop#MaxByComparison) computes the maximum value over an [iter.Seq] sequence using a comparison function. 127 | * [`MaxByComparison2`](https://pkg.go.dev/github.com/alvii147/gloop#MaxByComparison2) computes the maximum key and value over an [iter.Seq2] sequence using a comparison function. 128 | * [`MaxByRank`](https://pkg.go.dev/github.com/alvii147/gloop#MaxByRank) computes the maximum value over an [iter.Seq] sequence using a ranking function. 129 | * [`MaxByRank2`](https://pkg.go.dev/github.com/alvii147/gloop#MaxByRank2) computes the maximum value over an [iter.Seq2] sequence using a ranking function. 130 | * [`Mean`](https://pkg.go.dev/github.com/alvii147/gloop#Mean) computes the mean value over an [iter.Seq] sequence. 131 | * [`Min`](https://pkg.go.dev/github.com/alvii147/gloop#Min) computes the minimum value over an [iter.Seq] sequence. 132 | * [`MinByComparison`](https://pkg.go.dev/github.com/alvii147/gloop#MinByComparison) computes the minimum value over an [iter.Seq] sequence using a comparison function. 133 | * [`MinByComparison2`](https://pkg.go.dev/github.com/alvii147/gloop#MinByComparison2) computes the minimum key and value over an [iter.Seq2] sequence using a comparison function. 134 | * [`MinByRank`](https://pkg.go.dev/github.com/alvii147/gloop#MinByRank) computes the minimum value over an [iter.Seq] sequence using a ranking function. 135 | * [`MinByRank2`](https://pkg.go.dev/github.com/alvii147/gloop#MinByRank2) computes the minimum value over an [iter.Seq2] sequence using a ranking function. 136 | * [`Product`](https://pkg.go.dev/github.com/alvii147/gloop#Product) computes the product of values over an [iter.Seq] sequence. 137 | * [`Reduce`](https://pkg.go.dev/github.com/alvii147/gloop#Reduce) runs a given function on each adjacent pair in an [iter.Seq] sequence and accumulates the result into a single value. 138 | * [`Reduce2`](https://pkg.go.dev/github.com/alvii147/gloop#Reduce2) runs a given function on each adjacent pair of keys and values in an [iter.Seq2] sequence and accumulates the result into a single key and value pair. 139 | * [`Sum`](https://pkg.go.dev/github.com/alvii147/gloop#Sum) computes summation over an [iter.Seq] sequence. 140 | * [`ToList`](https://pkg.go.dev/github.com/alvii147/gloop#ToList) converts an [iter.Seq] sequence to a [container/list.List]. 141 | * [`ToList2`](https://pkg.go.dev/github.com/alvii147/gloop#ToList2) converts an [iter.Seq2] sequence to [container/list.List] of keys and values. 142 | * [`ToSlice`](https://pkg.go.dev/github.com/alvii147/gloop#ToSlice) converts an [iter.Seq] sequence to a slice. 143 | * [`ToSlice2`](https://pkg.go.dev/github.com/alvii147/gloop#ToSlice2) converts an [iter.Seq2] sequence to slices of keys and values. 144 | * [`ToString`](https://pkg.go.dev/github.com/alvii147/gloop#ToString) converts an [iter.Seq] sequence of runes to a string. 145 | 146 | ## Miscellaneous 147 | 148 | * [`DeferLoop`](https://pkg.go.dev/github.com/alvii147/gloop#DeferLoop) allows looping over an [iter.Seq] sequence, yielding a defer function that can register another function to be executed at the end of the currently running loop. If multiple functions are registered, they are executed in FIFO order. 149 | * [`Parallelize`](https://pkg.go.dev/github.com/alvii147/gloop#Parallelize) runs a function on each value in an [iter.Seq] sequence on separate goroutines. 150 | * [`Parallelize2`](https://pkg.go.dev/github.com/alvii147/gloop#Parallelize2) runs a function on each value in an [iter.Seq2] sequence on separate goroutines. 151 | 152 | [iter.Seq]: https://pkg.go.dev/iter#Seq 153 | [iter.Seq2]: https://pkg.go.dev/iter#Seq2 154 | [container/list.List]: https://pkg.go.dev/container/list#List 155 | 156 | # Contributing 157 | 158 | All contributions are welcome! Please see [CONTRIBUTING.md](.github/CONTRIBUTING.md) for contribution guidelines. 159 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | To report a security issue, please [create a new security advisory](https://github.com/alvii147/gloop/security/advisories/new) with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue. 6 | -------------------------------------------------------------------------------- /all.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "iter" 5 | ) 6 | 7 | // All computes whether or not all values in an [iter.Seq] sequence are 8 | // true. 9 | func All(seq iter.Seq[bool]) bool { 10 | for value := range seq { 11 | if !value { 12 | return false 13 | } 14 | } 15 | 16 | return true 17 | } 18 | -------------------------------------------------------------------------------- /all_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestAllTrue(t *testing.T) { 11 | values := []bool{true, true, true} 12 | require.True(t, gloop.All(gloop.Slice(values))) 13 | } 14 | 15 | func TestAllMixed(t *testing.T) { 16 | values := []bool{false, false, true, false} 17 | require.False(t, gloop.All(gloop.Slice(values))) 18 | } 19 | 20 | func TestAllFalse(t *testing.T) { 21 | values := []bool{false, false, false, false} 22 | require.False(t, gloop.All(gloop.Slice(values))) 23 | } 24 | -------------------------------------------------------------------------------- /any.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "iter" 5 | ) 6 | 7 | // Any computes whether or not any value in an [iter.Seq] sequence is 8 | // true. 9 | func Any(seq iter.Seq[bool]) bool { 10 | for value := range seq { 11 | if value { 12 | return true 13 | } 14 | } 15 | 16 | return false 17 | } 18 | -------------------------------------------------------------------------------- /any_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestAnyTrue(t *testing.T) { 11 | values := []bool{true, true, true} 12 | require.True(t, gloop.Any(gloop.Slice(values))) 13 | } 14 | 15 | func TestAnyMixed(t *testing.T) { 16 | values := []bool{false, false, true, false} 17 | require.True(t, gloop.Any(gloop.Slice(values))) 18 | } 19 | 20 | func TestAnyFalse(t *testing.T) { 21 | values := []bool{false, false, false, false} 22 | require.False(t, gloop.Any(gloop.Slice(values))) 23 | } 24 | -------------------------------------------------------------------------------- /batch.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "iter" 5 | ) 6 | 7 | // Batch allows looping over an [iter.Seq] sequence in batches of a 8 | // given size. The batch size must be positive. 9 | func Batch[V any](seq iter.Seq[V], size int) iter.Seq[iter.Seq[V]] { 10 | if size <= 0 { 11 | panic("size must be positive") 12 | } 13 | 14 | l := ToList(seq) 15 | 16 | return func(yield func(iter.Seq[V]) bool) { 17 | elem := l.Front() 18 | 19 | for { 20 | if elem == nil { 21 | return 22 | } 23 | 24 | if !yield(func(yield func(V) bool) { 25 | for range size { 26 | if elem == nil { 27 | return 28 | } 29 | 30 | if !yield(elem.Value.(V)) { 31 | return 32 | } 33 | 34 | elem = elem.Next() 35 | } 36 | }) { 37 | return 38 | } 39 | } 40 | } 41 | } 42 | 43 | // Batch2 allows looping over an [iter.Seq2] sequence in batches of a 44 | // given size. The batch size must be positive. 45 | func Batch2[K, V any](seq iter.Seq2[K, V], size int) iter.Seq[iter.Seq2[K, V]] { 46 | if size <= 0 { 47 | panic("size must be positive") 48 | } 49 | 50 | listKeys, listValues := ToList2(seq) 51 | 52 | return func(yield func(iter.Seq2[K, V]) bool) { 53 | keyElem := listKeys.Front() 54 | valueElem := listValues.Front() 55 | 56 | for { 57 | if keyElem == nil || valueElem == nil { 58 | return 59 | } 60 | 61 | if !yield(func(yield func(K, V) bool) { 62 | for range size { 63 | if keyElem == nil || valueElem == nil { 64 | return 65 | } 66 | 67 | key := keyElem.Value.(K) 68 | value := valueElem.Value.(V) 69 | if !yield(key, value) { 70 | return 71 | } 72 | 73 | keyElem = keyElem.Next() 74 | valueElem = valueElem.Next() 75 | } 76 | }) { 77 | return 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /batch_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestBatchSliceDivisibleLength(t *testing.T) { 11 | values := []int{3, 1, 4, 1, 5, 9, 2, 6, 5} 12 | wantBatches := [][]int{ 13 | {3, 1, 4}, 14 | {1, 5, 9}, 15 | {2, 6, 5}, 16 | } 17 | i := 0 18 | 19 | for seq := range gloop.Batch(gloop.Slice(values), 3) { 20 | batch := gloop.ToSlice(seq) 21 | require.Equal(t, wantBatches[i], batch) 22 | 23 | i++ 24 | } 25 | 26 | require.Equal(t, len(wantBatches), i) 27 | } 28 | 29 | func TestBatchSliceIndivisibleLength(t *testing.T) { 30 | values := []int{3, 1, 4, 1, 5, 9, 2, 6, 5} 31 | wantBatches := [][]int{ 32 | {3, 1, 4, 1}, 33 | {5, 9, 2, 6}, 34 | {5}, 35 | } 36 | i := 0 37 | 38 | for seq := range gloop.Batch(gloop.Slice(values), 4) { 39 | batch := gloop.ToSlice(seq) 40 | require.Equal(t, wantBatches[i], batch) 41 | 42 | i++ 43 | } 44 | 45 | require.Equal(t, len(wantBatches), i) 46 | } 47 | 48 | func TestBatchStringDivisibleLength(t *testing.T) { 49 | s := "FizzBuzz" 50 | wantBatches := []string{ 51 | "Fizz", 52 | "Buzz", 53 | } 54 | i := 0 55 | 56 | for seq := range gloop.Batch(gloop.String(s), 4) { 57 | batch := gloop.ToString(seq) 58 | require.Equal(t, wantBatches[i], batch) 59 | 60 | i++ 61 | } 62 | 63 | require.Equal(t, len(wantBatches), i) 64 | } 65 | 66 | func TestBatchStringIndivisibleLength(t *testing.T) { 67 | s := "FizzBuzz" 68 | wantBatches := []string{ 69 | "Fiz", 70 | "zBu", 71 | "zz", 72 | } 73 | i := 0 74 | 75 | for seq := range gloop.Batch(gloop.String(s), 3) { 76 | batch := gloop.ToString(seq) 77 | require.Equal(t, wantBatches[i], batch) 78 | 79 | i++ 80 | } 81 | 82 | require.Equal(t, len(wantBatches), i) 83 | } 84 | 85 | func TestBatchBreak(t *testing.T) { 86 | values := []int{3, 1, 4, 1, 5, 9, 2, 6, 5} 87 | i := 0 88 | 89 | for seq := range gloop.Batch(gloop.Slice(values), 3) { 90 | if i == 1 { 91 | break 92 | } 93 | 94 | for value := range seq { 95 | require.Equal(t, 3, value) 96 | 97 | break 98 | } 99 | 100 | i++ 101 | } 102 | } 103 | 104 | func TestBatchZeroSizePanics(t *testing.T) { 105 | require.Panics(t, func() { 106 | for range gloop.Batch(gloop.Slice([]int{3, 1, 4}), 0) { 107 | t.Fatal("expected no iteration") 108 | } 109 | }) 110 | } 111 | 112 | func TestBatchNegativeSizePanics(t *testing.T) { 113 | require.Panics(t, func() { 114 | for range gloop.Batch(gloop.Slice([]int{3, 1, 4}), -1) { 115 | t.Fatal("expected no iteration") 116 | } 117 | }) 118 | } 119 | 120 | func TestBatch2DivisibleLength(t *testing.T) { 121 | values := []int{3, 1, 4, 1, 5, 9, 2, 6, 5} 122 | wantKeys := [][]int{ 123 | {0, 1, 2}, 124 | {3, 4, 5}, 125 | {6, 7, 8}, 126 | } 127 | wantBatches := [][]int{ 128 | {3, 1, 4}, 129 | {1, 5, 9}, 130 | {2, 6, 5}, 131 | } 132 | i := 0 133 | 134 | for seq := range gloop.Batch2(gloop.Enumerate(gloop.Slice(values)), 3) { 135 | keys, batch := gloop.ToSlice2(seq) 136 | require.Equal(t, wantKeys[i], keys) 137 | require.Equal(t, wantBatches[i], batch) 138 | 139 | i++ 140 | } 141 | 142 | require.Equal(t, len(wantBatches), i) 143 | } 144 | 145 | func TestBatch2IndivisibleLength(t *testing.T) { 146 | values := []int{3, 1, 4, 1, 5, 9, 2, 6, 5} 147 | wantKeys := [][]int{ 148 | {0, 1, 2, 3}, 149 | {4, 5, 6, 7}, 150 | {8}, 151 | } 152 | wantBatches := [][]int{ 153 | {3, 1, 4, 1}, 154 | {5, 9, 2, 6}, 155 | {5}, 156 | } 157 | i := 0 158 | 159 | for seq := range gloop.Batch2(gloop.Enumerate(gloop.Slice(values)), 4) { 160 | keys, batch := gloop.ToSlice2(seq) 161 | require.Equal(t, wantKeys[i], keys) 162 | require.Equal(t, wantBatches[i], batch) 163 | 164 | i++ 165 | } 166 | 167 | require.Equal(t, len(wantBatches), i) 168 | } 169 | 170 | func TestBatch2Break(t *testing.T) { 171 | values := []int{3, 1, 4, 1, 5, 9, 2, 6, 5} 172 | i := 0 173 | 174 | for seq := range gloop.Batch2(gloop.Enumerate(gloop.Slice(values)), 3) { 175 | if i == 1 { 176 | break 177 | } 178 | 179 | for key, value := range seq { 180 | require.Equal(t, 0, key) 181 | require.Equal(t, 3, value) 182 | 183 | break 184 | } 185 | 186 | i++ 187 | } 188 | } 189 | 190 | func TestBatch2ZeroSizePanics(t *testing.T) { 191 | require.Panics(t, func() { 192 | for range gloop.Batch2(gloop.Enumerate(gloop.Slice([]int{3, 1, 4})), 0) { 193 | t.Fatal("expected no iteration") 194 | } 195 | }) 196 | } 197 | 198 | func TestBatch2NegativeSizePanics(t *testing.T) { 199 | require.Panics(t, func() { 200 | for range gloop.Batch2(gloop.Enumerate(gloop.Slice([]int{3, 1, 4})), -1) { 201 | t.Fatal("expected no iteration") 202 | } 203 | }) 204 | } 205 | -------------------------------------------------------------------------------- /cartesianproduct.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "container/list" 5 | "iter" 6 | ) 7 | 8 | // cartesianProduct recursively computes and yields the Cartesian 9 | // product for an [iter.Seq] sequence. 10 | func cartesianProduct[V any]( 11 | seq iter.Seq[V], 12 | size int, 13 | yield func(iter.Seq[V]) bool, 14 | visited *list.List, 15 | ) bool { 16 | if visited.Len() == size { 17 | return yield(func(yield func(V) bool) { 18 | for elem := range List(visited) { 19 | if !yield(elem.Value.(V)) { 20 | return 21 | } 22 | } 23 | }) 24 | } 25 | 26 | for value := range seq { 27 | visited.PushBack(value) 28 | 29 | if !cartesianProduct(seq, size, yield, visited) { 30 | return false 31 | } 32 | 33 | visited.Remove(visited.Back()) 34 | } 35 | 36 | return true 37 | } 38 | 39 | // CartesianProduct allows looping over the Cartesian product of a 40 | // given size for an [iter.Seq] sequence. The size must be positive. 41 | func CartesianProduct[V any](seq iter.Seq[V], size int) iter.Seq[iter.Seq[V]] { 42 | if size <= 0 { 43 | panic("size must be positive") 44 | } 45 | 46 | return func(yield func(iter.Seq[V]) bool) { 47 | cartesianProduct(seq, size, yield, list.New()) 48 | } 49 | } 50 | 51 | // cartesianProduct2 recursively computes and yields the Cartesian 52 | // product for an [iter.Seq2] sequence. 53 | func cartesianProduct2[K, V any]( 54 | seq iter.Seq2[K, V], 55 | size int, 56 | yield func(iter.Seq2[K, V]) bool, 57 | visitedKeys *list.List, 58 | visitedValues *list.List, 59 | ) bool { 60 | if visitedKeys.Len() == size || visitedValues.Len() == size { 61 | return yield(func(yield func(K, V) bool) { 62 | for keyElem, valueElem := range Zip(List(visitedKeys), List(visitedValues)) { 63 | key := keyElem.Value.(K) 64 | value := valueElem.Value.(V) 65 | 66 | if !yield(key, value) { 67 | return 68 | } 69 | } 70 | }) 71 | } 72 | 73 | for key, value := range seq { 74 | visitedKeys.PushBack(key) 75 | visitedValues.PushBack(value) 76 | 77 | if !cartesianProduct2(seq, size, yield, visitedKeys, visitedValues) { 78 | return false 79 | } 80 | 81 | visitedKeys.Remove(visitedKeys.Back()) 82 | visitedValues.Remove(visitedValues.Back()) 83 | } 84 | 85 | return true 86 | } 87 | 88 | // CartesianProduct2 allows looping over the Cartesian product of a 89 | // given size for an [iter.Seq2] sequence. The size must be positive. 90 | func CartesianProduct2[K, V any](seq iter.Seq2[K, V], size int) iter.Seq[iter.Seq2[K, V]] { 91 | if size <= 0 { 92 | panic("size must be positive") 93 | } 94 | 95 | return func(yield func(iter.Seq2[K, V]) bool) { 96 | cartesianProduct2(seq, size, yield, list.New(), list.New()) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /cartesianproduct_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestCartesianProductSlice(t *testing.T) { 11 | values := []int{3, 1, 4} 12 | wantCartesianProducts := [][]int{ 13 | {3, 3}, 14 | {3, 1}, 15 | {3, 4}, 16 | {1, 3}, 17 | {1, 1}, 18 | {1, 4}, 19 | {4, 3}, 20 | {4, 1}, 21 | {4, 4}, 22 | } 23 | i := 0 24 | 25 | for seq := range gloop.CartesianProduct(gloop.Slice(values), 2) { 26 | product := gloop.ToSlice(seq) 27 | require.Equal(t, wantCartesianProducts[i], product) 28 | 29 | i++ 30 | } 31 | 32 | require.Equal(t, len(wantCartesianProducts), i) 33 | } 34 | 35 | func TestCartesianProductString(t *testing.T) { 36 | s := "ABCD" 37 | wantCartesianProducts := []string{ 38 | "AA", "AB", "AC", "AD", 39 | "BA", "BB", "BC", "BD", 40 | "CA", "CB", "CC", "CD", 41 | "DA", "DB", "DC", "DD", 42 | } 43 | i := 0 44 | 45 | for seq := range gloop.CartesianProduct(gloop.String(s), 2) { 46 | product := gloop.ToString(seq) 47 | require.Equal(t, wantCartesianProducts[i], product) 48 | 49 | i++ 50 | } 51 | 52 | require.Equal(t, len(wantCartesianProducts), i) 53 | } 54 | 55 | func TestCartesianProductBreak(t *testing.T) { 56 | values := []int{3, 1, 4} 57 | i := 0 58 | 59 | for seq := range gloop.CartesianProduct(gloop.Slice(values), 2) { 60 | if i == 1 { 61 | break 62 | } 63 | 64 | for value := range seq { 65 | require.Equal(t, 3, value) 66 | 67 | break 68 | } 69 | 70 | i++ 71 | } 72 | } 73 | 74 | func TestCartesianProductZeroSizePanics(t *testing.T) { 75 | require.Panics(t, func() { 76 | for range gloop.CartesianProduct(gloop.Slice([]int{3, 1, 4}), 0) { 77 | t.Fatal("expected no iteration") 78 | } 79 | }) 80 | } 81 | 82 | func TestCartesianProductNegativeSizePanics(t *testing.T) { 83 | require.Panics(t, func() { 84 | for range gloop.CartesianProduct(gloop.Slice([]int{3, 1, 4}), -3) { 85 | t.Fatal("expected no iteration") 86 | } 87 | }) 88 | } 89 | 90 | func TestCartesianProduct2Slice(t *testing.T) { 91 | values := []int{3, 1, 4} 92 | wantKeys := [][]int{ 93 | {0, 0}, 94 | {0, 1}, 95 | {0, 2}, 96 | {1, 0}, 97 | {1, 1}, 98 | {1, 2}, 99 | {2, 0}, 100 | {2, 1}, 101 | {2, 2}, 102 | } 103 | wantCartesianProducts := [][]int{ 104 | {3, 3}, 105 | {3, 1}, 106 | {3, 4}, 107 | {1, 3}, 108 | {1, 1}, 109 | {1, 4}, 110 | {4, 3}, 111 | {4, 1}, 112 | {4, 4}, 113 | } 114 | i := 0 115 | 116 | for seq := range gloop.CartesianProduct2(gloop.Enumerate(gloop.Slice(values)), 2) { 117 | keys, product := gloop.ToSlice2(seq) 118 | require.Equal(t, wantKeys[i], keys) 119 | require.Equal(t, wantCartesianProducts[i], product) 120 | 121 | i++ 122 | } 123 | 124 | require.Equal(t, len(wantCartesianProducts), i) 125 | } 126 | 127 | func TestCartesianProduct2String(t *testing.T) { 128 | s := "ABCD" 129 | wantKeys := [][]int{ 130 | {0, 0}, {0, 1}, {0, 2}, {0, 3}, 131 | {1, 0}, {1, 1}, {1, 2}, {1, 3}, 132 | {2, 0}, {2, 1}, {2, 2}, {2, 3}, 133 | {3, 0}, {3, 1}, {3, 2}, {3, 3}, 134 | } 135 | wantCartesianProducts := []string{ 136 | "AA", "AB", "AC", "AD", 137 | "BA", "BB", "BC", "BD", 138 | "CA", "CB", "CC", "CD", 139 | "DA", "DB", "DC", "DD", 140 | } 141 | i := 0 142 | 143 | for seq := range gloop.CartesianProduct2(gloop.Enumerate(gloop.String(s)), 2) { 144 | keys, productRunes := gloop.ToSlice2(seq) 145 | product := string(productRunes) 146 | 147 | require.Equal(t, wantKeys[i], keys) 148 | require.Equal(t, wantCartesianProducts[i], product) 149 | 150 | i++ 151 | } 152 | 153 | require.Equal(t, len(wantCartesianProducts), i) 154 | } 155 | 156 | func TestCartesianProduct2Break(t *testing.T) { 157 | values := []int{3, 1, 4} 158 | i := 0 159 | 160 | for seq := range gloop.CartesianProduct2(gloop.Enumerate(gloop.Slice(values)), 2) { 161 | if i == 1 { 162 | break 163 | } 164 | 165 | for key, value := range seq { 166 | require.Equal(t, 0, key) 167 | require.Equal(t, 3, value) 168 | 169 | break 170 | } 171 | 172 | i++ 173 | } 174 | } 175 | 176 | func TestCartesianProduct2ZeroSizePanics(t *testing.T) { 177 | require.Panics(t, func() { 178 | for range gloop.CartesianProduct2(gloop.Enumerate(gloop.Slice([]int{3, 1, 4})), 0) { 179 | t.Fatal("expected no iteration") 180 | } 181 | }) 182 | } 183 | 184 | func TestCartesianProduct2NegativeSizePanics(t *testing.T) { 185 | require.Panics(t, func() { 186 | for range gloop.CartesianProduct2(gloop.Enumerate(gloop.Slice([]int{3, 1, 4})), -3) { 187 | t.Fatal("expected no iteration") 188 | } 189 | }) 190 | } 191 | -------------------------------------------------------------------------------- /chain.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import "iter" 4 | 5 | // Chain allows looping over multiple [iter.Seq] sequences. 6 | func Chain[V any](seqs ...iter.Seq[V]) iter.Seq[V] { 7 | return func(yield func(V) bool) { 8 | for _, seq := range seqs { 9 | for value := range seq { 10 | if !yield(value) { 11 | return 12 | } 13 | } 14 | } 15 | } 16 | } 17 | 18 | // Chain2 allows looping over multiple [iter.Seq2] sequences. 19 | func Chain2[K, V any](seqs ...iter.Seq2[K, V]) iter.Seq2[K, V] { 20 | return func(yield func(K, V) bool) { 21 | for _, seq := range seqs { 22 | for i, value := range seq { 23 | if !yield(i, value) { 24 | return 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /chain_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestChainSlices(t *testing.T) { 11 | values1 := []string{"a", "b", "c"} 12 | values2 := []string{"x", "y", "z"} 13 | values3 := []string{"1", "2", "3"} 14 | chainedValues := []string{"a", "b", "c", "x", "y", "z", "1", "2", "3"} 15 | i := 0 16 | 17 | for value := range gloop.Chain( 18 | gloop.Slice(values1), 19 | gloop.Slice(values2), 20 | gloop.Slice(values3), 21 | ) { 22 | require.Equal(t, chainedValues[i], value) 23 | 24 | i++ 25 | } 26 | 27 | require.Equal(t, len(chainedValues), i) 28 | } 29 | 30 | func TestChainStrings(t *testing.T) { 31 | s1 := "Fizz" 32 | s2 := "Buzz" 33 | chainedRunes := []rune{'F', 'i', 'z', 'z', 'B', 'u', 'z', 'z'} 34 | i := 0 35 | 36 | for value := range gloop.Chain(gloop.String(s1), gloop.String(s2)) { 37 | require.Equal(t, chainedRunes[i], value) 38 | 39 | i++ 40 | } 41 | 42 | require.Equal(t, len(chainedRunes), i) 43 | } 44 | 45 | func TestChainSliceAndString(t *testing.T) { 46 | values := []rune{'F', 'i', 'z', 'z'} 47 | s := "Buzz" 48 | chainedRunes := []rune{'F', 'i', 'z', 'z', 'B', 'u', 'z', 'z'} 49 | i := 0 50 | 51 | for value := range gloop.Chain(gloop.Slice(values), gloop.String(s)) { 52 | require.Equal(t, chainedRunes[i], value) 53 | 54 | i++ 55 | } 56 | 57 | require.Equal(t, len(chainedRunes), i) 58 | } 59 | 60 | func TestChainBreak(t *testing.T) { 61 | values := []string{"a", "b", "c"} 62 | chainedValues := []string{"a", "b"} 63 | i := 0 64 | 65 | for value := range gloop.Chain(gloop.Slice(values)) { 66 | if i == 2 { 67 | break 68 | } 69 | 70 | require.Equal(t, chainedValues[i], value) 71 | 72 | i++ 73 | } 74 | } 75 | 76 | func TestChain2Slices(t *testing.T) { 77 | values1 := []string{"a", "b", "c"} 78 | values2 := []string{"x", "y", "z"} 79 | values3 := []string{"1", "2", "3"} 80 | chainedIdx := []int{0, 1, 2, 0, 1, 2, 0, 1, 2} 81 | chainedValues := []string{"a", "b", "c", "x", "y", "z", "1", "2", "3"} 82 | i := 0 83 | 84 | for idx, value := range gloop.Chain2( 85 | gloop.Enumerate(gloop.Slice(values1)), 86 | gloop.Enumerate(gloop.Slice(values2)), 87 | gloop.Enumerate(gloop.Slice(values3)), 88 | ) { 89 | require.Equal(t, chainedIdx[i], idx) 90 | require.Equal(t, chainedValues[i], value) 91 | 92 | i++ 93 | } 94 | 95 | require.Equal(t, len(chainedValues), i) 96 | } 97 | 98 | func TestChain2Strings(t *testing.T) { 99 | s1 := "Fizz" 100 | s2 := "Buzz" 101 | chainedIdx := []int{0, 1, 2, 3, 0, 1, 2, 3} 102 | chainedRunes := []rune{'F', 'i', 'z', 'z', 'B', 'u', 'z', 'z'} 103 | i := 0 104 | 105 | for idx, value := range gloop.Chain2( 106 | gloop.Enumerate(gloop.String(s1)), 107 | gloop.Enumerate(gloop.String(s2)), 108 | ) { 109 | require.Equal(t, chainedIdx[i], idx) 110 | require.Equal(t, chainedRunes[i], value) 111 | 112 | i++ 113 | } 114 | 115 | require.Equal(t, len(chainedRunes), i) 116 | } 117 | 118 | func TestChain2SliceAndString(t *testing.T) { 119 | values := []rune{'F', 'i', 'z', 'z'} 120 | s := "Buzz" 121 | chainedIdx := []int{0, 1, 2, 3, 0, 1, 2, 3} 122 | chainedRunes := []rune{'F', 'i', 'z', 'z', 'B', 'u', 'z', 'z'} 123 | i := 0 124 | 125 | for idx, value := range gloop.Chain2( 126 | gloop.Enumerate(gloop.Slice(values)), 127 | gloop.Enumerate(gloop.String(s)), 128 | ) { 129 | require.Equal(t, chainedIdx[i], idx) 130 | require.Equal(t, chainedRunes[i], value) 131 | 132 | i++ 133 | } 134 | 135 | require.Equal(t, len(chainedRunes), i) 136 | } 137 | 138 | func TestChain2Break(t *testing.T) { 139 | values := []string{"a", "b", "c"} 140 | chainedIdx := []int{0, 1, 2} 141 | chainedValues := []string{"a", "b"} 142 | i := 0 143 | 144 | for idx, value := range gloop.Chain2(gloop.Enumerate(gloop.Slice(values))) { 145 | if i == 2 { 146 | break 147 | } 148 | 149 | require.Equal(t, chainedIdx[i], idx) 150 | require.Equal(t, chainedValues[i], value) 151 | 152 | i++ 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /channel.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import "iter" 4 | 5 | // Channel allows looping over values from a given channel. The values 6 | // are consumed from the channel. 7 | func Channel[V any](ch <-chan V) iter.Seq[V] { 8 | return func(yield func(V) bool) { 9 | for value := range ch { 10 | if !yield(value) { 11 | return 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /channel_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestChannel(t *testing.T) { 11 | values := []string{"Fizz", "Buzz", "Bazz"} 12 | ch := make(chan string, 3) 13 | 14 | go func() { 15 | for _, value := range values { 16 | ch <- value 17 | } 18 | 19 | close(ch) 20 | }() 21 | 22 | i := 0 23 | for value := range gloop.Channel(ch) { 24 | require.Equal(t, values[i], value) 25 | 26 | i++ 27 | } 28 | 29 | require.Equal(t, len(values), i) 30 | } 31 | 32 | func TestChannelBreak(t *testing.T) { 33 | values := []string{"Fizz", "Buzz", "Bazz"} 34 | ch := make(chan string, 3) 35 | 36 | go func() { 37 | for _, value := range values { 38 | ch <- value 39 | } 40 | 41 | close(ch) 42 | }() 43 | 44 | i := 0 45 | 46 | for value := range gloop.Channel(ch) { 47 | require.Equal(t, "Fizz", value) 48 | 49 | i++ 50 | 51 | break 52 | } 53 | 54 | require.Equal(t, 1, i) 55 | require.Len(t, ch, 2) 56 | } 57 | -------------------------------------------------------------------------------- /collect.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import "iter" 4 | 5 | // Collect allows looping over a given set of values. 6 | func Collect[V any](values ...V) iter.Seq[V] { 7 | return func(yield func(V) bool) { 8 | for _, value := range values { 9 | if !yield(value) { 10 | return 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /collect_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestCollect(t *testing.T) { 11 | wantValues := []int{3, 1, 4} 12 | i := 0 13 | 14 | for value := range gloop.Collect(3, 1, 4) { 15 | require.Equal(t, wantValues[i], value) 16 | 17 | i++ 18 | } 19 | 20 | require.Equal(t, len(wantValues), i) 21 | } 22 | 23 | func TestCollectBreak(t *testing.T) { 24 | wantValues := []int{3, 1} 25 | i := 0 26 | 27 | for value := range gloop.Collect(3, 1, 4) { 28 | if i == 2 { 29 | break 30 | } 31 | 32 | require.Equal(t, wantValues[i], value) 33 | 34 | i++ 35 | } 36 | 37 | require.Equal(t, len(wantValues), i) 38 | } 39 | -------------------------------------------------------------------------------- /combinations.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "container/list" 5 | "iter" 6 | ) 7 | 8 | // combinations recursively computes and yields the combinations of an 9 | // [iter.Seq] sequence. 10 | func combinations[V any]( 11 | size int, 12 | yield func(iter.Seq[V]) bool, 13 | visited *list.List, 14 | unvisitedElem *list.Element, 15 | ) bool { 16 | if visited.Len() == size { 17 | return yield(func(yield func(V) bool) { 18 | for elem := range List(visited) { 19 | if !yield(elem.Value.(V)) { 20 | return 21 | } 22 | } 23 | }) 24 | } 25 | 26 | elem := unvisitedElem 27 | for elem != nil { 28 | visited.PushBack(elem.Value) 29 | 30 | if !combinations(size, yield, visited, elem.Next()) { 31 | return false 32 | } 33 | 34 | visited.Remove(visited.Back()) 35 | 36 | elem = elem.Next() 37 | } 38 | 39 | return true 40 | } 41 | 42 | // Combinations allows looping over all combinations of a given size 43 | // for an [iter.Seq] sequence. The size must be positive. 44 | func Combinations[V any](seq iter.Seq[V], size int) iter.Seq[iter.Seq[V]] { 45 | if size <= 0 { 46 | panic("size must be positive") 47 | } 48 | 49 | return func(yield func(iter.Seq[V]) bool) { 50 | combinations(size, yield, list.New(), ToList(seq).Front()) 51 | } 52 | } 53 | 54 | // combinations2 recursively computes and yields the combinations of an 55 | // [iter.Seq2] sequence. 56 | func combinations2[K, V any]( 57 | size int, 58 | yield func(iter.Seq2[K, V]) bool, 59 | visitedKeys *list.List, 60 | visitedValues *list.List, 61 | unvisitedKeyElem *list.Element, 62 | unvisitedValueElem *list.Element, 63 | ) bool { 64 | if visitedKeys.Len() == size || visitedValues.Len() == size { 65 | return yield(func(yield func(K, V) bool) { 66 | for keyElem, valueElem := range Zip(List(visitedKeys), List(visitedValues)) { 67 | key := keyElem.Value.(K) 68 | value := valueElem.Value.(V) 69 | 70 | if !yield(key, value) { 71 | return 72 | } 73 | } 74 | }) 75 | } 76 | 77 | keyElem := unvisitedKeyElem 78 | valueElem := unvisitedValueElem 79 | 80 | for keyElem != nil && valueElem != nil { 81 | visitedKeys.PushBack(keyElem.Value) 82 | visitedValues.PushBack(valueElem.Value) 83 | 84 | if !combinations2(size, yield, visitedKeys, visitedValues, keyElem.Next(), valueElem.Next()) { 85 | return false 86 | } 87 | 88 | visitedKeys.Remove(visitedKeys.Back()) 89 | visitedValues.Remove(visitedValues.Back()) 90 | 91 | keyElem = keyElem.Next() 92 | valueElem = valueElem.Next() 93 | } 94 | 95 | return true 96 | } 97 | 98 | // Combinations2 allows looping over all combinations of a given size 99 | // for an [iter.Seq2] sequence. The size must be positive. 100 | func Combinations2[K, V any](seq iter.Seq2[K, V], size int) iter.Seq[iter.Seq2[K, V]] { 101 | if size <= 0 { 102 | panic("size must be positive") 103 | } 104 | 105 | listKeys, listValues := ToList2(seq) 106 | 107 | return func(yield func(iter.Seq2[K, V]) bool) { 108 | combinations2(size, yield, list.New(), list.New(), listKeys.Front(), listValues.Front()) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /combinations_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestCombinationsSlice(t *testing.T) { 11 | values := []int{3, 1, 4, 2} 12 | wantCombs := [][]int{ 13 | {3, 1, 4}, 14 | {3, 1, 2}, 15 | {3, 4, 2}, 16 | {1, 4, 2}, 17 | } 18 | i := 0 19 | 20 | for seq := range gloop.Combinations(gloop.Slice(values), 3) { 21 | comb := gloop.ToSlice(seq) 22 | require.Equal(t, wantCombs[i], comb) 23 | 24 | i++ 25 | } 26 | 27 | require.Equal(t, len(wantCombs), i) 28 | } 29 | 30 | func TestCombinationsString(t *testing.T) { 31 | s := "ABCD" 32 | wantCombs := []string{"AB", "AC", "AD", "BC", "BD", "CD"} 33 | i := 0 34 | 35 | for seq := range gloop.Combinations(gloop.String(s), 2) { 36 | comb := gloop.ToString(seq) 37 | require.Equal(t, wantCombs[i], comb) 38 | 39 | i++ 40 | } 41 | 42 | require.Equal(t, len(wantCombs), i) 43 | } 44 | 45 | func TestCombinationsBreak(t *testing.T) { 46 | values := []int{3, 1, 4, 2} 47 | i := 0 48 | 49 | for seq := range gloop.Combinations(gloop.Slice(values), 2) { 50 | if i == 1 { 51 | break 52 | } 53 | 54 | for value := range seq { 55 | require.Equal(t, 3, value) 56 | 57 | break 58 | } 59 | 60 | i++ 61 | } 62 | } 63 | 64 | func TestCombinationsZeroSizePanics(t *testing.T) { 65 | require.Panics(t, func() { 66 | for range gloop.Combinations(gloop.Slice([]int{3, 1, 4}), 0) { 67 | t.Fatal("expected no iteration") 68 | } 69 | }) 70 | } 71 | 72 | func TestCombinationsNegativeSizePanics(t *testing.T) { 73 | require.Panics(t, func() { 74 | for range gloop.Combinations(gloop.Slice([]int{3, 1, 4}), -3) { 75 | t.Fatal("expected no iteration") 76 | } 77 | }) 78 | } 79 | 80 | func TestCombinations2Slice(t *testing.T) { 81 | values := []int{3, 1, 4, 2} 82 | wantKeys := [][]int{ 83 | {0, 1, 2}, 84 | {0, 1, 3}, 85 | {0, 2, 3}, 86 | {1, 2, 3}, 87 | } 88 | wantCombs := [][]int{ 89 | {3, 1, 4}, 90 | {3, 1, 2}, 91 | {3, 4, 2}, 92 | {1, 4, 2}, 93 | } 94 | i := 0 95 | 96 | for seq := range gloop.Combinations2(gloop.Enumerate(gloop.Slice(values)), 3) { 97 | keys, comb := gloop.ToSlice2(seq) 98 | require.Equal(t, wantKeys[i], keys) 99 | require.Equal(t, wantCombs[i], comb) 100 | 101 | i++ 102 | } 103 | 104 | require.Equal(t, len(wantCombs), i) 105 | } 106 | 107 | func TestCombinations2String(t *testing.T) { 108 | s := "ABCD" 109 | wantKeys := [][]int{ 110 | {0, 1}, 111 | {0, 2}, 112 | {0, 3}, 113 | {1, 2}, 114 | {1, 3}, 115 | {2, 3}, 116 | } 117 | wantCombs := []string{"AB", "AC", "AD", "BC", "BD", "CD"} 118 | i := 0 119 | 120 | for seq := range gloop.Combinations2(gloop.Enumerate(gloop.String(s)), 2) { 121 | keys, combRunes := gloop.ToSlice2(seq) 122 | comb := string(combRunes) 123 | 124 | require.Equal(t, wantKeys[i], keys) 125 | require.Equal(t, wantCombs[i], comb) 126 | 127 | i++ 128 | } 129 | 130 | require.Equal(t, len(wantCombs), i) 131 | } 132 | 133 | func TestCombinations2Break(t *testing.T) { 134 | values := []int{3, 1, 4, 2} 135 | i := 0 136 | 137 | for seq := range gloop.Combinations2(gloop.Enumerate(gloop.Slice(values)), 2) { 138 | if i == 1 { 139 | break 140 | } 141 | 142 | for key, value := range seq { 143 | require.Equal(t, 0, key) 144 | require.Equal(t, 3, value) 145 | 146 | break 147 | } 148 | 149 | i++ 150 | } 151 | } 152 | 153 | func TestCombinations2ZeroSizePanics(t *testing.T) { 154 | require.Panics(t, func() { 155 | for range gloop.Combinations2(gloop.Enumerate(gloop.Slice([]int{3, 1, 4})), 0) { 156 | t.Fatal("expected no iteration") 157 | } 158 | }) 159 | } 160 | 161 | func TestCombinations2NegativeSizePanics(t *testing.T) { 162 | require.Panics(t, func() { 163 | for range gloop.Combinations2(gloop.Enumerate(gloop.Slice([]int{3, 1, 4})), -3) { 164 | t.Fatal("expected no iteration") 165 | } 166 | }) 167 | } 168 | -------------------------------------------------------------------------------- /deferloop.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "container/list" 5 | "iter" 6 | ) 7 | 8 | // DeferLoopFunc is the function signature of the defer function used 9 | // in [DeferLoop]. 10 | type DeferLoopFunc func(func()) 11 | 12 | // DeferLoop allows looping over an [iter.Seq] sequence, yielding a 13 | // defer function that can register another function to be executed at 14 | // the end of the currently running loop. If multiple functions are 15 | // registered, they are executed in FIFO order. 16 | func DeferLoop[V any](seq iter.Seq[V]) iter.Seq2[V, DeferLoopFunc] { 17 | return func(yield func(V, DeferLoopFunc) bool) { 18 | for value := range seq { 19 | funcs := list.New() 20 | setDeferFunc := func(deferFunc func()) { 21 | funcs.PushFront(deferFunc) 22 | } 23 | 24 | end := yield(value, setDeferFunc) 25 | 26 | for elem := range List(funcs) { 27 | f := elem.Value.(func()) 28 | if f != nil { 29 | f() 30 | } 31 | } 32 | 33 | if !end { 34 | return 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /deferloop_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestDeferLoop(t *testing.T) { 11 | values := []int{3, 1, 4} 12 | operations := make([]string, 0) 13 | wantOperations := []string{"Fizz", "Buzz", "Fizz", "Buzz", "Fizz", "Buzz"} 14 | 15 | i := 0 16 | 17 | for value, deferLoop := range gloop.DeferLoop(gloop.Slice(values)) { 18 | deferLoop(func() { 19 | operations = append(operations, "Buzz") 20 | }) 21 | 22 | operations = append(operations, "Fizz") 23 | 24 | require.Equal(t, values[i], value) 25 | 26 | i++ 27 | } 28 | 29 | require.Equal(t, wantOperations, operations) 30 | } 31 | 32 | func TestDeferLoopMultiple(t *testing.T) { 33 | values := []int{3, 1, 4} 34 | operations := make([]string, 0) 35 | wantOperations := []string{"Fizz", "Buzz", "Bazz", "Fizz", "Buzz", "Bazz", "Fizz", "Buzz", "Bazz"} 36 | 37 | i := 0 38 | 39 | for value, deferLoop := range gloop.DeferLoop(gloop.Slice(values)) { 40 | deferLoop(func() { 41 | operations = append(operations, "Bazz") 42 | }) 43 | 44 | deferLoop(func() { 45 | operations = append(operations, "Buzz") 46 | }) 47 | 48 | operations = append(operations, "Fizz") 49 | 50 | require.Equal(t, values[i], value) 51 | 52 | i++ 53 | } 54 | 55 | require.Equal(t, wantOperations, operations) 56 | } 57 | 58 | func TestDeferLoopNoDefer(t *testing.T) { 59 | values := []int{3, 1, 4} 60 | operations := make([]string, 0) 61 | wantOperations := []string{"Fizz", "Fizz", "Fizz"} 62 | 63 | i := 0 64 | 65 | for value := range gloop.DeferLoop(gloop.Slice(values)) { 66 | operations = append(operations, "Fizz") 67 | 68 | require.Equal(t, values[i], value) 69 | 70 | i++ 71 | } 72 | 73 | require.Equal(t, wantOperations, operations) 74 | } 75 | 76 | func TestDeferLoopDeferOnce(t *testing.T) { 77 | values := []int{3, 1, 4} 78 | operations := make([]string, 0) 79 | wantOperations := []string{"Fizz", "Fizz", "Buzz", "Fizz"} 80 | 81 | i := 0 82 | for value, deferLoop := range gloop.DeferLoop(gloop.Slice(values)) { 83 | if i == 1 { 84 | deferLoop(func() { 85 | operations = append(operations, "Buzz") 86 | }) 87 | } 88 | 89 | operations = append(operations, "Fizz") 90 | 91 | require.Equal(t, values[i], value) 92 | 93 | i++ 94 | } 95 | 96 | require.Equal(t, wantOperations, operations) 97 | } 98 | 99 | func TestDeferLoopBreak(t *testing.T) { 100 | values := []int{3, 1, 4} 101 | operations := make([]string, 0) 102 | wantOperations := []string{"Fizz", "Buzz"} 103 | 104 | for value, deferLoop := range gloop.DeferLoop(gloop.Slice(values)) { 105 | deferLoop(func() { 106 | operations = append(operations, "Buzz") 107 | }) 108 | 109 | operations = append(operations, "Fizz") 110 | 111 | require.Equal(t, 3, value) 112 | 113 | break 114 | } 115 | 116 | require.Equal(t, wantOperations, operations) 117 | } 118 | 119 | func TestDeferLoopNilSafety(t *testing.T) { 120 | values := []int{3, 1, 4} 121 | operations := make([]string, 0) 122 | wantOperations := []string{"Fizz", "Fizz", "Fizz"} 123 | 124 | i := 0 125 | 126 | for value, deferLoop := range gloop.DeferLoop(gloop.Slice(values)) { 127 | deferLoop(nil) 128 | 129 | operations = append(operations, "Fizz") 130 | 131 | require.Equal(t, values[i], value) 132 | 133 | i++ 134 | } 135 | 136 | require.Equal(t, wantOperations, operations) 137 | } 138 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package gloop is a Go utility library for convenient looping using 2 | // Go's [range-over-func] feature. 3 | // 4 | // [range-over-func]: https://go.dev/blog/range-functions 5 | package gloop 6 | -------------------------------------------------------------------------------- /enumerate.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import "iter" 4 | 5 | // Enumerate allows looping over an [iter.Seq] sequence with an index, 6 | // converting it to an [iter.Seq2] sequence. 7 | func Enumerate[V any](seq iter.Seq[V]) iter.Seq2[int, V] { 8 | return func(yield func(int, V) bool) { 9 | i := 0 10 | for value := range seq { 11 | if !yield(i, value) { 12 | return 13 | } 14 | 15 | i++ 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /enumerate_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestEnumerate(t *testing.T) { 11 | values := []string{"a", "b", "c"} 12 | i := 0 13 | 14 | for idx, value := range gloop.Enumerate(gloop.Slice(values)) { 15 | require.Equal(t, i, idx) 16 | require.Equal(t, values[i], value) 17 | 18 | i++ 19 | } 20 | 21 | require.Equal(t, len(values), i) 22 | } 23 | 24 | func TestEnumerateBreak(t *testing.T) { 25 | values := []string{"a", "b", "c"} 26 | i := 0 27 | 28 | for idx, value := range gloop.Enumerate(gloop.Slice(values)) { 29 | if i == 2 { 30 | break 31 | } 32 | 33 | require.Equal(t, i, idx) 34 | require.Equal(t, values[i], value) 35 | 36 | i++ 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /equal.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import "iter" 4 | 5 | // Equal checks if two given [iter.Seq] sequences are exactly equal in 6 | // contents and order. 7 | func Equal[V comparable](seq1 iter.Seq[V], seq2 iter.Seq[V]) bool { 8 | next1, stop1 := iter.Pull(seq1) 9 | defer stop1() 10 | 11 | next2, stop2 := iter.Pull(seq2) 12 | defer stop2() 13 | 14 | for { 15 | value1, ok1 := next1() 16 | value2, ok2 := next2() 17 | 18 | if !ok1 && !ok2 { 19 | break 20 | } 21 | 22 | if ok1 != ok2 { 23 | return false 24 | } 25 | 26 | if value1 != value2 { 27 | return false 28 | } 29 | } 30 | 31 | return true 32 | } 33 | 34 | // Equal2 checks if two given [iter.Seq2] sequences are exactly equal 35 | // in contents and order. 36 | func Equal2[K, V comparable](seq1 iter.Seq2[K, V], seq2 iter.Seq2[K, V]) bool { 37 | next1, stop1 := iter.Pull2(seq1) 38 | defer stop1() 39 | 40 | next2, stop2 := iter.Pull2(seq2) 41 | defer stop2() 42 | 43 | for { 44 | key1, value1, ok1 := next1() 45 | key2, value2, ok2 := next2() 46 | 47 | if !ok1 && !ok2 { 48 | break 49 | } 50 | 51 | if ok1 != ok2 { 52 | return false 53 | } 54 | 55 | if key1 != key2 { 56 | return false 57 | } 58 | 59 | if value1 != value2 { 60 | return false 61 | } 62 | } 63 | 64 | return true 65 | } 66 | -------------------------------------------------------------------------------- /equal_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestEqualSame(t *testing.T) { 11 | values1 := []int{3, 1, 4} 12 | values2 := []int{3, 1, 4} 13 | 14 | eq := gloop.Equal(gloop.Slice(values1), gloop.Slice(values2)) 15 | require.True(t, eq) 16 | } 17 | 18 | func TestEqualEmpty(t *testing.T) { 19 | values1 := []int{} 20 | values2 := []int{} 21 | 22 | eq := gloop.Equal(gloop.Slice(values1), gloop.Slice(values2)) 23 | require.True(t, eq) 24 | } 25 | 26 | func TestEqualDifferentLength(t *testing.T) { 27 | values1 := []int{3, 1, 4, 2} 28 | values2 := []int{3, 1, 4} 29 | 30 | eq := gloop.Equal(gloop.Slice(values1), gloop.Slice(values2)) 31 | require.False(t, eq) 32 | } 33 | 34 | func TestEqualDifferentOrder(t *testing.T) { 35 | values1 := []int{1, 3, 4} 36 | values2 := []int{3, 1, 4} 37 | 38 | eq := gloop.Equal(gloop.Slice(values1), gloop.Slice(values2)) 39 | require.False(t, eq) 40 | } 41 | 42 | func TestEqualDifferentValues(t *testing.T) { 43 | values1 := []int{3, 1, 4} 44 | values2 := []int{3, 8, 4} 45 | 46 | eq := gloop.Equal(gloop.Slice(values1), gloop.Slice(values2)) 47 | require.False(t, eq) 48 | } 49 | 50 | func TestEqual2Same(t *testing.T) { 51 | seq1 := func(yield func(string, int) bool) { 52 | if !yield("Fizz", 3) { 53 | return 54 | } 55 | 56 | if !yield("Buzz", 1) { 57 | return 58 | } 59 | 60 | if !yield("Bazz", 4) { 61 | return 62 | } 63 | } 64 | 65 | seq2 := func(yield func(string, int) bool) { 66 | if !yield("Fizz", 3) { 67 | return 68 | } 69 | 70 | if !yield("Buzz", 1) { 71 | return 72 | } 73 | 74 | if !yield("Bazz", 4) { 75 | return 76 | } 77 | } 78 | 79 | eq := gloop.Equal2(seq1, seq2) 80 | require.True(t, eq) 81 | } 82 | 83 | func TestEqual2Empty(t *testing.T) { 84 | seq1 := func(yield func(string, int) bool) {} 85 | seq2 := func(yield func(string, int) bool) {} 86 | 87 | eq := gloop.Equal2(seq1, seq2) 88 | require.True(t, eq) 89 | } 90 | 91 | func TestEqual2DifferentLength(t *testing.T) { 92 | seq1 := func(yield func(string, int) bool) { 93 | if !yield("Fizz", 3) { 94 | return 95 | } 96 | 97 | if !yield("Buzz", 1) { 98 | return 99 | } 100 | 101 | if !yield("Bazz", 4) { 102 | return 103 | } 104 | } 105 | 106 | seq2 := func(yield func(string, int) bool) { 107 | if !yield("Fizz", 3) { 108 | return 109 | } 110 | 111 | if !yield("Buzz", 1) { 112 | return 113 | } 114 | 115 | if !yield("Bazz", 4) { 116 | return 117 | } 118 | 119 | if !yield("Fuzz", 2) { 120 | return 121 | } 122 | } 123 | 124 | eq := gloop.Equal2(seq1, seq2) 125 | require.False(t, eq) 126 | } 127 | 128 | func TestEqual2DifferentOrder(t *testing.T) { 129 | seq1 := func(yield func(string, int) bool) { 130 | if !yield("Fizz", 3) { 131 | return 132 | } 133 | 134 | if !yield("Buzz", 1) { 135 | return 136 | } 137 | 138 | if !yield("Bazz", 4) { 139 | return 140 | } 141 | } 142 | 143 | seq2 := func(yield func(string, int) bool) { 144 | if !yield("Bazz", 4) { 145 | return 146 | } 147 | 148 | if !yield("Fizz", 3) { 149 | return 150 | } 151 | 152 | if !yield("Buzz", 1) { 153 | return 154 | } 155 | } 156 | 157 | eq := gloop.Equal2(seq1, seq2) 158 | require.False(t, eq) 159 | } 160 | 161 | func TestEqual2DifferentKeys(t *testing.T) { 162 | seq1 := func(yield func(string, int) bool) { 163 | if !yield("Fizz", 3) { 164 | return 165 | } 166 | 167 | if !yield("Cuzz", 1) { 168 | return 169 | } 170 | 171 | if !yield("Bazz", 4) { 172 | return 173 | } 174 | } 175 | 176 | seq2 := func(yield func(string, int) bool) { 177 | if !yield("Fizz", 3) { 178 | return 179 | } 180 | 181 | if !yield("Buzz", 1) { 182 | return 183 | } 184 | 185 | if !yield("Bazz", 4) { 186 | return 187 | } 188 | } 189 | 190 | eq := gloop.Equal2(seq1, seq2) 191 | require.False(t, eq) 192 | } 193 | 194 | func TestEqual2DifferentValues(t *testing.T) { 195 | seq1 := func(yield func(string, int) bool) { 196 | if !yield("Fizz", 3) { 197 | return 198 | } 199 | 200 | if !yield("Buzz", 1) { 201 | return 202 | } 203 | 204 | if !yield("Bazz", 4) { 205 | return 206 | } 207 | } 208 | 209 | seq2 := func(yield func(string, int) bool) { 210 | if !yield("Fizz", 3) { 211 | return 212 | } 213 | 214 | if !yield("Buzz", 1) { 215 | return 216 | } 217 | 218 | if !yield("Bazz", 8) { 219 | return 220 | } 221 | } 222 | 223 | eq := gloop.Equal2(seq1, seq2) 224 | require.False(t, eq) 225 | } 226 | -------------------------------------------------------------------------------- /equivalent.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import "iter" 4 | 5 | // Equivalent checks if two given [iter.Seq] sequences are equal in 6 | // contents, ignoring order. 7 | func Equivalent[V comparable](seq1 iter.Seq[V], seq2 iter.Seq[V]) bool { 8 | next1, stop1 := iter.Pull(seq1) 9 | defer stop1() 10 | 11 | next2, stop2 := iter.Pull(seq2) 12 | defer stop2() 13 | 14 | m1 := make(map[V]int) 15 | m2 := make(map[V]int) 16 | 17 | for { 18 | value1, ok1 := next1() 19 | value2, ok2 := next2() 20 | 21 | if !ok1 && !ok2 { 22 | break 23 | } 24 | 25 | if ok1 != ok2 { 26 | return false 27 | } 28 | 29 | if value1 == value2 { 30 | continue 31 | } 32 | 33 | n12, ok12 := m1[value2] 34 | if ok12 { 35 | if n12 > 1 { 36 | m1[value2] = n12 - 1 37 | } else { 38 | delete(m1, value2) 39 | } 40 | } 41 | 42 | n21, ok21 := m2[value1] 43 | if ok21 { 44 | if n21 > 1 { 45 | m2[value1] = n21 - 1 46 | } else { 47 | delete(m2, value1) 48 | } 49 | } 50 | 51 | if !ok21 { 52 | n11, ok11 := m1[value1] 53 | if ok11 { 54 | m1[value1] = n11 + 1 55 | } else { 56 | m1[value1] = 1 57 | } 58 | } 59 | 60 | if !ok12 { 61 | n22, ok22 := m2[value2] 62 | if ok22 { 63 | m2[value2] = n22 + 1 64 | } else { 65 | m2[value2] = 1 66 | } 67 | } 68 | } 69 | 70 | return len(m1) == 0 && len(m2) == 0 71 | } 72 | 73 | // Equivalent2 checks if two given [iter.Seq2] sequences are equal in 74 | // contents, ignoring order. 75 | func Equivalent2[K, V comparable](seq1 iter.Seq2[K, V], seq2 iter.Seq2[K, V]) bool { 76 | return Equivalent(KeyValue2(seq1), KeyValue2(seq2)) 77 | } 78 | -------------------------------------------------------------------------------- /equivalent_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestEquivalentEqual(t *testing.T) { 11 | values1 := []int{3, 1, 4} 12 | values2 := []int{3, 1, 4} 13 | 14 | eq := gloop.Equivalent(gloop.Slice(values1), gloop.Slice(values2)) 15 | require.True(t, eq) 16 | } 17 | 18 | func TestEquivalentEqualWithRepetition(t *testing.T) { 19 | values1 := []int{3, 1, 4, 1, 5, 9, 2} 20 | values2 := []int{3, 1, 4, 1, 5, 9, 2} 21 | 22 | eq := gloop.Equivalent(gloop.Slice(values1), gloop.Slice(values2)) 23 | require.True(t, eq) 24 | } 25 | 26 | func TestEquivalentDifferentOrder(t *testing.T) { 27 | values1 := []int{3, 4, 1} 28 | values2 := []int{3, 1, 4} 29 | 30 | eq := gloop.Equivalent(gloop.Slice(values1), gloop.Slice(values2)) 31 | require.True(t, eq) 32 | 33 | eq = gloop.Equivalent(gloop.Slice(values2), gloop.Slice(values1)) 34 | require.True(t, eq) 35 | } 36 | 37 | func TestEquivalentDifferentOrderWithRepetition(t *testing.T) { 38 | values1 := []int{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 9} 39 | values2 := []int{4, 6, 5, 1, 5, 5, 2, 9, 3, 1, 3, 9} 40 | 41 | eq := gloop.Equivalent(gloop.Slice(values1), gloop.Slice(values2)) 42 | require.True(t, eq) 43 | 44 | eq = gloop.Equivalent(gloop.Slice(values2), gloop.Slice(values1)) 45 | require.True(t, eq) 46 | } 47 | 48 | func TestEquivalentDifferentLength(t *testing.T) { 49 | values1 := []int{3, 1, 4, 2} 50 | values2 := []int{3, 1, 4} 51 | 52 | eq := gloop.Equivalent(gloop.Slice(values1), gloop.Slice(values2)) 53 | require.False(t, eq) 54 | 55 | eq = gloop.Equivalent(gloop.Slice(values2), gloop.Slice(values1)) 56 | require.False(t, eq) 57 | } 58 | 59 | func TestEquivalentDifferentValues(t *testing.T) { 60 | values1 := []int{3, 1, 4} 61 | values2 := []int{3, 8, 4} 62 | 63 | eq := gloop.Equivalent(gloop.Slice(values1), gloop.Slice(values2)) 64 | require.False(t, eq) 65 | 66 | eq = gloop.Equivalent(gloop.Slice(values2), gloop.Slice(values1)) 67 | require.False(t, eq) 68 | } 69 | 70 | func TestEquivalent2Equal(t *testing.T) { 71 | seq1 := func(yield func(string, int) bool) { 72 | if !yield("Fizz", 3) { 73 | return 74 | } 75 | 76 | if !yield("Buzz", 1) { 77 | return 78 | } 79 | 80 | if !yield("Bazz", 4) { 81 | return 82 | } 83 | } 84 | 85 | seq2 := func(yield func(string, int) bool) { 86 | if !yield("Fizz", 3) { 87 | return 88 | } 89 | 90 | if !yield("Buzz", 1) { 91 | return 92 | } 93 | 94 | if !yield("Bazz", 4) { 95 | return 96 | } 97 | } 98 | 99 | eq := gloop.Equivalent2(seq1, seq2) 100 | require.True(t, eq) 101 | } 102 | 103 | func TestEquivalent2EqualWithRepetition(t *testing.T) { 104 | seq1 := func(yield func(string, int) bool) { 105 | if !yield("Fizz", 3) { 106 | return 107 | } 108 | 109 | if !yield("Buzz", 1) { 110 | return 111 | } 112 | 113 | if !yield("Bazz", 4) { 114 | return 115 | } 116 | 117 | if !yield("Fuzz", 1) { 118 | return 119 | } 120 | 121 | if !yield("Fazz", 5) { 122 | return 123 | } 124 | 125 | if !yield("Bizz", 9) { 126 | return 127 | } 128 | } 129 | 130 | seq2 := func(yield func(string, int) bool) { 131 | if !yield("Fizz", 3) { 132 | return 133 | } 134 | 135 | if !yield("Buzz", 1) { 136 | return 137 | } 138 | 139 | if !yield("Bazz", 4) { 140 | return 141 | } 142 | 143 | if !yield("Fuzz", 1) { 144 | return 145 | } 146 | 147 | if !yield("Fazz", 5) { 148 | return 149 | } 150 | 151 | if !yield("Bizz", 9) { 152 | return 153 | } 154 | } 155 | 156 | eq := gloop.Equivalent2(seq1, seq2) 157 | require.True(t, eq) 158 | } 159 | 160 | func TestEquivalent2DifferentOrder(t *testing.T) { 161 | seq1 := func(yield func(string, int) bool) { 162 | if !yield("Fizz", 3) { 163 | return 164 | } 165 | 166 | if !yield("Buzz", 1) { 167 | return 168 | } 169 | 170 | if !yield("Bazz", 4) { 171 | return 172 | } 173 | } 174 | 175 | seq2 := func(yield func(string, int) bool) { 176 | if !yield("Fizz", 3) { 177 | return 178 | } 179 | 180 | if !yield("Buzz", 1) { 181 | return 182 | } 183 | 184 | if !yield("Bazz", 4) { 185 | return 186 | } 187 | } 188 | 189 | eq := gloop.Equivalent2(seq1, seq2) 190 | require.True(t, eq) 191 | 192 | eq = gloop.Equivalent2(seq2, seq1) 193 | require.True(t, eq) 194 | } 195 | 196 | func TestEquivalent2DifferentOrderWithRepetition(t *testing.T) { 197 | seq1 := func(yield func(string, int) bool) { 198 | if !yield("Fizz", 3) { 199 | return 200 | } 201 | 202 | if !yield("Buzz", 1) { 203 | return 204 | } 205 | 206 | if !yield("Bazz", 4) { 207 | return 208 | } 209 | 210 | if !yield("Fuzz", 1) { 211 | return 212 | } 213 | 214 | if !yield("Fazz", 5) { 215 | return 216 | } 217 | 218 | if !yield("Bizz", 9) { 219 | return 220 | } 221 | 222 | if !yield("Dizz", 2) { 223 | return 224 | } 225 | 226 | if !yield("Duzz", 6) { 227 | return 228 | } 229 | 230 | if !yield("Fazz", 5) { 231 | return 232 | } 233 | 234 | if !yield("Buzz", 3) { 235 | return 236 | } 237 | 238 | if !yield("Fazz", 5) { 239 | return 240 | } 241 | 242 | if !yield("Bizz", 9) { 243 | return 244 | } 245 | } 246 | 247 | seq2 := func(yield func(string, int) bool) { 248 | if !yield("Bazz", 4) { 249 | return 250 | } 251 | 252 | if !yield("Duzz", 6) { 253 | return 254 | } 255 | 256 | if !yield("Fazz", 5) { 257 | return 258 | } 259 | 260 | if !yield("Buzz", 1) { 261 | return 262 | } 263 | 264 | if !yield("Fazz", 5) { 265 | return 266 | } 267 | 268 | if !yield("Fazz", 5) { 269 | return 270 | } 271 | 272 | if !yield("Dizz", 2) { 273 | return 274 | } 275 | 276 | if !yield("Bizz", 9) { 277 | return 278 | } 279 | 280 | if !yield("Fizz", 3) { 281 | return 282 | } 283 | 284 | if !yield("Fuzz", 1) { 285 | return 286 | } 287 | 288 | if !yield("Buzz", 3) { 289 | return 290 | } 291 | 292 | if !yield("Bizz", 9) { 293 | return 294 | } 295 | } 296 | 297 | eq := gloop.Equivalent2(seq1, seq2) 298 | require.True(t, eq) 299 | 300 | eq = gloop.Equivalent2(seq2, seq1) 301 | require.True(t, eq) 302 | } 303 | 304 | func TestEquivalent2DifferentLength(t *testing.T) { 305 | seq1 := func(yield func(string, int) bool) { 306 | if !yield("Fizz", 3) { 307 | return 308 | } 309 | 310 | if !yield("Buzz", 1) { 311 | return 312 | } 313 | 314 | if !yield("Bazz", 4) { 315 | return 316 | } 317 | 318 | if !yield("Bizz", 2) { 319 | return 320 | } 321 | } 322 | 323 | seq2 := func(yield func(string, int) bool) { 324 | if !yield("Fizz", 3) { 325 | return 326 | } 327 | 328 | if !yield("Buzz", 1) { 329 | return 330 | } 331 | 332 | if !yield("Bazz", 4) { 333 | return 334 | } 335 | } 336 | 337 | eq := gloop.Equivalent2(seq1, seq2) 338 | require.False(t, eq) 339 | 340 | eq = gloop.Equivalent2(seq2, seq1) 341 | require.False(t, eq) 342 | } 343 | 344 | func TestEquivalent2DifferentValues(t *testing.T) { 345 | seq1 := func(yield func(string, int) bool) { 346 | if !yield("Fizz", 3) { 347 | return 348 | } 349 | 350 | if !yield("Huzz", 1) { 351 | return 352 | } 353 | 354 | if !yield("Bazz", 4) { 355 | return 356 | } 357 | } 358 | 359 | seq2 := func(yield func(string, int) bool) { 360 | if !yield("Fizz", 3) { 361 | return 362 | } 363 | 364 | if !yield("Buzz", 1) { 365 | return 366 | } 367 | 368 | if !yield("Bazz", 4) { 369 | return 370 | } 371 | } 372 | 373 | eq := gloop.Equivalent2(seq1, seq2) 374 | require.False(t, eq) 375 | 376 | eq = gloop.Equivalent2(seq2, seq1) 377 | require.False(t, eq) 378 | } 379 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import "iter" 4 | 5 | // FilterFunc is the function signature of the filtering function used 6 | // in [Filter]. 7 | type FilterFunc[V any] func(V) bool 8 | 9 | // Filter runs a given function on each value from an [iter.Seq] 10 | // sequence and allows looping over values for which the function 11 | // returns true. 12 | func Filter[V any](seq iter.Seq[V], f FilterFunc[V]) iter.Seq[V] { 13 | return Values(Filter2(Enumerate(seq), func(_ int, value V) bool { 14 | return f(value) 15 | })) 16 | } 17 | 18 | // Filter2Func is the function signature of the filtering function used 19 | // in [Filter2]. 20 | type Filter2Func[K, V any] func(K, V) bool 21 | 22 | // Filter2 runs a given function on each value from an [iter.Seq2] 23 | // sequence and allows looping over values for which the function 24 | // returns true. 25 | func Filter2[K, V any](seq iter.Seq2[K, V], f Filter2Func[K, V]) iter.Seq2[K, V] { 26 | return func(yield func(K, V) bool) { 27 | for i, value := range seq { 28 | if !f(i, value) { 29 | continue 30 | } 31 | 32 | if !yield(i, value) { 33 | return 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /filter_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | "unicode" 6 | 7 | "github.com/alvii147/gloop" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestFilterSliceOdd(t *testing.T) { 12 | values := []int{3, 4, 5} 13 | wantValues := []int{3, 5} 14 | i := 0 15 | 16 | for value := range gloop.Filter(gloop.Slice(values), func(value int) bool { 17 | return value%2 == 1 18 | }) { 19 | require.Equal(t, wantValues[i], value) 20 | 21 | i++ 22 | } 23 | 24 | require.Equal(t, len(wantValues), i) 25 | } 26 | 27 | func TestFilterSliceLen(t *testing.T) { 28 | values := []string{"Fizz", "Buzz", "FizzBuzz"} 29 | wantValues := []string{"Fizz", "Buzz"} 30 | i := 0 31 | 32 | for value := range gloop.Filter(gloop.Slice(values), func(value string) bool { 33 | return len(value) == 4 34 | }) { 35 | require.Equal(t, wantValues[i], value) 36 | 37 | i++ 38 | } 39 | 40 | require.Equal(t, len(wantValues), i) 41 | } 42 | 43 | func TestFilterStringLower(t *testing.T) { 44 | s := "FiZz" 45 | wantRunes := []rune{'i', 'z'} 46 | i := 0 47 | 48 | for r := range gloop.Filter(gloop.String(s), unicode.IsLower) { 49 | require.Equal(t, wantRunes[i], r) 50 | 51 | i++ 52 | } 53 | 54 | require.Equal(t, len(wantRunes), i) 55 | } 56 | 57 | func TestFilterBreak(t *testing.T) { 58 | values := []int{3, 4, 5} 59 | wantValues := []int{3, 4} 60 | i := 0 61 | 62 | for value := range gloop.Filter(gloop.Slice(values), func(value int) bool { 63 | return true 64 | }) { 65 | if i == 2 { 66 | break 67 | } 68 | 69 | require.Equal(t, wantValues[i], value) 70 | 71 | i++ 72 | } 73 | 74 | require.Equal(t, len(wantValues), i) 75 | } 76 | 77 | func TestFilter2MapPositiveProduct(t *testing.T) { 78 | m := map[int]int{ 79 | -1: -2, 80 | 8: -3, 81 | -4: 9, 82 | } 83 | i := 0 84 | 85 | for key, value := range gloop.Filter2(gloop.Map(m), func(key int, value int) bool { 86 | return (key * value) >= 0 87 | }) { 88 | require.Equal(t, -1, key) 89 | require.Equal(t, -2, value) 90 | 91 | i++ 92 | } 93 | 94 | require.Equal(t, 1, i) 95 | } 96 | 97 | func TestFilter2MapCorrectLen(t *testing.T) { 98 | m := map[string]int{ 99 | "Fizz": 8, 100 | "Buzz": 4, 101 | "FizzBuzz": 4, 102 | } 103 | i := 0 104 | 105 | for key, value := range gloop.Filter2(gloop.Map(m), func(key string, value int) bool { 106 | return len(key) == value 107 | }) { 108 | require.Equal(t, "Buzz", key) 109 | require.Equal(t, 4, value) 110 | 111 | i++ 112 | } 113 | 114 | require.Equal(t, 1, i) 115 | } 116 | 117 | func TestFilter2Break(t *testing.T) { 118 | values := []string{"Fizz", "Buzz", "Bazz"} 119 | wantValues := []string{"Fizz", "Buzz"} 120 | i := 0 121 | 122 | for idx, value := range gloop.Filter2(gloop.Enumerate(gloop.Slice(values)), func(_ int, value string) bool { 123 | return true 124 | }) { 125 | if i == 2 { 126 | break 127 | } 128 | 129 | require.Equal(t, i, idx) 130 | require.Equal(t, wantValues[i], value) 131 | 132 | i++ 133 | } 134 | 135 | require.Equal(t, 2, i) 136 | } 137 | -------------------------------------------------------------------------------- /fold.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import "iter" 4 | 5 | // FoldOptions defines configurable options for [Fold] and [Fold2]. 6 | type FoldOptions[A any] struct { 7 | // InitialValue is the starting value before folding. 8 | InitialValue *A 9 | } 10 | 11 | // FoldOptionFunc is the function signature of configuration helpers 12 | // for [Fold] and [Fold2]. 13 | type FoldOptionFunc[A any] func(*FoldOptions[A]) 14 | 15 | // WithFoldInitialValue is a helper for configuring initial value for 16 | // [Fold] and [Fold2]. 17 | func WithFoldInitialValue[A any](initialValue A) FoldOptionFunc[A] { 18 | return func(o *FoldOptions[A]) { 19 | o.InitialValue = &initialValue 20 | } 21 | } 22 | 23 | // FoldFunc is the function signature of the folding function used in 24 | // [Fold]. 25 | type FoldFunc[A, V any] func(A, V) A 26 | 27 | // Fold runs a given function on each value from an [iter.Seq] sequence 28 | // and accumulates the result into a single value. 29 | func Fold[A, V any]( 30 | seq iter.Seq[V], 31 | f FoldFunc[A, V], 32 | opts ...FoldOptionFunc[A], 33 | ) A { 34 | return Fold2(Enumerate(seq), func(acc A, _ int, value V) A { 35 | return f(acc, value) 36 | }, opts...) 37 | } 38 | 39 | // Fold2Func is the function signature of the reduction function used 40 | // in [Fold2]. 41 | type Fold2Func[A, K, V any] func(A, K, V) A 42 | 43 | // Fold2 runs a given function on each value from an [iter.Seq2] 44 | // sequence and accumulates the result into a single value. 45 | func Fold2[A, K, V any]( 46 | seq iter.Seq2[K, V], 47 | f Fold2Func[A, K, V], 48 | opts ...FoldOptionFunc[A], 49 | ) A { 50 | options := FoldOptions[A]{ 51 | InitialValue: nil, 52 | } 53 | 54 | for _, opt := range opts { 55 | opt(&options) 56 | } 57 | 58 | var acc A 59 | if options.InitialValue != nil { 60 | acc = *options.InitialValue 61 | } 62 | 63 | for key, value := range seq { 64 | acc = f(acc, key, value) 65 | } 66 | 67 | return acc 68 | } 69 | -------------------------------------------------------------------------------- /fold_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | "unicode" 6 | 7 | "github.com/alvii147/gloop" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestWithFoldInitialValue(t *testing.T) { 12 | initialValue := 42 13 | options := gloop.FoldOptions[int]{} 14 | gloop.WithFoldInitialValue(initialValue)(&options) 15 | 16 | require.NotNil(t, options.InitialValue) 17 | require.Equal(t, initialValue, *options.InitialValue) 18 | } 19 | 20 | func TestFoldSliceSum(t *testing.T) { 21 | values := []int{1, 2, 3} 22 | sum := gloop.Fold(gloop.Slice(values), func(acc int, value int) int { 23 | return acc + value 24 | }) 25 | require.Equal(t, 6, sum) 26 | } 27 | 28 | func TestFoldSliceProductWithInitialValue(t *testing.T) { 29 | values := []int{3, 4, 5} 30 | 31 | product := gloop.Fold(gloop.Slice(values), func(acc int, value int) int { 32 | return acc * value 33 | }, gloop.WithFoldInitialValue(1)) 34 | require.Equal(t, 60, product) 35 | } 36 | 37 | func TestFoldSliceConcatenate(t *testing.T) { 38 | values := []string{"Fizz", "Buzz"} 39 | 40 | concatString := gloop.Fold(gloop.Slice(values), func(acc string, value string) string { 41 | return acc + value 42 | }) 43 | require.Equal(t, "FizzBuzz", concatString) 44 | } 45 | 46 | func TestFoldSliceLen(t *testing.T) { 47 | values := []string{"Fizz", "Buzz"} 48 | 49 | lenStrings := gloop.Fold(gloop.Slice(values), func(acc int, value string) int { 50 | return acc + len(value) 51 | }) 52 | require.Equal(t, 8, lenStrings) 53 | } 54 | 55 | func TestFoldStringNumericCount(t *testing.T) { 56 | s := "F1Z2bU2z" 57 | 58 | sum := gloop.Fold(gloop.String(s), func(acc int, r rune) int { 59 | if unicode.IsNumber(r) { 60 | return acc + 1 61 | } 62 | 63 | return acc 64 | }) 65 | require.Equal(t, 3, sum) 66 | } 67 | 68 | func TestFold2MapSumOfProducts(t *testing.T) { 69 | m := map[int]int{ 70 | 3: 4, 71 | 8: -1, 72 | -2: -5, 73 | } 74 | sum := gloop.Fold2(gloop.Map(m), func(acc int, key int, value int) int { 75 | return acc + (key * value) 76 | }) 77 | require.Equal(t, 14, sum) 78 | } 79 | 80 | func TestFold2MapProductOfValues(t *testing.T) { 81 | m := map[string]int{ 82 | "Fizz": 4, 83 | "Buzz": -1, 84 | "Bazz": -5, 85 | } 86 | product := gloop.Fold2(gloop.Map(m), func(acc int, key string, value int) int { 87 | return acc * value 88 | }, gloop.WithFoldInitialValue(1)) 89 | require.Equal(t, 20, product) 90 | } 91 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alvii147/gloop 2 | 3 | go 1.23 4 | 5 | require github.com/stretchr/testify v1.9.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 6 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /img/logo1398x1048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvii147/gloop/26773c0565d41c8e400a35a2ceaf3442f5f7ba52/img/logo1398x1048.png -------------------------------------------------------------------------------- /img/logo1398x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alvii147/gloop/26773c0565d41c8e400a35a2ceaf3442f5f7ba52/img/logo1398x400.png -------------------------------------------------------------------------------- /interval.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import "iter" 4 | 5 | // IntervalOptions defines configurable options for [Interval]. 6 | type IntervalOptions struct { 7 | // Closed represents whether or not the interval is closed at the 8 | // stop point. 9 | Closed bool 10 | } 11 | 12 | // IntervalOptionFunc is the function signature of configuration 13 | // helpers for [Interval]. 14 | type IntervalOptionFunc func(*IntervalOptions) 15 | 16 | // WithIntervalClosed is a helper for configuring the interval to be 17 | // closed for [Interval]. 18 | func WithIntervalClosed(closed bool) IntervalOptionFunc { 19 | return func(o *IntervalOptions) { 20 | o.Closed = closed 21 | } 22 | } 23 | 24 | // Interval allows looping over values in a given interval of a given 25 | // step size. 26 | func Interval[N Number]( 27 | start N, 28 | stop N, 29 | step N, 30 | opts ...IntervalOptionFunc, 31 | ) iter.Seq[N] { 32 | options := IntervalOptions{ 33 | Closed: false, 34 | } 35 | 36 | for _, opt := range opts { 37 | opt(&options) 38 | } 39 | 40 | if step == 0 { 41 | return func(yield func(N) bool) {} 42 | } 43 | 44 | if step > 0 && stop-start < 0 { 45 | return func(yield func(N) bool) {} 46 | } 47 | 48 | if step < 0 && stop-start > 0 { 49 | return func(yield func(N) bool) {} 50 | } 51 | 52 | var loopCond func(i N, stop N) bool 53 | 54 | switch { 55 | case step > 0 && options.Closed: 56 | loopCond = func(i N, stop N) bool { 57 | return i <= stop 58 | } 59 | case step < 0 && options.Closed: 60 | loopCond = func(i N, stop N) bool { 61 | return i >= stop 62 | } 63 | case step > 0 && !options.Closed: 64 | loopCond = func(i N, stop N) bool { 65 | return i < stop 66 | } 67 | case step < 0 && !options.Closed: 68 | loopCond = func(i N, stop N) bool { 69 | return i > stop 70 | } 71 | } 72 | 73 | return func(yield func(N) bool) { 74 | for i := start; loopCond(i, stop); i += step { 75 | if !yield(i) { 76 | return 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /interval_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestIntervalInt(t *testing.T) { 11 | wantValues := []int{3, 5} 12 | i := 0 13 | 14 | for value := range gloop.Interval(3, 7, 2) { 15 | require.Equal(t, wantValues[i], value) 16 | 17 | i++ 18 | } 19 | 20 | require.Equal(t, len(wantValues), i) 21 | } 22 | 23 | func TestIntervalFloat(t *testing.T) { 24 | wantValues := []float64{2, 2.5, 3, 3.5, 4, 4.5} 25 | i := 0 26 | 27 | for value := range gloop.Interval(2, 5, 0.5) { 28 | require.InDelta(t, wantValues[i], value, 0.01) 29 | 30 | i++ 31 | } 32 | 33 | require.Equal(t, len(wantValues), i) 34 | } 35 | 36 | func TestIntervalNegativeStep(t *testing.T) { 37 | wantValues := []int{10, 7, 4} 38 | i := 0 39 | 40 | for value := range gloop.Interval(10, 3, -3) { 41 | require.Equal(t, wantValues[i], value) 42 | 43 | i++ 44 | } 45 | 46 | require.Equal(t, len(wantValues), i) 47 | } 48 | 49 | func TestIntervalZeroStepNoIteration(t *testing.T) { 50 | for range gloop.Interval(3, 7, 0) { 51 | t.Fatal("expected no iteration") 52 | } 53 | } 54 | 55 | func TestIntervalStopUnreachableNoIteration(t *testing.T) { 56 | for range gloop.Interval(10, 3, 2) { 57 | t.Fatal("expected no iteration") 58 | } 59 | } 60 | 61 | func TestIntervalNegativeStepNoIteration(t *testing.T) { 62 | for range gloop.Interval(3, 7, -3) { 63 | t.Fatal("expected no iteration") 64 | } 65 | } 66 | 67 | func TestIntervalClosed(t *testing.T) { 68 | wantValues := []int{3, 5, 7} 69 | i := 0 70 | 71 | for value := range gloop.Interval(3, 7, 2, gloop.WithIntervalClosed(true)) { 72 | require.Equal(t, wantValues[i], value) 73 | 74 | i++ 75 | } 76 | 77 | require.Equal(t, len(wantValues), i) 78 | } 79 | 80 | func TestIntervalClosedNegativeStep(t *testing.T) { 81 | wantValues := []int{10, 7, 4, 1} 82 | i := 0 83 | 84 | for value := range gloop.Interval(10, 1, -3, gloop.WithIntervalClosed(true)) { 85 | require.Equal(t, wantValues[i], value) 86 | 87 | i++ 88 | } 89 | 90 | require.Equal(t, len(wantValues), i) 91 | } 92 | 93 | func TestIntervalBreak(t *testing.T) { 94 | wantValues := []int{3, 6} 95 | i := 0 96 | 97 | for value := range gloop.Interval(3, 20, 3) { 98 | if i == 2 { 99 | break 100 | } 101 | 102 | require.Equal(t, wantValues[i], value) 103 | 104 | i++ 105 | } 106 | 107 | require.Equal(t, len(wantValues), i) 108 | } 109 | -------------------------------------------------------------------------------- /keys.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import "iter" 4 | 5 | // Keys allows looping over an [iter.Seq2], converting it to an 6 | // [iter.Seq] sequence by discarding the value. 7 | func Keys[K, V any](seq iter.Seq2[K, V]) iter.Seq[K] { 8 | return func(yield func(K) bool) { 9 | for key := range seq { 10 | if !yield(key) { 11 | return 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /keys_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestKeys(t *testing.T) { 11 | values := []string{"a", "b", "c"} 12 | i := 0 13 | 14 | for value := range gloop.Keys(gloop.Enumerate(gloop.Slice(values))) { 15 | require.Equal(t, i, value) 16 | 17 | i++ 18 | } 19 | 20 | require.Equal(t, len(values), i) 21 | } 22 | 23 | func TestKeysBreak(t *testing.T) { 24 | values := []string{"a", "b", "c"} 25 | i := 0 26 | 27 | for value := range gloop.Keys(gloop.Enumerate(gloop.Slice(values))) { 28 | if i == 2 { 29 | break 30 | } 31 | 32 | require.Equal(t, i, value) 33 | 34 | i++ 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /keyvalue.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import "iter" 4 | 5 | // KeyValuePair represents a generic key value pair. 6 | type KeyValuePair[K, V any] struct { 7 | Key K 8 | Value V 9 | } 10 | 11 | // KeyValue converts an [iter.Seq] sequence of [KeyValuePair] values to 12 | // an [iter.Seq2] sequence. 13 | func KeyValue[K, V any](seq iter.Seq[KeyValuePair[K, V]]) iter.Seq2[K, V] { 14 | return func(yield func(K, V) bool) { 15 | for pair := range seq { 16 | if !yield(pair.Key, pair.Value) { 17 | return 18 | } 19 | } 20 | } 21 | } 22 | 23 | // KeyValue2 converts an [iter.Seq2] sequence to an [iter.Seq] sequence 24 | // of [KeyValuePair] values. 25 | func KeyValue2[K, V any](seq iter.Seq2[K, V]) iter.Seq[KeyValuePair[K, V]] { 26 | return Transform2(seq, func(k K, v V) KeyValuePair[K, V] { 27 | return KeyValuePair[K, V]{ 28 | Key: k, 29 | Value: v, 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /keyvalue_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestKeyValue(t *testing.T) { 11 | pairs := []gloop.KeyValuePair[string, int]{ 12 | { 13 | Key: "Fizz", 14 | Value: 3, 15 | }, 16 | { 17 | Key: "Buzz", 18 | Value: 1, 19 | }, 20 | { 21 | Key: "Bazz", 22 | Value: 4, 23 | }, 24 | } 25 | 26 | i := 0 27 | for key, value := range gloop.KeyValue(gloop.Slice(pairs)) { 28 | require.Equal(t, pairs[i].Key, key) 29 | require.Equal(t, pairs[i].Value, value) 30 | 31 | i++ 32 | } 33 | 34 | require.Equal(t, len(pairs), i) 35 | } 36 | 37 | func TestKeyValueBreak(t *testing.T) { 38 | pairs := []gloop.KeyValuePair[string, int]{ 39 | { 40 | Key: "Fizz", 41 | Value: 3, 42 | }, 43 | { 44 | Key: "Buzz", 45 | Value: 1, 46 | }, 47 | { 48 | Key: "Bazz", 49 | Value: 4, 50 | }, 51 | } 52 | 53 | wantPairs := []gloop.KeyValuePair[string, int]{ 54 | { 55 | Key: "Fizz", 56 | Value: 3, 57 | }, 58 | { 59 | Key: "Buzz", 60 | Value: 1, 61 | }, 62 | } 63 | 64 | i := 0 65 | for key, value := range gloop.KeyValue(gloop.Slice(pairs)) { 66 | if i == 2 { 67 | break 68 | } 69 | 70 | require.Equal(t, pairs[i].Key, key) 71 | require.Equal(t, pairs[i].Value, value) 72 | 73 | i++ 74 | } 75 | 76 | require.Equal(t, len(wantPairs), i) 77 | } 78 | 79 | func TestKeyValue2(t *testing.T) { 80 | seq := func(yield func(string, int) bool) { 81 | if !yield("Fizz", 3) { 82 | return 83 | } 84 | 85 | if !yield("Buzz", 1) { 86 | return 87 | } 88 | 89 | if !yield("Bazz", 4) { 90 | return 91 | } 92 | } 93 | 94 | wantPairs := []gloop.KeyValuePair[string, int]{ 95 | { 96 | Key: "Fizz", 97 | Value: 3, 98 | }, 99 | { 100 | Key: "Buzz", 101 | Value: 1, 102 | }, 103 | { 104 | Key: "Bazz", 105 | Value: 4, 106 | }, 107 | } 108 | 109 | i := 0 110 | for pair := range gloop.KeyValue2(seq) { 111 | require.Equal(t, wantPairs[i], pair) 112 | 113 | i++ 114 | } 115 | 116 | require.Equal(t, len(wantPairs), i) 117 | } 118 | -------------------------------------------------------------------------------- /linspace.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "iter" 5 | ) 6 | 7 | // LinspaceOptions defines configurable options for [Linspace]. 8 | type LinspaceOptions struct { 9 | // Closed represents whether or not the interval is closed at the 10 | // stop point. 11 | Closed bool 12 | } 13 | 14 | // LinspaceOptionFunc is the function signature of configuration 15 | // helpers for [Linspace]. 16 | type LinspaceOptionFunc func(*LinspaceOptions) 17 | 18 | // WithLinspaceClosed is a helper for configuring the interval to be 19 | // closed for [Linspace]. 20 | func WithLinspaceClosed(closed bool) LinspaceOptionFunc { 21 | return func(o *LinspaceOptions) { 22 | o.Closed = closed 23 | } 24 | } 25 | 26 | // Linspace allows looping over evenly spaced values within a given 27 | // interval. n must be greater than 1. 28 | func Linspace[N Number]( 29 | start N, 30 | stop N, 31 | n int, 32 | opts ...LinspaceOptionFunc, 33 | ) iter.Seq[float64] { 34 | if n <= 1 { 35 | panic("n must be greater than 1") 36 | } 37 | 38 | options := LinspaceOptions{ 39 | Closed: false, 40 | } 41 | 42 | for _, opt := range opts { 43 | opt(&options) 44 | } 45 | 46 | fstart := float64(start) 47 | fstop := float64(stop) 48 | fstep := (fstop - fstart) / float64(n-1) 49 | 50 | return Interval( 51 | fstart, 52 | fstop, 53 | fstep, 54 | WithIntervalClosed(options.Closed), 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /linspace_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestLinspace(t *testing.T) { 11 | wantValues := []float64{2, 2.25, 2.5, 2.75} 12 | i := 0 13 | 14 | for value := range gloop.Linspace(2, 3, 5) { 15 | require.InDelta(t, wantValues[i], value, 0.01) 16 | 17 | i++ 18 | } 19 | 20 | require.Equal(t, len(wantValues), i) 21 | } 22 | 23 | func TestLinspaceClosed(t *testing.T) { 24 | wantValues := []float64{2, 2.25, 2.5, 2.75, 3} 25 | i := 0 26 | 27 | for value := range gloop.Linspace(2, 3, 5, gloop.WithLinspaceClosed(true)) { 28 | require.InDelta(t, wantValues[i], value, 0.01) 29 | 30 | i++ 31 | } 32 | 33 | require.Equal(t, len(wantValues), i) 34 | } 35 | 36 | func TestLinspaceBackwards(t *testing.T) { 37 | wantValues := []float64{10, 8.5, 7, 5.5} 38 | i := 0 39 | 40 | for value := range gloop.Linspace(10, 4, 5) { 41 | require.InDelta(t, wantValues[i], value, 0.01) 42 | 43 | i++ 44 | } 45 | 46 | require.Equal(t, len(wantValues), i) 47 | } 48 | 49 | func TestLinspaceBackwardsClosed(t *testing.T) { 50 | wantValues := []float64{10, 8.5, 7, 5.5, 4} 51 | i := 0 52 | 53 | for value := range gloop.Linspace(10, 4, 5, gloop.WithLinspaceClosed(true)) { 54 | require.InDelta(t, wantValues[i], value, 0.01) 55 | 56 | i++ 57 | } 58 | 59 | require.Equal(t, len(wantValues), i) 60 | } 61 | 62 | func TestLinspaceBreak(t *testing.T) { 63 | wantValues := []float64{2, 2.25} 64 | i := 0 65 | 66 | for value := range gloop.Linspace(2, 3, 5) { 67 | if i == 2 { 68 | break 69 | } 70 | 71 | require.InDelta(t, wantValues[i], value, 0.001) 72 | 73 | i++ 74 | } 75 | 76 | require.Equal(t, len(wantValues), i) 77 | } 78 | 79 | func TestLinspaceZeroDivisionsPanics(t *testing.T) { 80 | require.Panics(t, func() { 81 | for range gloop.Linspace(2, 3, 0) { 82 | t.Fatal("expected no iteration") 83 | } 84 | }) 85 | } 86 | 87 | func TestLinspaceOneDivisionPanics(t *testing.T) { 88 | require.Panics(t, func() { 89 | for range gloop.Linspace(2, 3, 1) { 90 | t.Fatal("expected no iteration") 91 | } 92 | }) 93 | } 94 | 95 | func TestLinspaceNegativeDivisionsPanics(t *testing.T) { 96 | require.Panics(t, func() { 97 | for range gloop.Linspace(2, 3, -5) { 98 | t.Fatal("expected no iteration") 99 | } 100 | }) 101 | } 102 | -------------------------------------------------------------------------------- /list.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "container/list" 5 | "iter" 6 | ) 7 | 8 | // List allows looping over a given [container/list.List]. 9 | func List(l *list.List) iter.Seq[*list.Element] { 10 | return func(yield func(*list.Element) bool) { 11 | elem := l.Front() 12 | for elem != nil { 13 | if !yield(elem) { 14 | return 15 | } 16 | 17 | elem = elem.Next() 18 | } 19 | } 20 | } 21 | 22 | // ToList converts an [iter.Seq] sequence to a [container/list.List]. 23 | func ToList[V any](seq iter.Seq[V]) *list.List { 24 | l := list.New() 25 | for value := range seq { 26 | l.PushBack(value) 27 | } 28 | 29 | return l 30 | } 31 | 32 | // ToList2 converts an [iter.Seq2] sequence to [container/list.List] of 33 | // keys and values. 34 | func ToList2[K, V any](seq iter.Seq2[K, V]) (*list.List, *list.List) { 35 | listKeys := list.New() 36 | listValues := list.New() 37 | 38 | for key, value := range seq { 39 | listKeys.PushBack(key) 40 | listValues.PushBack(value) 41 | } 42 | 43 | return listKeys, listValues 44 | } 45 | -------------------------------------------------------------------------------- /list_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "container/list" 5 | "testing" 6 | 7 | "github.com/alvii147/gloop" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestList(t *testing.T) { 12 | l := list.New() 13 | l.PushBack(3) 14 | l.PushBack(4) 15 | l.PushBack(5) 16 | 17 | wantValues := []int{3, 4, 5} 18 | i := 0 19 | 20 | for elem := range gloop.List(l) { 21 | require.Equal(t, wantValues[i], elem.Value) 22 | 23 | i++ 24 | } 25 | 26 | require.Equal(t, len(wantValues), i) 27 | } 28 | 29 | func TestListBreak(t *testing.T) { 30 | l := list.New() 31 | l.PushBack(3) 32 | l.PushBack(4) 33 | l.PushBack(5) 34 | 35 | wantValues := []int{3, 4} 36 | i := 0 37 | 38 | for elem := range gloop.List(l) { 39 | if i == 2 { 40 | break 41 | } 42 | 43 | require.Equal(t, wantValues[i], elem.Value) 44 | 45 | i++ 46 | } 47 | 48 | require.Equal(t, len(wantValues), i) 49 | } 50 | 51 | func TestToList(t *testing.T) { 52 | seq := func(yield func(int) bool) { 53 | if !yield(3) { 54 | return 55 | } 56 | 57 | if !yield(4) { 58 | return 59 | } 60 | 61 | if !yield(5) { 62 | return 63 | } 64 | } 65 | 66 | values := gloop.ToList(seq) 67 | elem := values.Front() 68 | require.Equal(t, 3, elem.Value) 69 | elem = elem.Next() 70 | require.Equal(t, 4, elem.Value) 71 | elem = elem.Next() 72 | require.Equal(t, 5, elem.Value) 73 | } 74 | 75 | func TestToList2(t *testing.T) { 76 | seq := func(yield func(int, int) bool) { 77 | if !yield(0, 3) { 78 | return 79 | } 80 | 81 | if !yield(1, 4) { 82 | return 83 | } 84 | 85 | if !yield(2, 5) { 86 | return 87 | } 88 | } 89 | 90 | keys, values := gloop.ToList2(seq) 91 | 92 | elem := keys.Front() 93 | require.Equal(t, 0, elem.Value) 94 | elem = elem.Next() 95 | require.Equal(t, 1, elem.Value) 96 | elem = elem.Next() 97 | require.Equal(t, 2, elem.Value) 98 | 99 | elem = values.Front() 100 | require.Equal(t, 3, elem.Value) 101 | elem = elem.Next() 102 | require.Equal(t, 4, elem.Value) 103 | elem = elem.Next() 104 | require.Equal(t, 5, elem.Value) 105 | } 106 | -------------------------------------------------------------------------------- /map.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import "iter" 4 | 5 | // Map allows looping over keys and values in a map. 6 | func Map[K comparable, V any](m map[K]V) iter.Seq2[K, V] { 7 | return func(yield func(K, V) bool) { 8 | for key, value := range m { 9 | if !yield(key, value) { 10 | return 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /map_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestMap(t *testing.T) { 11 | m := map[string]bool{ 12 | "Fizz": true, 13 | "Buzz": true, 14 | "Bazz": true, 15 | } 16 | 17 | i := 0 18 | 19 | for key, value := range gloop.Map(m) { 20 | require.Contains(t, m, key) 21 | require.Equal(t, m[key], value) 22 | 23 | i++ 24 | } 25 | 26 | require.Equal(t, len(m), i) 27 | } 28 | 29 | func TestMapBreak(t *testing.T) { 30 | m := map[string]bool{ 31 | "Fizz": true, 32 | "Buzz": true, 33 | "Bazz": true, 34 | } 35 | 36 | i := 0 37 | for key, value := range gloop.Map(m) { 38 | if i == 2 { 39 | break 40 | } 41 | 42 | require.Contains(t, m, key) 43 | require.Equal(t, m[key], value) 44 | 45 | i++ 46 | } 47 | 48 | require.Equal(t, 2, i) 49 | } 50 | -------------------------------------------------------------------------------- /max.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "cmp" 5 | "iter" 6 | ) 7 | 8 | // Max computes the maximum value over an [iter.Seq] sequence. 9 | func Max[V cmp.Ordered](seq iter.Seq[V]) V { 10 | return MaxByComparison(seq, func(acc V, value V) bool { 11 | return acc < value 12 | }) 13 | } 14 | 15 | // MaxByComparisonFunc is the function signature of the comparison 16 | // function used in MaxByComparison]. 17 | type MaxByComparisonFunc[V any] func(V, V) bool 18 | 19 | // MaxByComparison computes the maximum value over an [iter.Seq] 20 | // sequence using a comparison function. 21 | func MaxByComparison[V any]( 22 | seq iter.Seq[V], 23 | less SortByComparisonFunc[V], 24 | ) V { 25 | return Reduce(seq, func(acc V, value V) V { 26 | if less(acc, value) { 27 | return value 28 | } 29 | 30 | return acc 31 | }) 32 | } 33 | 34 | // MaxByComparison2Func is the function signature of the comparison 35 | // function used in [MaxByComparison2]. 36 | type MaxByComparison2Func[K, V any] func(K, V, K, V) bool 37 | 38 | // MaxByComparison2 computes the maximum key and value over an 39 | // [iter.Seq2] sequence using a comparison function. 40 | func MaxByComparison2[K, V any]( 41 | seq iter.Seq2[K, V], 42 | less MinByComparison2Func[K, V], 43 | ) (K, V) { 44 | return Reduce2(seq, func(accKey K, accValue V, key K, value V) (K, V) { 45 | if less(accKey, accValue, key, value) { 46 | return key, value 47 | } 48 | 49 | return accKey, accValue 50 | }) 51 | } 52 | 53 | // MaxByRankFunc is the function signature of the ranking function used 54 | // in [MaxByRank]. 55 | type MaxByRankFunc[V any, R cmp.Ordered] func(V) R 56 | 57 | // MaxByRank computes the maximum value over an [iter.Seq] sequence 58 | // using a ranking function. 59 | func MaxByRank[V any, R cmp.Ordered]( 60 | seq iter.Seq[V], 61 | rank MinByRankFunc[V, R], 62 | ) V { 63 | return MaxByComparison(seq, func(acc V, value V) bool { 64 | return rank(acc) < rank(value) 65 | }) 66 | } 67 | 68 | // MaxByRank2Func is the function signature of the ranking function 69 | // used in [MaxByRank2]. 70 | type MaxByRank2Func[K, V any, R cmp.Ordered] func(K, V) R 71 | 72 | // MaxByRank2 computes the maximum value over an [iter.Seq2] sequence 73 | // using a ranking function. 74 | func MaxByRank2[K, V any, R cmp.Ordered]( 75 | seq iter.Seq2[K, V], 76 | rank MinByRank2Func[K, V, R], 77 | ) (K, V) { 78 | return MaxByComparison2(seq, func(accKey K, accValue V, key K, value V) bool { 79 | return rank(accKey, accValue) < rank(key, value) 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /max_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/alvii147/gloop" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestMaxInt(t *testing.T) { 12 | values := []int{3, 4, -5} 13 | minValue := gloop.Max(gloop.Slice(values)) 14 | require.Equal(t, 4, minValue) 15 | } 16 | 17 | func TestMaxFloat(t *testing.T) { 18 | values := []float64{2.31, -0.03, 0.22} 19 | minValue := gloop.Max(gloop.Slice(values)) 20 | require.InDelta(t, 2.31, minValue, 0.001) 21 | } 22 | 23 | func TestMaxString(t *testing.T) { 24 | values := []string{"Fizz", "Buzz", "Bazz"} 25 | minValue := gloop.Max(gloop.Slice(values)) 26 | require.Equal(t, "Fizz", minValue) 27 | } 28 | func TestMaxDuration(t *testing.T) { 29 | values := []time.Duration{time.Hour, time.Minute, time.Second} 30 | duration := gloop.Max(gloop.Slice(values)) 31 | require.Equal(t, time.Hour, duration) 32 | } 33 | 34 | func TestMaxByComparisonStringLen(t *testing.T) { 35 | values := []string{"Fizzz", "Buzz", "Bazzzzzz"} 36 | maxValue := gloop.MaxByComparison(gloop.Slice(values), func(s1 string, s2 string) bool { 37 | return len(s1) < len(s2) 38 | }) 39 | require.Equal(t, "Bazzzzzz", maxValue) 40 | } 41 | 42 | func TestMaxByComparison2KeyValueProduct(t *testing.T) { 43 | seq := func(yield func(int, int) bool) { 44 | if !yield(1, 5) { 45 | return 46 | } 47 | 48 | if !yield(3, 1) { 49 | return 50 | } 51 | 52 | if !yield(4, 9) { 53 | return 54 | } 55 | } 56 | 57 | maxKey, maxValue := gloop.MaxByComparison2(seq, func(k1, v1, k2, v2 int) bool { 58 | return k1*v1 < k2*v2 59 | }) 60 | require.Equal(t, 4, maxKey) 61 | require.Equal(t, 9, maxValue) 62 | } 63 | 64 | func TestMaxByRankStringLen(t *testing.T) { 65 | values := []string{"Fizzz", "Buzz", "Bazzzzzz"} 66 | maxValue := gloop.MaxByRank(gloop.Slice(values), func(s string) int { 67 | return len(s) 68 | }) 69 | require.Equal(t, "Bazzzzzz", maxValue) 70 | } 71 | 72 | func TestMaxByRank2KeyValueProduct(t *testing.T) { 73 | seq := func(yield func(int, int) bool) { 74 | if !yield(1, 5) { 75 | return 76 | } 77 | 78 | if !yield(3, 1) { 79 | return 80 | } 81 | 82 | if !yield(4, 9) { 83 | return 84 | } 85 | } 86 | 87 | maxKey, maxValue := gloop.MaxByRank2(seq, func(k, v int) int { 88 | return k * v 89 | }) 90 | require.Equal(t, 4, maxKey) 91 | require.Equal(t, 9, maxValue) 92 | } 93 | -------------------------------------------------------------------------------- /mean.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "iter" 5 | ) 6 | 7 | // meanAccumulator helps accumulate both summation and length. 8 | type meanAccumulator[V Number] struct { 9 | // sum represents the current summation. 10 | sum V 11 | // len represents the current length. 12 | len int 13 | } 14 | 15 | // Mean computes the mean value over an [iter.Seq] sequence. 16 | func Mean[V Number](seq iter.Seq[V]) float64 { 17 | meanAcc := Fold(seq, func(acc meanAccumulator[V], value V) meanAccumulator[V] { 18 | return meanAccumulator[V]{ 19 | sum: acc.sum + value, 20 | len: acc.len + 1, 21 | } 22 | }) 23 | 24 | mean := float64(meanAcc.sum) / float64(meanAcc.len) 25 | 26 | return mean 27 | } 28 | -------------------------------------------------------------------------------- /mean_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/alvii147/gloop" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestMeanInt(t *testing.T) { 12 | values := []int{7, -2, 4} 13 | mean := gloop.Mean(gloop.Slice(values)) 14 | require.InDelta(t, 3.0, mean, 0.001) 15 | } 16 | 17 | func TestMeanFloat(t *testing.T) { 18 | values := []float64{0.4, 8.9, -4.8} 19 | mean := gloop.Mean(gloop.Slice(values)) 20 | require.InDelta(t, 1.5, mean, 0.001) 21 | } 22 | 23 | func TestMeanDuration(t *testing.T) { 24 | values := []time.Duration{time.Hour, 2 * time.Minute} 25 | mean := gloop.Mean(gloop.Slice(values)) 26 | require.InDelta(t, float64(31*time.Minute), mean, 0.001) 27 | } 28 | -------------------------------------------------------------------------------- /min.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "cmp" 5 | "iter" 6 | ) 7 | 8 | // Min computes the minimum value over an [iter.Seq] sequence. 9 | func Min[V cmp.Ordered](seq iter.Seq[V]) V { 10 | return MinByComparison(seq, func(acc V, value V) bool { 11 | return acc < value 12 | }) 13 | } 14 | 15 | // MinByComparisonFunc is the function signature of the comparison 16 | // function used in [MinByComparison]. 17 | type MinByComparisonFunc[V any] func(V, V) bool 18 | 19 | // MinByComparison computes the minimum value over an [iter.Seq] 20 | // sequence using a comparison function. 21 | func MinByComparison[V any]( 22 | seq iter.Seq[V], 23 | less SortByComparisonFunc[V], 24 | ) V { 25 | return Reduce(seq, func(acc V, value V) V { 26 | if less(acc, value) { 27 | return acc 28 | } 29 | 30 | return value 31 | }) 32 | } 33 | 34 | // MinByComparison2Func is the function signature of the comparison 35 | // function used in [MinByComparison2]. 36 | type MinByComparison2Func[K, V any] func(K, V, K, V) bool 37 | 38 | // MinByComparison2 computes the minimum key and value over an 39 | // [iter.Seq2] sequence using a comparison function. 40 | func MinByComparison2[K, V any]( 41 | seq iter.Seq2[K, V], 42 | less MinByComparison2Func[K, V], 43 | ) (K, V) { 44 | return Reduce2(seq, func(accKey K, accValue V, key K, value V) (K, V) { 45 | if less(accKey, accValue, key, value) { 46 | return accKey, accValue 47 | } 48 | 49 | return key, value 50 | }) 51 | } 52 | 53 | // MinByRankFunc is the function signature of the ranking function used 54 | // in [MinByRank]. 55 | type MinByRankFunc[V any, R cmp.Ordered] func(V) R 56 | 57 | // MinByRank computes the minimum value over an [iter.Seq] sequence 58 | // using a ranking function. 59 | func MinByRank[V any, R cmp.Ordered]( 60 | seq iter.Seq[V], 61 | rank MinByRankFunc[V, R], 62 | ) V { 63 | return MinByComparison(seq, func(acc V, value V) bool { 64 | return rank(acc) < rank(value) 65 | }) 66 | } 67 | 68 | // MinByRank2Func is the function signature of the ranking function 69 | // used in [MinByRank2]. 70 | type MinByRank2Func[K, V any, R cmp.Ordered] func(K, V) R 71 | 72 | // MinByRank2 computes the minimum value over an [iter.Seq2] sequence 73 | // using a ranking function. 74 | func MinByRank2[K, V any, R cmp.Ordered]( 75 | seq iter.Seq2[K, V], 76 | rank MinByRank2Func[K, V, R], 77 | ) (K, V) { 78 | return MinByComparison2(seq, func(accKey K, accValue V, key K, value V) bool { 79 | return rank(accKey, accValue) < rank(key, value) 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /min_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/alvii147/gloop" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestMinInt(t *testing.T) { 12 | values := []int{3, 4, -5} 13 | minValue := gloop.Min(gloop.Slice(values)) 14 | require.Equal(t, -5, minValue) 15 | } 16 | 17 | func TestMinFloat(t *testing.T) { 18 | values := []float64{2.31, -0.03, 0.22} 19 | minValue := gloop.Min(gloop.Slice(values)) 20 | require.InDelta(t, -0.03, minValue, 0.001) 21 | } 22 | 23 | func TestMinString(t *testing.T) { 24 | values := []string{"Fizz", "Buzz", "Bazz"} 25 | minValue := gloop.Min(gloop.Slice(values)) 26 | require.Equal(t, "Bazz", minValue) 27 | } 28 | 29 | func TestMinDuration(t *testing.T) { 30 | values := []time.Duration{time.Hour, time.Minute, time.Second} 31 | duration := gloop.Min(gloop.Slice(values)) 32 | require.Equal(t, time.Second, duration) 33 | } 34 | 35 | func TestMinByComparisonStringLen(t *testing.T) { 36 | values := []string{"Fizzz", "Buzz", "Bazzzzzz"} 37 | minValue := gloop.MinByComparison(gloop.Slice(values), func(s1 string, s2 string) bool { 38 | return len(s1) < len(s2) 39 | }) 40 | require.Equal(t, "Buzz", minValue) 41 | } 42 | 43 | func TestMinByComparison2KeyValueProduct(t *testing.T) { 44 | seq := func(yield func(int, int) bool) { 45 | if !yield(1, 5) { 46 | return 47 | } 48 | 49 | if !yield(4, 9) { 50 | return 51 | } 52 | 53 | if !yield(3, 1) { 54 | return 55 | } 56 | } 57 | 58 | minKey, minValue := gloop.MinByComparison2(seq, func(k1, v1, k2, v2 int) bool { 59 | return k1*v1 < k2*v2 60 | }) 61 | require.Equal(t, 3, minKey) 62 | require.Equal(t, 1, minValue) 63 | } 64 | 65 | func TestMinByRankStringLen(t *testing.T) { 66 | values := []string{"Fizzz", "Buzz", "Bazzzzzz"} 67 | minValue := gloop.MinByRank(gloop.Slice(values), func(s string) int { 68 | return len(s) 69 | }) 70 | require.Equal(t, "Buzz", minValue) 71 | } 72 | 73 | func TestMinByRank2KeyValueProduct(t *testing.T) { 74 | seq := func(yield func(int, int) bool) { 75 | if !yield(1, 5) { 76 | return 77 | } 78 | 79 | if !yield(4, 9) { 80 | return 81 | } 82 | 83 | if !yield(3, 1) { 84 | return 85 | } 86 | } 87 | 88 | minKey, minValue := gloop.MinByRank2(seq, func(k, v int) int { 89 | return k * v 90 | }) 91 | require.Equal(t, 3, minKey) 92 | require.Equal(t, 1, minValue) 93 | } 94 | -------------------------------------------------------------------------------- /parallelize.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "context" 5 | "iter" 6 | "sync" 7 | ) 8 | 9 | // ParallelizeOptions defines configurable options for [Parallelize] 10 | // and [Parallelize2]. 11 | type ParallelizeOptions struct { 12 | // Context is used to send a cancel signal. 13 | Context context.Context 14 | // MaxThreads defines the maximum number of concurrent threads 15 | // allowed. If nil, there is no maximum. 16 | MaxThreads *int 17 | } 18 | 19 | // ParallelizeOptionFunc is the function signature of configuration 20 | // helpers for [Parallelize] and [Parallelize2]. 21 | type ParallelizeOptionFunc func(*ParallelizeOptions) 22 | 23 | // WithParallelizeContext is a helper for configuring context in 24 | // [Parallelize] and [Parallelize2]. 25 | func WithParallelizeContext(ctx context.Context) ParallelizeOptionFunc { 26 | return func(o *ParallelizeOptions) { 27 | o.Context = ctx 28 | } 29 | } 30 | 31 | // WithParallelizeMaxThreads is a helper for configuring maximum number 32 | // of concurrent threads in [Parallelize] and [Parallelize2]. 33 | func WithParallelizeMaxThreads(maxThreads int) ParallelizeOptionFunc { 34 | return func(o *ParallelizeOptions) { 35 | o.MaxThreads = &maxThreads 36 | } 37 | } 38 | 39 | // ParallelizeFunc is the function signature of the function to be 40 | // parallelized in [Parallelize]. 41 | type ParallelizeFunc[V any] func(V) 42 | 43 | // Parallelize runs a function on each value in an [iter.Seq] sequence 44 | // on separate goroutines. 45 | func Parallelize[V any]( 46 | seq iter.Seq[V], 47 | f ParallelizeFunc[V], 48 | opts ...ParallelizeOptionFunc, 49 | ) { 50 | Parallelize2(Enumerate(seq), func(_ int, value V) { 51 | f(value) 52 | }, opts...) 53 | } 54 | 55 | // Parallelize2Func is the function signature of the function to be 56 | // parallelized in [Parallelize2]. 57 | type Parallelize2Func[K, V any] func(K, V) 58 | 59 | // Parallelize2 runs a function on each value in an [iter.Seq2] 60 | // sequence on separate goroutines. 61 | func Parallelize2[K, V any]( 62 | seq iter.Seq2[K, V], 63 | f Parallelize2Func[K, V], 64 | opts ...ParallelizeOptionFunc, 65 | ) { 66 | options := ParallelizeOptions{ 67 | Context: context.Background(), 68 | MaxThreads: nil, 69 | } 70 | 71 | for _, opt := range opts { 72 | opt(&options) 73 | } 74 | 75 | ctx := context.Background() 76 | if options.Context != nil { 77 | ctx = options.Context 78 | } 79 | 80 | var semaphore chan struct{} 81 | if options.MaxThreads != nil { 82 | semaphore = make(chan struct{}, *options.MaxThreads) 83 | defer close(semaphore) 84 | } 85 | 86 | var wg sync.WaitGroup 87 | 88 | for key, value := range seq { 89 | if semaphore != nil { 90 | semaphore <- struct{}{} 91 | } 92 | 93 | wg.Add(1) 94 | 95 | go func(k K, v V) { 96 | defer wg.Done() 97 | 98 | if semaphore != nil { 99 | defer func(s <-chan struct{}) { 100 | <-s 101 | }(semaphore) 102 | } 103 | 104 | select { 105 | case <-ctx.Done(): 106 | return 107 | default: 108 | f(k, v) 109 | 110 | return 111 | } 112 | }(key, value) 113 | } 114 | 115 | wg.Wait() 116 | } 117 | -------------------------------------------------------------------------------- /parallelize_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "context" 5 | "sync/atomic" 6 | "testing" 7 | "time" 8 | 9 | "github.com/alvii147/gloop" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestWithParallelizeContext(t *testing.T) { 14 | ctx, cancel := context.WithCancel(context.Background()) 15 | t.Cleanup(cancel) 16 | 17 | options := gloop.ParallelizeOptions{} 18 | gloop.WithParallelizeContext(ctx)(&options) 19 | 20 | ctxDone := false 21 | select { 22 | case <-options.Context.Done(): 23 | ctxDone = true 24 | default: 25 | } 26 | require.False(t, ctxDone) 27 | 28 | cancel() 29 | 30 | ctxDone = true 31 | select { 32 | case <-options.Context.Done(): 33 | default: 34 | ctxDone = false 35 | } 36 | require.True(t, ctxDone) 37 | } 38 | 39 | func TestWithParallelizeMaxThreads(t *testing.T) { 40 | maxThreads := 42 41 | options := gloop.ParallelizeOptions{} 42 | gloop.WithParallelizeMaxThreads(maxThreads)(&options) 43 | 44 | require.NotNil(t, options.MaxThreads) 45 | require.Equal(t, maxThreads, *options.MaxThreads) 46 | } 47 | 48 | func TestParallelizeSlice(t *testing.T) { 49 | values := []string{"a", "b", "c"} 50 | valuesCh := make(chan string, len(values)) 51 | 52 | done := make(chan struct{}, 1) 53 | channelOverflow := false 54 | 55 | go func() { 56 | gloop.Parallelize(gloop.Slice(values), func(v string) { 57 | select { 58 | case valuesCh <- v: 59 | default: 60 | channelOverflow = true 61 | } 62 | }) 63 | done <- struct{}{} 64 | }() 65 | 66 | select { 67 | case <-done: 68 | case <-time.After(time.Second * 10): 69 | t.Fatal("done signal took too long") 70 | } 71 | 72 | require.False(t, channelOverflow) 73 | 74 | close(valuesCh) 75 | 76 | gotValues := make([]string, 0) 77 | 78 | for v := range valuesCh { 79 | gotValues = append(gotValues, v) 80 | } 81 | 82 | require.ElementsMatch(t, values, gotValues) 83 | } 84 | 85 | func TestParallelizeString(t *testing.T) { 86 | s := "FizzBuzz" 87 | rCh := make(chan rune, len(s)) 88 | 89 | done := make(chan struct{}, 1) 90 | channelOverflow := false 91 | 92 | go func() { 93 | gloop.Parallelize(gloop.String(s), func(r rune) { 94 | select { 95 | case rCh <- r: 96 | default: 97 | channelOverflow = true 98 | } 99 | }) 100 | done <- struct{}{} 101 | }() 102 | 103 | select { 104 | case <-done: 105 | case <-time.After(time.Second * 10): 106 | t.Fatal("done signal took too long") 107 | } 108 | 109 | require.False(t, channelOverflow) 110 | 111 | close(rCh) 112 | 113 | gotRunes := make([]rune, 0) 114 | 115 | for r := range rCh { 116 | gotRunes = append(gotRunes, r) 117 | } 118 | 119 | require.ElementsMatch(t, []rune(s), gotRunes) 120 | } 121 | 122 | func TestParallelize2(t *testing.T) { 123 | m := map[string]int{ 124 | "Fizz": 3, 125 | "Buzz": 1, 126 | "Bazz": 4, 127 | } 128 | keysCh := make(chan string, len(m)) 129 | valuesCh := make(chan int, len(m)) 130 | 131 | done := make(chan struct{}, 1) 132 | channelOverflow := false 133 | 134 | go func() { 135 | gloop.Parallelize2(gloop.Map(m), func(k string, v int) { 136 | select { 137 | case keysCh <- k: 138 | default: 139 | channelOverflow = true 140 | } 141 | 142 | select { 143 | case valuesCh <- v: 144 | default: 145 | channelOverflow = true 146 | } 147 | }) 148 | done <- struct{}{} 149 | }() 150 | 151 | select { 152 | case <-done: 153 | case <-time.After(time.Second * 10): 154 | t.Fatal("done signal took too long") 155 | } 156 | 157 | require.False(t, channelOverflow) 158 | 159 | close(keysCh) 160 | close(valuesCh) 161 | 162 | gotKeys := make([]string, 0) 163 | for k := range keysCh { 164 | gotKeys = append(gotKeys, k) 165 | } 166 | 167 | gotValues := make([]int, 0) 168 | for v := range valuesCh { 169 | gotValues = append(gotValues, v) 170 | } 171 | 172 | for k, v := range m { 173 | require.Contains(t, gotKeys, k) 174 | require.Contains(t, gotValues, v) 175 | } 176 | } 177 | 178 | func TestParallelizeCancelContext(t *testing.T) { 179 | values := []string{"Fizz"} 180 | 181 | ctx, cancel := context.WithCancel(context.Background()) 182 | cancel() 183 | 184 | done := make(chan struct{}, 1) 185 | functionCalled := false 186 | 187 | go func() { 188 | gloop.Parallelize(gloop.Slice(values), func(v string) { 189 | functionCalled = true 190 | }, gloop.WithParallelizeContext(ctx)) 191 | done <- struct{}{} 192 | }() 193 | 194 | select { 195 | case <-done: 196 | case <-time.After(time.Second * 10): 197 | t.Fatal("done signal took too long") 198 | } 199 | 200 | require.False(t, functionCalled) 201 | } 202 | 203 | func TestParallelize2CancelContext(t *testing.T) { 204 | values := []string{"Fizz"} 205 | 206 | ctx, cancel := context.WithCancel(context.Background()) 207 | cancel() 208 | 209 | done := make(chan struct{}, 1) 210 | functionCalled := false 211 | 212 | go func() { 213 | gloop.Parallelize2(gloop.Enumerate(gloop.Slice(values)), func(_ int, _ string) { 214 | functionCalled = true 215 | }, gloop.WithParallelizeContext(ctx)) 216 | done <- struct{}{} 217 | }() 218 | 219 | select { 220 | case <-done: 221 | case <-time.After(time.Second * 10): 222 | t.Fatal("done signal took too long") 223 | } 224 | 225 | require.False(t, functionCalled) 226 | } 227 | 228 | func TestParallelizeSingleThreaded(t *testing.T) { 229 | values := []string{"a", "b", "c"} 230 | 231 | var ( 232 | concurrentCallers atomic.Int64 233 | maxConcurrentCallers atomic.Int64 234 | ) 235 | 236 | done := make(chan struct{}, 1) 237 | go func() { 238 | gloop.Parallelize(gloop.Slice(values), func(_ string) { 239 | concurrentCallers.Add(1) 240 | defer concurrentCallers.Add(-1) 241 | 242 | maxConcurrentCallers.Store(max(maxConcurrentCallers.Load(), concurrentCallers.Load())) 243 | }, gloop.WithParallelizeMaxThreads(1)) 244 | done <- struct{}{} 245 | }() 246 | 247 | select { 248 | case <-done: 249 | case <-time.After(time.Second * 10): 250 | t.Fatal("done signal took too long") 251 | } 252 | 253 | require.EqualValues(t, 1, maxConcurrentCallers.Load()) 254 | } 255 | 256 | func TestParallelize2SingleThreaded(t *testing.T) { 257 | values := []string{"a", "b", "c"} 258 | 259 | var ( 260 | concurrentCallers atomic.Int64 261 | maxConcurrentCallers atomic.Int64 262 | ) 263 | 264 | done := make(chan struct{}, 1) 265 | go func() { 266 | gloop.Parallelize2(gloop.Enumerate(gloop.Slice(values)), func(_ int, _ string) { 267 | concurrentCallers.Add(1) 268 | defer concurrentCallers.Add(-1) 269 | 270 | maxConcurrentCallers.Store(max(maxConcurrentCallers.Load(), concurrentCallers.Load())) 271 | }, gloop.WithParallelizeMaxThreads(1)) 272 | done <- struct{}{} 273 | }() 274 | 275 | select { 276 | case <-done: 277 | case <-time.After(time.Second * 10): 278 | t.Fatal("done signal took too long") 279 | } 280 | 281 | require.EqualValues(t, 1, maxConcurrentCallers.Load()) 282 | } 283 | -------------------------------------------------------------------------------- /permutations.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "container/list" 5 | "iter" 6 | ) 7 | 8 | // permutations recursively computes and yields the permutations of an 9 | // [iter.Seq] sequence. 10 | func permutations[V any]( 11 | size int, 12 | yield func(iter.Seq[V]) bool, 13 | l *list.List, 14 | visited *list.List, 15 | visitedIdx []bool, 16 | ) bool { 17 | if visited.Len() == size { 18 | return yield(func(yield func(V) bool) { 19 | for elem := range List(visited) { 20 | if !yield(elem.Value.(V)) { 21 | return 22 | } 23 | } 24 | }) 25 | } 26 | 27 | for i, elem := range Enumerate(List(l)) { 28 | if visitedIdx[i] { 29 | continue 30 | } 31 | 32 | visited.PushBack(elem.Value) 33 | 34 | visitedIdx[i] = true 35 | 36 | if !permutations(size, yield, l, visited, visitedIdx) { 37 | return false 38 | } 39 | 40 | visited.Remove(visited.Back()) 41 | 42 | visitedIdx[i] = false 43 | } 44 | 45 | return true 46 | } 47 | 48 | // Permutations allows looping over all permutations of a given size 49 | // for an [iter.Seq] sequence. The size must be positive. 50 | func Permutations[V any](seq iter.Seq[V], size int) iter.Seq[iter.Seq[V]] { 51 | if size <= 0 { 52 | panic("size must be positive") 53 | } 54 | 55 | return func(yield func(iter.Seq[V]) bool) { 56 | l := ToList(seq) 57 | permutations(size, yield, l, list.New(), make([]bool, l.Len())) 58 | } 59 | } 60 | 61 | // permutations2 recursively computes and yields the permutations of an 62 | // [iter.Seq2] sequence. 63 | func permutations2[K, V any]( 64 | size int, 65 | yield func(iter.Seq2[K, V]) bool, 66 | listKeys *list.List, 67 | listValues *list.List, 68 | visitedKeys *list.List, 69 | visitedValues *list.List, 70 | visitedIdx []bool, 71 | ) bool { 72 | if visitedKeys.Len() == size || visitedValues.Len() == size { 73 | return yield(func(yield func(K, V) bool) { 74 | for keyElem, valueElem := range Zip(List(visitedKeys), List(visitedValues)) { 75 | key := keyElem.Value.(K) 76 | value := valueElem.Value.(V) 77 | 78 | if !yield(key, value) { 79 | return 80 | } 81 | } 82 | }) 83 | } 84 | 85 | i := 0 86 | keyElem := listKeys.Front() 87 | valueElem := listValues.Front() 88 | 89 | for keyElem != nil && valueElem != nil { 90 | if visitedIdx[i] { 91 | i++ 92 | keyElem = keyElem.Next() 93 | valueElem = valueElem.Next() 94 | 95 | continue 96 | } 97 | 98 | visitedKeys.PushBack(keyElem.Value) 99 | visitedValues.PushBack(valueElem.Value) 100 | 101 | visitedIdx[i] = true 102 | 103 | if !permutations2(size, yield, listKeys, listValues, visitedKeys, visitedValues, visitedIdx) { 104 | return false 105 | } 106 | 107 | visitedKeys.Remove(visitedKeys.Back()) 108 | visitedValues.Remove(visitedValues.Back()) 109 | 110 | visitedIdx[i] = false 111 | 112 | i++ 113 | keyElem = keyElem.Next() 114 | valueElem = valueElem.Next() 115 | } 116 | 117 | return true 118 | } 119 | 120 | // Permutations2 allows looping over all permutations of a given size 121 | // for an [iter.Seq2] sequence. The size must be positive. 122 | func Permutations2[K, V any](seq iter.Seq2[K, V], size int) iter.Seq[iter.Seq2[K, V]] { 123 | if size <= 0 { 124 | panic("size must be positive") 125 | } 126 | 127 | listKeys, listValues := ToList2(seq) 128 | 129 | return func(yield func(iter.Seq2[K, V]) bool) { 130 | permutations2(size, yield, listKeys, listValues, list.New(), list.New(), make([]bool, listKeys.Len())) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /permutations_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestPermutationsSlice(t *testing.T) { 11 | values := []int{3, 1, 4, 2} 12 | wantPerms := [][]int{ 13 | {3, 1, 4}, 14 | {3, 1, 2}, 15 | {3, 4, 1}, 16 | {3, 4, 2}, 17 | {3, 2, 1}, 18 | {3, 2, 4}, 19 | {1, 3, 4}, 20 | {1, 3, 2}, 21 | {1, 4, 3}, 22 | {1, 4, 2}, 23 | {1, 2, 3}, 24 | {1, 2, 4}, 25 | {4, 3, 1}, 26 | {4, 3, 2}, 27 | {4, 1, 3}, 28 | {4, 1, 2}, 29 | {4, 2, 3}, 30 | {4, 2, 1}, 31 | {2, 3, 1}, 32 | {2, 3, 4}, 33 | {2, 1, 3}, 34 | {2, 1, 4}, 35 | {2, 4, 3}, 36 | {2, 4, 1}, 37 | } 38 | i := 0 39 | 40 | for seq := range gloop.Permutations(gloop.Slice(values), 3) { 41 | perm := gloop.ToSlice(seq) 42 | require.Equal(t, wantPerms[i], perm) 43 | 44 | i++ 45 | } 46 | 47 | require.Equal(t, len(wantPerms), i) 48 | } 49 | 50 | func TestPermutationsSliceFullLength(t *testing.T) { 51 | values := []int{3, 1, 4} 52 | wantPerms := [][]int{ 53 | {3, 1, 4}, 54 | {3, 4, 1}, 55 | {1, 3, 4}, 56 | {1, 4, 3}, 57 | {4, 3, 1}, 58 | {4, 1, 3}, 59 | } 60 | i := 0 61 | 62 | for seq := range gloop.Permutations(gloop.Slice(values), 3) { 63 | perm := gloop.ToSlice(seq) 64 | require.Equal(t, wantPerms[i], perm) 65 | 66 | i++ 67 | } 68 | 69 | require.Equal(t, len(wantPerms), i) 70 | } 71 | 72 | func TestPermutationsString(t *testing.T) { 73 | s := "ABCD" 74 | wantPerms := []string{"AB", "AC", "AD", "BA", "BC", "BD", "CA", "CB", "CD", "DA", "DB", "DC"} 75 | i := 0 76 | 77 | for seq := range gloop.Permutations(gloop.String(s), 2) { 78 | perm := gloop.ToString(seq) 79 | require.Equal(t, wantPerms[i], perm) 80 | 81 | i++ 82 | } 83 | 84 | require.Equal(t, len(wantPerms), i) 85 | } 86 | 87 | func TestPermutationsStringFullLength(t *testing.T) { 88 | s := "ABC" 89 | wantPerms := []string{"ABC", "ACB", "BAC", "BCA", "CAB", "CBA"} 90 | i := 0 91 | 92 | for seq := range gloop.Permutations(gloop.String(s), 3) { 93 | perm := gloop.ToString(seq) 94 | require.Equal(t, wantPerms[i], perm) 95 | 96 | i++ 97 | } 98 | 99 | require.Equal(t, len(wantPerms), i) 100 | } 101 | 102 | func TestPermutationsBreak(t *testing.T) { 103 | values := []int{3, 1, 4, 2} 104 | i := 0 105 | 106 | for seq := range gloop.Permutations(gloop.Slice(values), 2) { 107 | if i == 1 { 108 | break 109 | } 110 | 111 | for value := range seq { 112 | require.Equal(t, 3, value) 113 | 114 | break 115 | } 116 | 117 | i++ 118 | } 119 | } 120 | 121 | func TestPermutationsZeroSizePanics(t *testing.T) { 122 | require.Panics(t, func() { 123 | for range gloop.Permutations(gloop.Slice([]int{3, 1, 4}), 0) { 124 | t.Fatal("expected no iteration") 125 | } 126 | }) 127 | } 128 | 129 | func TestPermutationsNegativeSizePanics(t *testing.T) { 130 | require.Panics(t, func() { 131 | for range gloop.Permutations(gloop.Slice([]int{3, 1, 4}), -3) { 132 | t.Fatal("expected no iteration") 133 | } 134 | }) 135 | } 136 | 137 | func TestPermutations2Slice(t *testing.T) { 138 | values := []int{3, 1, 4, 2} 139 | wantKeys := [][]int{ 140 | {0, 1, 2}, 141 | {0, 1, 3}, 142 | {0, 2, 1}, 143 | {0, 2, 3}, 144 | {0, 3, 1}, 145 | {0, 3, 2}, 146 | {1, 0, 2}, 147 | {1, 0, 3}, 148 | {1, 2, 0}, 149 | {1, 2, 3}, 150 | {1, 3, 0}, 151 | {1, 3, 2}, 152 | {2, 0, 1}, 153 | {2, 0, 3}, 154 | {2, 1, 0}, 155 | {2, 1, 3}, 156 | {2, 3, 0}, 157 | {2, 3, 1}, 158 | {3, 0, 1}, 159 | {3, 0, 2}, 160 | {3, 1, 0}, 161 | {3, 1, 2}, 162 | {3, 2, 0}, 163 | {3, 2, 1}, 164 | } 165 | wantPerms := [][]int{ 166 | {3, 1, 4}, 167 | {3, 1, 2}, 168 | {3, 4, 1}, 169 | {3, 4, 2}, 170 | {3, 2, 1}, 171 | {3, 2, 4}, 172 | {1, 3, 4}, 173 | {1, 3, 2}, 174 | {1, 4, 3}, 175 | {1, 4, 2}, 176 | {1, 2, 3}, 177 | {1, 2, 4}, 178 | {4, 3, 1}, 179 | {4, 3, 2}, 180 | {4, 1, 3}, 181 | {4, 1, 2}, 182 | {4, 2, 3}, 183 | {4, 2, 1}, 184 | {2, 3, 1}, 185 | {2, 3, 4}, 186 | {2, 1, 3}, 187 | {2, 1, 4}, 188 | {2, 4, 3}, 189 | {2, 4, 1}, 190 | } 191 | i := 0 192 | 193 | for seq := range gloop.Permutations2(gloop.Enumerate(gloop.Slice(values)), 3) { 194 | keys, perm := gloop.ToSlice2(seq) 195 | require.Equal(t, wantKeys[i], keys) 196 | require.Equal(t, wantPerms[i], perm) 197 | 198 | i++ 199 | } 200 | 201 | require.Equal(t, len(wantPerms), i) 202 | } 203 | 204 | func TestPermutations2SliceFullLength(t *testing.T) { 205 | values := []int{3, 1, 4} 206 | wantKeys := [][]int{ 207 | {0, 1, 2}, 208 | {0, 2, 1}, 209 | {1, 0, 2}, 210 | {1, 2, 0}, 211 | {2, 0, 1}, 212 | {2, 1, 0}, 213 | } 214 | wantPerms := [][]int{ 215 | {3, 1, 4}, 216 | {3, 4, 1}, 217 | {1, 3, 4}, 218 | {1, 4, 3}, 219 | {4, 3, 1}, 220 | {4, 1, 3}, 221 | } 222 | i := 0 223 | 224 | for seq := range gloop.Permutations2(gloop.Enumerate(gloop.Slice(values)), 3) { 225 | keys, perm := gloop.ToSlice2(seq) 226 | require.Equal(t, wantKeys[i], keys) 227 | require.Equal(t, wantPerms[i], perm) 228 | 229 | i++ 230 | } 231 | 232 | require.Equal(t, len(wantPerms), i) 233 | } 234 | 235 | func TestPermutations2String(t *testing.T) { 236 | s := "ABCD" 237 | wantKeys := [][]int{ 238 | {0, 1}, {0, 2}, {0, 3}, {1, 0}, {1, 2}, {1, 3}, {2, 0}, {2, 1}, {2, 3}, {3, 0}, {3, 1}, {3, 2}, 239 | } 240 | wantPerms := []string{"AB", "AC", "AD", "BA", "BC", "BD", "CA", "CB", "CD", "DA", "DB", "DC"} 241 | i := 0 242 | 243 | for seq := range gloop.Permutations2(gloop.Enumerate(gloop.String(s)), 2) { 244 | keys, permRunes := gloop.ToSlice2(seq) 245 | perm := string(permRunes) 246 | 247 | require.Equal(t, wantKeys[i], keys) 248 | require.Equal(t, wantPerms[i], perm) 249 | 250 | i++ 251 | } 252 | 253 | require.Equal(t, len(wantPerms), i) 254 | } 255 | 256 | func TestPermutationsString2FullLength(t *testing.T) { 257 | s := "ABC" 258 | wantKeys := [][]int{ 259 | {0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}, 260 | } 261 | wantPerms := []string{"ABC", "ACB", "BAC", "BCA", "CAB", "CBA"} 262 | i := 0 263 | 264 | for seq := range gloop.Permutations2(gloop.Enumerate(gloop.String(s)), 3) { 265 | keys, permRunes := gloop.ToSlice2(seq) 266 | perm := string(permRunes) 267 | 268 | require.Equal(t, wantKeys[i], keys) 269 | require.Equal(t, wantPerms[i], perm) 270 | 271 | i++ 272 | } 273 | 274 | require.Equal(t, len(wantPerms), i) 275 | } 276 | 277 | func TestPermutations2Break(t *testing.T) { 278 | values := []int{3, 1, 4, 2} 279 | i := 0 280 | 281 | for seq := range gloop.Permutations2(gloop.Enumerate(gloop.Slice(values)), 2) { 282 | if i == 1 { 283 | break 284 | } 285 | 286 | for key, value := range seq { 287 | require.Equal(t, 0, key) 288 | require.Equal(t, 3, value) 289 | 290 | break 291 | } 292 | 293 | i++ 294 | } 295 | } 296 | 297 | func TestPermutations2ZeroSizePanics(t *testing.T) { 298 | require.Panics(t, func() { 299 | for range gloop.Permutations2(gloop.Enumerate(gloop.Slice([]int{3, 1, 4})), 0) { 300 | t.Fatal("expected no iteration") 301 | } 302 | }) 303 | } 304 | 305 | func TestPermutations2NegativeSizePanics(t *testing.T) { 306 | require.Panics(t, func() { 307 | for range gloop.Permutations2(gloop.Enumerate(gloop.Slice([]int{3, 1, 4})), -3) { 308 | t.Fatal("expected no iteration") 309 | } 310 | }) 311 | } 312 | -------------------------------------------------------------------------------- /product.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import "iter" 4 | 5 | // Product computes the product of values over an [iter.Seq] sequence. 6 | func Product[V Productable](seq iter.Seq[V]) V { 7 | return Reduce(seq, func(acc V, value V) V { 8 | return acc * value 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /product_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestProductInt(t *testing.T) { 11 | values := []int{3, 4, -5} 12 | sum := gloop.Product(gloop.Slice(values)) 13 | require.Equal(t, -60, sum) 14 | } 15 | 16 | func TestProductFloat(t *testing.T) { 17 | values := []float64{4.2, 0.5} 18 | sum := gloop.Product(gloop.Slice(values)) 19 | require.InDelta(t, 2.1, sum, 0.1) 20 | } 21 | 22 | func TestProductComplex(t *testing.T) { 23 | values := []complex128{complex(1, 2), complex(3, -4)} 24 | sum := gloop.Product(gloop.Slice(values)) 25 | require.Equal(t, complex(11, 2), sum) 26 | } 27 | -------------------------------------------------------------------------------- /random.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "iter" 5 | "math/rand" 6 | "time" 7 | ) 8 | 9 | // RandomOptions defines configurable options for [RandomUniform] and 10 | // [RandomNormal]. 11 | type RandomOptions struct { 12 | // Generator is the random number generator. 13 | Generator *rand.Rand 14 | } 15 | 16 | // RandomOptionFunc is the function signature of configuration helpers 17 | // for [RandomUniform] and [RandomNormal]. 18 | type RandomOptionFunc func(*RandomOptions) 19 | 20 | // WithRandomGenerator is a helper for configuring the random number 21 | // generator for [RandomUniform] and [RandomNormal]. 22 | func WithRandomGenerator(generator *rand.Rand) RandomOptionFunc { 23 | return func(o *RandomOptions) { 24 | o.Generator = generator 25 | } 26 | } 27 | 28 | // RandomUniform allows looping over a given number of random values 29 | // drawn from a uniform distribution. The size must not be negative. 30 | func RandomUniform[N Number]( 31 | low N, 32 | high N, 33 | size int, 34 | opts ...RandomOptionFunc, 35 | ) iter.Seq[float64] { 36 | if size < 0 { 37 | panic("size must not be negative") 38 | } 39 | 40 | options := RandomOptions{ 41 | Generator: rand.New(rand.NewSource(time.Now().UnixNano())), 42 | } 43 | 44 | for _, opt := range opts { 45 | opt(&options) 46 | } 47 | 48 | flow := float64(low) 49 | fhigh := float64(high) 50 | delta := fhigh - flow 51 | 52 | return func(yield func(float64) bool) { 53 | for range size { 54 | if !yield(((options.Generator.Float64() * delta) + flow)) { 55 | return 56 | } 57 | } 58 | } 59 | } 60 | 61 | // RandomNormal allows looping over a given number of random values 62 | // drawn from a Gaussian distribution. The size must not be negative 63 | // and the standard deviation must be positive. 64 | func RandomNormal[N Number]( 65 | mean N, 66 | stddev N, 67 | size int, 68 | opts ...RandomOptionFunc, 69 | ) iter.Seq[float64] { 70 | if size < 0 { 71 | panic("size must not be negative") 72 | } 73 | 74 | if stddev <= 0 { 75 | panic("standard deviation must be positive") 76 | } 77 | 78 | options := RandomOptions{ 79 | Generator: rand.New(rand.NewSource(time.Now().UnixNano())), 80 | } 81 | 82 | for _, opt := range opts { 83 | opt(&options) 84 | } 85 | 86 | fmean := float64(mean) 87 | fstddev := float64(stddev) 88 | 89 | return func(yield func(float64) bool) { 90 | for range size { 91 | if !yield((options.Generator.NormFloat64() * fstddev) + fmean) { 92 | return 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /random_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | 7 | "github.com/alvii147/gloop" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestWithRandomGenerator(t *testing.T) { 12 | options := gloop.RandomOptions{} 13 | gloop.WithRandomGenerator(rand.New(rand.NewSource(314)))(&options) 14 | 15 | generator := rand.New(rand.NewSource(314)) 16 | require.Equal(t, generator.Int63(), options.Generator.Int63()) 17 | } 18 | 19 | func TestRandomUniform(t *testing.T) { 20 | i := 0 21 | 22 | for value := range gloop.RandomUniform(-2, 3, 10) { 23 | require.GreaterOrEqual(t, value, -2.0) 24 | require.Less(t, value, 3.0) 25 | 26 | i++ 27 | } 28 | 29 | require.Equal(t, 10, i) 30 | } 31 | 32 | func TestRandomUniformWithRandomGenerator(t *testing.T) { 33 | i := 0 34 | generator := rand.New(rand.NewSource(314)) 35 | 36 | for value := range gloop.RandomUniform(-2, 3, 10, gloop.WithRandomGenerator(generator)) { 37 | require.GreaterOrEqual(t, value, -2.0) 38 | require.Less(t, value, 3.0) 39 | 40 | i++ 41 | } 42 | 43 | require.Equal(t, 10, i) 44 | } 45 | 46 | func TestRandomUniformBreak(t *testing.T) { 47 | i := 0 48 | for value := range gloop.RandomUniform(-2, 3, 10) { 49 | if i == 2 { 50 | break 51 | } 52 | 53 | require.GreaterOrEqual(t, value, -2.0) 54 | require.Less(t, value, 3.0) 55 | 56 | i++ 57 | } 58 | 59 | require.Equal(t, 2, i) 60 | } 61 | 62 | func TestRandomUniformZeroSizeNoIteration(t *testing.T) { 63 | for range gloop.RandomUniform(-2, 3, 0) { 64 | t.Fatal("expected no iteration") 65 | } 66 | } 67 | 68 | func TestRandomUniformNegativeSizePanics(t *testing.T) { 69 | require.Panics(t, func() { 70 | for range gloop.RandomUniform(2, 3, -1) { 71 | t.Fatal("expected no iteration") 72 | } 73 | }) 74 | } 75 | 76 | func TestRandomNormalWithRandomGenerator(t *testing.T) { 77 | i := 0 78 | generator := rand.New(rand.NewSource(314)) 79 | 80 | for value := range gloop.RandomNormal(5, 2, 10, gloop.WithRandomGenerator(generator)) { 81 | _ = value 82 | i++ 83 | } 84 | 85 | require.Equal(t, 10, i) 86 | } 87 | 88 | func TestRandomNormal(t *testing.T) { 89 | i := 0 90 | 91 | for value := range gloop.RandomNormal(5, 2, 10) { 92 | _ = value 93 | i++ 94 | } 95 | 96 | require.Equal(t, 10, i) 97 | } 98 | 99 | func TestRandomNormalBreak(t *testing.T) { 100 | i := 0 101 | 102 | for value := range gloop.RandomNormal(5, 2, 10) { 103 | _ = value 104 | 105 | if i == 2 { 106 | break 107 | } 108 | 109 | i++ 110 | } 111 | 112 | require.Equal(t, 2, i) 113 | } 114 | 115 | func TestRandomNormalZeroSizeNoIteration(t *testing.T) { 116 | for range gloop.RandomNormal(5, 2, 0) { 117 | t.Fatal("expected no iteration") 118 | } 119 | } 120 | 121 | func TestRandomNormalNegativeSizePanics(t *testing.T) { 122 | require.Panics(t, func() { 123 | for range gloop.RandomNormal(5, 2, -1) { 124 | t.Fatal("expected no iteration") 125 | } 126 | }) 127 | } 128 | 129 | func TestRandomNormalZeroStandardDeviationPanics(t *testing.T) { 130 | require.Panics(t, func() { 131 | for range gloop.RandomNormal(5, 0, 10) { 132 | t.Fatal("expected no iteration") 133 | } 134 | }) 135 | } 136 | 137 | func TestRandomNormalNegativeStandardDeviationPanics(t *testing.T) { 138 | require.Panics(t, func() { 139 | for range gloop.RandomNormal(5, -2, 10) { 140 | t.Fatal("expected no iteration") 141 | } 142 | }) 143 | } 144 | -------------------------------------------------------------------------------- /reduce.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import "iter" 4 | 5 | // ReduceFunc is the function signature of the reducing function used 6 | // in [Reduce]. 7 | type ReduceFunc[V any] func(V, V) V 8 | 9 | // Reduce runs a given function on each adjacent pair in an [iter.Seq] 10 | // sequence and accumulates the result into a single value. 11 | func Reduce[V any](seq iter.Seq[V], f ReduceFunc[V]) V { 12 | _, v := Reduce2(Enumerate(seq), func(_ int, v1 V, _ int, v2 V) (int, V) { 13 | return 0, f(v1, v2) 14 | }) 15 | 16 | return v 17 | } 18 | 19 | // Reduce2Func is the function signature of the reducing function used 20 | // in [Reduce2]. 21 | type Reduce2Func[K, V any] func(K, V, K, V) (K, V) 22 | 23 | // Reduce2 runs a given function on each adjacent pair of keys and 24 | // values in an [iter.Seq2] sequence and accumulates the result into a 25 | // single key and value pair. 26 | func Reduce2[K, V any](seq iter.Seq2[K, V], f Reduce2Func[K, V]) (K, V) { 27 | var ( 28 | reducedKey K 29 | reducedValue V 30 | ) 31 | 32 | first := true 33 | 34 | for key, value := range seq { 35 | if first { 36 | reducedKey = key 37 | reducedValue = value 38 | first = false 39 | 40 | continue 41 | } 42 | 43 | reducedKey, reducedValue = f(reducedKey, reducedValue, key, value) 44 | } 45 | 46 | return reducedKey, reducedValue 47 | } 48 | -------------------------------------------------------------------------------- /reduce_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestReduceSliceMin(t *testing.T) { 11 | values := []int{3, -1, 4} 12 | minValue := gloop.Reduce(gloop.Slice(values), func(value1 int, value2 int) int { 13 | return min(value1, value2) 14 | }) 15 | require.Equal(t, -1, minValue) 16 | } 17 | 18 | func TestReduceSliceMax(t *testing.T) { 19 | values := []int{3, -1, 4} 20 | maxValue := gloop.Reduce(gloop.Slice(values), func(value1 int, value2 int) int { 21 | return max(value1, value2) 22 | }) 23 | require.Equal(t, 4, maxValue) 24 | } 25 | 26 | func TestReduceSliceProduct(t *testing.T) { 27 | values := []int{3, -1, 4} 28 | product := gloop.Reduce(gloop.Slice(values), func(value1 int, value2 int) int { 29 | return value1 * value2 30 | }) 31 | require.Equal(t, -12, product) 32 | } 33 | 34 | func TestReduceStringMinRune(t *testing.T) { 35 | s := "FizzBuzz" 36 | 37 | minRune := gloop.Reduce(gloop.String(s), func(value1 rune, value2 rune) rune { 38 | return min(value1, value2) 39 | }) 40 | require.Equal(t, 'B', minRune) 41 | } 42 | 43 | func TestReduceStringMaxRune(t *testing.T) { 44 | s := "FizzBuzz" 45 | 46 | maxRune := gloop.Reduce(gloop.String(s), func(value1 rune, value2 rune) rune { 47 | return max(value1, value2) 48 | }) 49 | require.Equal(t, 'z', maxRune) 50 | } 51 | 52 | func TestReduce2MapMinValue(t *testing.T) { 53 | m := map[int]int{ 54 | 0: 3, 55 | 1: -1, 56 | 2: 4, 57 | } 58 | 59 | minValueKey, minValue := gloop.Reduce2(gloop.Map(m), func(key1 int, value1 int, key2 int, value2 int) (int, int) { 60 | if value1 < value2 { 61 | return key1, value1 62 | } 63 | 64 | return key2, value2 65 | }) 66 | 67 | require.Equal(t, 1, minValueKey) 68 | require.Equal(t, -1, minValue) 69 | } 70 | 71 | func TestReduce2MapMaxValue(t *testing.T) { 72 | m := map[int]int{ 73 | 0: 3, 74 | 1: -1, 75 | 2: 4, 76 | } 77 | 78 | maxValueKey, maxValue := gloop.Reduce2(gloop.Map(m), func(key1 int, value1 int, key2 int, value2 int) (int, int) { 79 | if value1 > value2 { 80 | return key1, value1 81 | } 82 | 83 | return key2, value2 84 | }) 85 | 86 | require.Equal(t, 2, maxValueKey) 87 | require.Equal(t, 4, maxValue) 88 | } 89 | -------------------------------------------------------------------------------- /reverse.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "iter" 5 | ) 6 | 7 | // Reverse allows looping over an [iter.Seq] sequence in order of 8 | // descending index. 9 | func Reverse[V any](seq iter.Seq[V]) iter.Seq[V] { 10 | l := ToList(seq) 11 | 12 | return func(yield func(V) bool) { 13 | for l.Len() > 0 { 14 | value := l.Remove(l.Back()).(V) 15 | if !yield(value) { 16 | return 17 | } 18 | } 19 | } 20 | } 21 | 22 | // Reverse2 allows looping over an [iter.Seq2] sequence in order of 23 | // descending index. 24 | func Reverse2[K, V any](seq iter.Seq2[K, V]) iter.Seq2[K, V] { 25 | listKeys, listValues := ToList2(seq) 26 | 27 | return func(yield func(K, V) bool) { 28 | for listKeys.Len() > 0 && listValues.Len() > 0 { 29 | key := listKeys.Remove(listKeys.Back()).(K) 30 | value := listValues.Remove(listValues.Back()).(V) 31 | 32 | if !yield(key, value) { 33 | return 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /reverse_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestReverseSlice(t *testing.T) { 11 | values := []string{"a", "b", "c"} 12 | reversedValues := []string{"c", "b", "a"} 13 | i := 0 14 | 15 | for value := range gloop.Reverse(gloop.Slice(values)) { 16 | require.Equal(t, reversedValues[i], value) 17 | 18 | i++ 19 | } 20 | 21 | require.Equal(t, len(reversedValues), i) 22 | } 23 | 24 | func TestReverseString(t *testing.T) { 25 | s := "FizzBuzz" 26 | reversedRunes := []rune{'z', 'z', 'u', 'B', 'z', 'z', 'i', 'F'} 27 | i := 0 28 | 29 | for r := range gloop.Reverse(gloop.String(s)) { 30 | require.Equal(t, reversedRunes[i], r) 31 | 32 | i++ 33 | } 34 | 35 | require.Equal(t, len(reversedRunes), i) 36 | } 37 | 38 | func TestReverseBreak(t *testing.T) { 39 | values := []string{"a", "b", "c"} 40 | reversedValues := []string{"c", "b"} 41 | i := 0 42 | 43 | for value := range gloop.Reverse(gloop.Slice(values)) { 44 | if i == 2 { 45 | break 46 | } 47 | 48 | require.Equal(t, reversedValues[i], value) 49 | 50 | i++ 51 | } 52 | 53 | require.Equal(t, len(reversedValues), i) 54 | } 55 | 56 | func TestReverse2(t *testing.T) { 57 | seq := func(yield func(string, int) bool) { 58 | if !yield("a", 3) { 59 | return 60 | } 61 | 62 | if !yield("b", 4) { 63 | return 64 | } 65 | 66 | if !yield("c", 5) { 67 | return 68 | } 69 | } 70 | reversedKeys := []string{"c", "b", "a"} 71 | reversedValues := []int{5, 4, 3} 72 | i := 0 73 | 74 | for key, value := range gloop.Reverse2(seq) { 75 | require.Equal(t, reversedKeys[i], key) 76 | require.Equal(t, reversedValues[i], value) 77 | 78 | i++ 79 | } 80 | 81 | require.Equal(t, 3, i) 82 | } 83 | 84 | func TestReverse2Break(t *testing.T) { 85 | seq := func(yield func(string, int) bool) { 86 | if !yield("a", 3) { 87 | return 88 | } 89 | 90 | if !yield("b", 4) { 91 | return 92 | } 93 | 94 | if !yield("c", 5) { 95 | return 96 | } 97 | } 98 | reversedKeys := []string{"c", "b"} 99 | reversedValues := []int{5, 4} 100 | i := 0 101 | 102 | for key, value := range gloop.Reverse2(seq) { 103 | if i == 2 { 104 | break 105 | } 106 | 107 | require.Equal(t, reversedKeys[i], key) 108 | require.Equal(t, reversedValues[i], value) 109 | 110 | i++ 111 | } 112 | 113 | require.Equal(t, 2, i) 114 | } 115 | -------------------------------------------------------------------------------- /slice.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "iter" 5 | ) 6 | 7 | // Slice allows looping over a given slice. 8 | func Slice[V any](values []V) iter.Seq[V] { 9 | return func(yield func(V) bool) { 10 | for _, value := range values { 11 | if !yield(value) { 12 | return 13 | } 14 | } 15 | } 16 | } 17 | 18 | // ToSlice converts an [iter.Seq] sequence to a slice. 19 | func ToSlice[V any](seq iter.Seq[V]) []V { 20 | l := ToList(seq) 21 | values := make([]V, l.Len()) 22 | 23 | for i := 0; i < len(values); i++ { 24 | values[i] = l.Remove(l.Front()).(V) 25 | } 26 | 27 | return values 28 | } 29 | 30 | // ToSlice2 converts an [iter.Seq2] sequence to slices of keys and 31 | // values. 32 | func ToSlice2[K, V any](seq iter.Seq2[K, V]) ([]K, []V) { 33 | listKeys, listValues := ToList2(seq) 34 | n := listKeys.Len() 35 | 36 | keys := make([]K, n) 37 | values := make([]V, n) 38 | 39 | for i := range n { 40 | keys[i] = listKeys.Remove(listKeys.Front()).(K) 41 | values[i] = listValues.Remove(listValues.Front()).(V) 42 | } 43 | 44 | return keys, values 45 | } 46 | -------------------------------------------------------------------------------- /slice_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestSlice(t *testing.T) { 11 | values := []int{3, 4, 5} 12 | i := 0 13 | 14 | for value := range gloop.Slice(values) { 15 | require.Equal(t, values[i], value) 16 | 17 | i++ 18 | } 19 | 20 | require.Equal(t, len(values), i) 21 | } 22 | 23 | func TestSliceBreak(t *testing.T) { 24 | values := []int{3, 4, 5} 25 | wantValues := []int{3, 4} 26 | i := 0 27 | 28 | for value := range gloop.Slice(values) { 29 | if i == 2 { 30 | break 31 | } 32 | 33 | require.Equal(t, wantValues[i], value) 34 | 35 | i++ 36 | } 37 | 38 | require.Equal(t, len(wantValues), i) 39 | } 40 | 41 | func TestToSlice(t *testing.T) { 42 | seq := func(yield func(int) bool) { 43 | if !yield(3) { 44 | return 45 | } 46 | 47 | if !yield(4) { 48 | return 49 | } 50 | 51 | if !yield(5) { 52 | return 53 | } 54 | } 55 | 56 | values := gloop.ToSlice(seq) 57 | require.Equal(t, []int{3, 4, 5}, values) 58 | } 59 | 60 | func TestToSlice2(t *testing.T) { 61 | seq := func(yield func(int, int) bool) { 62 | if !yield(0, 3) { 63 | return 64 | } 65 | 66 | if !yield(1, 4) { 67 | return 68 | } 69 | 70 | if !yield(2, 5) { 71 | return 72 | } 73 | } 74 | 75 | keys, values := gloop.ToSlice2(seq) 76 | require.Equal(t, []int{0, 1, 2}, keys) 77 | require.Equal(t, []int{3, 4, 5}, values) 78 | } 79 | -------------------------------------------------------------------------------- /sort.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "cmp" 5 | "iter" 6 | "sort" 7 | ) 8 | 9 | // Sort allows looping over an [iter.Seq] sequence in sorted order. 10 | func Sort[V cmp.Ordered](seq iter.Seq[V], ascending bool) iter.Seq[V] { 11 | return SortByComparison(seq, func(value1 V, value2 V) bool { 12 | return value1 < value2 13 | }, ascending) 14 | } 15 | 16 | // SortByComparisonFunc is the function signature of the comparison 17 | // function used in [SortByComparison]. 18 | type SortByComparisonFunc[V any] func(V, V) bool 19 | 20 | // SortByComparison allows looping over an [iter.Seq] sequence in 21 | // sorted order using a comparison function. 22 | func SortByComparison[V any]( 23 | seq iter.Seq[V], 24 | less SortByComparisonFunc[V], 25 | ascending bool, 26 | ) iter.Seq[V] { 27 | values := ToSlice(seq) 28 | sort.Slice(values, func(i int, j int) bool { 29 | isLess := less(values[i], values[j]) 30 | 31 | return (ascending && isLess) || (!ascending && !isLess) 32 | }) 33 | 34 | return func(yield func(V) bool) { 35 | for _, value := range values { 36 | if !yield(value) { 37 | return 38 | } 39 | } 40 | } 41 | } 42 | 43 | // SortByComparison2Func is the function signature of the comparison 44 | // function used in [SortByComparison2]. 45 | type SortByComparison2Func[K, V any] func(K, V, K, V) bool 46 | 47 | // SortByComparison2 allows looping over an [iter.Seq2] sequence in 48 | // sorted order using a comparison function. 49 | func SortByComparison2[K, V any]( 50 | seq iter.Seq2[K, V], 51 | less SortByComparison2Func[K, V], 52 | ascending bool, 53 | ) iter.Seq2[K, V] { 54 | return KeyValue(SortByComparison( 55 | KeyValue2(seq), 56 | func(pair1 KeyValuePair[K, V], pair2 KeyValuePair[K, V]) bool { 57 | return less(pair1.Key, pair1.Value, pair2.Key, pair2.Value) 58 | }, 59 | ascending, 60 | )) 61 | } 62 | 63 | // SortByRankFunc is the function signature of the ranking function 64 | // used in [SortByRank]. 65 | type SortByRankFunc[V any, R cmp.Ordered] func(V) R 66 | 67 | // SortByRank allows looping over an [iter.Seq] sequence in sorted 68 | // order using a ranking function. 69 | func SortByRank[V any, R cmp.Ordered]( 70 | seq iter.Seq[V], 71 | rank SortByRankFunc[V, R], 72 | ascending bool, 73 | ) iter.Seq[V] { 74 | return SortByComparison(seq, func(value1 V, value2 V) bool { 75 | return rank(value1) < rank(value2) 76 | }, ascending) 77 | } 78 | 79 | // SortByRank2Func is the function signature of the ranking function 80 | // used in [SortByRank2]. 81 | type SortByRank2Func[K, V any, R cmp.Ordered] func(K, V) R 82 | 83 | // SortByRank2 allows looping over an [iter.Seq2] sequence in sorted 84 | // order using a ranking function. 85 | func SortByRank2[K, V any, R cmp.Ordered]( 86 | seq iter.Seq2[K, V], 87 | rank SortByRank2Func[K, V, R], 88 | ascending bool, 89 | ) iter.Seq2[K, V] { 90 | return SortByComparison2(seq, func(key1 K, value1 V, key2 K, value2 V) bool { 91 | return rank(key1, value1) < rank(key2, value2) 92 | }, ascending) 93 | } 94 | -------------------------------------------------------------------------------- /sort_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestSortAscendingInt(t *testing.T) { 11 | values := []int{3, 1, 4, 1, 5, 9, 2, 6, 5} 12 | wantValues := []int{1, 1, 2, 3, 4, 5, 5, 6, 9} 13 | 14 | i := 0 15 | for value := range gloop.Sort(gloop.Slice(values), true) { 16 | require.Equal(t, wantValues[i], value) 17 | 18 | i++ 19 | } 20 | 21 | require.Equal(t, len(wantValues), i) 22 | } 23 | 24 | func TestSortDescendingInt(t *testing.T) { 25 | values := []int{3, 1, 4, 1, 5, 9, 2, 6, 5} 26 | wantValues := []int{9, 6, 5, 5, 4, 3, 2, 1, 1} 27 | 28 | i := 0 29 | for value := range gloop.Sort(gloop.Slice(values), false) { 30 | require.Equal(t, wantValues[i], value) 31 | 32 | i++ 33 | } 34 | 35 | require.Equal(t, len(wantValues), i) 36 | } 37 | 38 | func TestSortAscendingFloat(t *testing.T) { 39 | values := []float64{3.14, -1.59, 2.65} 40 | wantValues := []float64{-1.59, 2.65, 3.14} 41 | 42 | i := 0 43 | for value := range gloop.Sort(gloop.Slice(values), true) { 44 | require.InDelta(t, wantValues[i], value, 0.001) 45 | 46 | i++ 47 | } 48 | 49 | require.Equal(t, len(wantValues), i) 50 | } 51 | 52 | func TestSortDescendingFloat(t *testing.T) { 53 | values := []float64{3.14, -1.59, 2.65} 54 | wantValues := []float64{3.14, 2.65, -1.59} 55 | 56 | i := 0 57 | for value := range gloop.Sort(gloop.Slice(values), false) { 58 | require.InDelta(t, wantValues[i], value, 0.001) 59 | 60 | i++ 61 | } 62 | 63 | require.Equal(t, len(wantValues), i) 64 | } 65 | 66 | func TestSortAscendingString(t *testing.T) { 67 | values := []string{"Fizz", "Buzz", "Bazz"} 68 | wantValues := []string{"Bazz", "Buzz", "Fizz"} 69 | 70 | i := 0 71 | for value := range gloop.Sort(gloop.Slice(values), true) { 72 | require.Equal(t, wantValues[i], value) 73 | 74 | i++ 75 | } 76 | 77 | require.Equal(t, len(wantValues), i) 78 | } 79 | 80 | func TestSortDescendingString(t *testing.T) { 81 | values := []string{"Fizz", "Buzz", "Bazz"} 82 | wantValues := []string{"Fizz", "Buzz", "Bazz"} 83 | 84 | i := 0 85 | for value := range gloop.Sort(gloop.Slice(values), false) { 86 | require.Equal(t, wantValues[i], value) 87 | 88 | i++ 89 | } 90 | 91 | require.Equal(t, len(wantValues), i) 92 | } 93 | 94 | func TestSortBreak(t *testing.T) { 95 | values := []int{3, 1, 4, 1, 5, 9, 2, 6, 5} 96 | wantValues := []int{1, 1, 2} 97 | 98 | i := 0 99 | for value := range gloop.Sort(gloop.Slice(values), true) { 100 | if i == 3 { 101 | break 102 | } 103 | 104 | require.Equal(t, wantValues[i], value) 105 | 106 | i++ 107 | } 108 | 109 | require.Equal(t, len(wantValues), i) 110 | } 111 | 112 | func TestSortByComparisonAscending(t *testing.T) { 113 | values := []string{"Fizzzzzz", "Buz", "Bazzz"} 114 | wantValues := []string{"Buz", "Bazzz", "Fizzzzzz"} 115 | 116 | i := 0 117 | for value := range gloop.SortByComparison( 118 | gloop.Slice(values), 119 | func(s1 string, s2 string) bool { 120 | return len(s1) < len(s2) 121 | }, 122 | true, 123 | ) { 124 | require.Equal(t, wantValues[i], value) 125 | 126 | i++ 127 | } 128 | 129 | require.Equal(t, len(wantValues), i) 130 | } 131 | 132 | func TestSortByComparisonDescending(t *testing.T) { 133 | values := []string{"Fizzzzzz", "Buz", "Bazzz"} 134 | wantValues := []string{"Fizzzzzz", "Bazzz", "Buz"} 135 | 136 | i := 0 137 | for value := range gloop.SortByComparison( 138 | gloop.Slice(values), 139 | func(s1 string, s2 string) bool { 140 | return len(s1) < len(s2) 141 | }, 142 | false, 143 | ) { 144 | require.Equal(t, wantValues[i], value) 145 | 146 | i++ 147 | } 148 | 149 | require.Equal(t, len(wantValues), i) 150 | } 151 | 152 | func TestSortByComparisonBreak(t *testing.T) { 153 | values := []string{"Fizzzzzz", "Buz", "Bazzz"} 154 | wantValues := []string{"Buz", "Bazzz"} 155 | 156 | i := 0 157 | for value := range gloop.SortByComparison( 158 | gloop.Slice(values), 159 | func(s1 string, s2 string) bool { 160 | return len(s1) < len(s2) 161 | }, 162 | true, 163 | ) { 164 | if i == 2 { 165 | break 166 | } 167 | 168 | require.Equal(t, wantValues[i], value) 169 | 170 | i++ 171 | } 172 | 173 | require.Equal(t, len(wantValues), i) 174 | } 175 | 176 | func TestSortByComparison2Ascending(t *testing.T) { 177 | m := map[int]int{ 178 | 3: -1, 179 | 1: 5, 180 | -4: -9, 181 | } 182 | wantKeys := []int{3, 1, -4} 183 | wantValues := []int{-1, 5, -9} 184 | 185 | i := 0 186 | for key, value := range gloop.SortByComparison2( 187 | gloop.Map(m), 188 | func(key1 int, value1 int, key2 int, value2 int) bool { 189 | return key1*value1 < key2*value2 190 | }, 191 | true, 192 | ) { 193 | require.Equal(t, wantKeys[i], key) 194 | require.Equal(t, wantValues[i], value) 195 | 196 | i++ 197 | } 198 | 199 | require.Equal(t, len(wantKeys), i) 200 | } 201 | 202 | func TestSortByComparison2Descending(t *testing.T) { 203 | m := map[int]int{ 204 | 3: -1, 205 | 1: 5, 206 | -4: -9, 207 | } 208 | wantKeys := []int{-4, 1, 3} 209 | wantValues := []int{-9, 5, -1} 210 | 211 | i := 0 212 | for key, value := range gloop.SortByComparison2( 213 | gloop.Map(m), 214 | func(key1 int, value1 int, key2 int, value2 int) bool { 215 | return key1*value1 < key2*value2 216 | }, 217 | false, 218 | ) { 219 | require.Equal(t, wantKeys[i], key) 220 | require.Equal(t, wantValues[i], value) 221 | 222 | i++ 223 | } 224 | 225 | require.Equal(t, len(wantKeys), i) 226 | } 227 | 228 | func TestSortByComparison2Break(t *testing.T) { 229 | m := map[int]int{ 230 | 3: -1, 231 | 1: 5, 232 | -4: -9, 233 | } 234 | wantKeys := []int{3, 1} 235 | wantValues := []int{-1, 5} 236 | 237 | i := 0 238 | for key, value := range gloop.SortByComparison2( 239 | gloop.Map(m), 240 | func(key1 int, value1 int, key2 int, value2 int) bool { 241 | return key1*value1 < key2*value2 242 | }, 243 | true, 244 | ) { 245 | if i == 2 { 246 | break 247 | } 248 | 249 | require.Equal(t, wantKeys[i], key) 250 | require.Equal(t, wantValues[i], value) 251 | 252 | i++ 253 | } 254 | 255 | require.Equal(t, 2, i) 256 | } 257 | 258 | func TestSortByRankAscending(t *testing.T) { 259 | values := []string{"Fizzzzzz", "Buz", "Bazzz"} 260 | wantValues := []string{"Buz", "Bazzz", "Fizzzzzz"} 261 | 262 | i := 0 263 | for value := range gloop.SortByRank( 264 | gloop.Slice(values), 265 | func(s string) int { 266 | return len(s) 267 | }, 268 | true, 269 | ) { 270 | require.Equal(t, wantValues[i], value) 271 | 272 | i++ 273 | } 274 | 275 | require.Equal(t, len(wantValues), i) 276 | } 277 | 278 | func TestSortByRankDescending(t *testing.T) { 279 | values := []string{"Fizzzzzz", "Buz", "Bazzz"} 280 | wantValues := []string{"Fizzzzzz", "Bazzz", "Buz"} 281 | 282 | i := 0 283 | for value := range gloop.SortByRank( 284 | gloop.Slice(values), 285 | func(s string) int { 286 | return len(s) 287 | }, 288 | false, 289 | ) { 290 | require.Equal(t, wantValues[i], value) 291 | 292 | i++ 293 | } 294 | 295 | require.Equal(t, len(wantValues), i) 296 | } 297 | 298 | func TestSortByRankBreak(t *testing.T) { 299 | values := []string{"Fizzzzzz", "Buz", "Bazzz"} 300 | wantValues := []string{"Buz", "Bazzz"} 301 | 302 | i := 0 303 | for value := range gloop.SortByRank( 304 | gloop.Slice(values), 305 | func(s string) int { 306 | return len(s) 307 | }, 308 | true, 309 | ) { 310 | if i == 2 { 311 | break 312 | } 313 | 314 | require.Equal(t, wantValues[i], value) 315 | 316 | i++ 317 | } 318 | 319 | require.Equal(t, len(wantValues), i) 320 | } 321 | 322 | func TestSortByRank2Ascending(t *testing.T) { 323 | m := map[int]int{ 324 | 3: -1, 325 | 1: 5, 326 | -4: -9, 327 | } 328 | wantKeys := []int{3, 1, -4} 329 | wantValues := []int{-1, 5, -9} 330 | 331 | i := 0 332 | for key, value := range gloop.SortByRank2( 333 | gloop.Map(m), 334 | func(key int, value int) int { 335 | return key * value 336 | }, 337 | true, 338 | ) { 339 | require.Equal(t, wantKeys[i], key) 340 | require.Equal(t, wantValues[i], value) 341 | 342 | i++ 343 | } 344 | 345 | require.Equal(t, len(wantKeys), i) 346 | } 347 | 348 | func TestSortByRank2Descending(t *testing.T) { 349 | m := map[int]int{ 350 | 3: -1, 351 | 1: 5, 352 | -4: -9, 353 | } 354 | wantKeys := []int{-4, 1, 3} 355 | wantValues := []int{-9, 5, -1} 356 | 357 | i := 0 358 | for key, value := range gloop.SortByRank2( 359 | gloop.Map(m), 360 | func(key int, value int) int { 361 | return key * value 362 | }, 363 | false, 364 | ) { 365 | require.Equal(t, wantKeys[i], key) 366 | require.Equal(t, wantValues[i], value) 367 | 368 | i++ 369 | } 370 | 371 | require.Equal(t, len(wantKeys), i) 372 | } 373 | 374 | func TestSortByRank2Break(t *testing.T) { 375 | m := map[int]int{ 376 | 3: -1, 377 | 1: 5, 378 | -4: -9, 379 | } 380 | wantKeys := []int{3, 1} 381 | wantValues := []int{-1, 5} 382 | 383 | i := 0 384 | for key, value := range gloop.SortByRank2( 385 | gloop.Map(m), 386 | func(key int, value int) int { 387 | return key * value 388 | }, 389 | true, 390 | ) { 391 | if i == 2 { 392 | break 393 | } 394 | 395 | require.Equal(t, wantKeys[i], key) 396 | require.Equal(t, wantValues[i], value) 397 | 398 | i++ 399 | } 400 | 401 | require.Equal(t, 2, i) 402 | } 403 | -------------------------------------------------------------------------------- /string.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "iter" 5 | "strings" 6 | ) 7 | 8 | // String allows looping over the runes in a given string. 9 | func String(s string) iter.Seq[rune] { 10 | return func(yield func(rune) bool) { 11 | for _, r := range s { 12 | if !yield(r) { 13 | return 14 | } 15 | } 16 | } 17 | } 18 | 19 | // ToString converts an [iter.Seq] sequence of runes to a string. 20 | func ToString(seq iter.Seq[rune]) string { 21 | var sb strings.Builder 22 | for value := range seq { 23 | sb.WriteRune(value) 24 | } 25 | 26 | s := sb.String() 27 | 28 | return s 29 | } 30 | -------------------------------------------------------------------------------- /string_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestString(t *testing.T) { 11 | wantRunes := []rune{'F', 'i', 'z', 'z'} 12 | i := 0 13 | 14 | for r := range gloop.String("Fizz") { 15 | require.Equal(t, wantRunes[i], r) 16 | 17 | i++ 18 | } 19 | 20 | require.Equal(t, len(wantRunes), i) 21 | } 22 | 23 | func TestStringBreak(t *testing.T) { 24 | wantRunes := []rune{'F', 'i'} 25 | i := 0 26 | 27 | for r := range gloop.String("Fizz") { 28 | if i == 2 { 29 | break 30 | } 31 | 32 | require.Equal(t, wantRunes[i], r) 33 | 34 | i++ 35 | } 36 | 37 | require.Equal(t, len(wantRunes), i) 38 | } 39 | 40 | func TestToString(t *testing.T) { 41 | seq := func(yield func(rune) bool) { 42 | if !yield('F') { 43 | return 44 | } 45 | 46 | if !yield('i') { 47 | return 48 | } 49 | 50 | if !yield('z') { 51 | return 52 | } 53 | 54 | if !yield('z') { 55 | return 56 | } 57 | } 58 | 59 | s := gloop.ToString(seq) 60 | require.Equal(t, "Fizz", s) 61 | } 62 | -------------------------------------------------------------------------------- /sum.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "iter" 5 | ) 6 | 7 | // Sum computes summation over an [iter.Seq] sequence. 8 | func Sum[V Summable](seq iter.Seq[V]) V { 9 | return Fold(seq, func(acc V, value V) V { 10 | return acc + value 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /sum_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/alvii147/gloop" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestSumInt(t *testing.T) { 12 | values := []int{3, 4, -5} 13 | sum := gloop.Sum(gloop.Slice(values)) 14 | require.Equal(t, 2, sum) 15 | } 16 | 17 | func TestSumFloat(t *testing.T) { 18 | values := []float64{2.31, -0.03, 0.22} 19 | sum := gloop.Sum(gloop.Slice(values)) 20 | require.InDelta(t, 2.5, sum, 0.01) 21 | } 22 | 23 | func TestSumComplex(t *testing.T) { 24 | values := []complex128{complex(1, 2), complex(3, -4)} 25 | sum := gloop.Sum(gloop.Slice(values)) 26 | require.Equal(t, complex(4, -2), sum) 27 | } 28 | 29 | func TestSumString(t *testing.T) { 30 | values := []string{"Fizz", "Buzz"} 31 | concat := gloop.Sum(gloop.Slice(values)) 32 | require.Equal(t, "FizzBuzz", concat) 33 | } 34 | 35 | func TestSumDuration(t *testing.T) { 36 | values := []time.Duration{time.Hour, time.Minute, time.Second} 37 | duration := gloop.Sum(gloop.Slice(values)) 38 | require.Equal(t, 3661*time.Second, duration) 39 | } 40 | -------------------------------------------------------------------------------- /transform.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import "iter" 4 | 5 | // TransformFunc is the function signature of the transformation 6 | // function in [Transform]. 7 | type TransformFunc[V, T any] func(V) T 8 | 9 | // Transform runs a given function on each value over an [iter.Seq] 10 | // sequence and allows looping over the returned values. 11 | func Transform[V, T any](seq iter.Seq[V], f TransformFunc[V, T]) iter.Seq[T] { 12 | return Transform2(Enumerate(seq), func(_ int, value V) T { 13 | return f(value) 14 | }) 15 | } 16 | 17 | // Transform2Func is the function signature of the transformation 18 | // function in [Transform2]. 19 | type Transform2Func[K, V, T any] func(K, V) T 20 | 21 | // Transform2 runs a given function on each key and value over an 22 | // [iter.Seq2] sequence and allows looping over the returned values. 23 | func Transform2[K, V, T any](seq iter.Seq2[K, V], f Transform2Func[K, V, T]) iter.Seq[T] { 24 | return func(yield func(T) bool) { 25 | for key, value := range seq { 26 | if !yield(f(key, value)) { 27 | return 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /transform_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "testing" 7 | "unicode" 8 | 9 | "github.com/alvii147/gloop" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestTransformSliceDoubleValue(t *testing.T) { 14 | values := []int{3, 4, 5} 15 | i := 0 16 | 17 | for value := range gloop.Transform(gloop.Slice(values), func(value int) int { 18 | return value * 2 19 | }) { 20 | require.Equal(t, values[i]*2, value) 21 | 22 | i++ 23 | } 24 | 25 | require.Equal(t, len(values), i) 26 | } 27 | 28 | func TestTransformSliceItoa(t *testing.T) { 29 | values := []int{3, 4, 5} 30 | wantValues := []string{"3", "4", "5"} 31 | i := 0 32 | 33 | for value := range gloop.Transform(gloop.Slice(values), strconv.Itoa) { 34 | require.Equal(t, wantValues[i], value) 35 | 36 | i++ 37 | } 38 | 39 | require.Equal(t, len(wantValues), i) 40 | } 41 | 42 | func TestTransformSliceTrimSpace(t *testing.T) { 43 | values := []string{" Fizz", " Buzz "} 44 | wantValues := []string{"Fizz", "Buzz"} 45 | i := 0 46 | 47 | for value := range gloop.Transform(gloop.Slice(values), strings.TrimSpace) { 48 | require.Equal(t, wantValues[i], value) 49 | 50 | i++ 51 | } 52 | 53 | require.Equal(t, len(wantValues), i) 54 | } 55 | 56 | func TestTransformStringToLower(t *testing.T) { 57 | s := "FiZz" 58 | wantRunes := []rune{'f', 'i', 'z', 'z'} 59 | i := 0 60 | 61 | for r := range gloop.Transform(gloop.String(s), unicode.ToLower) { 62 | require.Equal(t, wantRunes[i], r) 63 | 64 | i++ 65 | } 66 | 67 | require.Equal(t, len(wantRunes), i) 68 | } 69 | 70 | func TestTransformBreak(t *testing.T) { 71 | values := []int{3, 4, 5} 72 | wantValues := []int{3, 4} 73 | i := 0 74 | 75 | for value := range gloop.Transform(gloop.Slice(values), func(value int) int { 76 | return value 77 | }) { 78 | if i == 2 { 79 | break 80 | } 81 | 82 | require.Equal(t, wantValues[i], value) 83 | 84 | i++ 85 | } 86 | 87 | require.Equal(t, len(wantValues), i) 88 | } 89 | 90 | func TestTransform2MapKeyValueSum(t *testing.T) { 91 | m := map[int]int{ 92 | -1: -2, 93 | 8: -2, 94 | -4: 9, 95 | } 96 | wantValues := []int{-3, 6, 5} 97 | i := 0 98 | 99 | for value := range gloop.Transform2(gloop.Map(m), func(key int, value int) int { 100 | return key + value 101 | }) { 102 | require.Contains(t, wantValues, value) 103 | 104 | i++ 105 | } 106 | 107 | require.Equal(t, len(wantValues), i) 108 | } 109 | 110 | func TestTransform2MapKeyValueProduct(t *testing.T) { 111 | m := map[int]int{ 112 | -1: -2, 113 | 8: -2, 114 | -4: 9, 115 | } 116 | wantValues := []int{2, -16, -36} 117 | i := 0 118 | 119 | for value := range gloop.Transform2(gloop.Map(m), func(key int, value int) int { 120 | return key * value 121 | }) { 122 | require.Contains(t, wantValues, value) 123 | 124 | i++ 125 | } 126 | 127 | require.Equal(t, len(wantValues), i) 128 | } 129 | 130 | func TestTransform2Break(t *testing.T) { 131 | m := map[int]int{ 132 | 3: 4, 133 | } 134 | i := 0 135 | 136 | for value := range gloop.Transform2(gloop.Map(m), func(key int, value int) int { 137 | return value 138 | }) { 139 | if i == 2 { 140 | break 141 | } 142 | 143 | require.Equal(t, 4, value) 144 | 145 | i++ 146 | } 147 | 148 | require.Equal(t, 1, i) 149 | } 150 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | // SignedInteger represents all signed integer types. 4 | type SignedInteger interface { 5 | ~int | 6 | ~int8 | 7 | ~int16 | 8 | ~int32 | 9 | ~int64 10 | } 11 | 12 | // UnsignedInteger represents all unsigned integer types. 13 | type UnsignedInteger interface { 14 | ~uint | 15 | ~uint8 | 16 | ~uint16 | 17 | ~uint32 | 18 | ~uint64 | 19 | ~uintptr 20 | } 21 | 22 | // FloatingPoint represents all floating point number types. 23 | type FloatingPoint interface { 24 | ~float32 | ~float64 25 | } 26 | 27 | // ComplexNumber represents all complex number types. 28 | type ComplexNumber interface { 29 | ~complex64 | ~complex128 30 | } 31 | 32 | // Number represents all numeric types. 33 | type Number interface { 34 | SignedInteger | 35 | UnsignedInteger | 36 | FloatingPoint 37 | } 38 | 39 | // Summable represents all types that support summation through the "+" 40 | // operator. 41 | type Summable interface { 42 | SignedInteger | 43 | UnsignedInteger | 44 | FloatingPoint | 45 | ComplexNumber | 46 | ~string 47 | } 48 | 49 | // Productable represents all types that support multiplication through 50 | // the "*" operator. 51 | type Productable interface { 52 | SignedInteger | 53 | UnsignedInteger | 54 | FloatingPoint | 55 | ComplexNumber 56 | } 57 | -------------------------------------------------------------------------------- /values.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import "iter" 4 | 5 | // Values allows looping over an [iter.Seq2] and converting it to an 6 | // [iter.Seq] sequence by discarding the key. 7 | func Values[K, V any](seq iter.Seq2[K, V]) iter.Seq[V] { 8 | return func(yield func(V) bool) { 9 | for _, value := range seq { 10 | if !yield(value) { 11 | return 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /values_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestValues(t *testing.T) { 11 | values := []string{"a", "b", "c"} 12 | i := 0 13 | 14 | for value := range gloop.Values(gloop.Enumerate(gloop.Slice(values))) { 15 | require.Equal(t, values[i], value) 16 | 17 | i++ 18 | } 19 | 20 | require.Equal(t, len(values), i) 21 | } 22 | 23 | func TestValuesBreak(t *testing.T) { 24 | values := []string{"a", "b", "c"} 25 | i := 0 26 | 27 | for value := range gloop.Values(gloop.Enumerate(gloop.Slice(values))) { 28 | if i == 2 { 29 | break 30 | } 31 | 32 | require.Equal(t, values[i], value) 33 | 34 | i++ 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /window.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "iter" 5 | ) 6 | 7 | // Window allows looping over an [iter.Seq] sequence in sliding windows 8 | // of a given size. 9 | func Window[V any](seq iter.Seq[V], size int) iter.Seq[iter.Seq[V]] { 10 | if size <= 0 { 11 | panic("size must be positive") 12 | } 13 | 14 | l := ToList(seq) 15 | 16 | return func(yield func(iter.Seq[V]) bool) { 17 | firstElem := l.Front() 18 | 19 | for range l.Len() - size + 1 { 20 | if !yield(func(yield func(V) bool) { 21 | elem := firstElem 22 | for range size { 23 | if !yield(elem.Value.(V)) { 24 | return 25 | } 26 | 27 | elem = elem.Next() 28 | } 29 | }) { 30 | return 31 | } 32 | 33 | firstElem = firstElem.Next() 34 | } 35 | } 36 | } 37 | 38 | // Window2 allows looping over an [iter.Seq2] sequence in sliding 39 | // windows of a given size. 40 | func Window2[K, V any](seq iter.Seq2[K, V], size int) iter.Seq[iter.Seq2[K, V]] { 41 | if size <= 0 { 42 | panic("size must be positive") 43 | } 44 | 45 | listKeys, listValues := ToList2(seq) 46 | 47 | return func(yield func(iter.Seq2[K, V]) bool) { 48 | firstKeyElem := listKeys.Front() 49 | firstValueElem := listValues.Front() 50 | 51 | for range listKeys.Len() - size + 1 { 52 | if !yield(func(yield func(K, V) bool) { 53 | keyElem := firstKeyElem 54 | valueElem := firstValueElem 55 | for range size { 56 | if !yield(keyElem.Value.(K), valueElem.Value.(V)) { 57 | return 58 | } 59 | 60 | keyElem = keyElem.Next() 61 | valueElem = valueElem.Next() 62 | } 63 | }) { 64 | return 65 | } 66 | 67 | firstKeyElem = firstKeyElem.Next() 68 | firstValueElem = firstValueElem.Next() 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /window_test.go: -------------------------------------------------------------------------------- 1 | package gloop_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alvii147/gloop" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestWindowSlice(t *testing.T) { 11 | values := []int{3, 1, 4, 1, 5, 9, 2, 6, 5} 12 | wantWindows := [][]int{ 13 | {3, 1, 4}, 14 | {1, 4, 1}, 15 | {4, 1, 5}, 16 | {1, 5, 9}, 17 | {5, 9, 2}, 18 | {9, 2, 6}, 19 | {2, 6, 5}, 20 | } 21 | i := 0 22 | 23 | for seq := range gloop.Window(gloop.Slice(values), 3) { 24 | window := gloop.ToSlice(seq) 25 | require.Equal(t, wantWindows[i], window) 26 | 27 | i++ 28 | } 29 | 30 | require.Equal(t, len(wantWindows), i) 31 | } 32 | 33 | func TestWindowString(t *testing.T) { 34 | s := "FizzBuzz" 35 | wantWindows := []string{ 36 | "Fizz", 37 | "izzB", 38 | "zzBu", 39 | "zBuz", 40 | "Buzz", 41 | } 42 | i := 0 43 | 44 | for seq := range gloop.Window(gloop.String(s), 4) { 45 | window := gloop.ToString(seq) 46 | require.Equal(t, wantWindows[i], window) 47 | 48 | i++ 49 | } 50 | 51 | require.Equal(t, len(wantWindows), i) 52 | } 53 | 54 | func TestWindowBreak(t *testing.T) { 55 | values := []int{3, 1, 4, 1, 5, 9, 2, 6, 5} 56 | 57 | for seq := range gloop.Window(gloop.Slice(values), 3) { 58 | for value := range seq { 59 | require.Equal(t, 3, value) 60 | 61 | break 62 | } 63 | 64 | break 65 | } 66 | } 67 | 68 | func TestWindowZeroSizePanics(t *testing.T) { 69 | require.Panics(t, func() { 70 | for range gloop.Window(gloop.Slice([]int{3, 1, 4}), 0) { 71 | t.Fatal("expected no iteration") 72 | } 73 | }) 74 | } 75 | 76 | func TestWindowNegativeSizePanics(t *testing.T) { 77 | require.Panics(t, func() { 78 | for range gloop.Window(gloop.Slice([]int{3, 1, 4}), -1) { 79 | t.Fatal("expected no iteration") 80 | } 81 | }) 82 | } 83 | 84 | func TestWindow2Slice(t *testing.T) { 85 | values := []int{3, 1, 4, 1, 5, 9, 2, 6, 5} 86 | wantKeys := [][]int{ 87 | {0, 1, 2}, 88 | {1, 2, 3}, 89 | {2, 3, 4}, 90 | {3, 4, 5}, 91 | {4, 5, 6}, 92 | {5, 6, 7}, 93 | {6, 7, 8}, 94 | } 95 | wantWindows := [][]int{ 96 | {3, 1, 4}, 97 | {1, 4, 1}, 98 | {4, 1, 5}, 99 | {1, 5, 9}, 100 | {5, 9, 2}, 101 | {9, 2, 6}, 102 | {2, 6, 5}, 103 | } 104 | i := 0 105 | 106 | for seq := range gloop.Window2(gloop.Enumerate(gloop.Slice(values)), 3) { 107 | keys, window := gloop.ToSlice2(seq) 108 | require.Equal(t, wantKeys[i], keys) 109 | require.Equal(t, wantWindows[i], window) 110 | 111 | i++ 112 | } 113 | 114 | require.Equal(t, len(wantWindows), i) 115 | } 116 | 117 | func TestWindow2String(t *testing.T) { 118 | s := "FizzBuzz" 119 | wantKeys := [][]int{ 120 | {0, 1, 2, 3}, 121 | {1, 2, 3, 4}, 122 | {2, 3, 4, 5}, 123 | {3, 4, 5, 6}, 124 | {4, 5, 6, 7}, 125 | } 126 | wantWindows := []string{ 127 | "Fizz", 128 | "izzB", 129 | "zzBu", 130 | "zBuz", 131 | "Buzz", 132 | } 133 | i := 0 134 | 135 | for seq := range gloop.Window2(gloop.Enumerate(gloop.String(s)), 4) { 136 | keys, windowRunes := gloop.ToSlice2(seq) 137 | window := string(windowRunes) 138 | 139 | require.Equal(t, wantKeys[i], keys) 140 | require.Equal(t, wantWindows[i], window) 141 | 142 | i++ 143 | } 144 | 145 | require.Equal(t, len(wantWindows), i) 146 | } 147 | 148 | func TestWindow2Break(t *testing.T) { 149 | values := []int{3, 1, 4, 1, 5, 9, 2, 6, 5} 150 | i := 0 151 | 152 | for seq := range gloop.Window2(gloop.Enumerate(gloop.Slice(values)), 3) { 153 | if i == 1 { 154 | break 155 | } 156 | 157 | for key, value := range seq { 158 | require.Equal(t, 0, key) 159 | require.Equal(t, 3, value) 160 | 161 | break 162 | } 163 | 164 | i++ 165 | } 166 | } 167 | 168 | func TestWindow2ZeroSizePanics(t *testing.T) { 169 | require.Panics(t, func() { 170 | for range gloop.Window2(gloop.Enumerate(gloop.Slice([]int{3, 1, 4})), 0) { 171 | t.Fatal("expected no iteration") 172 | } 173 | }) 174 | } 175 | 176 | func TestWindow2NegativeSizePanics(t *testing.T) { 177 | require.Panics(t, func() { 178 | for range gloop.Window2(gloop.Enumerate(gloop.Slice([]int{3, 1, 4})), -1) { 179 | t.Fatal("expected no iteration") 180 | } 181 | }) 182 | } 183 | -------------------------------------------------------------------------------- /zip.go: -------------------------------------------------------------------------------- 1 | package gloop 2 | 3 | import ( 4 | "container/list" 5 | "iter" 6 | ) 7 | 8 | // ZipOptions defines configurable options for [Zip]. 9 | type ZipOptions[V1, V2 any] struct { 10 | // Padded indicates whether the shorter sequence will be padded. If 11 | // true, the shorter sequence is padded to match the length of the 12 | // longer one. If false, the number of iterations is equal to the 13 | // length of the shorter sequence. 14 | Padded bool 15 | // PadValue1 is the value the first sequence is padded with. This 16 | // is not used if Padded is false or if the first sequence is 17 | // shorter than the second. 18 | PadValue1 *V1 19 | // PadValue2 is the value the second sequence is padded with. This 20 | // is not used if Padded is false or if the second sequence is 21 | // shorter than the first. 22 | PadValue2 *V2 23 | } 24 | 25 | // ZipOptionFunc is the function signature of configuration helpers for 26 | // [Zip]. 27 | type ZipOptionFunc[V1, V2 any] func(*ZipOptions[V1, V2]) 28 | 29 | // WithZipPadded is a helper for configuring [Zip] to pad the shorter 30 | // sequence. 31 | func WithZipPadded[V1, V2 any](padded bool) ZipOptionFunc[V1, V2] { 32 | return func(o *ZipOptions[V1, V2]) { 33 | o.Padded = padded 34 | } 35 | } 36 | 37 | // WithZipPadValue1 is a helper for configuring padded values for the 38 | // first sequence in [Zip]. 39 | func WithZipPadValue1[V1, V2 any](value V1) ZipOptionFunc[V1, V2] { 40 | return func(o *ZipOptions[V1, V2]) { 41 | o.PadValue1 = &value 42 | } 43 | } 44 | 45 | // WithZipPadValue2 is a helper for configuring padded values for the 46 | // second slice in [Zip]. 47 | func WithZipPadValue2[V1, V2 any](value V2) ZipOptionFunc[V1, V2] { 48 | return func(o *ZipOptions[V1, V2]) { 49 | o.PadValue2 = &value 50 | } 51 | } 52 | 53 | // Zip allows looping over two [iter.Seq] sequences in pairs. 54 | func Zip[V1, V2 any]( 55 | seq1 iter.Seq[V1], 56 | seq2 iter.Seq[V2], 57 | opts ...ZipOptionFunc[V1, V2], 58 | ) iter.Seq2[V1, V2] { 59 | options := ZipOptions[V1, V2]{ 60 | Padded: false, 61 | PadValue1: nil, 62 | PadValue2: nil, 63 | } 64 | 65 | for _, opt := range opts { 66 | opt(&options) 67 | } 68 | 69 | return func(yield func(V1, V2) bool) { 70 | next1, stop1 := iter.Pull(seq1) 71 | defer stop1() 72 | 73 | next2, stop2 := iter.Pull(seq2) 74 | defer stop2() 75 | 76 | for { 77 | var value1 V1 78 | 79 | nextValue1, ok1 := next1() 80 | 81 | if ok1 { 82 | value1 = nextValue1 83 | } else if options.PadValue1 != nil { 84 | value1 = *options.PadValue1 85 | } 86 | 87 | var value2 V2 88 | 89 | nextValue2, ok2 := next2() 90 | 91 | if ok2 { 92 | value2 = nextValue2 93 | } else if options.PadValue2 != nil { 94 | value2 = *options.PadValue2 95 | } 96 | 97 | if !ok1 && !ok2 { 98 | return 99 | } 100 | 101 | if !options.Padded && (!ok1 || !ok2) { 102 | return 103 | } 104 | 105 | if !yield(value1, value2) { 106 | return 107 | } 108 | } 109 | } 110 | } 111 | 112 | // Zip2Options defines configurable options for [Zip2]. 113 | type Zip2Options[K1, V1, K2, V2 any] struct { 114 | // Padded indicates whether the shorter sequence will be padded. If 115 | // true, the shorter sequence is padded to match the length of the 116 | // longer one. If false, the number of iterations is equal to the 117 | // length of the shorter sequence. 118 | Padded bool 119 | // PadKey1 is the key the first sequence is padded with. This is 120 | // not used if Padded is false or if the first sequence is shorter 121 | // than the second. 122 | PadKey1 *K1 123 | // PadValue1 is the value the first sequence is padded with. This 124 | // is not used if Padded is false or if the first sequence is 125 | // shorter than the second. 126 | PadValue1 *V1 127 | // PadKey2 is the key the second sequence is padded with. This is 128 | // not used if Padded is false or if the first sequence is shorter 129 | // than the second. 130 | PadKey2 *K2 131 | // PadValue2 is the value the second sequence is padded with. This 132 | // is not used if Padded is false or if the second sequence is 133 | // shorter than the first. 134 | PadValue2 *V2 135 | } 136 | 137 | // Zip2OptionFunc is the function signature of configuration helpers 138 | // for [Zip2]. 139 | type Zip2OptionFunc[K1, V1, K2, V2 any] func(*Zip2Options[K1, V1, K2, V2]) 140 | 141 | // WithZip2Padded is a helper for configuring [Zip2] to pad the shorter 142 | // sequence. 143 | func WithZip2Padded[K1, V1, K2, V2 any](padded bool) Zip2OptionFunc[K1, V1, K2, V2] { 144 | return func(o *Zip2Options[K1, V1, K2, V2]) { 145 | o.Padded = padded 146 | } 147 | } 148 | 149 | // WithZip2PadKey1 is a helper for configuring padded keys for the 150 | // first sequence in [Zip2]. 151 | func WithZip2PadKey1[K1, V1, K2, V2 any](key K1) Zip2OptionFunc[K1, V1, K2, V2] { 152 | return func(o *Zip2Options[K1, V1, K2, V2]) { 153 | o.PadKey1 = &key 154 | } 155 | } 156 | 157 | // WithZip2PadValue1 is a helper for configuring padded values for the 158 | // first sequence in [Zip2]. 159 | func WithZip2PadValue1[K1, V1, K2, V2 any](value V1) Zip2OptionFunc[K1, V1, K2, V2] { 160 | return func(o *Zip2Options[K1, V1, K2, V2]) { 161 | o.PadValue1 = &value 162 | } 163 | } 164 | 165 | // WithZip2PadKey2 is a helper for configuring padded keys for the 166 | // second sequence in [Zip2]. 167 | func WithZip2PadKey2[K1, V1, K2, V2 any](key K2) Zip2OptionFunc[K1, V1, K2, V2] { 168 | return func(o *Zip2Options[K1, V1, K2, V2]) { 169 | o.PadKey2 = &key 170 | } 171 | } 172 | 173 | // WithZip2PadValue2 is a helper for configuring padded values for the 174 | // second sequence in [Zip2]. 175 | func WithZip2PadValue2[K1, V1, K2, V2 any](value V2) Zip2OptionFunc[K1, V1, K2, V2] { 176 | return func(o *Zip2Options[K1, V1, K2, V2]) { 177 | o.PadValue2 = &value 178 | } 179 | } 180 | 181 | // Zip2 allows looping over two [iter.Seq2] sequences in pairs. 182 | func Zip2[K1, V1, K2, V2 any]( 183 | seq1 iter.Seq2[K1, V1], 184 | seq2 iter.Seq2[K2, V2], 185 | opts ...Zip2OptionFunc[K1, V1, K2, V2], 186 | ) iter.Seq2[KeyValuePair[K1, V1], KeyValuePair[K2, V2]] { 187 | options := Zip2Options[K1, V1, K2, V2]{ 188 | Padded: false, 189 | PadKey1: nil, 190 | PadValue1: nil, 191 | PadKey2: nil, 192 | PadValue2: nil, 193 | } 194 | 195 | for _, opt := range opts { 196 | opt(&options) 197 | } 198 | 199 | padPair1 := KeyValuePair[K1, V1]{} 200 | padPair2 := KeyValuePair[K2, V2]{} 201 | 202 | if options.PadKey1 != nil { 203 | padPair1.Key = *options.PadKey1 204 | } 205 | 206 | if options.PadValue1 != nil { 207 | padPair1.Value = *options.PadValue1 208 | } 209 | 210 | if options.PadKey2 != nil { 211 | padPair2.Key = *options.PadKey2 212 | } 213 | 214 | if options.PadValue2 != nil { 215 | padPair2.Value = *options.PadValue2 216 | } 217 | 218 | return Zip( 219 | KeyValue2(seq1), 220 | KeyValue2(seq2), 221 | WithZipPadded[KeyValuePair[K1, V1], KeyValuePair[K2, V2]](options.Padded), 222 | WithZipPadValue1[KeyValuePair[K1, V1], KeyValuePair[K2, V2]](padPair1), 223 | WithZipPadValue2[KeyValuePair[K1, V1]](padPair2), 224 | ) 225 | } 226 | 227 | // ZipNOptions defines configurable options for [ZipN]. 228 | type ZipNOptions[V any] struct { 229 | // Padded indicates whether the shorter sequence will be padded. If 230 | // true, the shorter sequence is padded to match the length of the 231 | // longer one. If false, the number of iterations is equal to the 232 | // length of the shorter sequence. 233 | Padded bool 234 | // PadValue is the value the shorter sequence is padded with. This 235 | // is not used if Padded is false or if both sequences are equal 236 | // in length. 237 | PadValue *V 238 | } 239 | 240 | // ZipNOptionFunc is the function signature of configuration helpers 241 | // for [ZipN]. 242 | type ZipNOptionFunc[V any] func(*ZipNOptions[V]) 243 | 244 | // WithZipNPadded is a helper for configuring [ZipN] to pad the shorter 245 | // sequence. 246 | func WithZipNPadded[V any](padded bool) ZipNOptionFunc[V] { 247 | return func(o *ZipNOptions[V]) { 248 | o.Padded = padded 249 | } 250 | } 251 | 252 | // WithZipNPadValue is a helper for configuring padded values in 253 | // [ZipN]. 254 | func WithZipNPadValue[V any](value V) ZipNOptionFunc[V] { 255 | return func(o *ZipNOptions[V]) { 256 | o.PadValue = &value 257 | } 258 | } 259 | 260 | // ZipN allows looping over multiple [iter.Seq] sequences 261 | // simultaneously. 262 | func ZipN[V any]( 263 | seqs iter.Seq[iter.Seq[V]], 264 | opts ...ZipNOptionFunc[V], 265 | ) iter.Seq[iter.Seq[V]] { 266 | options := ZipNOptions[V]{ 267 | Padded: false, 268 | PadValue: nil, 269 | } 270 | 271 | for _, opt := range opts { 272 | opt(&options) 273 | } 274 | 275 | return func(yield func(iter.Seq[V]) bool) { 276 | nexts := list.New() 277 | 278 | for seq := range seqs { 279 | next, stop := iter.Pull(seq) 280 | defer stop() 281 | 282 | nexts.PushBack(next) 283 | } 284 | 285 | for { 286 | i := 0 287 | exhaused := true 288 | values := make([]V, nexts.Len()) 289 | 290 | for elem := range List(nexts) { 291 | next := elem.Value.(func() (V, bool)) 292 | 293 | var ( 294 | ok bool 295 | value V 296 | ) 297 | 298 | value, ok = next() 299 | if ok { 300 | exhaused = false 301 | } 302 | 303 | if !options.Padded && !ok { 304 | return 305 | } 306 | 307 | if options.PadValue != nil && !ok { 308 | value = *options.PadValue 309 | } 310 | 311 | values[i] = value 312 | i++ 313 | } 314 | 315 | if exhaused { 316 | return 317 | } 318 | 319 | if !yield(Slice(values)) { 320 | return 321 | } 322 | } 323 | } 324 | } 325 | 326 | // ZipN2Options defines configurable options for [ZipN2]. 327 | type ZipN2Options[K, V any] struct { 328 | // Padded indicates whether the shorter sequence will be padded. If 329 | // true, the shorter sequence is padded to match the length of the 330 | // longer one. If false, the number of iterations is equal to the 331 | // length of the shorter sequence. 332 | Padded bool 333 | // PadKey is the key the shorter sequence is padded with. This is 334 | // not used if Padded is false or if both sequences are equal in 335 | // length. 336 | PadKey *K 337 | // PadValue is the value the shorter sequence is padded with. This 338 | // is not used if Padded is false or if both sequences are equal 339 | // in length. 340 | PadValue *V 341 | } 342 | 343 | // ZipN2OptionFunc is the function signature of configuration helpers 344 | // for [ZipN2]. 345 | type ZipN2OptionFunc[K, V any] func(*ZipN2Options[K, V]) 346 | 347 | // WithZipN2Padded is a helper for configuring [ZipN2] to pad the shorter 348 | // sequence. 349 | func WithZipN2Padded[K, V any](padded bool) ZipN2OptionFunc[K, V] { 350 | return func(o *ZipN2Options[K, V]) { 351 | o.Padded = padded 352 | } 353 | } 354 | 355 | // WithZipN2PadKey is a helper for configuring padded keys in [ZipN2]. 356 | // first sequence in [Zip2]. 357 | func WithZipN2PadKey[K, V any](key K) ZipN2OptionFunc[K, V] { 358 | return func(o *ZipN2Options[K, V]) { 359 | o.PadKey = &key 360 | } 361 | } 362 | 363 | // WithZipN2PadValue is a helper for configuring padded values in 364 | // [ZipN2]. 365 | func WithZipN2PadValue[K, V any](value V) ZipN2OptionFunc[K, V] { 366 | return func(o *ZipN2Options[K, V]) { 367 | o.PadValue = &value 368 | } 369 | } 370 | 371 | // ZipN2 allows looping over multiple [iter.Seq2] sequences 372 | // simultaneously. 373 | func ZipN2[K, V any]( 374 | seqs iter.Seq[iter.Seq2[K, V]], 375 | opts ...ZipN2OptionFunc[K, V], 376 | ) iter.Seq[iter.Seq2[K, V]] { 377 | options := ZipN2Options[K, V]{ 378 | Padded: false, 379 | PadKey: nil, 380 | PadValue: nil, 381 | } 382 | 383 | for _, opt := range opts { 384 | opt(&options) 385 | } 386 | 387 | padPair := KeyValuePair[K, V]{} 388 | 389 | if options.PadKey != nil { 390 | padPair.Key = *options.PadKey 391 | } 392 | 393 | if options.PadValue != nil { 394 | padPair.Value = *options.PadValue 395 | } 396 | 397 | return Transform( 398 | ZipN( 399 | Transform(seqs, KeyValue2), 400 | WithZipNPadded[KeyValuePair[K, V]](options.Padded), 401 | WithZipNPadValue(padPair), 402 | ), 403 | KeyValue, 404 | ) 405 | } 406 | --------------------------------------------------------------------------------