├── .tool-versions ├── logos ├── iterex-logo.png ├── iterex-logo-small.png ├── iterex-logo-tiny.png ├── iterex-logo-medium.png ├── iterex-logo.png.license ├── iterex-logo-medium.png.license ├── iterex-logo-small.png.license └── iterex-logo-tiny.png.license ├── mix.lock.license ├── .tool-versions.license ├── .github ├── workflows │ └── elixir.yml └── dependabot.yml ├── test ├── test_helper.exs ├── iter_test.exs └── iter │ ├── iterable │ ├── empty_test.exs │ ├── stepper_test.exs │ ├── mapper_test.exs │ ├── uniquer_test.exs │ ├── filterer_test.exs │ ├── itersperser_test.exs │ ├── flattener_test.exs │ ├── map_test.exs │ ├── with_indexer_test.exs │ ├── flat_mapper_test.exs │ ├── cycler_test.exs │ ├── concatenator_test.exs │ ├── resource_test.exs │ ├── enumerable_test.exs │ ├── deduper_test.exs │ ├── by_chunker_test.exs │ ├── date_range_test.exs │ ├── range_test.exs │ └── peeker_test.exs │ ├── into_iterable │ ├── file_stream_test.exs │ └── io_stream_test.exs │ ├── collectable_test.exs │ ├── impl_test.exs │ └── enumerable_test.exs ├── .check.exs ├── .formatter.exs ├── lib ├── iter │ ├── into_iterable │ │ ├── iter.ex │ │ ├── list.ex │ │ ├── map_set.ex │ │ ├── range.ex │ │ ├── map.ex │ │ ├── date_range.ex │ │ ├── io_stream.ex │ │ └── file_stream.ex │ ├── iter │ │ ├── collectable.ex │ │ └── enumerable.ex │ ├── iterable │ │ ├── list.ex │ │ ├── empty.ex │ │ ├── prepender.ex │ │ ├── with_indexer.ex │ │ ├── uniquer.ex │ │ ├── head_dropper.ex │ │ ├── stepper.ex │ │ ├── head_taker.ex │ │ ├── mapper.ex │ │ ├── map_set.ex │ │ ├── while_taker.ex │ │ ├── appender.ex │ │ ├── flattener.ex │ │ ├── while_dropper.ex │ │ ├── cycler.ex │ │ ├── filterer.ex │ │ ├── range.ex │ │ ├── concatenator.ex │ │ ├── flat_mapper.ex │ │ ├── deduper.ex │ │ ├── date_range.ex │ │ ├── intersperser.ex │ │ ├── every_dropper.ex │ │ ├── every_mapper.ex │ │ ├── zipper.ex │ │ ├── map.ex │ │ ├── resource.ex │ │ ├── tail_dropper.ex │ │ ├── tail_taker.ex │ │ ├── by_chunker.ex │ │ ├── every_chunker.ex │ │ ├── while_chunker.ex │ │ ├── peeker.ex │ │ └── enumerable.ex │ ├── into_iterable.ex │ ├── iterable.ex │ └── impl.ex └── iter.ex ├── config └── config.exs ├── .gitignore ├── LICENSES └── MIT.txt ├── CHANGELOG.md ├── mix.exs ├── README.md └── mix.lock /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 26.2.5 2 | elixir 1.18.2 3 | pipx 1.8.0 4 | -------------------------------------------------------------------------------- /logos/iterex-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ash-project/iterex/main/logos/iterex-logo.png -------------------------------------------------------------------------------- /logos/iterex-logo-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ash-project/iterex/main/logos/iterex-logo-small.png -------------------------------------------------------------------------------- /logos/iterex-logo-tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ash-project/iterex/main/logos/iterex-logo-tiny.png -------------------------------------------------------------------------------- /logos/iterex-logo-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ash-project/iterex/main/logos/iterex-logo-medium.png -------------------------------------------------------------------------------- /mix.lock.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | SPDX-FileCopyrightText: 2024 iterex contributors 3 | 4 | SPDX-License-Identifier: MIT 5 | -------------------------------------------------------------------------------- /.tool-versions.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | SPDX-FileCopyrightText: 2024 iterex contributors 3 | 4 | SPDX-License-Identifier: MIT 5 | -------------------------------------------------------------------------------- /logos/iterex-logo.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | SPDX-FileCopyrightText: 2024 iterex contributors 3 | 4 | SPDX-License-Identifier: MIT 5 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | -------------------------------------------------------------------------------- /logos/iterex-logo-medium.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | SPDX-FileCopyrightText: 2024 iterex contributors 3 | 4 | SPDX-License-Identifier: MIT 5 | -------------------------------------------------------------------------------- /logos/iterex-logo-small.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | SPDX-FileCopyrightText: 2024 iterex contributors 3 | 4 | SPDX-License-Identifier: MIT 5 | -------------------------------------------------------------------------------- /logos/iterex-logo-tiny.png.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | SPDX-FileCopyrightText: 2024 iterex contributors 3 | 4 | SPDX-License-Identifier: MIT 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | ExUnit.start() 7 | -------------------------------------------------------------------------------- /.check.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | [ 7 | tools: [ 8 | {:reuse, command: ["pipx", "run", "reuse", "lint", "-q"]} 9 | ] 10 | ] 11 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | # Used by "mix format" 7 | [ 8 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 9 | ] 10 | -------------------------------------------------------------------------------- /test/iter_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule IterTest do 7 | @moduledoc false 8 | use ExUnit.Case, async: true 9 | doctest Iter 10 | end 11 | -------------------------------------------------------------------------------- /lib/iter/into_iterable/iter.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defimpl Iter.IntoIterable, for: Iter do 7 | @moduledoc false 8 | def into_iterable(iter), do: iter.iterable 9 | end 10 | -------------------------------------------------------------------------------- /lib/iter/into_iterable/list.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defimpl Iter.IntoIterable, for: List do 7 | @moduledoc false 8 | 9 | @doc false 10 | def into_iterable(list), do: list 11 | end 12 | -------------------------------------------------------------------------------- /lib/iter/into_iterable/map_set.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defimpl Iter.IntoIterable, for: MapSet do 7 | @moduledoc false 8 | 9 | @doc false 10 | def into_iterable(set), do: set 11 | end 12 | -------------------------------------------------------------------------------- /lib/iter/into_iterable/range.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defimpl Iter.IntoIterable, for: Range do 7 | @moduledoc false 8 | 9 | @doc false 10 | def into_iterable(range), do: range 11 | end 12 | -------------------------------------------------------------------------------- /lib/iter/into_iterable/map.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defimpl Iter.IntoIterable, for: Map do 7 | @moduledoc false 8 | 9 | @doc false 10 | def into_iterable(map), do: Iter.Iterable.Map.new(map) 11 | end 12 | -------------------------------------------------------------------------------- /lib/iter/into_iterable/date_range.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defimpl Iter.IntoIterable, for: Date.Range do 7 | @moduledoc false 8 | 9 | @doc false 10 | def into_iterable(date_range), do: date_range 11 | end 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | --- 7 | updates: 8 | - directory: / 9 | package-ecosystem: mix 10 | schedule: 11 | interval: monthly 12 | versioning-strategy: lockfile-only 13 | version: 2 14 | -------------------------------------------------------------------------------- /lib/iter/iter/collectable.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defimpl Collectable, for: Iter do 7 | @doc false 8 | def into(iter), do: {iter, &collector/2} 9 | 10 | defp collector(iter, {:cont, elem}), do: Iter.append(iter, elem) 11 | defp collector(iter, :done), do: iter 12 | defp collector(_iter, :halt), do: :ok 13 | end 14 | -------------------------------------------------------------------------------- /test/iter/iterable/empty_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.EmptyTest do 7 | @moduledoc false 8 | alias Iter.{Iterable, Iterable.Empty} 9 | use ExUnit.Case, async: true 10 | 11 | test "it always finishes iteration" do 12 | assert [] = 13 | Empty.new() 14 | |> Iterable.to_list() 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/iter/iterable/stepper_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.StepperTest do 7 | @moduledoc false 8 | alias Iter.{Iterable, Iterable.Stepper} 9 | use ExUnit.Case, async: true 10 | 11 | test "it steps over an iterable" do 12 | assert [1, 3, 5, 7, 9] = 13 | 1..9 14 | |> Stepper.new(2) 15 | |> Iterable.to_list() 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/iter/iterable/mapper_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.MapperTest do 7 | @moduledoc false 8 | alias Iter.{Iterable, Iterable.Mapper} 9 | use ExUnit.Case, async: true 10 | 11 | test "it maps over it's elements" do 12 | assert [2, 4, 6, 8] = 13 | [1, 2, 3, 4] 14 | |> Mapper.new(&(&1 * 2)) 15 | |> Iterable.to_list() 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/iter/iterable/uniquer_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.UniquerTest do 7 | @moduledoc false 8 | alias Iter.{Iterable, Iterable.Uniquer} 9 | use ExUnit.Case, async: true 10 | 11 | test "it emits only unique elements" do 12 | assert [1, 2, 3] = 13 | [1, 2, 3, 3, 2, 1] 14 | |> Uniquer.new() 15 | |> Iterable.to_list() 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/iter/iterable/filterer_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.FilterTest do 7 | @moduledoc false 8 | alias Iter.{Iterable, Iterable.Filterer} 9 | use ExUnit.Case, async: true 10 | 11 | test "it keeps only matching elements" do 12 | assert [2, 4] = 13 | [1, 2, 3, 4] 14 | |> Filterer.new(&(rem(&1, 2) == 0)) 15 | |> Iterable.to_list() 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/iter/iterable/itersperser_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.IntersperserTest do 7 | @moduledoc false 8 | alias Iter.{Iterable, Iterable.Intersperser} 9 | use ExUnit.Case, async: true 10 | 11 | test "it intersperserates" do 12 | assert [1, :wat, 2, :wat, 3] = 13 | [1, 2, 3] 14 | |> Intersperser.new(:wat) 15 | |> Iterable.to_list() 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/iter/iterable/flattener_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.FlattenerTest do 7 | @moduledoc false 8 | alias Iter.{Iterable, Iterable.Flattener} 9 | use ExUnit.Case, async: true 10 | 11 | test "it flattens it's input" do 12 | assert [1, 2, 3, 6, 7, 8, 10, 11, 12] = 13 | [1..3, 6..8, 10..12] 14 | |> Flattener.new() 15 | |> Iterable.to_list() 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/iter/iterable/map_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.MapTest do 7 | @moduledoc false 8 | alias Iter.{Iterable, Iterable.Map} 9 | use ExUnit.Case, async: true 10 | 11 | test "it iterates maps in arbitrary order" do 12 | assert [a: 1, b: 2, c: 3] = 13 | %{a: 1, b: 2, c: 3} 14 | |> Map.new() 15 | |> Iterable.to_list() 16 | |> Enum.sort() 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/iter/iterable/with_indexer_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.WithIndexerTest do 7 | @moduledoc false 8 | alias Iter.{Iterable, Iterable.WithIndexer} 9 | use ExUnit.Case, async: true 10 | 11 | test "it adds an index to each element" do 12 | assert [{:a, 0}, {:b, 1}, {:c, 2}] = 13 | [:a, :b, :c] 14 | |> WithIndexer.new() 15 | |> Iterable.to_list() 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/iter/iterable/flat_mapper_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.FlatMapperTest do 7 | @moduledoc false 8 | alias Iter.{Iterable, Iterable.FlatMapper} 9 | use ExUnit.Case, async: true 10 | 11 | test "it flats as it maps" do 12 | assert [1, 2, 3, 6, 7, 8, 10, 11, 12] = 13 | [1, 6, 10] 14 | |> FlatMapper.new(&Range.new(&1, &1 + 2)) 15 | |> Iterable.to_list() 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/iter/iterable/cycler_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.CyclerTest do 7 | @moduledoc false 8 | alias Iter.{Iterable, Iterable.Cycler} 9 | use ExUnit.Case, async: true 10 | 11 | test "it cycles it's input continually" do 12 | assert [:a, :b, :c, :a, :b, :c, :a, :b, :c, :a, :b, :c] = 13 | [:a, :b, :c] 14 | |> Cycler.new() 15 | |> Iterable.take_head(12) 16 | |> Iterable.to_list() 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/iter/into_iterable/file_stream_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.IntoIterable.File.StreamTest do 7 | @moduledoc false 8 | use ExUnit.Case, async: true 9 | 10 | test "it can iterate a file stream identically to enum" do 11 | enum_stream = File.stream!(__ENV__.file, [:line]) 12 | iter_stream = File.stream!(__ENV__.file, [:line]) |> Iter.IntoIterable.into_iterable() 13 | 14 | assert Enum.to_list(enum_stream) == Iter.Iterable.to_list(iter_stream) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/iter/iterable/concatenator_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.ConcatenatorTest do 7 | @moduledoc false 8 | alias Iter.{Iterable, Iterable.Concatenator} 9 | use ExUnit.Case, async: true 10 | 11 | describe "next/1" do 12 | test "it sequentially concatenates iterables" do 13 | assert [1, 2, 3, :a, :b, :c, "a", "b", "c"] = 14 | [[1, 2, 3], [:a, :b, :c], ["a", "b", "c"]] 15 | |> Concatenator.new() 16 | |> Iterable.to_list() 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/iter/collectable_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.CollectableTest do 7 | @moduledoc false 8 | use ExUnit.Case, async: true 9 | 10 | test "empty iterators can be collected into" do 11 | iter = Enum.into(1..6, Iter.empty()) 12 | 13 | assert Iter.to_list(iter) == [1, 2, 3, 4, 5, 6] 14 | end 15 | 16 | test "non-empty iterators can be collected into" do 17 | iter = Enum.into(10..16, Iter.from(1..6)) 18 | assert Iter.to_list(iter) == [1, 2, 3, 4, 5, 6, 10, 11, 12, 13, 14, 15, 16] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/iter/into_iterable/io_stream.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defimpl Iter.IntoIterable, for: IO.Stream do 7 | @moduledoc false 8 | 9 | @doc false 10 | @impl true 11 | def into_iterable(%{device: device, raw: raw, line_or_bytes: line_or_bytes}) do 12 | next_fun = 13 | case raw do 14 | true -> &IO.each_binstream(&1, line_or_bytes) 15 | false -> &IO.each_stream(&1, line_or_bytes) 16 | end 17 | 18 | Iter.Iterable.Resource.new( 19 | fn -> device end, 20 | next_fun, 21 | & &1 22 | ) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/iter/iterable/list.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defimpl Iter.Iterable, for: List do 7 | @moduledoc false 8 | 9 | use Iter.Impl 10 | 11 | @doc false 12 | @impl true 13 | def next([head | tail]), do: {:ok, head, tail} 14 | def next([]), do: :done 15 | 16 | @doc false 17 | @impl true 18 | def peek([]), do: :done 19 | def peek([head | _] = list), do: {:ok, head, list} 20 | 21 | @doc false 22 | @impl true 23 | def count(list), do: length(list) 24 | 25 | @doc false 26 | def empty?([]), do: true 27 | def empty?(_), do: false 28 | end 29 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | import Config 7 | 8 | if Mix.env() == :dev do 9 | config :git_ops, 10 | mix_project: Iter.MixProject, 11 | changelog_file: "CHANGELOG.md", 12 | repository_url: "https://github.com/ash-project/iterex", 13 | # Instructs the tool to manage your mix version in your `mix.exs` file 14 | # See below for more information 15 | manage_mix_version?: true, 16 | # Instructs the tool to manage the version in your README.md 17 | # Pass in `true` to use `"README.md"` or a string to customize 18 | manage_readme_version: true, 19 | version_tag_prefix: "v" 20 | end 21 | -------------------------------------------------------------------------------- /test/iter/iterable/resource_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.ResourceTest do 7 | @moduledoc false 8 | alias Iter.{Iterable, Iterable.Resource} 9 | use ExUnit.Case, async: true 10 | 11 | test "it can make a stream-like resource" do 12 | assert [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] = 13 | Resource.new( 14 | fn -> 0 end, 15 | fn 16 | acc when acc < 10 -> {[acc], acc + 1} 17 | acc -> {:halt, acc} 18 | end, 19 | fn _acc -> :ok end 20 | ) 21 | |> Iterable.to_list() 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/iter/iterable/enumerable_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.EnumerableTest do 7 | @moduledoc false 8 | use ExUnit.Case, async: true 9 | alias Iter.Iterable 10 | 11 | test "it can iterate a normal enum" do 12 | iterable = Iterable.Enumerable.new([1, 2, 3]) 13 | assert [1, 2, 3] = Iterable.to_list(iterable) 14 | end 15 | 16 | test "it can iterate an infinite stream" do 17 | stream = Stream.cycle([1, 2, 3]) 18 | 19 | assert [1, 2, 3, 1, 2] = 20 | stream 21 | |> Iterable.Enumerable.new() 22 | |> Iterable.take_head(5) 23 | |> Iterable.to_list() 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/iter/iterable/deduper_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.DeduperTest do 7 | @moduledoc false 8 | alias Iter.{Iterable, Iterable.Deduper} 9 | use ExUnit.Case, async: true 10 | 11 | test "it removes consecutive duplicated elements" do 12 | assert [1, 2, 3, 2, 1] = 13 | [1, 2, 3, 3, 2, 1] 14 | |> Deduper.new() 15 | |> Iterable.to_list() 16 | end 17 | 18 | test "it removes consecutive duplicate result elements" do 19 | assert [{1, :a}, {2, :b}, {1, :a}] = 20 | [{1, :a}, {2, :b}, {2, :c}, {1, :a}] 21 | |> Deduper.new(&elem(&1, 0)) 22 | |> Iterable.to_list() 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/iter/impl_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.ImplTest do 7 | @moduledoc false 8 | use ExUnit.Case, async: true 9 | alias Iter.Impl 10 | doctest Iter.Impl 11 | 12 | describe "each/2" do 13 | setup do 14 | {:ok, pid} = start_supervised({Agent, fn -> [] end}) 15 | {:ok, agent: pid} 16 | end 17 | 18 | test "it applies the function for every element", %{agent: agent} do 19 | assert :done = 20 | [1, 2, 3] 21 | |> Impl.each(fn element -> 22 | Agent.update(agent, &[element | &1]) 23 | end) 24 | 25 | assert Agent.get(agent, &:lists.reverse(&1)) == [1, 2, 3] 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/iter/iterable/by_chunker_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.ByChunkerTest do 7 | @moduledoc false 8 | alias Iter.{Iterable, Iterable.ByChunker, Iterable.Empty} 9 | use ExUnit.Case, async: true 10 | 11 | describe "next/1" do 12 | test "it correctly chunks the input" do 13 | chunker = ByChunker.new([1, 2, 2, 3, 4, 4, 6, 7, 7], &(rem(&1, 2) == 1)) 14 | 15 | assert {:ok, [1], chunker} = Iterable.next(chunker) 16 | assert {:ok, [2, 2], chunker} = Iterable.next(chunker) 17 | assert {:ok, [3], chunker} = Iterable.next(chunker) 18 | assert {:ok, [4, 4, 6], chunker} = Iterable.next(chunker) 19 | assert {:ok, [7, 7], %Empty{}} = Iterable.next(chunker) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | # The directory Mix will write compiled artifacts to. 7 | /_build/ 8 | 9 | # If you run "mix test --cover", coverage assets end up here. 10 | /cover/ 11 | 12 | # The directory Mix downloads your dependencies sources to. 13 | /deps/ 14 | 15 | # Where third-party dependencies like ExDoc output generated docs. 16 | /doc/ 17 | 18 | # Ignore .fetch files in case you like to edit your project deps locally. 19 | /.fetch 20 | 21 | # If the VM crashes, it generates a dump, let's ignore it too. 22 | erl_crash.dump 23 | 24 | # Also ignore archive artifacts (built via "mix archive.build"). 25 | *.ez 26 | 27 | # Ignore package tarball (built via "mix hex.build"). 28 | iterex-*.tar 29 | 30 | # Temporary files, for example, from tests. 31 | /tmp/ 32 | -------------------------------------------------------------------------------- /test/iter/iterable/date_range_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Date.RangeTest do 7 | @moduledoc false 8 | use ExUnit.Case, async: true 9 | alias Iter.Iterable 10 | 11 | test "it enumerates the same forwards" do 12 | first = Date.utc_today() 13 | last = Date.add(first, days_to_add()) 14 | range = Date.range(first, last) 15 | 16 | assert Enum.to_list(range) == Iterable.to_list(range) 17 | end 18 | 19 | test "it enumerates the same backwards" do 20 | first = Date.utc_today() 21 | last = Date.add(first, 0 - days_to_add()) 22 | range = Date.range(first, last, -1) 23 | 24 | assert Enum.to_list(range) == Iterable.to_list(range) 25 | end 26 | 27 | defp days_to_add do 28 | 99 + :rand.uniform(99) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/iter/into_iterable/io_stream_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.IntoIterable.IO.StreamTest do 7 | @moduledoc false 8 | use ExUnit.Case, async: true 9 | 10 | test "it can iterate an IO stream" do 11 | words = words() 12 | {:ok, pid} = StringIO.open(words) 13 | 14 | expected = String.split(words, ~r/(?<=\n)/) 15 | 16 | actual = 17 | pid 18 | |> IO.stream(:line) 19 | |> Iter.IntoIterable.into_iterable() 20 | |> Iter.Iterable.to_list() 21 | 22 | assert actual == expected 23 | end 24 | 25 | defp words do 26 | how_many = 20 + :rand.uniform(20) 27 | 28 | words = 29 | 1..how_many 30 | |> Iter.from() 31 | |> Iter.map(fn _ -> Faker.Lorem.word() end) 32 | |> Enum.join("\n") 33 | 34 | words 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/iter/iterable/empty.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Empty do 7 | defstruct [] 8 | 9 | @moduledoc """ 10 | An iterable that's always exhausted 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @opaque t :: %__MODULE__{} 16 | 17 | @doc """ 18 | Creates an iterable that's always exhausted. 19 | """ 20 | @spec new :: t 21 | def new, do: %__MODULE__{} 22 | 23 | defimpl Iterable do 24 | use Impl 25 | 26 | @doc false 27 | @impl true 28 | def next(_), do: :done 29 | 30 | @doc false 31 | @impl true 32 | def empty?(_), do: true 33 | 34 | @doc false 35 | @impl true 36 | def count(_), do: 0 37 | end 38 | 39 | defimpl IntoIterable do 40 | @doc false 41 | def into_iterable(self), do: self 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/iter/iter/enumerable.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defimpl Enumerable, for: Iter do 7 | alias Iter.Iterable 8 | 9 | @moduledoc false 10 | 11 | @doc false 12 | def count(iter), do: {:ok, Iterable.count(iter.iterable)} 13 | 14 | @doc false 15 | def member?(iter, element), do: {:ok, Iterable.member?(iter.iterable, element)} 16 | 17 | @doc false 18 | def reduce(_iter, {:halt, acc}, _fun), do: {:halted, acc} 19 | def reduce(iter, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(iter, &1, fun)} 20 | 21 | def reduce(iter, {:cont, acc}, fun) do 22 | case Iterable.next(iter.iterable) do 23 | {:ok, element, iterable} -> reduce(Iter.from(iterable), fun.(element, acc), fun) 24 | :done -> {:done, acc} 25 | end 26 | end 27 | 28 | @doc false 29 | def slice(_iter), do: {:error, __MODULE__} 30 | end 31 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | associated documentation files (the "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial 12 | portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 15 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 16 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 18 | USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /test/iter/enumerable_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.EnumerableTest do 7 | @moduledoc false 8 | use ExUnit.Case, async: true 9 | 10 | test "it implements the `count/1` callback" do 11 | assert {:ok, 6} == 12 | 1..6 13 | |> Iter.from() 14 | |> Enumerable.count() 15 | end 16 | 17 | test "it implements the `member?/2` callback" do 18 | assert {:ok, true} = 19 | 1..5 20 | |> Iter.from() 21 | |> Enumerable.member?(3) 22 | 23 | assert {:ok, false} = 24 | 1..5 25 | |> Iter.from() 26 | |> Enumerable.member?(30) 27 | end 28 | 29 | test "it implements the `reduce/3` callback" do 30 | assert 15 = 31 | 1..5 32 | |> Iter.from() 33 | |> Enum.reduce(&(&1 + &2)) 34 | end 35 | 36 | test "it does not implement the `slice/1` callback (should it?)" do 37 | assert {:error, Enumerable.Iter} = Enumerable.slice(Iter.from(1..5)) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/iter/into_iterable.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defprotocol Iter.IntoIterable do 7 | @moduledoc """ 8 | A protocol for converting a value into an iterable. 9 | 10 | Required by `Iter.from/1` and others. 11 | 12 | By default this protocol is implemented for `List`, `Map`, `MapSet`, `Range`, 13 | `Date.Range`, `File.Stream` and `IO.Stream` as well as all iterex's internal 14 | types. 15 | 16 | Allows the data and the iterable for that data to be separate. It could be as 17 | simple as a reference to some external data source and a position marker or it 18 | may be more efficient to read data in batches and iterate it. Regardless, 19 | most types simply return themselves from this function. 20 | 21 | It is important that your `into_iterable/1` callback must not actually start 22 | iterating, it simply returns a data structure suitable to track the iteration 23 | of the underlying data. 24 | """ 25 | 26 | @doc """ 27 | Convert a value into an iterable. 28 | """ 29 | @spec into_iterable(t) :: Iter.Iterable.t() 30 | def into_iterable(value) 31 | end 32 | -------------------------------------------------------------------------------- /lib/iter/iterable/prepender.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Prepender do 7 | defstruct iterable: nil, element: nil 8 | 9 | @moduledoc """ 10 | An iterable which prepends a single element to the end of another iterable. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{iterable: Iterable.t(), element: Iterable.element()} 16 | 17 | @doc """ 18 | Create a new prepender iterable. 19 | """ 20 | @spec new(Iterable.t(), Iterable.element()) :: t 21 | def new(iterable, element), do: %__MODULE__{iterable: iterable, element: element} 22 | 23 | defimpl Iterable do 24 | use Impl 25 | 26 | @doc false 27 | @impl true 28 | def next(prepender), do: {:ok, prepender.element, prepender.iterable} 29 | 30 | @doc false 31 | @impl true 32 | def count(prepender), do: Iterable.count(prepender.iterable) + 1 33 | 34 | @doc false 35 | @impl true 36 | def empty?(_), do: false 37 | end 38 | 39 | defimpl IntoIterable do 40 | def into_iterable(self), do: self 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/iter/iterable/with_indexer.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.WithIndexer do 7 | defstruct iterable: nil, count: 0 8 | 9 | @moduledoc """ 10 | Creates an iterator which emits the current iteration count as well as the next value. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{ 16 | iterable: Iterable.t(), 17 | count: 0 18 | } 19 | 20 | @doc """ 21 | Create a new filter iterable. 22 | """ 23 | @spec new(Iterable.t()) :: Iterable.t() 24 | def new(iterable), do: %__MODULE__{iterable: iterable} 25 | 26 | defimpl Iterable do 27 | use Impl 28 | 29 | @doc false 30 | @impl true 31 | def next(enumerator) do 32 | with {:ok, element, iterable} <- Iterable.next(enumerator.iterable) do 33 | {:ok, {element, enumerator.count}, 34 | %{enumerator | iterable: iterable, count: enumerator.count + 1}} 35 | end 36 | end 37 | end 38 | 39 | defimpl IntoIterable do 40 | @doc false 41 | def into_iterable(self), do: self 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/iter/iterable/uniquer.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Uniquer do 7 | defstruct seen: :sets.new(version: 2), iterable: nil 8 | 9 | @moduledoc """ 10 | An iterable that only emits unique elements. 11 | """ 12 | alias Iter.{Impl, IntoIterable, Iterable} 13 | 14 | @type t :: %__MODULE__{seen: :sets.set(Iterable.element()), iterable: Iterable.t()} 15 | 16 | @doc """ 17 | Creates an iterable that only emits unique elements. 18 | """ 19 | def new(iterable), do: %__MODULE__{iterable: iterable} 20 | 21 | defimpl Iterable do 22 | use Impl 23 | 24 | @doc false 25 | @impl true 26 | def next(uniq) do 27 | with {:ok, element, iterable} <- Iterable.next(uniq.iterable) do 28 | if :sets.is_element(element, uniq.seen) do 29 | next(%{uniq | iterable: iterable}) 30 | else 31 | {:ok, element, %{uniq | seen: :sets.add_element(element, uniq.seen)}} 32 | end 33 | end 34 | end 35 | end 36 | 37 | defimpl IntoIterable do 38 | @doc false 39 | def into_iterable(self), do: self 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | # Change Log 9 | 10 | All notable changes to this project will be documented in this file. 11 | See [Conventional Commits](Https://conventionalcommits.org) for commit guidelines. 12 | 13 | 14 | 15 | ## [v0.1.3](https://github.com/ash-project/iterex/compare/v0.1.2...v0.1.3) (2025-01-27) 16 | 17 | 18 | 19 | 20 | ### Improvements: 21 | 22 | * lower min elixir version 23 | 24 | ## [v0.1.2](https://github.com/ash-project/iterex/compare/v0.1.1...v0.1.2) (2024-07-19) 25 | 26 | 27 | 28 | 29 | ### Improvements: 30 | 31 | * Add generic enumerable conversion with adequate warnings. (#1) 32 | 33 | ## [v0.1.1](https://github.com/ash-project/iterex/compare/v0.1.0...v0.1.1) (2024-06-14) 34 | 35 | 36 | 37 | 38 | ### Improvements: 39 | 40 | * File.Stream: Add `IntoIterable` for `File.Stream`. 41 | 42 | * IO.Stream: Add `IntoIterable` for `IO.Stream`. 43 | 44 | * Date.Range: Add `IntoIterator` for `Date.Range`. 45 | 46 | ## [v0.1.0](https://github.com/ash-project/iterex/compare/v0.1.0...v0.1.0) (2024-06-11) 47 | 48 | 49 | 50 | 51 | ### Features: 52 | 53 | * `Iter` is ready for the general public. 54 | -------------------------------------------------------------------------------- /lib/iter/iterable/head_dropper.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.HeadDropper do 7 | defstruct iterable: nil, how_many: nil 8 | 9 | @moduledoc """ 10 | An iterable which drops the first `how_many` elements. 11 | """ 12 | alias Iter.{Impl, IntoIterable, Iterable} 13 | 14 | @type t :: %__MODULE__{iterable: Iterable.t(), how_many: non_neg_integer()} 15 | 16 | @doc """ 17 | Creates an iterable which drops the first `how_many` elements. 18 | """ 19 | @spec new(Iterable.t(), pos_integer()) :: t 20 | def new(iterable, how_many) when is_integer(how_many) and how_many > 0, 21 | do: %__MODULE__{iterable: iterable, how_many: how_many} 22 | 23 | defimpl Iterable do 24 | use Impl 25 | 26 | @doc false 27 | @impl true 28 | def next(drop) when drop.how_many == 0, do: Iterable.next(drop.iterable) 29 | 30 | def next(drop) do 31 | with {:ok, _element, iterable} <- Iterable.next(drop.iterable) do 32 | next(%{drop | iterable: iterable, how_many: drop.how_many - 1}) 33 | end 34 | end 35 | end 36 | 37 | defimpl IntoIterable do 38 | @doc false 39 | def into_iterable(self), do: self 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/iter/iterable/stepper.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Stepper do 7 | defstruct iterable: nil, step_size: nil 8 | 9 | @moduledoc """ 10 | An iterable which advances it's internal iterable by a specific amount each time. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{ 16 | iterable: Iterable.t(), 17 | step_size: pos_integer() 18 | } 19 | 20 | @doc """ 21 | Create a new iterable which wraps another iterable. 22 | """ 23 | @spec new(Iterable.t(), pos_integer()) :: t 24 | def new(iterable, step_size) when is_integer(step_size) and step_size > 1, 25 | do: %__MODULE__{iterable: iterable, step_size: step_size} 26 | 27 | defimpl Iterable do 28 | use Impl 29 | 30 | @doc false 31 | @impl true 32 | def next(stepper) do 33 | with {:ok, element, iterable} <- Iterable.next(stepper.iterable) do 34 | {:ok, element, %{stepper | iterable: Iterable.drop(iterable, stepper.step_size - 1)}} 35 | end 36 | end 37 | end 38 | 39 | defimpl IntoIterable do 40 | @doc false 41 | def into_iterable(self), do: self 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/iter/iterable/head_taker.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.HeadTaker do 7 | defstruct iterable: nil, how_many: nil 8 | 9 | @moduledoc """ 10 | An iterable which takes the first `how_many` elements. 11 | """ 12 | alias Iter.{Impl, IntoIterable, Iterable} 13 | 14 | @type t :: %__MODULE__{iterable: Iterable.t(), how_many: pos_integer()} 15 | 16 | @doc """ 17 | Creates an iterable which takes the first `how_many` elements. 18 | """ 19 | @spec new(Iterable.t(), pos_integer()) :: t 20 | def new(iterable, how_many) when is_integer(how_many) and how_many > 0, 21 | do: %__MODULE__{iterable: iterable, how_many: how_many} 22 | 23 | defimpl Iterable do 24 | use Impl 25 | 26 | @doc false 27 | @impl true 28 | def next(take) when take.how_many == 0, do: :done 29 | 30 | def next(take) when take.how_many > 0 do 31 | with {:ok, element, iterable} <- Iterable.next(take.iterable) do 32 | {:ok, element, %{take | iterable: iterable, how_many: take.how_many - 1}} 33 | end 34 | end 35 | end 36 | 37 | defimpl IntoIterable do 38 | @doc false 39 | @impl true 40 | def into_iterable(self), do: self 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/iter/iterable/mapper.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Mapper do 7 | defstruct iterable: nil, mapper: nil 8 | 9 | @moduledoc """ 10 | An iterable which applies a mapper function to all it's elements and returns their new values. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{ 16 | iterable: Iterable.t(), 17 | mapper: (Iterable.element() -> Iterable.element()) 18 | } 19 | 20 | @doc """ 21 | Create a new map iterable. 22 | """ 23 | @spec new(Iterable.t(), (Iterable.element() -> Iterable.element())) :: Iterable.t() 24 | def new(iterable, mapper) when is_function(mapper, 1), 25 | do: %__MODULE__{iterable: iterable, mapper: mapper} 26 | 27 | defimpl Iterable do 28 | use Impl 29 | 30 | @doc false 31 | @impl true 32 | def next(mapper) do 33 | case Iterable.next(mapper.iterable) do 34 | {:ok, element, iterable} -> {:ok, mapper.mapper.(element), %{mapper | iterable: iterable}} 35 | :done -> :done 36 | end 37 | end 38 | end 39 | 40 | defimpl IntoIterable do 41 | @doc false 42 | def into_iterable(self), do: self 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/iter/iterable/map_set.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defimpl Iter.Iterable, for: MapSet do 7 | @moduledoc false 8 | 9 | ## Note: 10 | # 11 | # This implementation knows that the `map` key in `MapSet` contains a version 12 | # 2 `:sets` record and cheats accordingly. 13 | # If the internals of either change then I guess the tests will fail. 14 | 15 | use Iter.Impl 16 | 17 | @doc false 18 | @impl true 19 | def next(map_set) when map_size(map_set.map) == 0, do: :done 20 | 21 | def next(map_set) do 22 | with {:ok, element} <- get_next_element(map_set) do 23 | {:ok, element, %{map_set | map: :sets.del_element(element, map_set.map)}} 24 | end 25 | end 26 | 27 | defp get_next_element(map_set) do 28 | map_set.map 29 | |> :maps.iterator() 30 | |> :maps.next() 31 | |> case do 32 | :none -> :done 33 | {key, _, _iterator} -> {:ok, key} 34 | end 35 | end 36 | 37 | @doc false 38 | def count(map_set), do: MapSet.size(map_set) 39 | 40 | @doc false 41 | def dedup(map_set), do: map_set 42 | 43 | @doc false 44 | def uniq(map_set), do: map_set 45 | 46 | @doc false 47 | def member?(map_set, element), do: MapSet.member?(map_set, element) 48 | end 49 | -------------------------------------------------------------------------------- /lib/iter/iterable/while_taker.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.WhileTaker do 7 | defstruct iterable: nil, predicate: nil 8 | 9 | @moduledoc """ 10 | An iterable which emits elements until `predicate` returns `false`. 11 | """ 12 | alias Iter.{Impl, IntoIterable, Iterable} 13 | 14 | @type t :: %__MODULE__{iterable: Iterable.t(), predicate: (Iterable.element() -> boolean)} 15 | 16 | @doc """ 17 | Creates an iterable which emits elements until `predicate` returns `false`. 18 | """ 19 | @spec new(Iterable.t(), (Iterable.element() -> boolean)) :: t 20 | def new(iterable, predicate) when is_function(predicate, 1), 21 | do: %__MODULE__{iterable: iterable, predicate: predicate} 22 | 23 | defimpl Iterable do 24 | use Impl 25 | 26 | @doc false 27 | @impl true 28 | def next(take_while) do 29 | with {:ok, element, iterable} <- Iterable.next(take_while.iterable) do 30 | if take_while.predicate.(element) == true do 31 | {:ok, element, %{take_while | iterable: iterable}} 32 | else 33 | :done 34 | end 35 | end 36 | end 37 | end 38 | 39 | defimpl IntoIterable do 40 | @doc false 41 | def into_iterable(self), do: self 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/iter/iterable/appender.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Appender do 7 | defstruct iterable: nil, element: nil 8 | 9 | @moduledoc """ 10 | An iterable which appends a single element to the end of another iterable. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{iterable: Iterable.t(), element: {:ok, Iterable.element()} | :done} 16 | 17 | @doc """ 18 | Create a new appender iterable. 19 | """ 20 | @spec new(Iterable.t(), Iterable.element()) :: t 21 | def new(iterable, element), do: %__MODULE__{iterable: iterable, element: {:ok, element}} 22 | 23 | defimpl Iterable do 24 | use Impl 25 | 26 | @doc false 27 | @impl true 28 | def next(appender) do 29 | case {Iterable.next(appender.iterable), appender.element} do 30 | {{:ok, element, iterable}, _} -> 31 | {:ok, element, %{appender | iterable: iterable}} 32 | 33 | {:done, :done} -> 34 | :done 35 | 36 | {:done, {:ok, element}} -> 37 | {:ok, element, %{appender | iterable: Iterable.Empty.new(), element: :done}} 38 | end 39 | end 40 | end 41 | 42 | defimpl IntoIterable do 43 | def into_iterable(self), do: self 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/iter/iterable/flattener.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Flattener do 7 | defstruct iterable: nil 8 | 9 | @moduledoc """ 10 | An iterable which flattens nested iterables. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{iterable: Iterable.t()} 16 | 17 | @doc """ 18 | Creates an iterable which flattens nested iterables. 19 | """ 20 | @spec new(Iterable.t()) :: t 21 | def new(iterable), do: %__MODULE__{iterable: iterable} 22 | 23 | defimpl Iterable do 24 | use Impl 25 | 26 | @doc false 27 | @impl true 28 | def next(flatten) do 29 | with {:ok, element, iterable} <- Iterable.next(flatten.iterable) do 30 | maybe_wrap(element, %{flatten | iterable: iterable}) 31 | end 32 | end 33 | 34 | defp maybe_wrap(element, tail) do 35 | element = IntoIterable.into_iterable(element) 36 | head = Iterable.Flattener.new(element) 37 | concat = Iterable.Concatenator.new([head, tail]) 38 | Iterable.next(concat) 39 | rescue 40 | Protocol.UndefinedError -> {:ok, element, tail} 41 | end 42 | end 43 | 44 | defimpl IntoIterable do 45 | @doc false 46 | def into_iterable(self), do: self 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/iter/iterable/while_dropper.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.WhileDropper do 7 | defstruct iterable: nil, predicate: nil 8 | 9 | @moduledoc """ 10 | An iterable that drops elements until `predicate` returns a truthy value. 11 | """ 12 | alias Iter.{Impl, IntoIterable, Iterable} 13 | 14 | @type t :: %__MODULE__{iterable: Iterable.t(), predicate: (Iterable.element() -> boolean)} 15 | 16 | @doc """ 17 | Creates an iterable that drops elements until `predicate` returns a truthy 18 | value. 19 | """ 20 | @spec new(Iterable.t(), (Iterable.element() -> boolean)) :: t 21 | def new(iterable, predicate) when is_function(predicate, 1), 22 | do: %__MODULE__{iterable: iterable, predicate: predicate} 23 | 24 | defimpl Iterable do 25 | use Impl 26 | 27 | @doc false 28 | @impl true 29 | def next(drop_while) do 30 | with {:ok, element, iterable} <- Iterable.next(drop_while.iterable) do 31 | if drop_while.predicate.(element) do 32 | next(%{drop_while | iterable: iterable}) 33 | else 34 | {:ok, element, iterable} 35 | end 36 | end 37 | end 38 | end 39 | 40 | defimpl IntoIterable do 41 | @doc false 42 | def into_iterable(self), do: self 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/iter/iterable/cycler.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Cycler do 7 | defstruct source: nil, buffer: [] 8 | 9 | @moduledoc """ 10 | An iterable which emits elements for ever. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{source: Iterable.t(), buffer: [Iterable.element()]} 16 | 17 | @doc """ 18 | Create an eternal iterable. 19 | """ 20 | @spec new(Iterable.t()) :: Iterable.t() 21 | def new(iterable), do: %__MODULE__{source: iterable} 22 | 23 | defimpl Iterable do 24 | use Impl 25 | 26 | @doc false 27 | @impl true 28 | def next(cycler) do 29 | case Iterable.next(cycler.source) do 30 | {:ok, element, iterable} -> 31 | {:ok, element, %{cycler | source: iterable, buffer: [element | cycler.buffer]}} 32 | 33 | :done when cycler.buffer == [] -> 34 | :done 35 | 36 | :done -> 37 | iterator = 38 | cycler.buffer 39 | |> :lists.reverse() 40 | |> IntoIterable.into_iterable() 41 | 42 | next(%{cycler | source: iterator, buffer: []}) 43 | end 44 | end 45 | end 46 | 47 | defimpl IntoIterable do 48 | @doc false 49 | def into_iterable(self), do: self 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/iter/iterable/filterer.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Filterer do 7 | defstruct iterable: nil, predicate: nil 8 | 9 | @moduledoc """ 10 | An iterable which drops elements for which `predicate` doesn't return a truthy value. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{ 16 | iterable: Iterable.t(), 17 | predicate: (Iterable.element() -> as_boolean(any)) 18 | } 19 | 20 | @doc """ 21 | Create a new filter iterable. 22 | """ 23 | @spec new(Iterable.t(), (Iterable.element() -> boolean)) :: Iterable.t() 24 | def new(iterable, predicate) when is_function(predicate, 1), 25 | do: %__MODULE__{iterable: iterable, predicate: predicate} 26 | 27 | defimpl Iterable do 28 | use Impl 29 | 30 | @doc false 31 | @impl true 32 | def next(filter) do 33 | case Iterable.next(filter.iterable) do 34 | {:ok, element, iterable} -> 35 | if filter.predicate.(element) do 36 | {:ok, element, %{filter | iterable: iterable}} 37 | else 38 | next(%{filter | iterable: iterable}) 39 | end 40 | 41 | :done -> 42 | :done 43 | end 44 | end 45 | end 46 | 47 | defimpl IntoIterable do 48 | @doc false 49 | def into_iterable(self), do: self 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/iter/iterable/range.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defimpl Iter.Iterable, for: Range do 7 | @moduledoc false 8 | 9 | use Iter.Impl 10 | 11 | @doc false 12 | @impl true 13 | def next(%{first: first, last: last, step: step}) when first > last and step > 0, do: :done 14 | def next(%{first: first, last: last, step: step}) when first < last and step < 0, do: :done 15 | 16 | def next(%{first: first, step: step} = range) do 17 | {:ok, first, %{range | first: first + step}} 18 | end 19 | 20 | @doc false 21 | @impl true 22 | def step_by(range, step_size), do: %{range | step: step_size} 23 | 24 | @doc false 25 | @impl true 26 | def member?(range, element) when is_integer(element), 27 | do: do_member?(range, element) 28 | 29 | def member?(_, _), do: false 30 | 31 | defp do_member?(range, element) when range.first == element, 32 | do: true 33 | 34 | defp do_member?(range, element) 35 | when range.step == 1 and element >= range.first and element <= range.last, 36 | do: true 37 | 38 | defp do_member?(range, element) when element > range.last, 39 | do: false 40 | 41 | defp do_member?(range, element), 42 | do: rem(element - range.first, range.step) == 0 43 | 44 | @doc false 45 | @impl true 46 | def dedup(range), do: range 47 | 48 | @doc false 49 | @impl true 50 | def uniq(range), do: range 51 | end 52 | -------------------------------------------------------------------------------- /lib/iter/iterable/concatenator.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Concatenator do 7 | defstruct current_iterable: nil, outer_iterable: nil 8 | 9 | @moduledoc """ 10 | An iterable which can concatenate a number of iterables. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{outer_iterable: Iterable.t(), current_iterable: Iterable.t()} 16 | 17 | @doc """ 18 | Create a new concatenator out of an iterable of iterables. 19 | """ 20 | @spec new(Iterable.t()) :: Iterable.t() 21 | def new(iterable), do: %__MODULE__{outer_iterable: iterable} 22 | 23 | defimpl Iterable do 24 | use Impl 25 | 26 | @doc false 27 | @impl true 28 | def next(concat) when is_nil(concat.current_iterable) do 29 | case Iterable.next(concat.outer_iterable) do 30 | {:ok, element, iterable} -> 31 | next(%{concat | current_iterable: element, outer_iterable: iterable}) 32 | 33 | :done -> 34 | :done 35 | end 36 | end 37 | 38 | def next(concat) do 39 | case Iterable.next(concat.current_iterable) do 40 | {:ok, element, iterable} -> {:ok, element, %{concat | current_iterable: iterable}} 41 | :done -> next(%{concat | current_iterable: nil}) 42 | end 43 | end 44 | end 45 | 46 | defimpl IntoIterable do 47 | @doc false 48 | def into_iterable(self), do: self 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/iter/iterable/flat_mapper.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.FlatMapper do 7 | defstruct iterable: nil, mapper: nil 8 | 9 | @moduledoc """ 10 | An iterable which works like `map/2` but flattens nested iterables. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type mapper :: (Iterable.t() -> Iterable.t() | Iterable.element()) 16 | @type t :: %__MODULE__{iterable: Iterable.t(), mapper: mapper} 17 | 18 | @doc """ 19 | Creates an iterable which works like `map/2` but flattens nested iterables. 20 | """ 21 | @spec new(Iterable.t(), mapper) :: t 22 | def new(iterable, mapper) when is_function(mapper, 1), 23 | do: %__MODULE__{iterable: iterable, mapper: mapper} 24 | 25 | defimpl Iterable do 26 | use Impl 27 | 28 | @doc false 29 | @impl true 30 | def next(flat_map) do 31 | with {:ok, element, iterable} <- Iterable.next(flat_map.iterable) do 32 | maybe_wrap(flat_map.mapper.(element), %{flat_map | iterable: iterable}) 33 | end 34 | end 35 | 36 | defp maybe_wrap(element, tail) do 37 | head = IntoIterable.into_iterable(element) 38 | concat = Iterable.Concatenator.new([head, tail]) 39 | Iterable.next(concat) 40 | rescue 41 | Protocol.UndefinedError -> {:ok, element, tail} 42 | end 43 | end 44 | 45 | defimpl IntoIterable do 46 | @doc false 47 | def into_iterable(self), do: self 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/iter/iterable/deduper.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Deduper do 7 | defstruct iterable: nil, last_result: nil, fun: nil 8 | 9 | @moduledoc """ 10 | An iterable that only emits elements if they are different from the previous element. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{ 16 | iterable: Iterable.t(), 17 | last_result: nil | Iterable.element(), 18 | fun: (Iterable.element() -> any) 19 | } 20 | 21 | @doc """ 22 | Creates an iterable that only emits elements if they are different from the previous element. 23 | """ 24 | @spec new(Iterable.t(), (Iterable.element() -> any)) :: Iterable.t() 25 | def new(iterable, fun \\ &Function.identity/1), do: %__MODULE__{iterable: iterable, fun: fun} 26 | 27 | defimpl Iterable do 28 | use Impl 29 | 30 | @doc false 31 | @impl true 32 | def next(deduper) do 33 | with {:ok, element, iterable} <- Iterable.next(deduper.iterable) do 34 | case deduper.fun.(element) do 35 | result when result == deduper.last_result -> 36 | next(%{deduper | iterable: iterable}) 37 | 38 | result -> 39 | {:ok, element, %{deduper | iterable: iterable, last_result: result}} 40 | end 41 | end 42 | end 43 | end 44 | 45 | defimpl IntoIterable do 46 | @doc false 47 | def into_iterable(self), do: self 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/iter/iterable/date_range.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defimpl Iter.Iterable, for: Date.Range do 7 | @moduledoc false 8 | 9 | use Iter.Impl 10 | 11 | @doc false 12 | @impl true 13 | def next(date_range) 14 | when date_range.step > 0 and date_range.first_in_iso_days <= date_range.last_in_iso_days 15 | when date_range.step < 0 and date_range.first_in_iso_days >= date_range.last_in_iso_days do 16 | next = 17 | date_from_iso_days( 18 | date_range.first_in_iso_days + date_range.step, 19 | date_range.first.calendar 20 | ) 21 | 22 | next_date_range = Date.range(next, date_range.last, date_range.step) 23 | {:ok, date_range.first, next_date_range} 24 | end 25 | 26 | def next(_date_range) do 27 | :done 28 | end 29 | 30 | defp date_from_iso_days(days, Calendar.ISO) do 31 | {year, month, day} = Calendar.ISO.date_from_iso_days(days) 32 | %Date{year: year, month: month, day: day, calendar: Calendar.ISO} 33 | end 34 | 35 | defp date_from_iso_days(days, calendar) do 36 | {year, month, day, _, _, _, _} = 37 | calendar.naive_datetime_from_iso_days({days, {0, 86_400_000_000}}) 38 | 39 | %Date{year: year, month: month, day: day, calendar: calendar} 40 | end 41 | 42 | @doc false 43 | @impl true 44 | def count(date_range) do 45 | {:ok, count} = Enumerable.count(date_range) 46 | count 47 | end 48 | 49 | @doc false 50 | @impl true 51 | def member?(date_range, element) do 52 | case Enumerable.member?(date_range, element) do 53 | {:ok, true} -> true 54 | _ -> false 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/iter/iterable/intersperser.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Intersperser do 7 | defstruct iterable: nil, separator: nil, next: :init 8 | 9 | @moduledoc """ 10 | An iterable which places a separator value in between consecutive elements. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{ 16 | iterable: Iterable.t(), 17 | separator: any, 18 | next: {:ok, Iterable.element()} | :none | :init 19 | } 20 | 21 | @doc """ 22 | Create a new intersperser iterable out of an iterable and a separator 23 | """ 24 | @spec new(Iterable.t(), any) :: Iterable.t() 25 | def new(iterable, separator), 26 | do: %__MODULE__{iterable: iterable, separator: separator} 27 | 28 | defimpl Iterable do 29 | use Impl 30 | 31 | @doc false 32 | @impl true 33 | def next(%{next: :init} = intersperser) do 34 | with {:ok, element, iterable} <- Iterable.next(intersperser.iterable) do 35 | {:ok, element, %{intersperser | next: :none, iterable: iterable}} 36 | end 37 | end 38 | 39 | def next(%{next: {:ok, element}} = intersperser) do 40 | {:ok, element, %{intersperser | next: :none}} 41 | end 42 | 43 | def next(intersperser) do 44 | with {:ok, element, iterable} <- Iterable.next(intersperser.iterable) do 45 | {:ok, intersperser.separator, %{intersperser | next: {:ok, element}, iterable: iterable}} 46 | end 47 | end 48 | end 49 | 50 | defimpl IntoIterable do 51 | @doc false 52 | def into_iterable(self), do: self 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/iter/iterable/every_dropper.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.EveryDropper do 7 | defstruct iterable: nil, every: nil, count: 0 8 | 9 | @moduledoc """ 10 | An iterable which drops every `nth` element from the iterable. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{ 16 | iterable: Iterable.t(), 17 | every: non_neg_integer, 18 | count: non_neg_integer 19 | } 20 | 21 | @doc """ 22 | Creates an iterable which drops every `nth` element from the iterable. 23 | """ 24 | @spec new(Iterable.t(), non_neg_integer) :: t 25 | def new(iterable, 0), do: iterable 26 | def new(_iterable, 1), do: Iterable.Empty.new() 27 | 28 | def new(iterable, every) when is_integer(every) and every > 1, 29 | do: %__MODULE__{iterable: iterable, every: every} 30 | 31 | defimpl Iterable do 32 | use Impl 33 | 34 | @doc false 35 | @impl true 36 | def next(drop_every) when drop_every.count == 0 do 37 | with {:ok, _element, iterable} <- Iterable.next(drop_every.iterable), 38 | {:ok, element, iterable} <- Iterable.next(iterable) do 39 | {:ok, element, %{drop_every | iterable: iterable, count: drop_every.every - 2}} 40 | end 41 | end 42 | 43 | def next(drop_every) do 44 | with {:ok, element, iterable} <- Iterable.next(drop_every.iterable) do 45 | {:ok, element, %{drop_every | iterable: iterable, count: drop_every.count - 1}} 46 | end 47 | end 48 | end 49 | 50 | defimpl IntoIterable do 51 | @doc false 52 | def into_iterable(self), do: self 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/iter/iterable/range_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.RangeTest do 7 | @moduledoc false 8 | use ExUnit.Case, async: true 9 | 10 | describe "member?/2" do 11 | test "when the element isn't within the range" do 12 | refute Iter.Iterable.Range.member?(1..3, 4) 13 | end 14 | 15 | test "when the range has a step of 1 and the element equals the first value" do 16 | assert Iter.Iterable.Range.member?(1..3, 1) 17 | end 18 | 19 | test "when the range has a step of 1 and the element equals the last value" do 20 | assert Iter.Iterable.Range.member?(1..3, 3) 21 | end 22 | 23 | test "when the range has a step of 1 and the element is contained in the range" do 24 | assert Iter.Iterable.Range.member?(1..3, 2) 25 | end 26 | 27 | test "when the range has a step greater than one and the element equals the first value" do 28 | assert Iter.Iterable.Range.member?(1..3//2, 1) 29 | end 30 | 31 | test "when the range has a step greater than one and the element isn't on step" do 32 | refute Iter.Iterable.Range.member?(1..3//2, 2) 33 | end 34 | 35 | test "when the range has a step greater than one and the element is in step" do 36 | assert Iter.Iterable.Range.member?(1..5//2, 3) 37 | end 38 | 39 | test "when the range has a step greater than one and the element is the last _and_ the last isn't on step" do 40 | refute Iter.Iterable.Range.member?(1..4//2, 4) 41 | end 42 | 43 | test "when the range has a step greater than one and the element is the last _and_ the last is on step" do 44 | assert Iter.Iterable.Range.member?(1..5//2, 5) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/iter/iterable/every_mapper.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.EveryMapper do 7 | defstruct iterable: nil, every: nil, count: 0, mapper: nil 8 | 9 | @moduledoc """ 10 | An iterable which maps every `nth` element in the iterable. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{ 16 | iterable: Iterable.t(), 17 | every: non_neg_integer, 18 | count: non_neg_integer, 19 | mapper: (Iterable.element() -> any) 20 | } 21 | 22 | @doc """ 23 | Creates a new iterable which maps every `nth` element in the iterable. 24 | """ 25 | @spec new(Iterable.t(), non_neg_integer, (Iterable.element() -> any)) :: t 26 | def new(iterable, 0, _mapper), do: iterable 27 | def new(iterable, 1, mapper), do: Iterable.Mapper.new(iterable, mapper) 28 | 29 | def new(iterable, nth, mapper) when is_integer(nth) and nth > 1, 30 | do: %__MODULE__{iterable: iterable, every: nth, mapper: mapper} 31 | 32 | defimpl Iterable do 33 | use Impl 34 | 35 | @doc false 36 | @impl true 37 | def next(map_every) when map_every.count == 0 do 38 | with {:ok, element, iterable} <- Iterable.next(map_every.iterable) do 39 | {:ok, map_every.mapper.(element), 40 | %{map_every | iterable: iterable, count: map_every.every - 1}} 41 | end 42 | end 43 | 44 | def next(map_every) do 45 | with {:ok, element, iterable} <- Iterable.next(map_every.iterable) do 46 | {:ok, element, %{map_every | iterable: iterable, count: map_every.count - 1}} 47 | end 48 | end 49 | end 50 | 51 | defimpl IntoIterable do 52 | def into_iterable(self), do: self 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/iter/iterable/zipper.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Zipper do 7 | defstruct iterables: nil, zipper: nil 8 | 9 | @moduledoc """ 10 | An iterable which returns the elements of two iterables as tuple pairs. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{ 16 | iterables: [Iterable.t()], 17 | zipper: ([Iterable.element()] -> any) 18 | } 19 | 20 | @doc """ 21 | Create a new zip out of two iterables. 22 | """ 23 | @spec new(Iterable.t(), ([Iterable.element()] -> any)) :: Iterable.t() 24 | def new(iterables, zipper) when is_function(zipper, 1), 25 | do: %__MODULE__{iterables: Iterable.to_list(iterables), zipper: zipper} 26 | 27 | defimpl Iterable do 28 | use Impl 29 | 30 | @doc false 31 | @impl true 32 | def next(zipper) do 33 | with {:ok, element, iterables} <- get_next(zipper.iterables, [], [], zipper.zipper) do 34 | {:ok, element, %{zipper | iterables: iterables}} 35 | end 36 | end 37 | 38 | defp get_next([], iterables, elements, zipper) do 39 | elements = 40 | elements 41 | |> :lists.reverse() 42 | |> then(zipper) 43 | 44 | iterables = 45 | iterables 46 | |> :lists.reverse() 47 | 48 | {:ok, elements, iterables} 49 | end 50 | 51 | defp get_next([head | tail], iterables, elements, zipper) do 52 | case Iterable.next(head) do 53 | {:ok, element, head} -> get_next(tail, [head | iterables], [element | elements], zipper) 54 | :done -> :done 55 | end 56 | end 57 | end 58 | 59 | defimpl IntoIterable do 60 | @doc false 61 | def into_iterable(self), do: self 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/iter/iterable/map.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Map do 7 | defstruct iterator: nil, map: nil 8 | 9 | @moduledoc """ 10 | A wrapper around Erlang's `:maps.iterator`. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{iterator: :maps.iterator(), map: map} 16 | 17 | @doc """ 18 | Convert a map into a map iterable. 19 | """ 20 | @spec new(map) :: t 21 | def new(map) when is_map(map), do: %__MODULE__{iterator: :maps.iterator(map), map: map} 22 | 23 | defimpl Iterable do 24 | use Impl 25 | import :erlang, only: [map_get: 2] 26 | 27 | @doc false 28 | @impl true 29 | def next(map_iterable) do 30 | case :maps.next(map_iterable.iterator) do 31 | :none -> :done 32 | {key, value, iterator} -> {:ok, {key, value}, %{map_iterable | iterator: iterator}} 33 | end 34 | end 35 | 36 | @doc false 37 | @impl true 38 | def filter(map_iterable, predicate) when is_function(predicate, 1) do 39 | new_map = :maps.filter(&filter_predicate(predicate, &1, &2), map_iterable.iterator) 40 | 41 | IntoIterable.into_iterable(new_map) 42 | end 43 | 44 | defp filter_predicate(predicate, key, value) do 45 | case predicate.({key, value}) do 46 | true -> true 47 | {true, _} -> true 48 | _ -> false 49 | end 50 | end 51 | 52 | @doc false 53 | @impl true 54 | def each(map_iterable, fun) when is_function(fun, 1) do 55 | :maps.foreach(&each_fun(fun, &1, &2), map_iterable.iterator) 56 | 57 | :done 58 | end 59 | 60 | defp each_fun(fun, key, value), do: fun.({key, value}) 61 | 62 | @doc false 63 | @impl true 64 | def member?(map, {key, value}) when map_get(key, map.map) == value, do: true 65 | def member?(_, _), do: false 66 | end 67 | 68 | defimpl IntoIterable do 69 | @doc false 70 | def into_iterable(self), do: self 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/iter/iterable/resource.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Resource do 7 | defstruct acc: nil, start_fun: nil, next_fun: nil, after_fun: nil, buffer: [] 8 | 9 | @moduledoc """ 10 | An iterable which provides compatibility with `Stream.resource/3` 11 | """ 12 | alias Iter.{Impl, IntoIterable, Iterable} 13 | 14 | @type acc :: any 15 | @type start_fun :: (-> acc) 16 | @type next_fun :: (acc -> {[Iterable.element()], acc} | {:halt, acc}) 17 | @type after_fun :: (acc -> any) 18 | @type t :: %__MODULE__{ 19 | acc: acc, 20 | start_fun: nil | start_fun(), 21 | next_fun: next_fun(), 22 | after_fun: after_fun(), 23 | buffer: [Iterable.element()] 24 | } 25 | 26 | @doc """ 27 | Create an iterable from functions in a manner compatible with `Stream.resource/3`. 28 | """ 29 | @spec new(start_fun, next_fun, after_fun) :: t 30 | def new(start_fun, next_fun, after_fun), 31 | do: %__MODULE__{start_fun: start_fun, next_fun: next_fun, after_fun: after_fun} 32 | 33 | defimpl Iterable do 34 | use Impl 35 | 36 | @doc false 37 | @impl true 38 | def next(%{buffer: [head | tail]} = resource), do: {:ok, head, %{resource | buffer: tail}} 39 | 40 | def next(resource) when is_nil(resource.start_fun) do 41 | case resource.next_fun.(resource.acc) do 42 | {[element], acc} -> 43 | {:ok, element, %{resource | acc: acc}} 44 | 45 | {[head | tail], acc} -> 46 | {:ok, head, %{resource | buffer: tail, acc: acc}} 47 | 48 | {[], acc} -> 49 | next(%{resource | acc: acc}) 50 | 51 | {:halt, acc} -> 52 | resource.after_fun.(acc) 53 | :done 54 | end 55 | end 56 | 57 | def next(resource) do 58 | acc = resource.start_fun.() 59 | next(%{resource | acc: acc, start_fun: nil}) 60 | end 61 | end 62 | 63 | defimpl IntoIterable do 64 | @doc false 65 | def into_iterable(self), do: self 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/iter/iterable/tail_dropper.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.TailDropper do 7 | defstruct iterable: nil, how_many: nil, buffer: nil 8 | 9 | @moduledoc """ 10 | An iterable which drops the last `how_many` elements. 11 | """ 12 | alias Iter.{Impl, IntoIterable, Iterable} 13 | 14 | @type t :: %__MODULE__{ 15 | iterable: Iterable.t(), 16 | how_many: non_neg_integer(), 17 | buffer: nil | Iterable.t() 18 | } 19 | 20 | @doc """ 21 | Creates an iterable which drops the last `how_many` elements. 22 | """ 23 | @spec new(Iterable.t(), pos_integer()) :: t 24 | def new(iterable, how_many) when is_integer(how_many) and how_many > 0, 25 | do: %__MODULE__{iterable: iterable, how_many: how_many} 26 | 27 | defimpl Iterable do 28 | use Impl 29 | 30 | @doc false 31 | @impl true 32 | def next(drop) when is_nil(drop.buffer) do 33 | with {:ok, drop} <- fill_buffer(drop) do 34 | next(drop) 35 | end 36 | end 37 | 38 | def next(drop) do 39 | with {:ok, to_append, iterable} <- Iterable.next(drop.iterable), 40 | {:ok, to_emit, buffer} <- Iterable.next(drop.buffer) do 41 | {:ok, to_emit, %{drop | iterable: iterable, buffer: Iterable.append(buffer, to_append)}} 42 | end 43 | end 44 | 45 | # Ensure that at least `how_many` elements exist. 46 | defp fill_buffer(drop), do: fill_buffer(%{drop | buffer: Iterable.Empty.new()}, 0) 47 | 48 | defp fill_buffer(drop, buffer_size) when buffer_size == drop.how_many, 49 | do: {:ok, drop} 50 | 51 | defp fill_buffer(drop, buffer_size) do 52 | with {:ok, element, iterable} <- Iterable.next(drop.iterable) do 53 | fill_buffer( 54 | %{drop | iterable: iterable, buffer: Iterable.append(drop.buffer, element)}, 55 | buffer_size + 1 56 | ) 57 | end 58 | end 59 | end 60 | 61 | defimpl IntoIterable do 62 | @doc false 63 | def into_iterable(self), do: self 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/iter/iterable/tail_taker.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.TailTaker do 7 | defstruct iterable: nil, how_many: nil, buffer: nil 8 | 9 | @moduledoc """ 10 | An iterable which takes `how_many` elements from the end of the iterable. 11 | """ 12 | alias Iter.{Impl, IntoIterable, Iterable} 13 | 14 | @type t :: %__MODULE__{ 15 | iterable: Iterable.t(), 16 | how_many: pos_integer(), 17 | buffer: nil | [Iterable.element()] 18 | } 19 | 20 | @doc """ 21 | Creates an iterable which takes `how_many` elements from the end of the iterable. 22 | """ 23 | @spec new(Iterable.t(), pos_integer()) :: t 24 | def new(iterable, how_many) when is_integer(how_many) and how_many > 0, 25 | do: %__MODULE__{iterable: iterable, how_many: how_many} 26 | 27 | defimpl Iterable do 28 | use Impl 29 | 30 | @doc false 31 | @impl true 32 | def next(take) when is_nil(take.buffer) do 33 | take 34 | |> fill_buffer() 35 | |> next() 36 | end 37 | 38 | def next(%{buffer: []}), do: :done 39 | def next(%{buffer: [hd | tail]} = take), do: {:ok, hd, %{take | buffer: tail}} 40 | 41 | defp fill_buffer(take), do: fill_buffer(%{take | buffer: []}, 0) 42 | 43 | defp fill_buffer(take, buffer_size) do 44 | case Iterable.next(take.iterable) do 45 | {:ok, element, iterable} when buffer_size == take.how_many -> 46 | fill_buffer( 47 | %{take | buffer: [element | :lists.droplast(take.buffer)], iterable: iterable}, 48 | buffer_size 49 | ) 50 | 51 | {:ok, element, iterable} -> 52 | fill_buffer( 53 | %{take | buffer: [element | take.buffer], iterable: iterable}, 54 | buffer_size + 1 55 | ) 56 | 57 | :done -> 58 | %{take | buffer: :lists.reverse(take.buffer), iterable: Iterable.Empty.new()} 59 | end 60 | end 61 | end 62 | 63 | defimpl IntoIterable do 64 | @doc false 65 | def into_iterable(self), do: self 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/iter/iterable/by_chunker.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.ByChunker do 7 | defstruct iterable: nil, chunker: nil, state: :none, buffer: [] 8 | 9 | @moduledoc """ 10 | An iterable that chunks elements by subsequent return values of `fun`. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{ 16 | iterable: Iterable.t(), 17 | chunker: (Iterable.element() -> any), 18 | state: {:ok, any} | :none, 19 | buffer: [] 20 | } 21 | 22 | @doc """ 23 | Creates an iterable that chunks elements by subsequent return values of `fun`. 24 | """ 25 | @spec new(Iterable.t(), (Iterable.element() -> any)) :: t 26 | def new(iterable, chunker) when is_function(chunker, 1), 27 | do: %__MODULE__{iterable: iterable, chunker: chunker} 28 | 29 | defimpl Iterable do 30 | use Impl 31 | 32 | @doc false 33 | @impl true 34 | def next(chunk_by) when chunk_by.buffer == [] do 35 | with {:ok, element, iterable} <- Iterable.next(chunk_by.iterable) do 36 | next(%{ 37 | chunk_by 38 | | iterable: iterable, 39 | state: {:ok, chunk_by.chunker.(element)}, 40 | buffer: [element] 41 | }) 42 | end 43 | end 44 | 45 | def next(chunk_by) do 46 | case Iterable.next(chunk_by.iterable) do 47 | {:ok, element, iterable} -> 48 | chunk_result = chunk_by.chunker.(element) 49 | 50 | if {:ok, chunk_result} == chunk_by.state do 51 | next(%{chunk_by | iterable: iterable, buffer: [element | chunk_by.buffer]}) 52 | else 53 | {:ok, :lists.reverse(chunk_by.buffer), 54 | %{chunk_by | iterable: iterable, state: {:ok, chunk_result}, buffer: [element]}} 55 | end 56 | 57 | :done -> 58 | {:ok, :lists.reverse(chunk_by.buffer), Iterable.Empty.new()} 59 | end 60 | end 61 | end 62 | 63 | defimpl IntoIterable do 64 | @doc false 65 | def into_iterable(self), do: self 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/iter/iterable/every_chunker.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.EveryChunker do 7 | defstruct iterable: nil, count: nil, step: nil, leftover: nil 8 | 9 | @moduledoc """ 10 | An iterable that chunks into `count` size elements, where each new chunk 11 | starts `step` elements into the enumerable. 12 | """ 13 | 14 | alias Iter.{Impl, IntoIterable, Iterable} 15 | 16 | @type t :: %__MODULE__{ 17 | iterable: Iterable.t(), 18 | count: pos_integer, 19 | step: pos_integer, 20 | leftover: Iterable.t() | :discard 21 | } 22 | 23 | @doc """ 24 | Creates an iterable that chunks into `count` size elements, where each new 25 | chunk starts `step` elements into the enumerable. 26 | """ 27 | @spec new(Iterable.t(), pos_integer, pos_integer, Iterable.t() | :discard) :: t 28 | def new(iterable, count, step, leftover) 29 | when is_integer(count) and count > 0 and is_integer(step) and step > 0, 30 | do: %__MODULE__{ 31 | iterable: iterable, 32 | count: count, 33 | step: step, 34 | leftover: leftover 35 | } 36 | 37 | defimpl Iterable do 38 | use Impl 39 | 40 | @doc false 41 | @impl true 42 | def next(chunk_every) do 43 | how_many = chunk_every.count 44 | 45 | case Iterable.peek(chunk_every.iterable, how_many) do 46 | {:ok, elements, ^how_many, iterable} -> 47 | {:ok, elements, %{chunk_every | iterable: Iterable.drop(iterable, chunk_every.step)}} 48 | 49 | {:ok, _elements, _how_many, _iterable} when chunk_every.leftover == :discard -> 50 | :done 51 | 52 | {:ok, _elements, 0, _iterable} -> 53 | :done 54 | 55 | {:ok, _elements, _how_many, iterable} -> 56 | elements = 57 | [iterable, chunk_every.leftover] 58 | |> Iterable.concat() 59 | |> Iterable.take_head(chunk_every.count) 60 | |> Iterable.to_list() 61 | 62 | {:ok, elements, Iterable.Empty.new()} 63 | 64 | :done -> 65 | :done 66 | end 67 | end 68 | end 69 | 70 | defimpl IntoIterable do 71 | @doc false 72 | def into_iterable(self), do: self 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/iter/iterable/while_chunker.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.WhileChunker do 7 | defstruct iterable: nil, acc: nil, chunk_fun: nil, after_fun: nil 8 | 9 | @moduledoc """ 10 | An iterable that chunks based on a chunk function. 11 | """ 12 | alias Iter.{Impl, IntoIterable, Iterable} 13 | 14 | @type acc :: any 15 | @type chunk :: any 16 | @type t :: %__MODULE__{ 17 | iterable: Iterable.t(), 18 | acc: nil, 19 | chunk_fun: (Iterable.element(), acc -> 20 | {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}), 21 | after_fun: (acc -> {:cont, chunk, acc} | {:cont, acc}) 22 | } 23 | 24 | @doc """ 25 | Creates an iterable that chunks based on a chunk function. 26 | """ 27 | @spec new( 28 | Iterable.t(), 29 | any, 30 | (Iterable.element(), acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}), 31 | (acc -> {:cont, chunk, acc} | {:cont, acc}) 32 | ) :: t 33 | def new(iterable, acc, chunk_fun, after_fun) 34 | when is_function(chunk_fun, 2) and is_function(after_fun, 1), 35 | do: %__MODULE__{iterable: iterable, acc: acc, chunk_fun: chunk_fun, after_fun: after_fun} 36 | 37 | defimpl Iterable do 38 | use Impl 39 | 40 | @doc false 41 | @impl true 42 | def next(chunk_while) do 43 | case Iterable.next(chunk_while.iterable) do 44 | {:ok, element, iterable} -> handle_chunk_fun(chunk_while, element, iterable) 45 | :done -> handle_after_fun(chunk_while, chunk_while.acc) 46 | end 47 | end 48 | 49 | defp handle_chunk_fun(chunk_while, element, iterable) do 50 | case chunk_while.chunk_fun.(element, chunk_while.acc) do 51 | {:cont, chunk, acc} -> {:ok, chunk, %{chunk_while | acc: acc, iterable: iterable}} 52 | {:cont, acc} -> next(%{chunk_while | acc: acc, iterable: iterable}) 53 | {:halt, acc} -> handle_after_fun(chunk_while, acc) 54 | end 55 | end 56 | 57 | defp handle_after_fun(chunk_while, acc) do 58 | case chunk_while.after_fun.(acc) do 59 | {:cont, chunk, _acc} -> {:ok, chunk, Iterable.Empty.new()} 60 | {:cont, _acc} -> :done 61 | end 62 | end 63 | end 64 | 65 | defimpl IntoIterable do 66 | @doc false 67 | def into_iterable(self), do: self 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/iter/into_iterable/file_stream.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defimpl Iter.IntoIterable, for: File.Stream do 7 | @moduledoc false 8 | 9 | @doc false 10 | @impl true 11 | def into_iterable(%{modes: modes, line_or_bytes: line_or_bytes, raw: raw} = stream) do 12 | start_fun = fn -> 13 | case File.Stream.__open__(stream, read_modes(modes)) do 14 | {:ok, device} -> 15 | skip_bom_and_offset(device, raw, modes) 16 | 17 | {:error, reason} -> 18 | raise File.Error, reason: reason, action: "stream", path: stream.path 19 | end 20 | end 21 | 22 | next_fun = 23 | case raw do 24 | true -> &IO.each_binstream(&1, line_or_bytes) 25 | false -> &IO.each_stream(&1, line_or_bytes) 26 | end 27 | 28 | Iter.Iterable.Resource.new(start_fun, next_fun, &:file.close/1) 29 | end 30 | 31 | defp read_modes(modes) do 32 | for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode 33 | end 34 | 35 | defp skip_bom_and_offset(device, raw, modes) do 36 | device = 37 | if :trim_bom in modes do 38 | device |> trim_bom(raw) |> elem(0) 39 | else 40 | device 41 | end 42 | 43 | offset = get_read_offset(modes) 44 | 45 | if offset > 0 do 46 | {:ok, _} = :file.position(device, {:cur, offset}) 47 | end 48 | 49 | device 50 | end 51 | 52 | defp trim_bom(device, true) do 53 | bom_length = device |> IO.binread(4) |> bom_length() 54 | {:ok, new_pos} = :file.position(device, bom_length) 55 | {device, new_pos} 56 | end 57 | 58 | defp trim_bom(device, false) do 59 | # Or we read the bom in the correct amount or it isn't there 60 | case bom_length(IO.read(device, 1)) do 61 | 0 -> 62 | {:ok, _} = :file.position(device, 0) 63 | {device, 0} 64 | 65 | _ -> 66 | {device, 1} 67 | end 68 | end 69 | 70 | defp bom_length(<<239, 187, 191, _rest::binary>>), do: 3 71 | defp bom_length(<<254, 255, _rest::binary>>), do: 2 72 | defp bom_length(<<255, 254, _rest::binary>>), do: 2 73 | defp bom_length(<<0, 0, 254, 255, _rest::binary>>), do: 4 74 | defp bom_length(<<254, 255, 0, 0, _rest::binary>>), do: 4 75 | defp bom_length(_binary), do: 0 76 | 77 | def get_read_offset(modes) do 78 | case :lists.keyfind(:read_offset, 1, modes) do 79 | {:read_offset, offset} -> offset 80 | false -> 0 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.MixProject do 7 | use Mix.Project 8 | 9 | @moduledoc """ 10 | Lazy, external iterators for Elixir. 11 | """ 12 | @version "0.1.3" 13 | 14 | def project do 15 | [ 16 | app: :iterex, 17 | version: @version, 18 | description: @moduledoc, 19 | elixir: "~> 1.15", 20 | consolidate_protocols: Mix.env() == :prod, 21 | start_permanent: Mix.env() == :prod, 22 | deps: deps(), 23 | package: package(), 24 | docs: docs(), 25 | source_url: "https://github.com/ash-project/iterex", 26 | homepage_url: "https://github.com/ash-project/iterex" 27 | ] 28 | end 29 | 30 | def package do 31 | [ 32 | licenses: ["MIT"], 33 | files: ~w[lib .formatter.exs mix.exs README* LICENSE* CHANGELOG*], 34 | maintainers: [ 35 | "James Harton ", 36 | "Zach Daniel " 37 | ], 38 | links: %{ 39 | "GitHub" => "https://github.com/ash-project/iterex", 40 | "Changelog" => "https://github.com/ash-project/iterex/blob/main/CHANGELOG.md", 41 | "Discord" => "https://discord.gg/HTHRaaVPUc", 42 | "Website" => "https://ash-hq.org", 43 | "Forum" => "https://elixirforum.com/c/elixir-framework-forums/ash-framework-forum", 44 | "REUSE Compliance" => "https://api.reuse.software/info/github.com/ash-project/iterex", 45 | "Sponsor" => "https://github.com/sponsors/jimsynz" 46 | } 47 | ] 48 | end 49 | 50 | # Run "mix help compile.app" to learn about applications. 51 | def application do 52 | [ 53 | extra_applications: [:logger] 54 | ] 55 | end 56 | 57 | defp docs do 58 | [ 59 | main: "readme", 60 | formatters: ["html"], 61 | extras: ["README.md"], 62 | logo: "logos/iterex-logo-small.png", 63 | before_closing_head_tag: fn type -> 64 | if type == :html do 65 | """ 66 | 75 | """ 76 | end 77 | end 78 | ] 79 | end 80 | 81 | # Run "mix help deps" to learn about dependencies. 82 | defp deps do 83 | opts = [only: [:dev, :test], runtime: false] 84 | 85 | [ 86 | {:credo, "~> 1.7", opts}, 87 | {:dialyxir, "~> 1.3", opts}, 88 | {:doctor, "~> 0.21", opts}, 89 | {:earmark, ">= 0.0.0", opts}, 90 | {:ex_check, "~> 0.16", opts}, 91 | {:ex_doc, ">= 0.0.0", opts}, 92 | {:faker, "~> 0.18", opts}, 93 | {:git_ops, "~> 2.6", opts}, 94 | {:mix_audit, "~> 2.1", opts} 95 | ] 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/iter/iterable/peeker.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Peeker do 7 | defstruct iterable: nil, elements: [], size: 0 8 | 9 | @moduledoc """ 10 | The result of "peeking" into an iterable. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{ 16 | iterable: Iterable.t(), 17 | elements: [Iterable.element()], 18 | size: non_neg_integer() 19 | } 20 | 21 | @doc """ 22 | Create a new "peeker" iterable. 23 | """ 24 | @spec new(Iterable.t()) :: t 25 | def new(iterable), do: %__MODULE__{iterable: iterable} 26 | 27 | defimpl Iterable do 28 | use Impl 29 | 30 | @doc false 31 | @impl true 32 | def next(%{elements: [], iterable: iterable}), do: Iterable.next(iterable) 33 | 34 | def next(%{elements: [element], iterable: iterable}), do: {:ok, element, iterable} 35 | 36 | def next(%{elements: [element | rest], size: size} = peeker), 37 | do: {:ok, element, %{peeker | elements: rest, size: size - 1}} 38 | 39 | @doc false 40 | @impl true 41 | def peek(%{elements: [], iterable: iterable} = peeker) do 42 | case Iterable.next(iterable) do 43 | {:ok, element, iterable} -> 44 | {:ok, element, %{peeker | elements: [element], size: 1, iterable: iterable}} 45 | 46 | :done -> 47 | :done 48 | end 49 | end 50 | 51 | def peek(%{elements: [element | _]} = peeker) do 52 | {:ok, element, peeker} 53 | end 54 | 55 | @doc false 56 | @impl true 57 | def peek(peeker, 0), do: {:ok, [], 0, peeker} 58 | 59 | def peek(%{elements: elements, size: size} = peeker, how_many) when how_many == size do 60 | {:ok, elements, size, peeker} 61 | end 62 | 63 | def peek(%{elements: elements, size: size} = peeker, how_many) 64 | when how_many < size and how_many > 0 do 65 | {:ok, :lists.sublist(elements, how_many), how_many, peeker} 66 | end 67 | 68 | def peek(%{elements: elements, iterable: iterable, size: size} = peeker, how_many) do 69 | remaining = how_many - size 70 | 71 | with {:ok, got, extra_elements, iterable} <- do_peek(iterable, remaining, [], 0) do 72 | elements = elements ++ extra_elements 73 | size = got + size 74 | {:ok, elements, size, %{peeker | elements: elements, iterable: iterable, size: size}} 75 | end 76 | end 77 | 78 | defp do_peek(iterable, 0, result, so_far), do: {:ok, so_far, :lists.reverse(result), iterable} 79 | 80 | defp do_peek(iterable, remaining, result, so_far) do 81 | case Iterable.next(iterable) do 82 | {:ok, element, iterable} -> 83 | do_peek(iterable, remaining - 1, [element | result], so_far + 1) 84 | 85 | :done when result == [] -> 86 | :done 87 | 88 | :done -> 89 | {:ok, so_far, :lists.reverse(result), Iterable.Empty.new()} 90 | end 91 | end 92 | end 93 | 94 | defimpl IntoIterable do 95 | @doc false 96 | def into_iterable(self), do: self 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | Logo Light 12 | Logo Dark 13 | 14 | # Iterex 15 | 16 | ![Elixir CI](https://github.com/ash-project/iterex/actions/workflows/elixir.yml/badge.svg) 17 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 18 | [![Hex version badge](https://img.shields.io/hexpm/v/iterex.svg)](https://hex.pm/packages/iterex) 19 | [![Hexdocs badge](https://img.shields.io/badge/docs-hexdocs-purple)](https://hexdocs.pm/iterex) 20 | [![REUSE status](https://api.reuse.software/badge/github.com/ash-project/iterex)](https://api.reuse.software/info/github.com/ash-project/iterex) 21 | 22 | Iterex is a library that provides external iterators for Elixir collections. 23 | 24 | Iterators provide the flexibility of `Enum` with the laziness of `Stream` and the ability to pause and resume iteration. 25 | 26 | The `Iter` module provides the public interface to working with iterators, which wraps an `Iter.Iterable` (to make it easier to pattern match, etc). You'll find most of the functions you'd want from `Stream` and `Enum` provided by this module, but often with different return values to enable you to resume iteration where possible. The `Enumerable` and `Collectable` protocols have been implemented for `Iter` so you can use them as drop in replacements for other collection types where needed. 27 | 28 | Some differences from `Enum` and `Stream`: 29 | 30 | - `Iter.next/1` - the core advantage of iterators over streams. Allows you to retrieve the next element from an iterator and a new iterator. 31 | - `Iter.prepend/2`, `Iter.append/2` and `Iter.peek/1..2` - iterators can be easily composed allowing features that might otherwise break `Stream` semantics. 32 | 33 | See the [documentation on hexdocs](https://hexdocs.pm/iterex) for more information. 34 | 35 | ## Sponsors 36 | 37 | Thanks to [Alembic Pty Ltd](https://alembic.com.au/) for sponsoring a portion of 38 | this project's development. 39 | 40 | ## Installation 41 | 42 | The package can be installed by adding `iterex` to your list of dependencies in `mix.exs`: 43 | 44 | ```elixir 45 | def deps do 46 | [ 47 | {:iterex, "~> 0.1.3"} 48 | ] 49 | end 50 | ``` 51 | 52 | ## Contributing 53 | 54 | - To contribute updates, fixes or new features please fork and open a pull-request against `main`. 55 | - Please use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) - this allows us to dynamically generate the changelog. 56 | 57 | ## Licence 58 | 59 | `iterex` is licensed under the terms of the [MIT license](https://opensource.org/licenses/MIT). See the [`LICENSE` file in this repository](https://github.com/ash-project/iterex/blob/main/LICENSE) 60 | for details. 61 | -------------------------------------------------------------------------------- /lib/iter/iterable/enumerable.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.Enumerable do 7 | defstruct pid: nil 8 | 9 | @moduledoc """ 10 | Can we convert a enum into an iterable? Let's find out. 11 | """ 12 | 13 | alias Iter.{Impl, IntoIterable, Iterable} 14 | 15 | @type t :: %__MODULE__{pid: pid} 16 | 17 | use GenServer 18 | 19 | @doc "Wrap an enumerable in a genserver" 20 | @spec new(Enumerable.t()) :: t 21 | def new(enum) do 22 | case GenServer.start_link(__MODULE__, enum) do 23 | {:ok, pid} -> %__MODULE__{pid: pid} 24 | {:error, reason} -> raise reason 25 | end 26 | end 27 | 28 | defimpl Iterable do 29 | use Impl 30 | 31 | @doc false 32 | @impl true 33 | def next(%{pid: pid} = enum) do 34 | case GenServer.call(pid, :next, :infinity) do 35 | {:ok, element} -> {:ok, element, enum} 36 | :done -> :done 37 | end 38 | catch 39 | :exit, _ -> :done 40 | end 41 | end 42 | 43 | defimpl IntoIterable do 44 | @doc false 45 | @impl true 46 | def into_iterable(self), do: self 47 | end 48 | 49 | @doc false 50 | @impl GenServer 51 | def init(enum) do 52 | {:ok, %{enum: enum}} 53 | end 54 | 55 | @doc false 56 | @impl GenServer 57 | def handle_call(:next, from, %{enum: enum}) do 58 | {:ok, pid} = reduce_in_task(enum, self()) 59 | 60 | Process.monitor(pid) 61 | 62 | {:noreply, %{next_reply_to: from, source: pid}} 63 | end 64 | 65 | def handle_call(:next, _from, %{element: element, element_reply_to: from} = state) do 66 | GenServer.reply(from, :ok) 67 | {:reply, {:ok, element}, Map.drop(state, [:element, :element_reply_to])} 68 | end 69 | 70 | def handle_call(:next, _from, %{source: :done} = state) do 71 | {:stop, :normal, :done, state} 72 | end 73 | 74 | def handle_call(:next, from, state) do 75 | {:noreply, Map.put(state, :next_reply_to, from)} 76 | end 77 | 78 | def handle_call({:element, element}, from, state) do 79 | case Map.pop(state, :next_reply_to) do 80 | {nil, state} -> 81 | state = Map.merge(state, %{element: element, element_reply_to: from}) 82 | {:noreply, state} 83 | 84 | {from, state} -> 85 | GenServer.reply(from, {:ok, element}) 86 | {:reply, :ok, state} 87 | end 88 | end 89 | 90 | @doc false 91 | @impl true 92 | def handle_info({:DOWN, _, :process, pid, _}, %{source: pid} = state) 93 | when is_map_key(state, :next_reply_to) do 94 | GenServer.reply(state.next_reply_to, :done) 95 | 96 | {:stop, :normal} 97 | end 98 | 99 | def handle_info({:DOWN, _, :process, pid, _}, %{source: pid} = state) do 100 | {:noreply, %{state | source: :done}} 101 | end 102 | 103 | defp reduce_in_task(enum, receiver) do 104 | Task.start_link(fn -> 105 | Enum.reduce_while(enum, :ok, &task_reducer(&1, &2, receiver)) 106 | end) 107 | end 108 | 109 | defp task_reducer(element, :ok, receiver) do 110 | case GenServer.call(receiver, {:element, element}, :infinity) do 111 | :ok -> {:cont, :ok} 112 | :halt -> {:halt, :ok} 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /test/iter/iterable/peeker_test.exs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Iterable.PeekerTest do 7 | @moduledoc false 8 | alias Iter.{Iterable, Iterable.Empty, Iterable.Peeker} 9 | use ExUnit.Case, async: true 10 | 11 | describe "next/1" do 12 | test "when there are no elements in the peek buffer, it just advances and returns the iterable" do 13 | assert {:ok, :a, [:b, :c]} = 14 | [:a, :b, :c] 15 | |> Peeker.new() 16 | |> Iterable.next() 17 | end 18 | 19 | test "when there are no elements in the peek buffer _and_ the iterable is empty, it returns done" do 20 | assert :done = 21 | [] 22 | |> Peeker.new() 23 | |> Iterable.next() 24 | end 25 | 26 | test "when there is one element in the peek buffer it returns the element and the iterable" do 27 | assert {:ok, :a, [:b, :c]} = 28 | %Peeker{elements: [:a], iterable: [:b, :c], size: 1} 29 | |> Iterable.next() 30 | end 31 | 32 | test "when there is more than one element in the peek buffer it returns the element and reduces the peek buffer" do 33 | assert {:ok, :a, %Peeker{elements: [:b], iterable: [:c], size: 1}} = 34 | %Peeker{ 35 | elements: [:a, :b], 36 | iterable: [:c], 37 | size: 2 38 | } 39 | |> Iterable.next() 40 | end 41 | end 42 | 43 | describe "peek/1" do 44 | test "when there are no elements in the peek buffer but elements in the iterable, it advances the iterable" do 45 | assert {:ok, :a, %Peeker{elements: [:a], iterable: [:b, :c], size: 1}} = 46 | %Peeker{elements: [], iterable: [:a, :b, :c], size: 0} 47 | |> Iterable.peek() 48 | end 49 | 50 | test "when there are no elements in the peek buffer and no elements in the iterable, it returns done" do 51 | assert :done = 52 | %Peeker{elements: [], iterable: [], size: 0} 53 | |> Iterable.peek() 54 | end 55 | 56 | test "when there is one element in the peek buffer, it returns the element" do 57 | peeker = %Peeker{elements: [:a], iterable: [:b, :c], size: 1} 58 | assert {:ok, :a, ^peeker} = Iterable.peek(peeker) 59 | end 60 | 61 | test "when there is more than one element in the peek buffer, it returns the first element" do 62 | peeker = %Peeker{elements: [:a, :b, :c], iterable: [:d], size: 3} 63 | assert {:ok, :a, ^peeker} = Iterable.peek(peeker) 64 | end 65 | end 66 | 67 | describe "peek/2" do 68 | test "when there are no elements in the peek buffer but elements in the iterable, it advances the iterable" do 69 | assert {:ok, [:a, :b], 2, %Peeker{elements: [:a, :b], iterable: [:c, :d], size: 2}} = 70 | Peeker.new([:a, :b, :c, :d]) 71 | |> Iterable.peek(2) 72 | end 73 | 74 | test "when there are no elements in the peek buffer and no elements in the iterable, it returns done" do 75 | assert :done = 76 | Peeker.new([]) 77 | |> Iterable.peek(2) 78 | end 79 | 80 | test "when there are enough elements in the peek buffer, it returns them" do 81 | peeker = %Peeker{elements: [:a, :b], iterable: [], size: 2} 82 | 83 | assert {:ok, [:a, :b], 2, ^peeker} = 84 | peeker 85 | |> Iterable.peek(2) 86 | end 87 | 88 | test "when there are more than enough elements in the peek buffer, it returns them" do 89 | peeker = %Peeker{elements: [:a, :b, :c], iterable: [], size: 3} 90 | 91 | assert {:ok, [:a, :b], 2, ^peeker} = 92 | peeker 93 | |> Iterable.peek(2) 94 | end 95 | 96 | test "when there are not enough elements in the peek buffer, it advances the iterable" do 97 | assert {:ok, [:a, :b, :c, :d], 4, 98 | %Peeker{elements: [:a, :b, :c, :d], iterable: [:e], size: 4}} = 99 | %Peeker{elements: [:a, :b], iterable: [:c, :d, :e], size: 2} 100 | |> Iterable.peek(4) 101 | end 102 | 103 | test "when there are not enough elements in the peek buffer or the iterable returns what it can get with an empty iterable" do 104 | assert {:ok, [:a, :b, :c], 3, %Peeker{elements: [:a, :b, :c], iterable: %Empty{}, size: 3}} = 105 | %Peeker{elements: [:a], iterable: [:b, :c], size: 1} 106 | |> Iterable.peek(4) 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "credo": {:hex, :credo, "1.7.13", "126a0697df6b7b71cd18c81bc92335297839a806b6f62b61d417500d1070ff4e", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "47641e6d2bbff1e241e87695b29f617f1a8f912adea34296fb10ecc3d7e9e84f"}, 4 | "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, 5 | "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, 6 | "doctor": {:hex, :doctor, "0.22.0", "223e1cace1f16a38eda4113a5c435fa9b10d804aa72d3d9f9a71c471cc958fe7", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "96e22cf8c0df2e9777dc55ebaa5798329b9028889c4023fed3305688d902cd5b"}, 7 | "earmark": {:hex, :earmark, "1.4.48", "5f41e579d85ef812351211842b6e005f6e0cef111216dea7d4b9d58af4608434", [:mix], [], "hexpm", "a461a0ddfdc5432381c876af1c86c411fd78a25790c75023c7a4c035fdc858f9"}, 8 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 9 | "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, 10 | "ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"}, 11 | "ex_doc": {:hex, :ex_doc, "0.39.1", "e19d356a1ba1e8f8cfc79ce1c3f83884b6abfcb79329d435d4bbb3e97ccc286e", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "8abf0ed3e3ca87c0847dfc4168ceab5bedfe881692f1b7c45f4a11b232806865"}, 12 | "faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"}, 13 | "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, 14 | "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, 15 | "git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"}, 16 | "git_ops": {:hex, :git_ops, "2.9.0", "b74f6040084f523055b720cc7ef718da47f2cbe726a5f30c2871118635cb91c1", [:mix], [{:git_cli, "~> 0.2", [hex: :git_cli, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.27 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "7fdf84be3490e5692c5dc1f8a1084eed47a221c1063e41938c73312f0bfea259"}, 17 | "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, 18 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 19 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 20 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 21 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 22 | "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, 23 | "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, 24 | "mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"}, 25 | "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, 26 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 27 | "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, 28 | "req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"}, 29 | "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, 30 | "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, 31 | "yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"}, 32 | } 33 | -------------------------------------------------------------------------------- /lib/iter/iterable.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defprotocol Iter.Iterable do 7 | @moduledoc """ 8 | This is the main iterable protocol. 9 | 10 | It is intentionally huge, however the only function you have to implement is 11 | `next/1`, for the remainder you can rely on the default implementations from 12 | `Iter.Impl` unless your data structure can provide a more 13 | efficient method of generating the correct answer. 14 | """ 15 | 16 | @default_impl """ 17 | > #### Optional callback {: .tip} 18 | > A default implementation of this function exists in the `Iter.Impl` module. 19 | > 20 | > You can add it to your protocol implementation by adding `use Iter.Impl`. 21 | """ 22 | 23 | @type element :: Iter.element() 24 | @type predicate :: Iter.predicate() 25 | @type mapper :: Iter.mapper() 26 | @type t :: any 27 | 28 | @doc """ 29 | Advance the iterable and return the next value. 30 | 31 | This is the only required callback in the `Iterable` protocol. 32 | 33 | ## Return values 34 | 35 | - `{:ok, element, new_iterable}` - returns the next element and an updated iterable. 36 | - `:done` - the iterable is exhausted. 37 | """ 38 | @spec next(t) :: {:ok, element, t} | :done 39 | def next(iterable) 40 | 41 | @doc """ 42 | Tests if every element in the iterable matches `predicate`. 43 | 44 | #{@default_impl} 45 | """ 46 | @spec all?(t, predicate) :: boolean 47 | def all?(iterable, predicate) 48 | 49 | @doc """ 50 | Tests if any element in the iterable matches `predicate`. 51 | 52 | #{@default_impl} 53 | """ 54 | @spec any?(t, predicate) :: boolean 55 | def any?(iterable, predicate) 56 | 57 | @doc """ 58 | Append an element onto the end of the iterable. 59 | 60 | #{@default_impl} 61 | """ 62 | @spec append(t, element) :: t 63 | def append(iterable, element) 64 | 65 | @doc """ 66 | Returns the element `index` items from the beginning of the iterator. 67 | 68 | Note that all preceding elements, as well as the returned element, will be consumed from the iterable. 69 | 70 | ## Return values 71 | 72 | - `{:ok, element, new_iterable}` - the next element and an updated iterable. 73 | - `:done` - the iterable was exhausted before the element was found. 74 | 75 | #{@default_impl} 76 | """ 77 | @spec at(t, non_neg_integer) :: {:ok, non_neg_integer, t} | :done 78 | def at(iterable, index) 79 | 80 | @doc """ 81 | Creates an iterable that only emits elements for which `fun` returns a new 82 | value. 83 | 84 | #{@default_impl} 85 | """ 86 | @spec chunk_by(t, (element -> any)) :: t 87 | def chunk_by(iterable, chunker) 88 | 89 | @doc """ 90 | Creates an iterable that chunks into `count` size elements, where each new 91 | chunk starts `step` elements into the enumerable. 92 | 93 | #{@default_impl} 94 | """ 95 | @spec chunk_every(t, pos_integer, pos_integer, t | :discard) :: t 96 | def chunk_every(iterable, count, step, leftover) 97 | 98 | @doc """ 99 | Creates an iterable that chunks based on a chunk function. 100 | 101 | #{@default_impl} 102 | """ 103 | @spec chunk_while( 104 | t, 105 | acc, 106 | (element, acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}), 107 | (acc -> {:cont, chunk, acc} | {:cont, acc}) 108 | ) :: t 109 | when acc: any, chunk: any 110 | def chunk_while(iterable, acc, chunk_fun, after_fun) 111 | 112 | @doc """ 113 | Creates an iterable that iterates each iterable in an iterable. 114 | 115 | #{@default_impl} 116 | """ 117 | @spec concat(t) :: t 118 | def concat(iterable) 119 | 120 | @doc """ 121 | Consumes the iterable, counting the number of iterations remaining. 122 | 123 | #{@default_impl} 124 | """ 125 | @spec count(t) :: non_neg_integer 126 | def count(iterable) 127 | 128 | @doc """ 129 | Consumes the iterable, counting the number of elements for which `fun` returns a truthy value. 130 | 131 | #{@default_impl} 132 | """ 133 | @spec count(t, (element -> as_boolean(any))) :: non_neg_integer 134 | def count(iterable, fun) 135 | 136 | @doc """ 137 | Creates an iterable that cycles it's elements eternally. 138 | 139 | #{@default_impl} 140 | """ 141 | @spec cycle(t) :: t 142 | def cycle(iterable) 143 | 144 | @doc """ 145 | Creates an iterable that only emits elements if they are different from the previous element. 146 | 147 | The function `fun` maps every element to a term which is used to determine if two elements are duplicates. 148 | 149 | #{@default_impl} 150 | """ 151 | @spec dedup_by(t, (element -> any)) :: t 152 | def dedup_by(iterable, fun) 153 | 154 | @doc """ 155 | Creates an iterable that only emits elements if they are different from the previous element. 156 | 157 | #{@default_impl} 158 | """ 159 | @spec dedup(t) :: t 160 | def dedup(iterable) 161 | 162 | @doc """ 163 | Returns a new iterable with every `nth` element in the `iterable` dropped, 164 | starting with the first element. 165 | 166 | #{@default_impl} 167 | """ 168 | @spec drop_every(t, non_neg_integer) :: t 169 | def drop_every(iterable, nth) 170 | 171 | @doc """ 172 | Drops elements at the beginning of the `iterable` while `predicate` returns a 173 | truthy value. 174 | 175 | #{@default_impl} 176 | """ 177 | @spec drop_while(t, predicate) :: t 178 | def drop_while(iterable, predicate) 179 | 180 | @doc """ 181 | Creates an iterable which drops the first `how_many` elements. 182 | 183 | #{@default_impl} 184 | """ 185 | @spec drop(t, non_neg_integer()) :: t 186 | def drop(iterable, how_many) 187 | 188 | @doc """ 189 | Consumes the iterable and applies `fun` to each element. 190 | 191 | Primarily used for side-effects. 192 | 193 | Always returns `:done`. 194 | 195 | #{@default_impl} 196 | """ 197 | @spec each(t, (element -> any)) :: :done 198 | def each(iterable, fun) 199 | 200 | @doc """ 201 | Determines if the iterable is empty. 202 | 203 | #{@default_impl} 204 | """ 205 | @spec empty?(t) :: boolean 206 | def empty?(iter) 207 | 208 | @doc """ 209 | Creates an iterable which drops elements for which `predicate` doesn't return 210 | a truthy value. 211 | 212 | #{@default_impl} 213 | """ 214 | @spec filter(t, predicate) :: t 215 | def filter(iterable, predicate) 216 | 217 | @doc """ 218 | Searches for the first element in the iterable which matches `predicate`. 219 | 220 | #{@default_impl} 221 | """ 222 | @spec find(t, predicate) :: {:ok, element, t} | :done 223 | def find(iterable, predicate) 224 | 225 | @doc """ 226 | Returns the index of the first element in the iterable which matches `predicate`. 227 | 228 | #{@default_impl} 229 | """ 230 | @spec find_index(t, predicate) :: {:ok, non_neg_integer(), t} | :done 231 | def find_index(iterable, predicate) 232 | 233 | @doc """ 234 | Returns the first non-falsy result of `fun`. 235 | 236 | #{@default_impl} 237 | """ 238 | @spec find_value(t, (element -> result)) :: {:ok, result, t} | :done when result: any 239 | def find_value(iterable, fun) 240 | 241 | @doc """ 242 | Creates an iterable which works like `map/2` but flattens nested iterables. 243 | 244 | #{@default_impl} 245 | """ 246 | @spec flat_map(t, (element -> t | element)) :: t 247 | def flat_map(iterable, fun) 248 | 249 | @doc """ 250 | Creates an iterable which flattens nested iterables. 251 | 252 | #{@default_impl} 253 | """ 254 | @spec flatten(t) :: t 255 | def flatten(iterable) 256 | 257 | @doc """ 258 | Creates a new iterable which places `separator` between adjacent items of the original iterable. 259 | 260 | #{@default_impl} 261 | """ 262 | @spec intersperse(t, any) :: t 263 | def intersperse(iterable, separator) 264 | 265 | @doc """ 266 | Creates a new iterable which applies `mapper` on every `nth` element of the 267 | iterable, starting with the first element. 268 | 269 | The first element is always mapped unless `nth` is `0`. 270 | 271 | #{@default_impl} 272 | """ 273 | @spec map_every(t, non_neg_integer, (element -> new_element)) :: t when new_element: any 274 | def map_every(iterable, nth, mapper) 275 | 276 | @doc """ 277 | Creates a new iterable which applies `mapper` to each element and using it's 278 | result as the new element value. 279 | 280 | #{@default_impl} 281 | """ 282 | @spec map(t, (element -> new_element)) :: t when new_element: any 283 | def map(iterable, mapper) 284 | 285 | @doc """ 286 | Returns the maximal element in the `iterable` according to Erlang's term ordering. 287 | 288 | #{@default_impl} 289 | """ 290 | @spec max(t, (element, element -> boolean)) :: {:ok, element} | :done 291 | def max(iterable, sorter) 292 | 293 | @doc """ 294 | Returns the maximal element in the `iterable` as calculated by `mapper`. 295 | 296 | #{@default_impl} 297 | """ 298 | @spec max_by(t, (element -> new_element), (new_element, new_element -> boolean)) :: 299 | {:ok, element} | :done 300 | when new_element: element 301 | def max_by(iterable, mapper, sorter) 302 | 303 | @doc """ 304 | Is the element a member of the iterable? 305 | 306 | #{@default_impl} 307 | """ 308 | @spec member?(t, element) :: boolean 309 | def member?(iterable, element) 310 | 311 | @doc """ 312 | Returns the minimal element in the `iterable` according to Erlang's term ordering. 313 | 314 | #{@default_impl} 315 | """ 316 | @spec min(t, (element, element -> boolean)) :: {:ok, element} | :done 317 | def min(iterable, sorter) 318 | 319 | @doc """ 320 | Returns the minimal element in the `iterable` as calculated by `mapper`. 321 | 322 | #{@default_impl} 323 | """ 324 | @spec min_by(t, (element -> new_element), (new_element, new_element -> boolean)) :: 325 | {:ok, element} | :done 326 | when new_element: element 327 | def min_by(iterable, mapper, sorter) 328 | 329 | @doc """ 330 | Return the minimal and maximal element of the iterable. 331 | 332 | #{@default_impl} 333 | """ 334 | @spec min_max(t) :: {:ok, element, element} | :done 335 | def min_max(iterable) 336 | 337 | @doc """ 338 | Peeks at the first element of the iterable, without consuming it. 339 | 340 | ## Return values 341 | 342 | - `{:ok, element, new_iterable}` - the next element and an updated iterable. 343 | - `:done` - the iterable is exhausted. 344 | 345 | #{@default_impl} 346 | """ 347 | @spec peek(t) :: {:ok, element, t} | :done 348 | def peek(iterable) 349 | 350 | @doc """ 351 | Peeks at the first n elements of the iterable, without consuming it. 352 | 353 | ## Return values 354 | 355 | - `{:ok, [element], how_many, new_iterable}` - the peekable elements and an 356 | updated iterable. Note that `how_many` may not be the same as you asked 357 | for if the underlying iterable is exhausted. 358 | - `:done` - the iterable is exhausted. 359 | 360 | #{@default_impl} 361 | """ 362 | @spec peek(t) :: {:ok, [element], non_neg_integer, t} | :done 363 | def peek(iterable, how_many) 364 | 365 | @doc """ 366 | Creates an iterable which prepends an element to the beginning of another 367 | iterable. 368 | 369 | #{@default_impl} 370 | """ 371 | @spec prepend(t, element) :: t 372 | def prepend(iterable, element) 373 | 374 | @doc """ 375 | Creates an iterable starting at the same point, but stepping by `step_size` each iteration. 376 | 377 | The first element of the iterable will always be returned, regardless of the step given. 378 | 379 | #{@default_impl} 380 | """ 381 | @spec step_by(t, pos_integer()) :: t 382 | def step_by(iterable, step_size) 383 | 384 | @doc """ 385 | Collects `how_many` elements into a chunk and returns it as well as the 386 | remaining iterable. 387 | 388 | #{@default_impl} 389 | """ 390 | @spec take_chunk(t, non_neg_integer()) :: {:ok, t, t} | {:done, t} 391 | def take_chunk(iterable, how_many) 392 | 393 | @doc """ 394 | Creates an iterable which takes the first `how_many` elements. 395 | 396 | #{@default_impl} 397 | """ 398 | @spec take_head(t, non_neg_integer()) :: t 399 | def take_head(iterable, how_many) 400 | 401 | @doc """ 402 | Creates an iterable which takes the last `how_many` elements. 403 | 404 | #{@default_impl} 405 | """ 406 | @spec take_tail(t, non_neg_integer()) :: t 407 | def take_tail(iterable, how_many) 408 | 409 | @doc """ 410 | Creates an iterable which emits elements until `predicate` returns `false`. 411 | 412 | #{@default_impl} 413 | """ 414 | @spec take_while(t, predicate) :: t 415 | def take_while(iterable, predicate) 416 | 417 | @doc """ 418 | Convert the iterable into a list. 419 | 420 | #{@default_impl} 421 | """ 422 | @spec to_list(t) :: [element] 423 | def to_list(iterable) 424 | 425 | @doc """ 426 | Creates an iterable that returns only unique elements. 427 | 428 | #{@default_impl} 429 | """ 430 | @spec uniq(t) :: t 431 | def uniq(iterable) 432 | 433 | @doc """ 434 | Creates an iterable which emits the current iteration count as well as the next value. 435 | 436 | #{@default_impl} 437 | """ 438 | @spec with_index(t) :: t 439 | def with_index(iterable) 440 | 441 | @doc """ 442 | Zips corresponding elements from a number of iterables into an iterable of 443 | results as computed by `zipper`. 444 | 445 | #{@default_impl} 446 | """ 447 | @spec zip(t, ([element] -> any)) :: t 448 | def zip(t, zipper) 449 | end 450 | -------------------------------------------------------------------------------- /lib/iter/impl.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter.Impl do 7 | @moduledoc """ 8 | The default implementations of all `Iter.Iterable` callbacks except `next/1`. 9 | 10 | By adding `use Iter.Impl` to your `Iter.Iterable` definition all of the 11 | default functions will be automatically delegated and marked as overridable. 12 | 13 | This allows you to implement only those callbacks which can reasonably be made 14 | faster for your particular iterable, and not have to implement all of them. 15 | 16 | For example, here's a fictional implementation of iterable for `List`: 17 | 18 | ```elixir 19 | defimpl Iter.Iterable, for: List do 20 | use Iter.Impl 21 | 22 | def next([head | tail]), do: {:ok, head, tail} 23 | def next([]), do: :done 24 | 25 | def peek([]), do: :done 26 | def peek([head | _] = list), do: {:ok, head, list} 27 | 28 | def empty?([]), do: true 29 | def empty?(_), do: false 30 | end 31 | ``` 32 | 33 | Be aware that all the default implementations rely on your implementation of 34 | `next/1` which you always must provide. 35 | """ 36 | 37 | alias Iter.{Impl, Iterable} 38 | 39 | @doc """ 40 | Generate overridable delegations to the default iterable callbacks. 41 | """ 42 | defmacro __using__(_) do 43 | quote generated: true do 44 | @impl Iterable 45 | defdelegate all?(iterable, predicate), to: Impl 46 | @impl Iterable 47 | defdelegate any?(iterable, predicate), to: Impl 48 | @impl Iterable 49 | defdelegate append(iterable, element), to: Impl 50 | @impl Iterable 51 | defdelegate at(iterable, index), to: Impl 52 | @impl Iterable 53 | defdelegate chunk_by(iterable, chunker), to: Impl 54 | @impl Iterable 55 | defdelegate chunk_every(iterable, count, step, leftover), to: Impl 56 | @impl Iterable 57 | defdelegate chunk_while(iterable, acc, chunk_fun, after_fun), to: Impl 58 | @impl Iterable 59 | defdelegate concat(iterable), to: Impl 60 | @impl Iterable 61 | defdelegate count(iterable), to: Impl 62 | @impl Iterable 63 | defdelegate count(iterable, fun), to: Impl 64 | @impl Iterable 65 | defdelegate cycle(iterable), to: Impl 66 | @impl Iterable 67 | defdelegate dedup_by(iterable, fun), to: Impl 68 | @impl Iterable 69 | defdelegate dedup(iterable), to: Impl 70 | @impl Iterable 71 | defdelegate drop_every(iterable, nth), to: Impl 72 | @impl Iterable 73 | defdelegate drop_while(iterable, predicate), to: Impl 74 | @impl Iterable 75 | defdelegate drop(iterable, how_many), to: Impl 76 | @impl Iterable 77 | defdelegate each(iterable, fun), to: Impl 78 | @impl Iterable 79 | defdelegate empty?(iterable), to: Impl 80 | @impl Iterable 81 | defdelegate filter(iterable, predicate), to: Impl 82 | @impl Iterable 83 | defdelegate find_index(iterable, predicate), to: Impl 84 | @impl Iterable 85 | defdelegate find_value(iterable, fun), to: Impl 86 | @impl Iterable 87 | defdelegate find(iterable, predicate), to: Impl 88 | @impl Iterable 89 | defdelegate flat_map(iterable, fun), to: Impl 90 | @impl Iterable 91 | defdelegate flatten(iterable), to: Impl 92 | @impl Iterable 93 | defdelegate intersperse(iterable, separator), to: Impl 94 | @impl Iterable 95 | defdelegate map_every(iterable, nth, mapper), to: Impl 96 | @impl Iterable 97 | defdelegate map(iterable, mapper), to: Impl 98 | @impl Iterable 99 | defdelegate max_by(iterable, mapper, sorter), to: Impl 100 | @impl Iterable 101 | defdelegate max(iterable, sorter), to: Impl 102 | @impl Iterable 103 | defdelegate member?(iterable, element), to: Impl 104 | @impl Iterable 105 | defdelegate min_by(iterable, mapper, sorter), to: Impl 106 | @impl Iterable 107 | defdelegate min_max(iterable), to: Impl 108 | @impl Iterable 109 | defdelegate min(iterable, sorter), to: Impl 110 | @impl Iterable 111 | defdelegate peek(iterable, how_many), to: Impl 112 | @impl Iterable 113 | defdelegate peek(iterable), to: Impl 114 | @impl Iterable 115 | defdelegate prepend(iterable, element), to: Impl 116 | @impl Iterable 117 | defdelegate step_by(iterable, step_size), to: Impl 118 | @impl Iterable 119 | defdelegate take_chunk(iterable, how_many), to: Impl 120 | @impl Iterable 121 | defdelegate take_head(iterable, how_many), to: Impl 122 | @impl Iterable 123 | defdelegate take_tail(iterable, how_many), to: Impl 124 | @impl Iterable 125 | defdelegate take_while(iterable, predicate), to: Impl 126 | @impl Iterable 127 | defdelegate to_list(iterable), to: Impl 128 | @impl Iterable 129 | defdelegate uniq(iterable), to: Impl 130 | @impl Iterable 131 | defdelegate with_index(iterable), to: Impl 132 | @impl Iterable 133 | defdelegate zip(iterables, zipper), to: Impl 134 | 135 | defoverridable all?: 2, 136 | any?: 2, 137 | append: 2, 138 | at: 2, 139 | chunk_by: 2, 140 | chunk_every: 4, 141 | chunk_while: 4, 142 | concat: 1, 143 | count: 1, 144 | count: 2, 145 | cycle: 1, 146 | dedup_by: 2, 147 | dedup: 1, 148 | drop_every: 2, 149 | drop_while: 2, 150 | drop: 2, 151 | each: 2, 152 | empty?: 1, 153 | filter: 2, 154 | find_index: 2, 155 | find_value: 2, 156 | find: 2, 157 | flat_map: 2, 158 | flatten: 1, 159 | intersperse: 2, 160 | map_every: 3, 161 | map: 2, 162 | max_by: 3, 163 | max: 2, 164 | member?: 2, 165 | min_by: 3, 166 | min_max: 1, 167 | min: 2, 168 | peek: 1, 169 | peek: 2, 170 | prepend: 2, 171 | step_by: 2, 172 | take_chunk: 2, 173 | take_head: 2, 174 | take_tail: 2, 175 | take_while: 2, 176 | to_list: 1, 177 | uniq: 1, 178 | with_index: 1, 179 | zip: 2 180 | end 181 | end 182 | 183 | @type iterable :: Iterable.t() 184 | @type element :: Iterable.element() 185 | @type predicate :: Iterable.predicate() 186 | 187 | @doc """ 188 | Tests if every element in the iterable matches `predicate`. 189 | 190 | ## Examples 191 | 192 | iex> Impl.all?([2, 4, 6, 8], &(rem(&1, 2) == 0)) 193 | true 194 | 195 | iex> Impl.all?([2, 3, 4], &(rem(&1, 2) == 0)) 196 | false 197 | """ 198 | @spec all?(iterable, predicate) :: boolean 199 | def all?(iterable, predicate) when is_function(predicate, 1), 200 | do: do_all(iterable, predicate, true) 201 | 202 | defp do_all(_iterable, _predicate, nil), do: false 203 | defp do_all(_iterable, _predicate, false), do: false 204 | 205 | defp do_all(iterable, predicate, _) do 206 | case Iterable.next(iterable) do 207 | {:ok, element, iterable} -> do_all(iterable, predicate, predicate.(element)) 208 | :done -> true 209 | end 210 | end 211 | 212 | @doc """ 213 | Tests if any element in the iterable matches `predicate`. 214 | 215 | ## Examples 216 | 217 | iex> Impl.any?([2, 4, 6], &(rem(&1, 2) == 1)) 218 | false 219 | 220 | iex> Impl.any?([2, 3, 4], &(rem(&1, 2) == 1)) 221 | true 222 | """ 223 | @spec any?(iterable, predicate) :: boolean 224 | def any?(iterable, predicate) when is_function(predicate, 1), 225 | do: do_any(iterable, predicate, false) 226 | 227 | defp do_any(_iterable, _predicate, truthy) when truthy not in [nil, false], do: true 228 | 229 | defp do_any(iterable, predicate, _) do 230 | case Iterable.next(iterable) do 231 | {:ok, element, iterable} -> do_any(iterable, predicate, predicate.(element)) 232 | :done -> false 233 | end 234 | end 235 | 236 | @doc """ 237 | Creates an iterable that appends an element to the end of the iterable. 238 | 239 | ## Examples 240 | 241 | iex> Impl.append(1..3, 4) 242 | ...> |> Impl.to_list() 243 | [1, 2, 3, 4] 244 | """ 245 | @spec append(iterable, element) :: iterable 246 | def append(iterable, element), do: Iterable.Appender.new(iterable, element) 247 | 248 | @doc """ 249 | Returns the element `index` items from the beginning of the iterable. 250 | 251 | ## Example 252 | 253 | iex> Impl.at([:a, :b, :c], 1) 254 | {:ok, :b, [:c]} 255 | """ 256 | @spec at(iterable, non_neg_integer()) :: {:ok, element, iterable} | :done 257 | def at(iterable, 0), do: Iterable.next(iterable) 258 | 259 | def at(iterable, n) do 260 | case Iterable.next(iterable) do 261 | {:ok, _element, iterable} -> at(iterable, n - 1) 262 | :done -> :done 263 | end 264 | end 265 | 266 | @doc """ 267 | Creates an iterable that chunks elements by subsequent return values of `fun`. 268 | 269 | ## Example 270 | 271 | iex> Impl.chunk_by([1, 2, 2, 3, 4, 4, 6, 7, 7], &(rem(&1, 2) == 1)) 272 | ...> |> Impl.to_list() 273 | [[1], [2, 2], [3], [4, 4, 6], [7, 7]] 274 | """ 275 | @spec chunk_by(iterable, (element -> any)) :: iterable 276 | def chunk_by(iterable, chunker), do: Iterable.ByChunker.new(iterable, chunker) 277 | 278 | @doc """ 279 | Creates an iterable that chunks elements into `count` sized chunks of `step` spacing. 280 | 281 | ## Examples 282 | 283 | iex> Impl.chunk_every([1, 2, 3, 4, 5, 6], 2, 2, []) |> Impl.to_list() 284 | [[1, 2], [3, 4], [5, 6]] 285 | 286 | iex> Impl.chunk_every([1, 2, 3, 4, 5, 6], 3, 2, :discard) |> Impl.to_list() 287 | [[1, 2, 3], [3, 4, 5]] 288 | 289 | iex> Impl.chunk_every([1, 2, 3, 4, 5, 6], 3, 2, [7]) |> Impl.to_list() 290 | [[1, 2, 3], [3, 4, 5], [5, 6, 7]] 291 | 292 | iex> Impl.chunk_every([1, 2, 3, 4, 5, 6], 3, 3, []) |> Impl.to_list() 293 | [[1, 2, 3], [4, 5, 6]] 294 | 295 | iex> cycler = Impl.cycle([0]) 296 | iex> Impl.chunk_every([1, 2, 3, 4], 3, 3, cycler) |> Impl.to_list() 297 | [[1, 2, 3], [4, 0, 0]] 298 | """ 299 | @spec chunk_every(iterable, pos_integer, pos_integer, iterable | :discard) :: iterable 300 | def chunk_every(iterable, count, step, leftover), 301 | do: Iterable.EveryChunker.new(iterable, count, step, leftover) 302 | 303 | @doc """ 304 | Creates an iterable that chunks based on a chunk function. 305 | 306 | ## Examples 307 | 308 | iex> chunk_fun = fn element, acc -> 309 | ...> if rem(element, 2) == 0 do 310 | ...> {:cont, Enum.reverse([element | acc]), []} 311 | ...> else 312 | ...> {:cont, [element | acc]} 313 | ...> end 314 | ...> end 315 | iex> after_fun = fn 316 | ...> [] -> {:cont, []} 317 | ...> acc -> {:cont, Enum.reverse(acc), []} 318 | ...> end 319 | iex> iter = 1..10 |> Impl.chunk_while([], chunk_fun, after_fun) 320 | iex> Impl.to_list(iter) 321 | [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]] 322 | """ 323 | @spec chunk_while( 324 | iterable, 325 | acc, 326 | (element, acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}), 327 | (acc -> {:cont, chunk, acc} | {:cont, acc}) 328 | ) :: iterable 329 | when acc: any, chunk: any 330 | def chunk_while(iterable, acc, chunk_fun, after_fun), 331 | do: Iterable.WhileChunker.new(iterable, acc, chunk_fun, after_fun) 332 | 333 | @doc """ 334 | Takes an iterable and iterates each iterable in an iterable. 335 | 336 | ## Example 337 | 338 | iex> Impl.concat([1..3, 2..4, 3..5]) |> Impl.to_list() 339 | [1, 2, 3, 2, 3, 4, 3, 4, 5] 340 | """ 341 | @spec concat(iterable) :: iterable 342 | def concat(iterable), do: Iterable.Concatenator.new(iterable) 343 | 344 | @doc """ 345 | Consumes the iterable, returning the number of elements within 346 | 347 | ## Examples 348 | 349 | iex> Impl.count([]) 350 | 0 351 | 352 | iex> Impl.count([1,2,3]) 353 | 3 354 | """ 355 | @spec count(iterable) :: non_neg_integer() 356 | def count(iterable), do: do_count(iterable, 0) 357 | 358 | defp do_count(iterable, so_far) do 359 | case Iterable.next(iterable) do 360 | {:ok, _element, iterable} -> do_count(iterable, so_far + 1) 361 | :done -> so_far 362 | end 363 | end 364 | 365 | @doc """ 366 | Consumes the iterable, returning the number of elements for which `fun` returns a truthy value. 367 | 368 | ## Example 369 | 370 | iex> 1..5 371 | ...> |> Impl.count(&(rem(&1, 2) == 0)) 372 | 2 373 | """ 374 | @spec count(iterable, (element -> as_boolean(any))) :: non_neg_integer() 375 | def count(iterable, fun) when is_function(fun, 1), 376 | do: do_count_matches(iterable, fun, 0) 377 | 378 | defp do_count_matches(iterable, fun, so_far) do 379 | case Iterable.next(iterable) do 380 | {:ok, element, iterable} -> 381 | if fun.(element) do 382 | do_count_matches(iterable, fun, so_far + 1) 383 | else 384 | do_count_matches(iterable, fun, so_far) 385 | end 386 | 387 | :done -> 388 | so_far 389 | end 390 | end 391 | 392 | @doc """ 393 | Creates an iterator that cycles it's elements eternally. 394 | 395 | ## Example 396 | 397 | iex> Impl.cycle(1..3) 398 | ...> |> Impl.take_head(5) 399 | ...> |> Impl.to_list() 400 | [1, 2, 3, 1, 2] 401 | """ 402 | @spec cycle(iterable) :: iterable 403 | def cycle(iterable), do: Iterable.Cycler.new(iterable) 404 | 405 | @doc """ 406 | Creates an iterable that only emits elements if they are different from the previous element. 407 | 408 | The function `fun` maps every element to a term which is used to determine if two elements are duplicates. 409 | 410 | ## Example 411 | 412 | iex> [{1, :a}, {2, :b}, {2, :c}, {1, :a}] 413 | ...> |> Impl.dedup_by(&elem(&1, 0)) 414 | ...> |> Impl.to_list() 415 | [{1, :a}, {2, :b}, {1, :a}] 416 | """ 417 | @spec dedup_by(iterable, (element -> any)) :: iterable 418 | def dedup_by(iterable, fun), do: Iterable.Deduper.new(iterable, fun) 419 | 420 | @doc """ 421 | Creates an iterable that only emits elements if they are different from the previous element. 422 | 423 | ## Example 424 | 425 | iex> Impl.dedup([:a, :a, :b, :c, :b, :c, :c, :d]) 426 | ...> |> Impl.to_list() 427 | [:a, :b, :c, :b, :c, :d] 428 | """ 429 | @spec dedup(iterable) :: iterable 430 | def dedup(iterable), do: Iterable.Deduper.new(iterable) 431 | 432 | @doc """ 433 | Returns a new iterable with every `nth` element in the `iterable` dropped, 434 | starting with the first element. 435 | 436 | ## Examples 437 | 438 | iex> 1..10 439 | ...> |> Impl.drop_every(2) 440 | ...> |> Impl.to_list() 441 | [2, 4, 6, 8, 10] 442 | 443 | iex> 1..12 444 | ...> |> Impl.drop_every(3) 445 | ...> |> Impl.to_list() 446 | [2, 3, 5, 6, 8, 9, 11, 12] 447 | 448 | iex> 1..10 449 | ...> |> Impl.drop_every(0) 450 | ...> |> Impl.to_list() 451 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 452 | 453 | iex> [1, 2, 3] 454 | ...> |> Impl.drop_every(1) 455 | ...> |> Impl.to_list() 456 | [] 457 | """ 458 | @spec drop_every(iterable, non_neg_integer) :: iterable 459 | def drop_every(iterable, nth), do: Iterable.EveryDropper.new(iterable, nth) 460 | 461 | @doc """ 462 | Drops elements at the beginning of `iterable` while fun returns a truthy 463 | value. 464 | 465 | ## Example 466 | 467 | iex> [1, 2, 3, 2, 1] 468 | ...> |> Impl.drop_while(&(&1 < 3)) 469 | ...> |> Impl.to_list() 470 | [3, 2, 1] 471 | """ 472 | @spec drop_while(iterable, predicate) :: iterable 473 | def drop_while(iterable, predicate), do: Iterable.WhileDropper.new(iterable, predicate) 474 | 475 | @doc """ 476 | Creates an iterable which drops the first `how_many` elements. 477 | 478 | ## Examples 479 | 480 | iex> Impl.drop([1, 2, 3], 2) 481 | ...> |> Impl.to_list() 482 | [3] 483 | 484 | iex> Impl.drop([1, 2, 3], 0) 485 | ...> |> Impl.to_list() 486 | [1, 2, 3] 487 | 488 | iex> Impl.drop([1, 2, 3], -2) 489 | ...> |> Impl.to_list() 490 | [1] 491 | """ 492 | @spec drop(iterable, non_neg_integer()) :: iterable 493 | def drop(iterable, how_many) when is_integer(how_many), do: do_drop(iterable, how_many) 494 | 495 | defp do_drop(iterable, 0), do: iterable 496 | 497 | defp do_drop(iterable, how_many) when how_many > 0, 498 | do: Iterable.HeadDropper.new(iterable, how_many) 499 | 500 | defp do_drop(iterable, how_many) when how_many < 0, 501 | do: Iterable.TailDropper.new(iterable, 0 - how_many) 502 | 503 | @doc ~S""" 504 | Consumes the iterable and applies `fun` to each element. 505 | 506 | Primarily used for side-effects. 507 | 508 | Always returns `:done`. 509 | 510 | ## Example 511 | 512 | ```elixir 513 | Impl.each([1, 2, 3], &IO.puts("#{&1}")) 514 | "1" 515 | "2" 516 | "3" 517 | #=> :done 518 | ``` 519 | """ 520 | @spec each(iterable, (element -> any)) :: :done 521 | def each(iterable, fun) when is_function(fun, 1) do 522 | with {:ok, element, iterable} <- Iterable.next(iterable) do 523 | fun.(element) 524 | each(iterable, fun) 525 | end 526 | end 527 | 528 | @doc """ 529 | Determines if the iterable is empty. 530 | 531 | ## Example 532 | 533 | iex> Impl.empty?([]) 534 | true 535 | 536 | iex> Impl.empty?(1..20) 537 | false 538 | """ 539 | @spec empty?(iterable) :: boolean 540 | def empty?(iterable) do 541 | case Iterable.peek(iterable) do 542 | {:ok, _element, _iter} -> false 543 | :done -> true 544 | end 545 | end 546 | 547 | @doc """ 548 | Creates an iterable which drops elements for which `predicate` doesn't return true. 549 | 550 | ## Example 551 | 552 | iex> Impl.filter([1, 2, 3, 4], &(rem(&1, 2) == 1)) 553 | ...> |> Impl.to_list() 554 | [1, 3] 555 | """ 556 | @spec filter(iterable, predicate) :: iterable 557 | def filter(iterable, predicate) when is_function(predicate, 1), 558 | do: Iterable.Filterer.new(iterable, predicate) 559 | 560 | @doc """ 561 | Returns the index of the first element in the iterable which matches `predicate`. 562 | 563 | ## Examples 564 | 565 | iex> Impl.find_index([1, 2, 3, 4], &(&1 > 2)) 566 | {:ok, 2, [4]} 567 | 568 | iex> Impl.find_index([1, 2, 3, 4], &(&1 > 4)) 569 | :done 570 | """ 571 | @spec find_index(iterable, predicate) :: {:ok, non_neg_integer(), iterable} | :done 572 | def find_index(iterable, predicate), do: do_find_index(iterable, predicate, 0) 573 | 574 | defp do_find_index(iterable, predicate, index) do 575 | with {:ok, element, iterable} <- Iterable.next(iterable) do 576 | if predicate.(element) == true do 577 | {:ok, index, iterable} 578 | else 579 | do_find_index(iterable, predicate, index + 1) 580 | end 581 | end 582 | end 583 | 584 | @doc """ 585 | Returns the first truthy value returned by `fun`. 586 | 587 | ## Example 588 | 589 | iex> Impl.find_value([1, 2, 3, 4], fn 590 | ...> i when i > 2 -> i * 2 591 | ...> _ -> nil 592 | ...> end) 593 | {:ok, 6, [4]} 594 | """ 595 | @spec find_value(iterable, (element -> result)) :: {:ok, result, iterable} | :done 596 | when result: any 597 | def find_value(iterable, fun) do 598 | with {:ok, element, iterable} <- Iterable.next(iterable) do 599 | result = fun.(element) 600 | 601 | if result do 602 | {:ok, result, iterable} 603 | else 604 | find_value(iterable, fun) 605 | end 606 | end 607 | end 608 | 609 | @doc """ 610 | Searches for the first element in the iterable which matches `predicate`. 611 | 612 | ## Example 613 | 614 | iex> Impl.find([1, 2, 3, 4], &(&1 > 2)) 615 | {:ok, 3, [4]} 616 | 617 | iex> Impl.find([1, 2, 3, 4], &(&1 > 4)) 618 | :done 619 | """ 620 | @spec find(iterable, predicate) :: {:ok, element} | :done 621 | def find(iterable, predicate) do 622 | with {:ok, element, iterable} <- Iterable.next(iterable) do 623 | if predicate.(element) == true do 624 | {:ok, element, iterable} 625 | else 626 | find(iterable, predicate) 627 | end 628 | end 629 | end 630 | 631 | @doc """ 632 | Creates an iterable which works like `map/2` but flattens nested iterables. 633 | 634 | ## Example 635 | 636 | iex> [:a, :b, :c] 637 | ...> |> Impl.flat_map(&[&1, &1]) 638 | ...> |> Impl.to_list() 639 | [:a, :a, :b, :b, :c, :c] 640 | """ 641 | @spec flat_map(iterable, (element -> iterable | element)) :: 642 | iterable 643 | def flat_map(iterable, fun) when is_function(fun, 1), 644 | do: Iterable.FlatMapper.new(iterable, fun) 645 | 646 | @doc """ 647 | Creates an iterable which flattens nested iterables. 648 | 649 | ## Example 650 | 651 | iex> Impl.flatten([[1, 2], [3, [4, [5, 6]]]]) 652 | ...> |> Impl.to_list() 653 | [1, 2, 3, 4, 5, 6] 654 | """ 655 | @spec flatten(iterable) :: iterable 656 | def flatten(iterable), do: Iterable.Flattener.new(iterable) 657 | 658 | @doc """ 659 | Creates a new iterable which applies `mapper` on every `nth` element of the 660 | iterable, starting with the first element. 661 | 662 | The first element is always mapped unless `nth` is `0`. 663 | 664 | ## Examples 665 | 666 | iex> Impl.map_every(1..10, 2, fn x -> x + 1000 end) 667 | ...> |> Impl.to_list() 668 | [1001, 2, 1003, 4, 1005, 6, 1007, 8, 1009, 10] 669 | 670 | iex> Impl.map_every(1..10, 3, fn x -> x + 1000 end) 671 | ...> |> Impl.to_list() 672 | [1001, 2, 3, 1004, 5, 6, 1007, 8, 9, 1010] 673 | 674 | iex> Impl.map_every(1..5, 0, fn x -> x + 1000 end) 675 | ...> |> Impl.to_list() 676 | [1, 2, 3, 4, 5] 677 | 678 | iex> Impl.map_every([1, 2, 3], 1, fn x -> x + 1000 end) 679 | ...> |> Impl.to_list() 680 | [1001, 1002, 1003] 681 | """ 682 | @spec map_every(iterable, non_neg_integer, (element -> new_element)) :: iterable 683 | when new_element: any 684 | def map_every(iterable, nth, mapper) 685 | when is_integer(nth) and nth >= 0 and is_function(mapper, 1), 686 | do: Iterable.EveryMapper.new(iterable, nth, mapper) 687 | 688 | @doc """ 689 | Creates a new iterable which applies `mapper` to each element and using it's 690 | result as the new element value. 691 | 692 | ## Example 693 | 694 | iex> Impl.map([1, 2, 3], &(&1 * &1)) 695 | ...> |> Impl.to_list() 696 | [1, 4, 9] 697 | """ 698 | @spec map(iterable, (element -> new_element)) :: iterable when new_element: any 699 | def map(iterable, mapper) when is_function(mapper, 1), do: Iterable.Mapper.new(iterable, mapper) 700 | 701 | @doc """ 702 | Returns the maximal element in the `iterable` as calculated by `mapper`. 703 | 704 | ## Example 705 | 706 | iex> Impl.max_by(["a", "aa", "aaa"], &String.length/1, &>=/2) 707 | {:ok, "aaa"} 708 | 709 | iex> Impl.max_by([], &String.length/1, &>=/2) 710 | :done 711 | """ 712 | @spec max_by(iterable, (element -> new_element), (new_element, new_element -> boolean)) :: 713 | {:ok, element} | :done 714 | when new_element: element 715 | def max_by(iterable, mapper, sorter) do 716 | with {:ok, element, iterable} <- Iterable.next(iterable) do 717 | do_max_by(iterable, mapper, sorter, element, mapper.(element)) 718 | end 719 | end 720 | 721 | defp do_max_by(iterable, mapper, sorter, current_max_element, current_max_mapped) do 722 | case Iterable.next(iterable) do 723 | {:ok, element, iterable} -> 724 | element_mapped = mapper.(element) 725 | 726 | if sorter.(element_mapped, current_max_mapped) do 727 | do_max_by(iterable, mapper, sorter, element, element_mapped) 728 | else 729 | do_max_by(iterable, mapper, sorter, current_max_element, current_max_mapped) 730 | end 731 | 732 | :done -> 733 | {:ok, current_max_element} 734 | end 735 | end 736 | 737 | @doc """ 738 | Returns the maximal element in the `iterable` according to Erlang's term ordering. 739 | 740 | ## Examples 741 | 742 | iex> Impl.max([1, 3, 2], &>=/2) 743 | {:ok, 3} 744 | 745 | iex> Impl.max([], &>=/2) 746 | :done 747 | """ 748 | @spec max(iterable, (element, element -> boolean)) :: {:ok, element} | :done 749 | def max(iterable, sorter) do 750 | with {:ok, element, iterable} <- Iterable.next(iterable) do 751 | do_max(iterable, sorter, element) 752 | end 753 | end 754 | 755 | defp do_max(iterable, sorter, current_max) do 756 | case Iterable.next(iterable) do 757 | {:ok, element, iterable} -> 758 | if sorter.(element, current_max) do 759 | do_max(iterable, sorter, element) 760 | else 761 | do_max(iterable, sorter, current_max) 762 | end 763 | 764 | :done -> 765 | {:ok, current_max} 766 | end 767 | end 768 | 769 | @doc """ 770 | Is the element a member of the iterable? 771 | """ 772 | @spec member?(iterable, element) :: boolean 773 | def member?(iterable, element), do: any?(iterable, &(&1 == element)) 774 | 775 | @doc """ 776 | Returns the minimal element in the `iterable` as calculated by `mapper`. 777 | 778 | ## Example 779 | 780 | iex> Impl.min_by(["a", "aa", "aaa"], &String.length/1, &<=/2) 781 | {:ok, "a"} 782 | 783 | iex> Impl.min_by([], &String.length/1, &<=/2) 784 | :done 785 | """ 786 | @spec min_by(iterable, (element -> new_element), (new_element, new_element -> boolean)) :: 787 | {:ok, element} | :done 788 | when new_element: element 789 | def min_by(iterable, mapper, sorter) do 790 | with {:ok, element, iterable} <- Iterable.next(iterable) do 791 | do_min_by(iterable, mapper, sorter, element, mapper.(element)) 792 | end 793 | end 794 | 795 | defp do_min_by(iterable, mapper, sorter, current_min_element, current_min_mapped) do 796 | case Iterable.next(iterable) do 797 | {:ok, element, iterable} -> 798 | element_mapped = mapper.(element) 799 | 800 | if sorter.(element_mapped, current_min_mapped) do 801 | do_min_by(iterable, mapper, sorter, element, element_mapped) 802 | else 803 | do_min_by(iterable, mapper, sorter, current_min_element, current_min_mapped) 804 | end 805 | 806 | :done -> 807 | {:ok, current_min_element} 808 | end 809 | end 810 | 811 | @doc """ 812 | Finds the minimal and maximal elements in the iterable. 813 | 814 | ## Example 815 | 816 | iex> Impl.min_max(1..12) 817 | {:ok, 1, 12} 818 | 819 | iex> Impl.min_max([]) 820 | :done 821 | """ 822 | @spec min_max(iterable) :: {:ok, element, element} | :done 823 | def min_max(iterable) do 824 | case Iterable.next(iterable) do 825 | {:ok, element, iterable} -> do_min_max(iterable, %{min: element, max: element}) 826 | :done -> :done 827 | end 828 | end 829 | 830 | defp do_min_max(iterable, state) do 831 | case Iterable.next(iterable) do 832 | {:ok, element, iterable} when element > state.max -> 833 | do_min_max(iterable, %{state | max: element}) 834 | 835 | {:ok, element, iterable} when element < state.min -> 836 | do_min_max(iterable, %{state | min: element}) 837 | 838 | :done -> 839 | {:ok, state.min, state.max} 840 | end 841 | end 842 | 843 | @doc """ 844 | Returns the minimal element in the `iterable` according to Erlang's term ordering. 845 | 846 | ## Examples 847 | 848 | iex> Impl.min([1, 3, 2], &<=/2) 849 | {:ok, 1} 850 | 851 | iex> Impl.min([], &<=/2) 852 | :done 853 | """ 854 | @spec min(iterable, (element, element -> boolean)) :: {:ok, element} | :done 855 | def min(iterable, sorter) do 856 | with {:ok, element, iterable} <- Iterable.next(iterable) do 857 | do_min(iterable, sorter, element) 858 | end 859 | end 860 | 861 | defp do_min(iterable, sorter, current_min) do 862 | case Iterable.next(iterable) do 863 | {:ok, element, iterable} -> 864 | if sorter.(element, current_min) do 865 | do_min(iterable, sorter, element) 866 | else 867 | do_min(iterable, sorter, current_min) 868 | end 869 | 870 | :done -> 871 | {:ok, current_min} 872 | end 873 | end 874 | 875 | @doc """ 876 | Peeks at the first element of the iterable, without consuming it. 877 | 878 | > #### Warning {: .warning} 879 | > Many iterables cannot be peeked, so this function simulates peeking by 880 | > consuming an element from the iterable and returning a new iterable which 881 | > pushes that element back on to the front. 882 | 883 | ## Example 884 | 885 | iex> {:ok, :a, iterable} = Impl.peek([:a, :b, :c]) 886 | ...> Impl.to_list(iterable) 887 | [:a, :b, :c] 888 | """ 889 | @spec peek(iterable) :: {:ok, element, iterable} | :done 890 | def peek(iterable), 891 | do: iterable |> Iterable.Peeker.new() |> Iterable.peek() 892 | 893 | @doc """ 894 | Peeks at the first `how_many` elements of the iterable, without consuming them. 895 | 896 | > #### Warning {: .warning} 897 | > Many iterables cannot be peeked, so this function simulates peeking by 898 | > consuming elements from the iterable and returning a new iterable which 899 | > pushes those elements back on to the front. 900 | 901 | ## Example 902 | 903 | iex> {:ok, [:a, :b, :c], 3, iterable} = Impl.peek([:a, :b, :c, :d], 3) 904 | ...> Impl.to_list(iterable) 905 | [:a, :b, :c, :d] 906 | """ 907 | @spec peek(iterable, how_many :: pos_integer) :: 908 | {:ok, [element], non_neg_integer, iterable} | :done 909 | def peek(iterable, how_many), 910 | do: iterable |> Iterable.Peeker.new() |> Iterable.peek(how_many) 911 | 912 | @doc """ 913 | Creates a new iterable which places `element` at the beginning of the iterable. 914 | 915 | ## Example 916 | 917 | iex> 1..5 918 | ...> |> Impl.prepend(6) 919 | ...> |> Impl.to_list() 920 | [6, 1, 2, 3, 4, 5] 921 | """ 922 | @spec prepend(iterable, element) :: iterable 923 | def prepend(iterable, element), do: Iterable.Prepender.new(iterable, element) 924 | 925 | @doc """ 926 | Creates a new iterable which places `separator` between adjacent items of the original iterable. 927 | 928 | ## Example 929 | 930 | iex> Impl.intersperse([:a, :b, :c], :wat) 931 | ...> |> Impl.to_list() 932 | [:a, :wat, :b, :wat, :c] 933 | """ 934 | @spec intersperse(iterable, any) :: iterable 935 | def intersperse(iterable, separator), do: Iterable.Intersperser.new(iterable, separator) 936 | 937 | @doc """ 938 | Creates an iterable starting at the same point, but stepping by `how_many` each iteration. 939 | 940 | ## Example 941 | 942 | iex> [0, 1, 2, 3, 4, 5] 943 | ...> |> Impl.step_by(2) 944 | ...> |> Impl.to_list() 945 | [0, 2, 4] 946 | """ 947 | @spec step_by(iterable, non_neg_integer()) :: iterable 948 | def step_by(iterable, step_size), do: Iterable.Stepper.new(iterable, step_size) 949 | 950 | @doc """ 951 | Collects the first `how_many` elements into a new iterable and returns it 952 | along with the advanced initial iterable. 953 | 954 | This is very much like `take/2` except that it returns the remaining iterable 955 | so that it can be called repeatedly. 956 | 957 | ## Example 958 | 959 | iex> iter = 1..9 960 | ...> {:ok, [1, 2, 3], iter} = Impl.take_chunk(iter, 3) 961 | ...> {:ok, [4, 5, 6], iter} = Impl.take_chunk(iter, 3) 962 | ...> Impl.to_list(iter) 963 | [7, 8, 9] 964 | """ 965 | @spec take_chunk(iterable, pos_integer) :: {:ok, iterable, iterable} | {:done, iterable} 966 | def take_chunk(iterable, how_many) when is_integer(how_many) and how_many > 0, 967 | do: do_take_chunk(iterable, [], how_many) 968 | 969 | defp do_take_chunk(iterable, result, 0), do: {:ok, :lists.reverse(result), iterable} 970 | 971 | defp do_take_chunk(iterable, result, how_many) do 972 | case Iterable.next(iterable) do 973 | {:ok, element, iterable} -> do_take_chunk(iterable, [element | result], how_many - 1) 974 | :done -> {:done, :lists.reverse(result)} 975 | end 976 | end 977 | 978 | @doc """ 979 | Creates an iterable which takes the first `how_many` elements. 980 | 981 | ## Example 982 | 983 | iex> Impl.take_head(1..5, 0) 984 | ...> |> Impl.to_list() 985 | [] 986 | 987 | iex> Impl.take_head(1..5, 3) 988 | ...> |> Impl.to_list() 989 | [1, 2, 3] 990 | 991 | """ 992 | @spec take_head(iterable, non_neg_integer()) :: iterable 993 | def take_head(iterable, how_many) when is_integer(how_many), 994 | do: do_take_head(iterable, how_many) 995 | 996 | defp do_take_head(_iterable, 0), do: Iterable.Empty.new() 997 | 998 | defp do_take_head(iterable, how_many) when how_many > 0, 999 | do: Iterable.HeadTaker.new(iterable, how_many) 1000 | 1001 | @doc """ 1002 | Creates an iterable which takes the last `how_many` elements. 1003 | 1004 | ## Example 1005 | 1006 | iex> Impl.take_tail(1..5, 0) 1007 | ...> |> Impl.to_list() 1008 | [] 1009 | 1010 | iex> Impl.take_tail(1..5, 3) 1011 | ...> |> Impl.to_list() 1012 | [3, 4, 5] 1013 | 1014 | """ 1015 | @spec take_tail(iterable, non_neg_integer()) :: iterable 1016 | def take_tail(iterable, how_many) when is_integer(how_many), 1017 | do: do_take_tail(iterable, how_many) 1018 | 1019 | defp do_take_tail(_iterable, 0), do: Iterable.Empty.new() 1020 | 1021 | defp do_take_tail(iterable, how_many) when how_many > 0, 1022 | do: Iterable.TailTaker.new(iterable, how_many) 1023 | 1024 | @doc """ 1025 | Creates an iterable which emits elements until `predicate` returns `false`. 1026 | 1027 | ## Example 1028 | 1029 | iex> [1, 2, 3] 1030 | ...> |> Impl.take_while(&(&1 < 3)) 1031 | ...> |> Impl.to_list() 1032 | [1, 2] 1033 | """ 1034 | @spec take_while(iterable, predicate) :: iterable 1035 | def take_while(iterable, predicate) when is_function(predicate, 1), 1036 | do: Iterable.WhileTaker.new(iterable, predicate) 1037 | 1038 | @doc """ 1039 | Convert the iterable into a list. 1040 | """ 1041 | @spec to_list(iterable) :: [element] 1042 | def to_list(iterable), do: do_to_list(iterable, []) 1043 | 1044 | defp do_to_list(iterable, result) do 1045 | case Iterable.next(iterable) do 1046 | {:ok, element, iterable} -> do_to_list(iterable, [element | result]) 1047 | :done -> :lists.reverse(result) 1048 | end 1049 | end 1050 | 1051 | @doc """ 1052 | Creates an iterable that only emits unique elements. 1053 | 1054 | ## Example 1055 | 1056 | iex> Impl.uniq([:a, :a, :b, :c, :b, :c, :c, :d]) 1057 | ...> |> Impl.to_list() 1058 | [:a, :b, :c, :d] 1059 | """ 1060 | @spec uniq(iterable) :: iterable 1061 | def uniq(iterable), do: Iterable.Uniquer.new(iterable) 1062 | 1063 | @doc """ 1064 | Creates an iterable which emits the current iteration count as well as the 1065 | next value. 1066 | 1067 | This is analogous to `Enum.with_index/1` except that counting starts from the 1068 | beginning of the iterable, meaning you can convert an iterable into an 1069 | enumerator after consuming some if it. 1070 | 1071 | ## Examples 1072 | 1073 | iex> Impl.with_index([:a, :b, :c]) 1074 | ...> |> Impl.to_list() 1075 | [a: 0, b: 1, c: 2] 1076 | 1077 | iex> [:a, :b, :c, :d] 1078 | ...> |> Impl.drop(2) 1079 | ...> |> Impl.with_index() 1080 | ...> |> Impl.to_list() 1081 | [c: 0, d: 1] 1082 | """ 1083 | @spec with_index(iterable) :: iterable 1084 | def with_index(iterable), do: Iterable.WithIndexer.new(iterable) 1085 | 1086 | @doc """ 1087 | Zips corresponding elements from a finite collection of iterables into one iterable of results as computed by `zipper`. 1088 | 1089 | ## Example 1090 | 1091 | iex> Impl.zip([1..3, 4..6, 7..9], &List.to_tuple/1) 1092 | ...> |> Impl.to_list() 1093 | [{1, 4, 7}, {2, 5, 8}, {3, 6, 9}] 1094 | """ 1095 | @spec zip(iterable, ([element] -> any)) :: iterable 1096 | def zip(iterable, zipper), do: Iterable.Zipper.new(iterable, zipper) 1097 | end 1098 | -------------------------------------------------------------------------------- /lib/iter.ex: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Alembic Pty Ltd 2 | # SPDX-FileCopyrightText: 2024 iterex contributors 3 | # 4 | # SPDX-License-Identifier: MIT 5 | 6 | defmodule Iter do 7 | @moduledoc """ 8 | Functions for working with iterators. 9 | 10 | An iterator is a struct that wraps any value which implements the 11 | `Iter.Iterable` protocol. Lists, maps and ranges are all common data types 12 | which can be used as iterators. 13 | 14 | ## Explicit conversion 15 | 16 | Whilst the `Iter.Iterable` protocol is designed to work with many different 17 | types, you must explicitly convert your iterables into an iterator using 18 | `Iter.from/1`. This allows for easy pattern matching of iterators as well as 19 | for default implementations of Elixir's `Enum` and `Collectable` protocols. 20 | 21 | Any value passed to `Iter.from/1` must implement the `Iter.IntoIterable` 22 | protocol. 23 | 24 | ## Lazy by default 25 | 26 | Like Elixir's `Stream`, an `Iter` is lazy by default. Any function which 27 | returns an `Iter.t` does so by simply composing iterables on top of each 28 | other. No iteration is performed until it is needed, and then only the 29 | minimum amount needed to return the result. 30 | """ 31 | 32 | defstruct iterable: nil 33 | alias Iter.{IntoIterable, Iterable} 34 | 35 | @type t :: %__MODULE__{iterable: Iterable.t()} 36 | @type element :: any 37 | @type predicate :: (element -> as_boolean(any)) 38 | @type mapper :: (element -> any) 39 | @type sorter :: (element, element -> as_boolean(any)) 40 | 41 | @doc """ 42 | Is the passed value an iterator? 43 | """ 44 | @spec is_iter(any) :: Macro.output() 45 | defguard is_iter(iter) when is_struct(iter, __MODULE__) 46 | 47 | @doc """ 48 | Returns `true` if all elements in the iterator are truthy. 49 | 50 | ## Examples 51 | 52 | iex> [1, 2, false] 53 | ...> |> Iter.from() 54 | ...> |> Iter.all?() 55 | false 56 | 57 | iex> [1, 2, nil] 58 | ...> |> Iter.from() 59 | ...> |> Iter.all?() 60 | false 61 | 62 | iex> [1, 2, 3] 63 | ...> |> Iter.from() 64 | ...> |> Iter.all?() 65 | true 66 | """ 67 | @spec all?(t) :: boolean 68 | def all?(iter), do: all?(iter, &(&1 not in [nil, false])) 69 | 70 | @doc """ 71 | Returns `true` if `fun.(element)` is truthy for all elements in the iterator. 72 | 73 | Iterates over the iterator and invokes `fun` on each element. If `fun` ever 74 | returns a falsy value (`false` or `nil`), iteration stops immediately and 75 | `false` is returned. Otherwise `true` is returned. 76 | 77 | ## Examples 78 | 79 | iex> [2, 4, 6] 80 | ...> |> Iter.from() 81 | ...> |> Iter.all?(&(rem(&1, 2) == 0)) 82 | true 83 | 84 | iex> [2, 3, 4] 85 | ...> |> Iter.from() 86 | ...> |> Iter.all?(&(rem(&1, 2) == 0)) 87 | false 88 | 89 | iex> [] 90 | ...> |> Iter.from() 91 | ...> |> Iter.all?() 92 | true 93 | """ 94 | @spec all?(t, predicate) :: boolean 95 | def all?(iter, predicate) when is_iter(iter) and is_function(predicate, 1), 96 | do: Iterable.all?(iter.iterable, predicate) 97 | 98 | @doc """ 99 | Returns `true` if at least one element in the iterator is truthy. 100 | 101 | When an element is a truthy value (neither `false` nor `nil`) iteration stops 102 | immediately and `true` is returned. In all other cases `false` is returned. 103 | 104 | ## Examples 105 | 106 | iex> [false, false, false] 107 | ...> |> Iter.from() 108 | ...> |> Iter.any?() 109 | false 110 | 111 | iex> [false, true, false] 112 | ...> |> Iter.from() 113 | ...> |> Iter.any?() 114 | true 115 | 116 | iex> [] 117 | ...> |> Iter.from() 118 | ...> |> Iter.any?() 119 | false 120 | """ 121 | @spec any?(t) :: boolean 122 | def any?(iter), do: any?(iter, &(&1 not in [nil, false])) 123 | 124 | @doc """ 125 | Returns `true` if `fun.(element)` is truthy for at least one element in the 126 | iterator. 127 | 128 | Consumes the iterator and invokes `fun` on each element. When an invocation 129 | of `fun` returns a truthy value (neither `false` nor `nil`) iteration stops 130 | immediately and `true` is returned. In all other cases `false` is returned. 131 | 132 | ## Examples 133 | 134 | iex> [2, 4, 6] 135 | ...> |> Iter.from() 136 | ...> |> Iter.any?(&(rem(&1, 2) == 1)) 137 | false 138 | 139 | iex> [2, 3, 4] 140 | ...> |> Iter.from() 141 | ...> |> Iter.any?(&(rem(&1, 2) == 1)) 142 | true 143 | 144 | iex> [] 145 | ...> |> Iter.from() 146 | ...> |> Iter.any?(&(rem(&1, 2) == 1)) 147 | false 148 | """ 149 | @spec any?(t, predicate) :: boolean 150 | def any?(iter, predicate) when is_iter(iter) and is_function(predicate, 1), 151 | do: Iterable.any?(iter.iterable, predicate) 152 | 153 | @doc """ 154 | Append a new element to the end of the iterable. 155 | 156 | ## Example 157 | 158 | iex> 1..3 159 | ...> |> Iter.from() 160 | ...> |> Iter.append(4) 161 | ...> |> Iter.to_list() 162 | [1, 2, 3, 4] 163 | """ 164 | @spec append(t, element) :: t 165 | def append(iter, element) when is_iter(iter), 166 | do: iter.iterable |> Iterable.append(element) |> new() 167 | 168 | @doc """ 169 | Return the element `index` items from the beginning of the iterator. 170 | 171 | Works by advancing the iterator the specified number of elements and then 172 | returning the element requested and an iterator of the remaining elements. 173 | 174 | ## Return values 175 | 176 | - `{:ok, element, new_iterator}` - the element requested and the iterator of 177 | the remaining elements. 178 | - `:done` - the iterator was exhausted before the element was found. 179 | 180 | ## Examples 181 | 182 | iex> 10..20 183 | ...> |> Iter.from() 184 | ...> |> Iter.at(5) 185 | {:ok, 15, Iter.from(16..20)} 186 | """ 187 | @spec at(t, non_neg_integer) :: {:ok, element, t} | :done 188 | def at(iter, index) when is_iter(iter) and is_integer(index) and index >= 0 do 189 | with {:ok, element, iterable} <- Iterable.at(iter.iterable, index) do 190 | {:ok, element, new(iterable)} 191 | end 192 | end 193 | 194 | @doc """ 195 | Chunks the iterator by buffering elements for which `fun` returns the same 196 | value. 197 | 198 | Elements are only emitted when `fun` returns a new value or `iterable` is 199 | exhausted. 200 | 201 | ## Examples 202 | 203 | iex> [1, 2, 2, 3, 4, 4, 6, 7, 7] 204 | ...> |> Iter.from() 205 | ...> |> Iter.chunk_by(&(rem(&1, 2) == 1)) 206 | ...> |> Iter.to_list() 207 | [[1], [2, 2], [3], [4, 4, 6], [7, 7]] 208 | """ 209 | @spec chunk_by(t, (element -> any)) :: t 210 | def chunk_by(iter, fun) when is_iter(iter) and is_function(fun, 1) do 211 | iter.iterable 212 | |> Iterable.chunk_by(fun) 213 | |> new() 214 | end 215 | 216 | @doc """ 217 | Shortcut to `chunk_every(iterable, count, count)`. 218 | """ 219 | @spec chunk_every(t, pos_integer) :: t 220 | def chunk_every(iter, count) when is_iter(iter) and is_integer(count) and count > 0 do 221 | iter.iterable 222 | |> Iterable.chunk_every(count, count, empty()) 223 | |> Iterable.map(&new/1) 224 | |> new() 225 | end 226 | 227 | @doc """ 228 | Consumes the iterator in chunks, containing `count` elements each, where each 229 | new chunk steps `step` elements into the iterator. 230 | 231 | `step` is optional and, if not passed defaults to `count`, i.e. chunks do not 232 | overlap. Chunking will stop as soon as the iterable is exhausted or when we 233 | emit an incomplete chunk. 234 | 235 | If the last chunk does not have `chunk` elements to fill the chunk, elements 236 | are taken from `leftover` to fill in the chunk, if `leftover` does not have 237 | enough elements to fill the chunk, then a partial chunk is returned with less 238 | than `count` elements. 239 | 240 | If `:discard` is given in `leftover` the last chunk is discarded unless it has 241 | exactly `count` elements. 242 | 243 | ## Examples 244 | 245 | iex> [a, b, c] = [1, 2, 3, 4, 5, 6] 246 | ...> |> Iter.from() 247 | ...> |> Iter.chunk_every(2) 248 | ...> |> Iter.to_list() 249 | iex> Iter.to_list(a) 250 | [1, 2] 251 | iex> Iter.to_list(b) 252 | [3, 4] 253 | iex> Iter.to_list(c) 254 | [5, 6] 255 | 256 | iex> [a, b] = [1, 2, 3, 4, 5, 6] 257 | ...> |> Iter.from() 258 | ...> |> Iter.chunk_every(3, 2, :discard) 259 | ...> |> Iter.to_list() 260 | iex> Iter.to_list(a) 261 | [1, 2, 3] 262 | iex> Iter.to_list(b) 263 | [3, 4, 5] 264 | 265 | iex> [a, b, c] = [1, 2, 3, 4, 5, 6] 266 | ...> |> Iter.from() 267 | ...> |> Iter.chunk_every(3, 2, [7] |> Iter.from()) 268 | ...> |> Iter.to_list() 269 | iex> Iter.to_list(a) 270 | [1, 2, 3] 271 | iex> Iter.to_list(b) 272 | [3, 4, 5] 273 | iex> Iter.to_list(c) 274 | [5, 6, 7] 275 | 276 | iex> [a, b] = [1, 2, 3, 4, 5, 6] 277 | ...> |> Iter.from() 278 | ...> |> Iter.chunk_every(3, 3, [] |> Iter.from()) 279 | ...> |> Iter.to_list() 280 | iex> Iter.to_list(a) 281 | [1, 2, 3] 282 | iex> Iter.to_list(b) 283 | [4, 5, 6] 284 | 285 | iex> [a, b] = [1, 2, 3, 4] 286 | ...> |> Iter.from() 287 | ...> |> Iter.chunk_every(3, 3, [0] |> Iter.from() |> Iter.cycle()) 288 | ...> |> Iter.to_list() 289 | iex> Iter.to_list(a) 290 | [1, 2, 3] 291 | iex> Iter.to_list(b) 292 | [4, 0, 0] 293 | """ 294 | @spec chunk_every(t, pos_integer, pos_integer, t | :discard) :: Enumerable.t() 295 | def chunk_every(iter, count, step), do: chunk_every(iter, count, step, empty()) 296 | 297 | def chunk_every(iter, count, step, :discard) 298 | when is_iter(iter) and is_integer(count) and is_integer(step) and count > 0 and step > 0 do 299 | iter.iterable 300 | |> Iterable.chunk_every(count, step, :discard) 301 | |> Iterable.map(&new/1) 302 | |> new() 303 | end 304 | 305 | def chunk_every(iter, count, step, leftover) 306 | when is_iter(iter) and is_iter(leftover) and is_integer(count) and is_integer(step) and 307 | count > 0 and step > 0 do 308 | iter.iterable 309 | |> Iterable.chunk_every(count, step, leftover.iterable) 310 | |> Iterable.map(&new/1) 311 | |> new() 312 | end 313 | 314 | @doc """ 315 | Chunks the iterator with fine grained control of when every chunk is emitted. 316 | 317 | `chunk_fun` receives the current element and the accumulator and must return 318 | `{:cont, element, acc}` to emit the given chunk and continue with accumulator 319 | or `{:cont, acc}` to not emit any chunk and continue with the return 320 | accumulator. 321 | 322 | `after_fun` is invoked when iteration is done and must also return `{:cont, 323 | element, acc}` or `{:cont, acc}`. 324 | 325 | ## Examples 326 | 327 | iex> chunk_fun = fn element, acc -> 328 | ...> if rem(element, 2) == 0 do 329 | ...> {:cont, Enum.reverse([element | acc]), []} 330 | ...> else 331 | ...> {:cont, [element | acc]} 332 | ...> end 333 | ...> end 334 | iex> after_fun = fn 335 | ...> [] -> {:cont, []} 336 | ...> acc -> {:cont, Enum.reverse(acc), []} 337 | ...> end 338 | iex> 1..10 339 | ...> |> Iter.from() 340 | ...> |> Iter.chunk_while([], chunk_fun, after_fun) 341 | ...> |> Iter.to_list() 342 | [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]] 343 | """ 344 | @spec chunk_while( 345 | t, 346 | acc, 347 | (element, acc -> {:cont, chunk, acc} | {:cont, acc} | {:halt, acc}), 348 | (acc -> {:cont, chunk, acc} | {:cont, acc}) 349 | ) :: t 350 | when acc: any, chunk: any 351 | def chunk_while(iter, acc, chunk_fun, after_fun) 352 | when is_iter(iter) and is_function(chunk_fun, 2) and is_function(after_fun, 1) do 353 | iter.iterable 354 | |> Iterable.chunk_while(acc, chunk_fun, after_fun) 355 | |> new() 356 | end 357 | 358 | @doc """ 359 | Creates an iterator that concatenates an iterator of iterators. 360 | 361 | ## Example 362 | 363 | iex> [Iter.from(1..2), Iter.from(3..4)] 364 | ...> |> Iter.from() 365 | ...> |> Iter.concat() 366 | ...> |> Iter.to_list() 367 | [1, 2, 3, 4] 368 | """ 369 | @spec concat(t) :: t 370 | def concat(iter) when is_iter(iter) do 371 | iter.iterable 372 | |> Iterable.map(&IntoIterable.into_iterable/1) 373 | |> Iterable.concat() 374 | |> new() 375 | end 376 | 377 | @doc """ 378 | Creates an iterator that iterates the first argument, followed by the second argument. 379 | 380 | ## Example 381 | 382 | iex> lhs = Iter.from(1..3) 383 | ...> rhs = Iter.from(4..6) 384 | ...> Iter.concat(lhs, rhs) |> Iter.to_list() 385 | [1, 2, 3, 4, 5, 6] 386 | """ 387 | @spec concat(t, t) :: t 388 | def concat(lhs, rhs) when is_iter(lhs) and is_iter(rhs) do 389 | [lhs.iterable, rhs.iterable] 390 | |> Iterable.concat() 391 | |> new() 392 | end 393 | 394 | @doc """ 395 | Counts the elements in iterator stopping at `limit`. 396 | 397 | ## Examples 398 | 399 | iex> 1..20 400 | ...> |> Iter.from() 401 | ...> |> Iter.count_until(5) 402 | {:ok, 5, Iter.from(6..20)} 403 | 404 | iex> 1..3 405 | ...> |> Iter.from() 406 | ...> |> Iter.count_until(5) 407 | {:ok, 3, Iter.empty()} 408 | 409 | iex> [] 410 | ...> |> Iter.from() 411 | ...> |> Iter.count_until(5) 412 | {:ok, 0, Iter.empty()} 413 | """ 414 | @spec count_until(t, pos_integer) :: {:ok, non_neg_integer, t} 415 | def count_until(iter, limit) when is_iter(iter) and is_integer(limit) and limit > 0, 416 | do: do_count_until(iter.iterable, limit, 0) 417 | 418 | defp do_count_until(iter, limit, limit), do: {:ok, limit, new(iter)} 419 | 420 | defp do_count_until(iter, limit, so_far) do 421 | case Iterable.next(iter) do 422 | {:ok, _element, iter} -> do_count_until(iter, limit, so_far + 1) 423 | :done -> {:ok, so_far, empty()} 424 | end 425 | end 426 | 427 | @doc """ 428 | Counts the elements of iterator for which `predicate` returns a truthy value, stopping at `limit`. 429 | 430 | ## Examples 431 | 432 | iex> 1..20 433 | ...> |> Iter.from() 434 | ...> |> Iter.count_until(&(rem(&1, 2) == 0), 7) 435 | {:ok, 7, Iter.from(15..20)} 436 | 437 | iex> 1..20 438 | ...> |> Iter.from() 439 | ...> |> Iter.count_until(&(rem(&1, 2) == 0), 11) 440 | {:ok, 10, Iter.empty()} 441 | """ 442 | @spec count_until(t, predicate, pos_integer) :: {:ok, non_neg_integer, t} 443 | def count_until(iter, predicate, limit) 444 | when is_iter(iter) and is_function(predicate, 1) and is_integer(limit) and limit > 0, 445 | do: do_count_until_with(iter.iterable, predicate, limit, 0) 446 | 447 | defp do_count_until_with(iter, _predicate, limit, limit), do: {:ok, limit, new(iter)} 448 | 449 | defp do_count_until_with(iter, predicate, limit, so_far) do 450 | case Iterable.next(iter) do 451 | {:ok, element, iter} -> 452 | if predicate.(element) do 453 | do_count_until_with(iter, predicate, limit, so_far + 1) 454 | else 455 | do_count_until_with(iter, predicate, limit, so_far) 456 | end 457 | 458 | :done -> 459 | {:ok, so_far, empty()} 460 | end 461 | end 462 | 463 | @doc """ 464 | Count the number of elements remaining in the iterator. 465 | 466 | Some iterators can be counted without consuming the iterator, but most cannot 467 | and you should consider the iterator passed to this function as having been 468 | exhausted. 469 | 470 | ## Example 471 | 472 | iex> 1..10 473 | ...> |> Iter.from() 474 | ...> |> Iter.count() 475 | 10 476 | """ 477 | @spec count(t) :: non_neg_integer 478 | def count(iter) when is_iter(iter), 479 | do: Iterable.count(iter.iterable) 480 | 481 | @doc """ 482 | Count the number of elements for which `fun` returns a truthy value. 483 | 484 | ## Example 485 | 486 | iex> 1..5 487 | ...> |> Iter.from() 488 | ...> |> Iter.count(&(rem(&1, 2) == 0)) 489 | 2 490 | """ 491 | @spec count(t, (element -> as_boolean(any))) :: non_neg_integer 492 | def count(iter, fun) when is_iter(iter) and is_function(fun, 1), 493 | do: Iterable.count(iter.iterable, fun) 494 | 495 | @doc """ 496 | Create an iterator that cycles it's elements eternally. 497 | 498 | iex> [:a, :b, :c] 499 | ...> |> Iter.from() 500 | ...> |> Iter.cycle() 501 | ...> |> Iter.take(5) 502 | ...> |> Iter.to_list() 503 | [:a, :b, :c, :a, :b] 504 | """ 505 | @spec cycle(t) :: t | no_return 506 | def cycle(iter) when is_iter(iter), 507 | do: iter.iterable |> Iterable.cycle() |> new() 508 | 509 | @doc """ 510 | Remove consecutive elements for which `fun` returns duplicate values from the iterator. 511 | 512 | ## Example 513 | 514 | iex> [{1, :a}, {2, :b}, {2, :c}, {1, :a}] 515 | ...> |> Iter.from() 516 | ...> |> Iter.dedup_by(&elem(&1, 0)) 517 | ...> |> Iter.to_list() 518 | [{1, :a}, {2, :b}, {1, :a}] 519 | """ 520 | @spec dedup_by(t, (element -> any)) :: t 521 | def dedup_by(iter, fun) when is_iter(iter) and is_function(fun, 1), 522 | do: iter.iterable |> Iterable.dedup_by(fun) |> new() 523 | 524 | @doc """ 525 | Remove consecutive duplicate elements from the iterator. 526 | 527 | ## Example 528 | 529 | iex> [1, 1, 2, 3, 3, 4, 5, 4] 530 | ...> |> Iter.from() 531 | ...> |> Iter.dedup() 532 | ...> |> Iter.to_list() 533 | [1, 2, 3, 4, 5, 4] 534 | """ 535 | @spec dedup(t) :: t 536 | def dedup(iter) when is_iter(iter), 537 | do: iter.iterable |> Iterable.dedup() |> new() 538 | 539 | @doc """ 540 | Returns a new iterator with every `nth` element in the iterator dropped, 541 | starting with the first element. 542 | 543 | ## Examples 544 | 545 | iex> 1..10 546 | ...> |> Iter.from() 547 | ...> |> Iter.drop_every(2) 548 | ...> |> Iter.to_list() 549 | [2, 4, 6, 8, 10] 550 | 551 | iex> 1..10 552 | ...> |> Iter.from() 553 | ...> |> Iter.drop_every(0) 554 | ...> |> Iter.to_list() 555 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 556 | 557 | iex> [1, 2, 3] 558 | ...> |> Iter.from() 559 | ...> |> Iter.drop_every(1) 560 | ...> |> Iter.to_list() 561 | [] 562 | """ 563 | @spec drop_every(t, non_neg_integer) :: t 564 | def drop_every(iter, nth) when is_iter(iter) and is_integer(nth) and nth >= 0, 565 | do: iter.iterable |> Iterable.drop_every(nth) |> new() 566 | 567 | @doc """ 568 | Returns a new iterator which drops elements at the beginning of the iterator 569 | until `predicate` returns a truthy value. 570 | 571 | ## Example 572 | 573 | iex> [1, 2, 3, 2, 1] 574 | ...> |> Iter.from() 575 | ...> |> Iter.drop_while(&(&1 < 3)) 576 | ...> |> Iter.to_list() 577 | [3, 2, 1] 578 | """ 579 | @spec drop_while(t, predicate) :: t 580 | def drop_while(iter, predicate), 581 | do: iter.iterable |> Iterable.drop_while(predicate) |> new() 582 | 583 | @doc """ 584 | Returns a new iterator which drops the first `how_many` elements. 585 | 586 | ## Examples 587 | 588 | iex> 1..3 589 | ...> |> Iter.from() 590 | ...> |> Iter.drop(2) 591 | ...> |> Iter.to_list() 592 | [3] 593 | """ 594 | @spec drop(t, non_neg_integer) :: t 595 | def drop(iter, how_many) when is_iter(iter) and is_integer(how_many) and how_many >= 0, 596 | do: iter.iterable |> Iterable.drop(how_many) |> new() 597 | 598 | @doc """ 599 | Call `fun` for every element in the iterator. 600 | 601 | The return value is not used. 602 | 603 | ## Example 604 | 605 | ```elixir 606 | ["marty", "doc"] 607 | |> Iter.from() 608 | |> Iter.each(&IO.puts/1) 609 | "marty" 610 | "doc" 611 | #=> :done 612 | ``` 613 | """ 614 | @spec each(t, (element -> any)) :: :done 615 | def each(iter, fun) when is_iter(iter), 616 | do: Iterable.each(iter.iterable, fun) 617 | 618 | @doc """ 619 | Determines if the iterator is empty. 620 | 621 | ## Examples 622 | 623 | iex> Iter.empty() 624 | ...> |> Iter.empty?() 625 | true 626 | 627 | iex> 1..20 628 | ...> |> Iter.from() 629 | ...> |> Iter.empty? 630 | false 631 | """ 632 | @spec empty?(t) :: boolean 633 | def empty?(iter), 634 | do: Iterable.empty?(iter.iterable) 635 | 636 | @doc """ 637 | Returns an iterator that contains no elements. 638 | 639 | ## Example 640 | 641 | iex> Iter.empty() 642 | ...> |> Iter.to_list() 643 | [] 644 | """ 645 | @spec empty :: t 646 | def empty, do: Iterable.Empty.new() |> new() 647 | 648 | @doc """ 649 | Remove elements for which `predicate` returns a truthy value. 650 | 651 | ## Example 652 | 653 | iex> [1, 2, 3, 4] 654 | ...> |> Iter.from() 655 | ...> |> Iter.filter(&(rem(&1, 2) == 0)) 656 | ...> |> Iter.to_list() 657 | [2, 4] 658 | """ 659 | @spec filter(t, predicate) :: t 660 | def filter(iter, predicate) when is_iter(iter) and is_function(predicate, 1), 661 | do: iter.iterable |> Iterable.filter(predicate) |> new() 662 | 663 | @doc """ 664 | Finds the index of the first value in the iterator that matches `predicate`. 665 | 666 | ## Example 667 | 668 | iex> [1, 2, 3, 4, 5] 669 | ...> |> Iter.from() 670 | ...> |> Iter.find_index(&(&1 > 3)) 671 | {:ok, 3, Iter.from([5])} 672 | """ 673 | @spec find_index(t, predicate) :: {:ok, non_neg_integer, t} | :done 674 | def find_index(iter, predicate) when is_iter(iter) and is_function(predicate, 1) do 675 | with {:ok, index, iterable} <- Iterable.find_index(iter.iterable, predicate) do 676 | {:ok, index, new(iterable)} 677 | end 678 | end 679 | 680 | @doc """ 681 | Similar to `find/3`, but returns the value of the function invocation instead 682 | of the element itself. 683 | 684 | ## Example 685 | 686 | iex> [2, 3, 4] 687 | ...> |> Iter.from() 688 | ...> |> Iter.find_value(fn x -> 689 | ...> if x > 2, do: x * x 690 | ...> end) 691 | {:ok, 9, Iter.from([4])} 692 | 693 | iex> [2, 4, 6] 694 | ...> |> Iter.from() 695 | ...> |> Iter.find_value(&(rem(&1, 2) == 1)) 696 | :done 697 | 698 | iex> [2, 3, 4] 699 | ...> |> Iter.from() 700 | ...> |> Iter.find_value(&(rem(&1, 2) == 1)) 701 | {:ok, true, Iter.from([4])} 702 | """ 703 | @spec find_value(t, (element -> any)) :: {:ok, any, t} | :done 704 | def find_value(iter, fun) do 705 | with {:ok, result, iterable} <- Iterable.find_value(iter.iterable, fun) do 706 | {:ok, result, new(iterable)} 707 | end 708 | end 709 | 710 | @doc """ 711 | Searches for the first element in the iterator which matches `predicate`. 712 | 713 | ## Example 714 | 715 | iex> [1, 2, 3, 4, 5] 716 | ...> |> Iter.from() 717 | ...> |> Iter.find(&(&1 > 3)) 718 | {:ok, 4, Iter.from([5])} 719 | 720 | iex> [1, 2, 3] 721 | ...> |> Iter.from() 722 | ...> |> Iter.find(&(&1 > 4)) 723 | :done 724 | """ 725 | @spec find(t, predicate) :: {:ok, element, t} | :done 726 | def find(iter, predicate) when is_iter(iter) and is_function(predicate, 1) do 727 | with {:ok, element, iterable} <- Iterable.find(iter.iterable, predicate) do 728 | {:ok, element, new(iterable)} 729 | end 730 | end 731 | 732 | @doc """ 733 | Maps `fun` over the iterator flattening the result. 734 | 735 | ## Example 736 | 737 | iex> [:a, :b, :c] 738 | ...> |> Iter.from() 739 | ...> |> Iter.flat_map(&Iter.from([&1, &1])) 740 | ...> |> Iter.to_list() 741 | [:a, :a, :b, :b, :c, :c] 742 | """ 743 | @spec flat_map(t, mapper) :: t 744 | def flat_map(iter, mapper) when is_iter(iter) and is_function(mapper, 1), 745 | do: iter.iterable |> Iterable.flat_map(mapper) |> new() 746 | 747 | @doc """ 748 | Flattens nested iterators. 749 | 750 | ## Example 751 | 752 | iex> [[:a, :a], [:b, :b], [:c, :c]] 753 | ...> |> Iter.from() 754 | ...> |> Iter.flatten() 755 | ...> |> Iter.to_list() 756 | [:a, :a, :b, :b, :c, :c] 757 | """ 758 | @spec flatten(t) :: t 759 | def flatten(iter) when is_iter(iter), 760 | do: iter.iterable |> Iterable.flatten() |> new() 761 | 762 | @doc """ 763 | Convert anything that implements `Iter.IntoIterable` into an `Iter`. 764 | """ 765 | @spec from(IntoIterable.t()) :: t 766 | def from(iter) when is_iter(iter), 767 | do: iter 768 | 769 | def from(maybe_iterable), 770 | do: maybe_iterable |> IntoIterable.into_iterable() |> new() 771 | 772 | @doc """ 773 | Convert an `Enumerable` into an `Iter`. 774 | 775 | Provides an `Enumerable` compatible source for `Iter` using a `GenServer` to 776 | orchestrate reduction and block as required. 777 | 778 | > #### Warning {: .warning} 779 | > You should almost always implement `IntoIterable` for your enumerable and 780 | > use `from/1` rather than resort to calling this function. Unfortunately it 781 | > cannot always be avoided. 782 | """ 783 | @spec from_enum(Enumerable.t()) :: t 784 | def from_enum(enumerable) do 785 | enumerable 786 | |> Iterable.Enumerable.new() 787 | |> new() 788 | end 789 | 790 | @doc """ 791 | Intersperses `separator` between each element of the iterator. 792 | 793 | ## Examples 794 | 795 | iex> 1..3 796 | ...> |> Iter.from() 797 | ...> |> Iter.intersperse(0) 798 | ...> |> Iter.to_list() 799 | [1, 0, 2, 0, 3] 800 | 801 | iex> [1] 802 | ...> |> Iter.from() 803 | ...> |> Iter.intersperse(0) 804 | ...> |> Iter.to_list() 805 | [1] 806 | 807 | iex> [] 808 | ...> |> Iter.from() 809 | ...> |> Iter.intersperse(0) 810 | ...> |> Iter.to_list() 811 | [] 812 | """ 813 | @spec intersperse(t, any) :: t 814 | def intersperse(iter, separator) when is_iter(iter), 815 | do: iter.iterable |> Iterable.intersperse(separator) |> new() 816 | 817 | @doc """ 818 | Emits a sequence of values, starting with `start_value`. Successive values are 819 | generated by calling `next_fun` on the previous value. 820 | 821 | iex> Iter.iterate(0, &(&1 + 1)) 822 | ...> |> Iter.take(5) 823 | ...> |> Iter.to_list() 824 | [0, 1, 2, 3, 4] 825 | """ 826 | @spec iterate(element, (element -> element)) :: t 827 | def iterate(start_value, next_fun) when is_function(next_fun, 1) do 828 | Iterable.Resource.new( 829 | fn -> start_value end, 830 | fn acc -> 831 | {[acc], next_fun.(acc)} 832 | end, 833 | fn _ -> :ok end 834 | ) 835 | |> new() 836 | end 837 | 838 | @doc """ 839 | Creates a new iterator which applies `mapper` on every `nth` element of the 840 | iterator, starting with the first element. 841 | 842 | The first element is always mapped unless `nth` is `0`. 843 | 844 | ## Examples 845 | 846 | iex> 1..10 847 | ...> |> Iter.from() 848 | ...> |> Iter.map_every(2, fn x -> x + 1000 end) 849 | ...> |> Iter.to_list() 850 | [1001, 2, 1003, 4, 1005, 6, 1007, 8, 1009, 10] 851 | 852 | iex> 1..10 853 | ...> |> Iter.from() 854 | ...> |> Iter.map_every(3, fn x -> x + 1000 end) 855 | ...> |> Iter.to_list() 856 | [1001, 2, 3, 1004, 5, 6, 1007, 8, 9, 1010] 857 | 858 | iex> 1..5 859 | ...> |> Iter.from() 860 | ...> |> Iter.map_every(0, fn x -> x + 1000 end) 861 | ...> |> Iter.to_list() 862 | [1, 2, 3, 4, 5] 863 | 864 | iex> 1..3 865 | ...> |> Iter.from() 866 | ...> |> Iter.map_every(1, fn x -> x + 1000 end) 867 | ...> |> Iter.to_list() 868 | [1001, 1002, 1003] 869 | """ 870 | @spec map_every(t, non_neg_integer, (element -> new_element)) :: t when new_element: any 871 | def map_every(iter, nth, mapper) 872 | when is_iter(iter) and is_integer(nth) and nth >= 0 and is_function(mapper, 1), 873 | do: iter.iterable |> Iterable.map_every(nth, mapper) |> new() 874 | 875 | @doc """ 876 | Apply `fun` to each element in the iterator and collect the result. 877 | 878 | ## Example 879 | 880 | iex> [1, 2, 3, 4] 881 | ...> |> Iter.from() 882 | ...> |> Iter.map(&(&1 * 2)) 883 | ...> |> Iter.to_list() 884 | [2, 4, 6, 8] 885 | """ 886 | @spec map(t, mapper) :: t 887 | def map(iter, mapper) when is_iter(iter) and is_function(mapper, 1), 888 | do: iter.iterable |> Iterable.map(mapper) |> new() 889 | 890 | @doc """ 891 | Returns the maximal element in the iterator according to Erlang's term sorting. 892 | 893 | ## Example 894 | 895 | iex> [1, 4, 3, 2] 896 | ...> |> Iter.from() 897 | ...> |> Iter.max() 898 | {:ok, 4} 899 | 900 | iex> Iter.empty() 901 | ...> |> Iter.max() 902 | :done 903 | """ 904 | @spec max(t, sorter) :: {:ok, element} | :done 905 | def max(iter, sorter \\ &>=/2) when is_iter(iter) and is_function(sorter, 2), 906 | do: Iterable.max(iter.iterable, sorter) 907 | 908 | @doc """ 909 | Returns the maximal element in the iterator as calculated by `mapper`. 910 | 911 | ## Example 912 | 913 | iex> ["a", "aa", "aaa"] 914 | ...> |> Iter.from() 915 | ...> |> Iter.max_by(&String.length/1) 916 | {:ok, "aaa"} 917 | 918 | iex> Iter.empty() 919 | ...> |> Iter.max_by(&String.length/1) 920 | :done 921 | """ 922 | @spec max_by(t, mapper, sorter) :: {:ok, element} | :done 923 | def max_by(iter, mapper, sorter \\ &>=/2) 924 | when is_iter(iter) and is_function(mapper, 1) and is_function(sorter, 2), 925 | do: Iterable.max_by(iter.iterable, mapper, sorter) 926 | 927 | @doc """ 928 | Checks if `element` is a member of `iterable`. 929 | 930 | ## Examples 931 | 932 | iex> 1..5 933 | ...> |> Iter.from() 934 | ...> |> Iter.member?(3) 935 | true 936 | 937 | iex> 1..5 938 | ...> |> Iter.from() 939 | ...> |> Iter.member?(6) 940 | false 941 | """ 942 | @spec member?(t, element) :: boolean 943 | def member?(iter, element) when is_iter(iter), 944 | do: Iterable.member?(iter.iterable, element) 945 | 946 | @doc """ 947 | Returns the minimal element in the iterator according to Erlang's term sorting. 948 | 949 | ## Example 950 | 951 | iex> [1, 4, 3, 2] 952 | ...> |> Iter.from() 953 | ...> |> Iter.min() 954 | {:ok, 1} 955 | """ 956 | @spec min(t, sorter) :: {:ok, element} | :done 957 | def min(iter, sorter \\ &<=/2) 958 | when is_iter(iter) and is_function(sorter, 2), 959 | do: Iterable.min(iter.iterable, sorter) 960 | 961 | @doc """ 962 | Returns the minimal element in the iterator as calculated by `mapper`. 963 | 964 | ## Example 965 | 966 | iex> ["a", "aa", "aaa"] 967 | ...> |> Iter.from() 968 | ...> |> Iter.min_by(&String.length/1) 969 | {:ok, "a"} 970 | 971 | iex> Iter.empty() 972 | ...> |> Iter.min_by(&String.length/1) 973 | :done 974 | """ 975 | @spec min_by(t, mapper, sorter) :: {:ok, element} | :done 976 | def min_by(iter, mapper, sorter \\ &<=/2) 977 | when is_iter(iter) and is_function(mapper, 1) and is_function(sorter, 2), 978 | do: Iterable.min_by(iter.iterable, mapper, sorter) 979 | 980 | @doc """ 981 | Returns the minimal and maximal element in the iterator according to Erlang's 982 | term ordering. 983 | 984 | ## Example 985 | 986 | iex> [2, 3, 1] 987 | ...> |> Iter.from() 988 | ...> |> Iter.min_max() 989 | {:ok, 1, 3} 990 | 991 | iex> Iter.empty() 992 | ...> |> Iter.min_max() 993 | :done 994 | """ 995 | @spec min_max(t) :: {:ok, min, max} | :done when min: element, max: element 996 | def min_max(iter) when is_iter(iter), 997 | do: Iterable.min_max(iter.iterable) 998 | 999 | @doc """ 1000 | Advance the iterator and return the next value. 1001 | 1002 | ## Return values 1003 | 1004 | - `{:ok, element, new_iterator}` - returns the next element and an updated iterator. 1005 | - `:done` - the iterator is exhausted. 1006 | """ 1007 | @spec next(t) :: {:ok, element, t} | :done 1008 | def next(iter) when is_iter(iter) do 1009 | with {:ok, element, iterable} <- Iter.next(iter.iterable) do 1010 | {:ok, element, new(iterable)} 1011 | end 1012 | end 1013 | 1014 | @doc """ 1015 | Peeks at the first element of the iterator, without consuming it. 1016 | 1017 | > #### Warning {: .warning} 1018 | > Many iterators cannot be peeked, so this function simulates peeking by 1019 | > consuming an element from the iterator and returning a new iterator which 1020 | > pushes that element back onto the front. 1021 | 1022 | ## Example 1023 | 1024 | iex> {:ok, 1, iter} = 1..3 1025 | ...> |> Iter.from() 1026 | ...> |> Iter.peek() 1027 | ...> Iter.to_list(iter) 1028 | [1, 2, 3] 1029 | """ 1030 | @spec peek(t) :: {:ok, element, t} | :done 1031 | def peek(iter) when is_iter(iter) do 1032 | with {:ok, element, iterable} <- Iterable.peek(iter.iterable) do 1033 | {:ok, element, new(iterable)} 1034 | end 1035 | end 1036 | 1037 | @doc """ 1038 | Peeks at the first `how_many` elements of the iterator, without consuming 1039 | them. 1040 | 1041 | > #### Warning {: .warning} 1042 | > Many iterables cannot be peeked, so this function simulates peeking by 1043 | > consuming elements from the iterator and returning a new iterator which 1044 | > pushes those elements back on to the front. 1045 | 1046 | Because it's possible to try and peek past the end of an iterator you 1047 | shouldn't expect the number of elements returned to always be the same as how 1048 | many you asked for. For this reason the return value includes the number of 1049 | elements that were able to be peeked. 1050 | 1051 | ## Example 1052 | 1053 | iex> {:ok, peeks, 3, iter} = 1..5 1054 | ...> |> Iter.from() 1055 | ...> |> Iter.peek(3) 1056 | iex> Iter.to_list(peeks) 1057 | [1, 2, 3] 1058 | iex> Iter.to_list(iter) 1059 | [1, 2, 3, 4, 5] 1060 | 1061 | iex> {:ok, peeks, 3, iter} = 1..3 1062 | ...> |> Iter.from() 1063 | ...> |> Iter.peek(5) 1064 | iex> Iter.to_list(peeks) 1065 | [1, 2, 3] 1066 | iex> Iter.to_list(iter) 1067 | [1, 2, 3] 1068 | """ 1069 | @spec peek(t, how_many :: pos_integer) :: {:ok, [element], non_neg_integer, t} | :done 1070 | def peek(iter, how_many) when is_iter(iter) do 1071 | with {:ok, peeks, got, iterable} <- Iterable.peek(iter.iterable, how_many) do 1072 | {:ok, from(peeks), got, new(iterable)} 1073 | end 1074 | end 1075 | 1076 | @doc """ 1077 | Prepend a new element to the beginning of the iterable. 1078 | 1079 | ## Example 1080 | 1081 | iex> 1..3 1082 | ...> |> Iter.from() 1083 | ...> |> Iter.prepend(4) 1084 | ...> |> Iter.to_list() 1085 | [4, 1, 2, 3] 1086 | """ 1087 | @spec prepend(t, element) :: t 1088 | def prepend(iter, element) when is_iter(iter), 1089 | do: iter.iterable |> Iterable.prepend(element) |> new() 1090 | 1091 | @doc """ 1092 | Keep elements for which `predicate` returns a truthy value. 1093 | 1094 | ## Example 1095 | 1096 | iex> [1, 2, 3, 4] 1097 | ...> |> Iter.from() 1098 | ...> |> Iter.reject(&(rem(&1, 2) == 0)) 1099 | ...> |> Iter.to_list() 1100 | [1, 3] 1101 | """ 1102 | @spec reject(t, predicate) :: t 1103 | def reject(iter, predicate) when is_iter(iter) and is_function(predicate, 1) do 1104 | iter.iterable 1105 | |> Iterable.filter(fn element -> 1106 | if predicate.(element), do: false, else: true 1107 | end) 1108 | |> new() 1109 | end 1110 | 1111 | @doc """ 1112 | Returns an iterator generated by calling `generator_fun` repeatedly. 1113 | 1114 | ## Examples 1115 | 1116 | # Although not necessary, let's seed the random algorithm 1117 | iex> :rand.seed(:exsss, {1, 2, 3}) 1118 | iex> Iter.repeatedly(&:rand.uniform/0) |> Iter.take(3) |> Iter.to_list() 1119 | [0.5455598952593053, 0.6039309974353404, 0.6684893034823949] 1120 | """ 1121 | @spec repeatedly((-> element)) :: t 1122 | def repeatedly(generator_fun) when is_function(generator_fun, 0) do 1123 | resource( 1124 | fn -> nil end, 1125 | fn _ -> {[generator_fun.()], nil} end, 1126 | fn _ -> nil end 1127 | ) 1128 | end 1129 | 1130 | @doc """ 1131 | Create an iterator from a resource. 1132 | 1133 | iex> Iter.resource( 1134 | ...> fn -> 1135 | ...> {:ok, pid} = StringIO.open("Marty") 1136 | ...> pid 1137 | ...> end, 1138 | ...> fn pid -> 1139 | ...> case IO.read(pid, 1) do 1140 | ...> :eof -> {:halt, pid} 1141 | ...> char -> {[char], pid} 1142 | ...> end 1143 | ...> end, 1144 | ...> fn pid -> 1145 | ...> StringIO.close(pid) 1146 | ...> end 1147 | ...> ) 1148 | ...> |> Iter.to_list() 1149 | ["M", "a", "r", "t", "y"] 1150 | """ 1151 | @spec resource( 1152 | start_fun :: (-> acc), 1153 | next_fun :: (acc -> {[element], acc} | {:halt, acc}), 1154 | after_fun :: (acc -> any) 1155 | ) :: t 1156 | when acc: any 1157 | def resource(start_fun, next_fun, after_fun) 1158 | when is_function(start_fun, 0) and is_function(next_fun, 1) and is_function(after_fun, 1), 1159 | do: Iterable.Resource.new(start_fun, next_fun, after_fun) |> new() 1160 | 1161 | @doc """ 1162 | Creates an iterator starting at the same point, but stepping by `step_size` 1163 | each iteration. 1164 | 1165 | The first element of the iterator will always be returned, regardless of the step given. 1166 | 1167 | ## Examples 1168 | 1169 | iex> 1..9 1170 | ...> |> Iter.from() 1171 | ...> |> Iter.step_by(3) 1172 | ...> |> Iter.to_list() 1173 | [1, 4, 7] 1174 | """ 1175 | @spec step_by(t, pos_integer) :: t 1176 | def step_by(iter, step) when is_iter(iter) and is_integer(step) and step > 0, 1177 | do: iter.iterable |> Iterable.step_by(step) |> new() 1178 | 1179 | @doc """ 1180 | Collects the first `how_many` elements into a new iterator and returns it 1181 | along with the advanced initial iterator. 1182 | 1183 | This is very much like `take/2` except that it returns the remaining iterator 1184 | so that it can be called repeatedly. 1185 | 1186 | ## Example 1187 | 1188 | iex> iter = Iter.from(1..9) 1189 | ...> {:ok, chunk_a, iter} = Iter.take_chunk(iter, 3) 1190 | ...> {:ok, chunk_b, remainder} = Iter.take_chunk(iter, 3) 1191 | ...> Iter.to_list(chunk_a) 1192 | [1, 2, 3] 1193 | iex> Iter.to_list(chunk_b) 1194 | [4, 5, 6] 1195 | iex> Iter.to_list(remainder) 1196 | [7, 8, 9] 1197 | """ 1198 | @spec take_chunk(t, pos_integer()) :: {:ok, t, t} | {:done, t} 1199 | def take_chunk(iter, how_many) when is_iter(iter) and is_integer(how_many) and how_many > 0 do 1200 | case Iterable.take_chunk(iter.iterable, how_many) do 1201 | {:ok, chunk, remainder} -> {:ok, new(chunk), new(remainder)} 1202 | {:done, chunk} -> {:done, new(chunk)} 1203 | end 1204 | end 1205 | 1206 | @doc """ 1207 | Creates an iterable which emits elements until `predicate` returns `false`. 1208 | 1209 | The rest of the underlying iterable is discarded. 1210 | 1211 | ## Example 1212 | 1213 | iex> 1..3 1214 | ...> |> Iter.from() 1215 | ...> |> Iter.take_while(&(&1 < 3)) 1216 | ...> |> Iter.to_list() 1217 | [1, 2] 1218 | """ 1219 | @spec take_while(t, predicate) :: t 1220 | def take_while(iter, predicate) when is_iter(iter) and is_function(predicate, 1), 1221 | do: iter.iterable |> Iterable.take_while(predicate) |> new() 1222 | 1223 | @doc """ 1224 | Takes the next `count` elements from the iterable and stops iteration. 1225 | 1226 | If a negative count is given, the last count values will be taken. For such, 1227 | the collection is fully enumerated keeping up to `count` elements in memory. 1228 | Once the end of the collection is reached, the last `count` elements will be 1229 | iterated. Therefore, using a negative count on an infinite collection will 1230 | never return. 1231 | 1232 | The rest of the underlying iterable is discarded. 1233 | 1234 | ## Examples 1235 | 1236 | iex> Iter.empty() 1237 | ...> |> Iter.take(3) 1238 | ...> |> Iter.to_list() 1239 | [] 1240 | 1241 | iex> Iter.empty() 1242 | ...> |> Iter.take(-3) 1243 | ...> |> Iter.to_list() 1244 | [] 1245 | 1246 | iex> 1..5 1247 | ...> |> Iter.from() 1248 | ...> |> Iter.take(3) 1249 | ...> |> Iter.to_list() 1250 | [1, 2, 3] 1251 | 1252 | iex> 1..5 1253 | ...> |> Iter.from() 1254 | ...> |> Iter.take(-3) 1255 | ...> |> Iter.to_list() 1256 | [3, 4, 5] 1257 | """ 1258 | @spec take(t, integer) :: t 1259 | def take(iter, count) when is_iter(iter) and is_integer(count), 1260 | do: do_take(iter, count) 1261 | 1262 | defp do_take(iter, count) when count >= 0, 1263 | do: iter.iterable |> Iterable.take_head(count) |> new() 1264 | 1265 | defp do_take(iter, count) when count < 0, 1266 | do: iter.iterable |> Iterable.take_tail(0 - count) |> new() 1267 | 1268 | @doc """ 1269 | Convert an iterator into a list. 1270 | """ 1271 | @spec to_list(t) :: [element] 1272 | def to_list(iter) when is_iter(iter), 1273 | do: Iterable.to_list(iter.iterable) 1274 | 1275 | @doc """ 1276 | Convert an iterator into an Elixir stream. 1277 | 1278 | ## Example 1279 | 1280 | iex> [:a, :b, :c] 1281 | ...> |> Iter.from() 1282 | ...> |> Iter.cycle() 1283 | ...> |> Iter.to_stream() 1284 | ...> |> Enum.take(5) 1285 | [:a, :b, :c, :a, :b] 1286 | """ 1287 | @spec to_stream(t) :: Enumerable.t() 1288 | def to_stream(iter) do 1289 | Stream.resource( 1290 | fn -> iter end, 1291 | fn iter -> 1292 | case Iterable.next(iter.iterable) do 1293 | {:ok, element, iterable} -> {[element], %{iter | iterable: iterable}} 1294 | :done -> {:halt, iter} 1295 | end 1296 | end, 1297 | fn _ -> :ok end 1298 | ) 1299 | end 1300 | 1301 | @doc """ 1302 | Creates a new iterator which returns only unique elements. 1303 | 1304 | > #### Warning {: .warning} 1305 | > Except for specific data structures (eg `MapSet` and `Range`) most iterators 1306 | > will need to store a set of "seen values" in order to provide this function. 1307 | > In such cases memory usage will grow in direct relation to the number of 1308 | > unique elements in the iterator. 1309 | 1310 | ## Example 1311 | 1312 | iex> 1..5 1313 | ...> |> Iter.from() 1314 | ...> |> Iter.uniq() 1315 | Iter.from(1..5) 1316 | 1317 | iex> [1, 2, 3, 2, 1] 1318 | ...> |> Iter.from() 1319 | ...> |> Iter.uniq() 1320 | ...> |> Iter.to_list() 1321 | [1, 2, 3] 1322 | """ 1323 | @spec uniq(t) :: t 1324 | def uniq(iter) when is_iter(iter), 1325 | do: iter.iterable |> Iterable.uniq() |> new() 1326 | 1327 | @doc """ 1328 | Creates a new iterator which replaces each element with a tuple containing the 1329 | original element and the count of elements so far. 1330 | 1331 | ## Example 1332 | 1333 | iex> 1..3 1334 | ...> |> Iter.from() 1335 | ...> |> Iter.with_index() 1336 | ...> |> Iter.to_list() 1337 | [{1, 0}, {2, 1}, {3, 2}] 1338 | """ 1339 | @spec with_index(t) :: t 1340 | def with_index(iter) when is_iter(iter), 1341 | do: iter.iterable |> Iterable.with_index() |> new() 1342 | 1343 | @doc """ 1344 | Zips corresponding elements from a finite collection of iterators into a new 1345 | iterator, transforming them with `zip_fun` as it goes. 1346 | 1347 | The first element from each of the iterators will be put into a list which is 1348 | then passed to the one-arity `zip_fun` function. Then, the second elements 1349 | from each of the iterators are put into a list, and so on until any of the 1350 | iterators are exhausted. 1351 | 1352 | ## Example 1353 | 1354 | iex> first = Iter.from(1..3) 1355 | ...> second = Iter.from(4..6) 1356 | ...> third = Iter.from(7..9) 1357 | ...> [first, second, third] 1358 | ...> |> Iter.from() 1359 | ...> |> Iter.zip_with(fn [a, b, c] -> a + b + c end) 1360 | ...> |> Iter.to_list() 1361 | [12, 15, 18] 1362 | """ 1363 | @spec zip_with(t, ([element] -> any)) :: t 1364 | def zip_with(iter, zipper) when is_iter(iter) and is_function(zipper, 1) do 1365 | iter.iterable 1366 | |> Iterable.map(&IntoIterable.into_iterable/1) 1367 | |> Iterable.zip(zipper) 1368 | |> new() 1369 | end 1370 | 1371 | @doc """ 1372 | Zips corresponding elements from two iterators into a new one, transforming 1373 | them with `zip_fun` as it goes. 1374 | 1375 | The `zip_fun` will be called with the first elements from the iterators, then 1376 | the second elements and so on. 1377 | 1378 | ## Example 1379 | 1380 | iex> first = Iter.from(1..3) 1381 | ...> second = Iter.from(4..6) 1382 | ...> Iter.zip_with(first, second, &(&1 + &2)) 1383 | ...> |> Iter.to_list() 1384 | [5, 7, 9] 1385 | """ 1386 | @spec zip_with(t, t, (element, element -> any)) :: t 1387 | def zip_with(lhs, rhs, zipper) when is_iter(lhs) and is_iter(rhs) and is_function(zipper, 2) do 1388 | [lhs.iterable, rhs.iterable] 1389 | |> Iterable.zip(fn [a, b] -> zipper.(a, b) end) 1390 | |> new() 1391 | end 1392 | 1393 | @doc """ 1394 | Zips corresponding elements from a finite collection of iterators into one iterator of tuples. 1395 | 1396 | The zipping finishes as soon as any iterable in the collection is exhausted. 1397 | 1398 | ## Example 1399 | 1400 | iex> first = Iter.from(1..3) 1401 | ...> second = Iter.from([:a, :b, :c]) 1402 | ...> third = Iter.from(["a", "b", "c"]) 1403 | ...> [first, second, third] 1404 | ...> |> Iter.from() 1405 | ...> |> Iter.zip() 1406 | ...> |> Iter.to_list() 1407 | [{1, :a, "a"}, {2, :b, "b"}, {3, :c, "c"}] 1408 | """ 1409 | @spec zip(t) :: t 1410 | def zip(iter) when is_iter(iter) do 1411 | iter.iterable 1412 | |> Iterable.map(&IntoIterable.into_iterable/1) 1413 | |> Iterable.zip(&List.to_tuple/1) 1414 | |> new() 1415 | end 1416 | 1417 | @doc """ 1418 | Zips to iterators together. 1419 | 1420 | The zipping finishes as soon as either iterator is exhausted. 1421 | 1422 | ## Example 1423 | 1424 | iex> first = Iter.from(1..3) 1425 | ...> second = Iter.from([:a, :b, :c]) 1426 | ...> Iter.zip(first, second) 1427 | ...> |> Iter.to_list() 1428 | [{1, :a}, {2, :b}, {3, :c}] 1429 | """ 1430 | @spec zip(t, t) :: t 1431 | def zip(lhs, rhs) when is_iter(lhs) and is_iter(rhs) do 1432 | [lhs.iterable, rhs.iterable] 1433 | |> Iterable.zip(&List.to_tuple/1) 1434 | |> new() 1435 | end 1436 | 1437 | defp new(iterable), do: %__MODULE__{iterable: iterable} 1438 | end 1439 | --------------------------------------------------------------------------------