├── .github └── FUNDING.yml ├── bin ├── setup └── console ├── Rakefile ├── Gemfile ├── test ├── test_helper.rb ├── stream_test.rb └── fromarray_test.rb ├── .travis.yml ├── .rultor.yml ├── Gemfile.lock ├── rcfg └── rubygems.yml.asc ├── release.sh ├── ruby-stream-api.gemspec ├── .gitignore ├── LICENSE ├── lib ├── version.rb ├── stream.rb └── fromarray.rb └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | custom: https://paypal.me/amihaiemil 3 | github: amihaiemil 4 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/*_test.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in ruby-stream-api.gemspec 4 | gemspec 5 | 6 | gem "rake", "~> 12.3.3" 7 | gem "minitest", "~> 5.0" 8 | gem 'simplecov', require: false, group: :test 9 | gem 'codecov', require: false 10 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start do 3 | add_filter 'test' 4 | add_filter 'Rakefile' 5 | end 6 | if ENV['CI'] == 'true' 7 | require 'codecov' 8 | SimpleCov.formatter = SimpleCov::Formatter::Codecov 9 | end 10 | require "minitest/autorun" 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | cache: bundler 4 | rvm: 5 | - 2.7.0 6 | before_install: gem install bundler -v 2.1.4 7 | branches: 8 | only: 9 | - master 10 | script: 11 | - bundle install 12 | - bundler exec rake 13 | after_success: 14 | - "bash <(curl -s https://codecov.io/bash)" 15 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env stream 2 | 3 | require "bundler/setup" 4 | require "stream" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /.rultor.yml: -------------------------------------------------------------------------------- 1 | architect: 2 | - amihaiemil 3 | - andronachev 4 | install: | 5 | export GEM_HOME=~/.ruby 6 | export GEM_PATH=$GEM_HOME:$GEM_PATH 7 | bundle install 8 | merge: 9 | script: |- 10 | bundle exec rake 11 | deploy: 12 | script: |- 13 | echo 'Nothing to deploy yet' 14 | exit -1 15 | decrypt: 16 | rubygems.yml: "repo/rcfg/rubygems.yml.asc" 17 | release: 18 | script: |- 19 | chmod +x ./release.sh 20 | ./release.sh 21 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | ruby-stream-api (0.0.3.pre.SNAPSHOT) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | codecov (0.1.14) 10 | json 11 | simplecov 12 | url 13 | docile (1.3.2) 14 | json (2.3.0) 15 | minitest (5.14.0) 16 | rake (12.3.3) 17 | simplecov (0.18.5) 18 | docile (~> 1.1) 19 | simplecov-html (~> 0.11) 20 | simplecov-html (0.12.2) 21 | url (0.3.2) 22 | 23 | PLATFORMS 24 | ruby 25 | 26 | DEPENDENCIES 27 | codecov 28 | minitest (~> 5.0) 29 | rake (~> 12.3.3) 30 | ruby-stream-api! 31 | simplecov 32 | 33 | BUNDLED WITH 34 | 2.1.4 35 | -------------------------------------------------------------------------------- /rcfg/rubygems.yml.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP MESSAGE----- 2 | 3 | hQEMA5qETcGag5w6AQf+NeUGuqLOXx3q0+CUyTnjLwRG6eqlMGzTSYLpTRPbjAS+ 4 | xNForun5aXHN1fUEHwrca8Ny2Blf+NcpNG+EL1dv5dOhkc0Ljbeq7oS84c0JpXYa 5 | h/e1SC3s1U/7vkJuPoTrTvBF+vqGil75FrSlrPWLVrJ/m3d3ICYYCQSlpsdzHMXz 6 | SPiTBZ4BCTE9AmadFCxD9HAS65mzPUbEiS1iMkQsmGFL8LRccgwXkccRSXst+Tjz 7 | L3au3+DYkFHIOReULORXzwn2dRy9aNEtQtmsatZcqjisqG9EHraFKff6dFzSenpD 8 | mEdlohBYqBObtUR2Rwpm9yTlLzfpZp9wurF5qurGSNLAWAFv1T1P2gjrG+pwvK0C 9 | vdg6UArcypUC0m9PHIl1VtugAxzFfRMLMAh1S1o0LqdkWscWAYY/zZvOBY10jT1H 10 | PcuFSuzfJP8gK89aAjTZCgRW2lI0DS3tLzt4dfV+CmcOXwAaPAQpK+HVfgsvVjSB 11 | OI4+7lcDPCZxUadtlm97h9i03I8X5OuG2O1fVty1+2FNHda+/MeVp4EWP4xiSaBj 12 | 1tgSQT+Dv3VDehjd+7bxugQw4vS5nPOCo4sq5MM7eUr1FVJ+nVwDs1VsEJhstTGl 13 | WQQu8avfq0txvIfMus8YWGIHi90zvBlpynlrbhKZuJVlhwCOAmhE2hm1WHt7VYIr 14 | cTQ3vbrmplHLQeJztGR9o9UlGd/bD6K5l4g= 15 | =tPM8 16 | -----END PGP MESSAGE----- 17 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Release script to be run by Rultor. 3 | 4 | set -e 5 | set -o pipefail 6 | 7 | CURRENT_VERSION=$(grep -o '[0-9]*\.[0-9]*\.[0-9]*-SNAPSHOT' -m 1 ./lib/version.rb) 8 | 9 | NUMBERS=($(echo $tag | grep -o -E '[0-9]+')) 10 | 11 | echo "CURRENT VERSION IS" 12 | echo $CURRENT_VERSION 13 | 14 | NEXT_VERSION=${NUMBERS[0]}'.'${NUMBERS[1]}'.'$((${NUMBERS[2]}+1))'-SNAPSHOT' 15 | 16 | echo "RELEASE VERSION IS" 17 | echo $tag 18 | 19 | echo "NEXT VERSION IS" 20 | echo $NEXT_VERSION 21 | 22 | ### Actual Script Here 23 | rm -rf *.gem 24 | sed -i "s/'${CURRENT_VERSION}'.freeze # rultor/'${tag}'.freeze # rultor/" ./lib/version.rb 25 | gem build ruby-stream-api.gemspec 26 | chmod 0600 /home/r/rubygems.yml 27 | gem push *.gem --config-file /home/r/rubygems.yml 28 | ### 29 | 30 | # set next dev version in version.rb 31 | sed -i "s/'${tag}'.freeze # rultor/'${NEXT_VERSION}'.freeze # rultor/" ./lib/version.rb 32 | sed -i "s/latest version is \`.*\`/latest version is \`${tag}\`/" README.md 33 | bundle install 34 | 35 | git commit -am "${NEXT_VERSION}" 36 | git checkout master 37 | git merge __rultor 38 | git checkout __rultor 39 | 40 | -------------------------------------------------------------------------------- /ruby-stream-api.gemspec: -------------------------------------------------------------------------------- 1 | require_relative 'lib/version.rb' 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "ruby-stream-api" 5 | spec.version = Stream::VERSION 6 | spec.authors = ["amihaiemil"] 7 | spec.email = ["amihaiemil@gmail.com"] 8 | 9 | spec.summary = "Ruby Stream API" 10 | spec.description = "Stream API for Ruby, inspired by Java 8's Stream API" 11 | spec.homepage = "https://github.com/ruby-ee/ruby-stream-api" 12 | spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") 13 | 14 | spec.metadata["allowed_push_host"] = "https://rubygems.org/" 15 | 16 | spec.metadata["homepage_uri"] = spec.homepage 17 | spec.metadata["source_code_uri"] = "https://github.com/ruby-ee/ruby-stream-api" 18 | spec.metadata["changelog_uri"] = "https://github.com/ruby-ee/ruby-stream-api/releases" 19 | 20 | # Specify which files should be added to the gem when it is released. 21 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 22 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 23 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 24 | end 25 | spec.bindir = "exe" 26 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 27 | spec.require_paths = ["lib"] 28 | end 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | rubygems.yml 2 | .idea/* 3 | .DS_Store 4 | *.gem 5 | *.rbc 6 | /.config 7 | /coverage/ 8 | /InstalledFiles 9 | /pkg/ 10 | /spec/reports/ 11 | /spec/examples.txt 12 | /test/tmp/ 13 | /test/version_tmp/ 14 | /tmp/ 15 | 16 | # Used by dotenv library to load environment variables. 17 | # .env 18 | 19 | # Ignore Byebug command history file. 20 | .byebug_history 21 | 22 | ## Specific to RubyMotion: 23 | .dat* 24 | .repl_history 25 | build/ 26 | *.bridgesupport 27 | build-iPhoneOS/ 28 | build-iPhoneSimulator/ 29 | 30 | ## Specific to RubyMotion (use of CocoaPods): 31 | # 32 | # We recommend against adding the Pods directory to your .gitignore. However 33 | # you should judge for yourself, the pros and cons are mentioned at: 34 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 35 | # 36 | # vendor/Pods/ 37 | 38 | ## Documentation cache and generated files: 39 | /.yardoc/ 40 | /_yardoc/ 41 | /doc/ 42 | /rdoc/ 43 | 44 | ## Environment normalization: 45 | /.bundle/ 46 | /vendor/bundle 47 | /lib/bundler/man/ 48 | 49 | # for a library or gem, you might want to ignore these files since the code is 50 | # intended to run in multiple environments; otherwise, check them in: 51 | # Gemfile.lock 52 | # .stream-version 53 | # .stream-gemset 54 | 55 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 56 | .rvmrc 57 | 58 | # Used by RuboCop. Remote config files pulled in from inherit_from directive. 59 | # .rubocop-https?--* 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Ruby Enterprise Edition 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /lib/version.rb: -------------------------------------------------------------------------------- 1 | # BSD 3-Clause License 2 | # Copyright (c) 2020, Ruby Enterprise Edition 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this 7 | # list of conditions and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, 9 | # this list of conditions and the following disclaimer in the documentation 10 | # and/or other materials provided with the distribution. 11 | # 3. Neither the name of the copyright holder nor the names of its 12 | # contributors may be used to endorse or promote products derived from 13 | # this software without specific prior written permission. 14 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | module Stream 26 | VERSION = '0.0.3-SNAPSHOT'.freeze # rultor 27 | end 28 | -------------------------------------------------------------------------------- /lib/stream.rb: -------------------------------------------------------------------------------- 1 | # BSD 3-Clause License 2 | # Copyright (c) 2020, Ruby Enterprise Edition 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this 7 | # list of conditions and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, 9 | # this list of conditions and the following disclaimer in the documentation 10 | # and/or other materials provided with the distribution. 11 | # 3. Neither the name of the copyright holder nor the names of its 12 | # contributors may be used to endorse or promote products derived from 13 | # this software without specific prior written permission. 14 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | require 'fromarray.rb' 26 | 27 | # Ruby Stream API main module. 28 | # Author:: Mihai Andronache (amihaiemil@gmail.com) 29 | module Stream 30 | 31 | # If it breaks, go to stderr 32 | class Error < StandardError 33 | end 34 | 35 | # Build a stream from an array. 36 | # Since:: 0.0.1 37 | def self.from_array(array) 38 | FromArray.new(array) 39 | end 40 | 41 | # Generate a Stream based on a seed function. 42 | # Since this would be an infinite Stream, a limit has to be applied. 43 | # If no limit is specified, the default is 100 elements. 44 | # Since:: 0.0.1 45 | def self.generate(limit = 100, &seed) 46 | raise ArgumentError, 'limit has to be a positive integer' unless limit.positive? and limit.is_a? Integer 47 | 48 | elements = [] 49 | limit.times { elements.push(seed.call) } 50 | FromArray.new(elements) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ruby Stream API 2 | 3 | [![Gem Version](https://badge.fury.io/rb/ruby-stream-api.svg)](https://badge.fury.io/rb/ruby-stream-api) 4 | [![Build Status](https://travis-ci.org/ruby-ee/ruby-stream-api.svg?branch=master)](https://travis-ci.org/ruby-ee/ruby-stream-api) 5 | [![Test Coverage](https://img.shields.io/codecov/c/github/ruby-ee/ruby-stream-api.svg)](https://codecov.io/github/ruby-ee/ruby-stream-api?branch=master) 6 | 7 | [![DevOps By Rultor.com](http://www.rultor.com/b/ruby-ee/ruby-stream-api)](http://www.rultor.com/p/ruby-ee/ruby-stream-api) 8 | [![We recommend RubyMine](https://amihaiemil.com/images/rubymine-recommend.svg)](https://www.jetbrains.com/ruby/) 9 | 10 | A Stream is a wrapper over a collection of elements offering a number of useful 11 | operations to modify and/or get information about the collection. The operations are chainable and can be categorized as follows: 12 | 13 | * **Source** operations -- these are the operations which are generating the Stream. 14 | * **Intermediate** operations (skip, filter, map etc) -- operations which are altering the Stream and still leave it open for further modifications. 15 | * **Terminal** operations (count, collect etc) -- operations which are executed after all the modifications have been done and are returning a finite result. 16 | 17 | First glance: 18 | 19 | 1) **Finite** Stream from an array: 20 | 21 | ```ruby 22 | array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 23 | stream = Stream::FromArray.new(array) 24 | collected = stream 25 | .filter { |num| num % 2 == 0 } 26 | .skip(2) 27 | .collect 28 | puts collected # [6, 8, 10] 29 | ``` 30 | 31 | 2) Generate a (potentially) **infinite** stream: 32 | 33 | ```ruby 34 | stream = Stream.generate(150) { &seed } 35 | ``` 36 | 37 | The ``generate`` method takes a ``limit`` (max number of elements) and a ``&seed`` Block function which 38 | returns a new element at each ``seed.call``. A limit is necessary as, without it, this Stream would be infinite. 39 | If no limit is specified, the default is ``100`` elements. 40 | 41 | This mechanism is useful, for instance, when you have to consume an incomming stream of objects from some ``IO`` objects. 42 | 43 | More info and methods' overview, in the [Wiki](https://github.com/ruby-ee/ruby-stream-api/wiki). 44 | 45 | ## Installation 46 | 47 | Add this line to your application's Gemfile: 48 | ```ruby 49 | gem 'ruby-stream-api' 50 | ``` 51 | 52 | Or install it as a separate gem: 53 | ```bash 54 | $gem install ruby-stream-api 55 | ``` 56 | 57 | To require it inside your Ruby program do: 58 | ```ruby 59 | require 'stream' 60 | ``` 61 | 62 | The latest version is `0.0.2`. 63 | 64 | ## Not a Mixin 65 | 66 | This is **not** a Mixin! The Stream is a proper object wrapping your collection(s). Furthermore, each object in this gem is immutable and therefore thread-safe -> the **intermediate** operations are not altering the instance on which they are called; instead, they create a new instance of the Stream with a modified version of the underlying collection. 67 | 68 | ## Contribute 69 | 70 | If you would like to contribute, just open an Issue (bugs, feature requests, any improvement idea) or a PR. 71 | 72 | In order to build the project, you need Bundler and Ruby >= 2.3.0. 73 | 74 | Make sure the build passes: 75 | 76 | ```shell 77 | $bundle install 78 | $bundle exec rake 79 | ``` 80 | -------------------------------------------------------------------------------- /test/stream_test.rb: -------------------------------------------------------------------------------- 1 | # BSD 3-Clause License 2 | # Copyright (c) 2020, Ruby Enterprise Edition 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this 7 | # list of conditions and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, 9 | # this list of conditions and the following disclaimer in the documentation 10 | # and/or other materials provided with the distribution. 11 | # 3. Neither the name of the copyright holder nor the names of its 12 | # contributors may be used to endorse or promote products derived from 13 | # this software without specific prior written permission. 14 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | require 'test_helper' 25 | require 'stream.rb' 26 | 27 | module Stream 28 | class StreamTest < Minitest::Test 29 | # The Stream module can be created from an array 30 | def test_that_from_array_works 31 | refute_nil Stream.from_array([1, 2, 3]) 32 | end 33 | 34 | # A Stream can be generated with the default limit of 100. 35 | def test_generates_hundred_elements 36 | assert( 37 | Stream.generate { 1 }.count == 100, 38 | 'Default size of a generated Stream should be 100' 39 | ) 40 | end 41 | 42 | # A Stream can be generated with a specified limit. 43 | def test_generates_with_limit 44 | assert( 45 | Stream.generate(15) { 1 }.count == 15, 46 | 'Default size of a generated Stream should be 100' 47 | ) 48 | end 49 | 50 | # Stream.generate should throw an exception if the given limit is 51 | # not a positive integer. 52 | def test_generates_with_negative_limit 53 | begin 54 | Stream.generate(-1) { 1 } 55 | assert(false, 'Exception was expected!') 56 | rescue ArgumentError => e 57 | assert( 58 | e.message == 'limit has to be a positive integer', 59 | 'Unexpected ArgumentError message!' 60 | ) 61 | end 62 | end 63 | 64 | # Stream.generate should throw an exception if the given limit is not 65 | # a positive integer. 66 | def test_generates_with_rational_limit 67 | begin 68 | Stream.generate(2.23) { 1 } 69 | assert(false, 'Exception was expected!') 70 | rescue ArgumentError => e 71 | assert( 72 | e.message == 'limit has to be a positive integer', 73 | 'Unexpected ArgumentError message!' 74 | ) 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/fromarray.rb: -------------------------------------------------------------------------------- 1 | # BSD 3-Clause License 2 | # Copyright (c) 2020, Ruby Enterprise Edition 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this 7 | # list of conditions and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, 9 | # this list of conditions and the following disclaimer in the documentation 10 | # and/or other materials provided with the distribution. 11 | # 3. Neither the name of the copyright holder nor the names of its 12 | # contributors may be used to endorse or promote products derived from 13 | # this software without specific prior written permission. 14 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | module Stream 26 | # A stream implemented based on an Array. 27 | # This class is immutable and thread-safe. 28 | # Author:: Mihai Andronache (amihaiemil@gmail.com) 29 | # Since:: 0.0.1 30 | class FromArray 31 | def initialize(array) 32 | @array = array 33 | end 34 | 35 | # Return the number of elements in this stream. 36 | # This is a terminal operation. 37 | # Since:: 0.0.1 38 | def count 39 | @array.length 40 | end 41 | 42 | # Filter out the elements which are not satisfying the given condition. 43 | # Example: 44 | # 45 | # stream = Stream::FromArray.new([1, 2, 3, 4, 5]) 46 | # collected = stream.filter {|num| num % 2 == 0}.collect 47 | # puts collected # [2, 4] 48 | # 49 | # This is an intermediary operation. 50 | # +condition+:: Ruby Block taking one parameter (the stream element) and 51 | # returning a boolean check on it. 52 | # Since:: 0.0.1 53 | def filter(&condition) 54 | filtered = [] 55 | @array.each do |val| 56 | filtered.push(val) unless condition.call(val) == false 57 | end 58 | FromArray.new(filtered) 59 | end 60 | 61 | # Map the stream's elements to a given value using a function. 62 | # Example (map int to string): 63 | # 64 | # stream = Stream::FromArray.new([1, 2, 3]) 65 | # collected = stream.map {|num| num.to_s}.collect 66 | # puts collected # ['1', '2', '3'] 67 | # 68 | # This is an intermediary operation. 69 | # +function+:: Ruby Block function taking one parameter 70 | # (the element in the stream). 71 | # Since:: 0.0.1 72 | def map(&function) 73 | mapped = [] 74 | @array.each do |val| 75 | mapped.push(function.call(val)) 76 | end 77 | FromArray.new(mapped) 78 | end 79 | 80 | # Remove all the duplicates from the stream. 81 | # This is an intermediary operation. 82 | # Since:: 0.0.2 83 | def distinct 84 | unique = [] 85 | @array.each do |val| 86 | unique.push(val) unless unique.include? val 87 | end 88 | FromArray.new(unique) 89 | end 90 | 91 | # Skip the first n elements of the stream. 92 | # This is an intermediary operation. 93 | # +count+:: Number of elements to skip from the beginning of the stream. 94 | # Since:: 0.0.1 95 | def skip(count) 96 | raise ArgumentError, 'count has to be positive integer' unless count.positive? and count.is_a? Integer 97 | 98 | skipped = [] 99 | @array.each_with_index do |val, index| 100 | skipped.push(val) unless index + 1 <= count 101 | end 102 | FromArray.new(skipped) 103 | end 104 | 105 | # Returns true if all the elements of the Stream are matching the 106 | # given predicate (a function which performs a test on the value and 107 | # should return a boolean). 108 | # 109 | # If the stream is empty, the returned value is true and the predicate 110 | # is not called at all. 111 | # 112 | # This is a terminal operation. 113 | # 114 | # +test+:: A function which should perform some boolean test on the value. 115 | # Since:: 0.0.2 116 | def all_match(&test) 117 | @array.each do |val| 118 | return false unless test.call(val) 119 | end 120 | true 121 | end 122 | 123 | # Returns true if any of the elements of the Stream are matching 124 | # the given predicate (a function which performs a test on the value and 125 | # should return a boolean). Iteration will stop at the first match. 126 | # 127 | # If the stream is empty, the returned value is false and the predicate 128 | # is not called at all. 129 | # 130 | # This is a terminal operation. 131 | # 132 | # +test+:: A function which should perform some boolean test on the 133 | # given value. 134 | # Since:: 0.0.2 135 | def any_match(&test) 136 | @array.each do |val| 137 | return true if test.call(val) 138 | end 139 | false 140 | end 141 | 142 | # Collect the stream's data into an array and return it. 143 | # This is a terminal operation. 144 | # Since:: 0.0.1 145 | def collect 146 | @array.dup 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /test/fromarray_test.rb: -------------------------------------------------------------------------------- 1 | # BSD 3-Clause License 2 | # Copyright (c) 2020, Ruby Enterprise Edition 3 | # All rights reserved. 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # 1. Redistributions of source code must retain the above copyright notice, this 7 | # list of conditions and the following disclaimer. 8 | # 2. Redistributions in binary form must reproduce the above copyright notice, 9 | # this list of conditions and the following disclaimer in the documentation 10 | # and/or other materials provided with the distribution. 11 | # 3. Neither the name of the copyright holder nor the names of its 12 | # contributors may be used to endorse or promote products derived from 13 | # this software without specific prior written permission. 14 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | require 'test_helper' 25 | require 'fromarray.rb' 26 | 27 | module Stream 28 | # Unit tests for class FromArray 29 | class FromArrayTest < Minitest::Test 30 | 31 | # FromArray can be instantiated with an array. 32 | def test_instantiates 33 | refute_nil FromArray.new([1, 2, 3]) 34 | refute_nil FromArray.new(['1', '2', '3']) 35 | end 36 | 37 | # FromArray can return its count 38 | def test_returns_count 39 | assert( 40 | FromArray.new([1, 2, 3]).count == 3 41 | ) 42 | assert( 43 | FromArray.new([]).count.zero? 44 | ) 45 | end 46 | 47 | # FromArray can collect its values into a new array. 48 | # The arrays should be equal since there is no 49 | # intermediary operation performed. 50 | def test_collects_no_modifications 51 | seed = [1, 2, 3] 52 | stream = FromArray.new(seed) 53 | assert( 54 | seed == stream.collect, 55 | 'The seed and collected array should be the equal!' 56 | ) 57 | assert( 58 | !seed.equal?(stream.collect), 59 | 'The seed and collected array should not be the same!' 60 | ) 61 | end 62 | 63 | # FromArray raises ArgumentError if the given count to skip 64 | # is <=0 65 | def test_skip_argument_error_count_negative 66 | stream = FromArray.new([1, 2, 3]) 67 | begin 68 | stream.skip(-1) 69 | assert(false, 'Exception was expected!') 70 | rescue ArgumentError => e 71 | assert( 72 | e.message == 'count has to be positive integer', 73 | 'Unexpected ArgumentError message!' 74 | ) 75 | end 76 | end 77 | 78 | # FromArray raises ArgumentError if the given count to skip 79 | # is positive but not an integer. 80 | def test_skip_argument_error_count_not_integer 81 | stream = FromArray.new([1, 2, 3]) 82 | begin 83 | stream.skip(2.23) 84 | assert(false, 'Exception was expected!') 85 | rescue ArgumentError => e 86 | assert( 87 | e.message == 'count has to be positive integer', 88 | 'Unexpected ArgumentError message!' 89 | ) 90 | end 91 | end 92 | 93 | # FromArray returns self after skipping some elements. 94 | def test_skip_returns_self 95 | stream = FromArray.new([1, 2, 3]) 96 | assert( 97 | !stream.skip(1).equal?(stream), 98 | 'Method skip should return a new instance of the modified stream' 99 | ) 100 | end 101 | 102 | # FromArray can skip the first element. 103 | def test_skip_first_element 104 | stream = FromArray.new([1, 2, 3]) 105 | collected = stream.skip(1).collect 106 | assert( 107 | collected == [2, 3], 108 | 'Expected ' + [2, 3].to_s + ' but got ' + collected.to_s 109 | ) 110 | end 111 | 112 | # FromArray can skip more than 1 element. 113 | def test_skip_more_elements 114 | stream = FromArray.new([1, 2, 3]) 115 | collected = stream.skip(2).collect 116 | assert( 117 | collected == [3], 118 | 'Expected ' + [3].to_s + ' but got ' + collected.to_s 119 | ) 120 | end 121 | 122 | # FromArray can skip all the elements. 123 | def test_skips_all_elements 124 | stream = FromArray.new([1, 2, 3]) 125 | collected = stream.skip(3).collect 126 | assert( 127 | collected == [], 128 | 'Expected ' + [].to_s + ' but got ' + collected.to_s 129 | ) 130 | end 131 | 132 | # FromArray can skip all the elements because the given 133 | # count is greater than the stream's size. 134 | def test_skips_all_elements_count_gt_size 135 | stream = FromArray.new([1, 2, 3]) 136 | collected = stream.skip(4).collect 137 | assert( 138 | collected == [], 139 | 'Expected ' + [].to_s + ' but got ' + collected.to_s 140 | ) 141 | end 142 | 143 | # FromArray can filter out elements which are not satisfying the 144 | # condition. 145 | def test_filter_elements 146 | stream = FromArray.new([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) 147 | collected = stream.filter { |num| num % 2 == 0 }.collect 148 | collected.each do |val| 149 | assert(val.even?) 150 | end 151 | end 152 | 153 | # FromArray filters out no elements because all of them are satisfying 154 | # the condition. 155 | def test_filters_no_elements 156 | stream = FromArray.new([2, 4, 6, 8]) 157 | collected = stream.filter { |num| num % 2 == 0 }.collect 158 | assert(stream.collect == collected) 159 | end 160 | 161 | # FromArray filters out all elements because none of are satisfying 162 | # the condition. 163 | def test_filters_all_elements 164 | stream = FromArray.new([2, 4, 6, 8]) 165 | assert(stream.filter { |num| num % 2 != 0 }.count == 0) 166 | end 167 | 168 | # Filtering a stream should return a new instance rather than modifying 169 | # the current one. 170 | def test_filter_returns_new_instance 171 | stream = FromArray.new([2, 4, 6, 8]) 172 | assert(!stream.filter(&:odd?).equal?(stream)) 173 | assert(stream.collect == [2, 4, 6, 8]) 174 | end 175 | 176 | # FromArray can map its elements using a given function. 177 | def test_map_elements 178 | stream = FromArray.new([1, 2, 3]) 179 | strings = stream.map { |num| num.to_s }.collect 180 | strings.each do |val| 181 | assert(val.is_a?(String)) 182 | end 183 | end 184 | 185 | # FromArray can map its single element. 186 | def test_maps_one_element 187 | stream = FromArray.new([1]) 188 | strings = stream.map { |num| num.to_s }.collect 189 | strings.each do |val| 190 | assert(val.is_a?(String)) 191 | end 192 | end 193 | 194 | # FromArray.map works when the stream is empty. 195 | def test_maps_no_elements 196 | stream = FromArray.new([]) 197 | collected = stream.map { |val| val.to_s }.collect 198 | assert(collected.empty?) 199 | end 200 | 201 | # FromArray.distinct removes all duplicates. 202 | def test_removes_duplicates 203 | stream = FromArray.new([2, 2, 3, 4, 1, 1, 2, 5, 4, 3, 6]) 204 | collected = stream.distinct.collect 205 | assert(collected == collected.uniq) 206 | assert(collected.length == collected.uniq.length) 207 | end 208 | 209 | # FromArray.distinct works when the stream is empty. 210 | def test_empty_distinct 211 | stream = FromArray.new([]) 212 | collected = stream.distinct.collect 213 | assert(collected.length.zero?) 214 | end 215 | 216 | # FromArray.distinct works when there are no duplicates in the array. 217 | def test_distinct_no_duplicates 218 | stream = FromArray.new([1, 2, 3, 4, 5]) 219 | collected = stream.distinct.collect 220 | assert(collected == collected.uniq) 221 | assert(collected.length == collected.uniq.length) 222 | end 223 | 224 | # FromArray.distinct works when there is only 1 element with 225 | # its duplicate 226 | def test_distinct_one_duplicate_element 227 | stream = FromArray.new([1, 1]) 228 | collected = stream.distinct.collect 229 | assert(collected.length == 1) 230 | assert(collected == collected.uniq) 231 | end 232 | 233 | # FromArray.all_match returns true when all 234 | # elements are a match. 235 | def test_all_elements_match 236 | stream = FromArray.new([2, 4, 6, 8]) 237 | assert( 238 | stream.all_match { |val| val % 2 == 0 }, 239 | 'All of the stream\'s elements should be even!' 240 | ) 241 | end 242 | 243 | # FromArray.all_match should return true if the stream is empty. 244 | def test_all_match_no_elements 245 | stream = FromArray.new([]) 246 | assert( 247 | stream.all_match { |val| val % 2 == 1 }, 248 | 'Expected true because the stream is empty!' 249 | ) 250 | end 251 | 252 | # FromArray.all_match returns false because not all the 253 | # elements are a match 254 | def test_not_all_elements_match 255 | stream = FromArray.new([2, 4, 5, 6, 8]) 256 | assert( 257 | stream.all_match { |val| val % 2 == 0 } == false, 258 | 'Expected false because not all elements are a match!' 259 | ) 260 | end 261 | 262 | # FromArray.any_match returns true if one element matches. 263 | def test_any_element_matches 264 | stream = FromArray.new([2, 4, 6, 8]) 265 | assert( 266 | stream.any_match { |val| val == 4 }, 267 | 'Stream contains element 4, it should match!' 268 | ) 269 | end 270 | 271 | # FromArray.any_match returns true if all elements match. 272 | def test_any_match_all 273 | stream = FromArray.new([2, 4, 6, 8]) 274 | assert( 275 | stream.any_match { |val| val %2 == 0 }, 276 | 'All elements are even, it should match!' 277 | ) 278 | end 279 | 280 | # FromArray.any_match returns false and the predicate is never called, 281 | # because the stream is empty. 282 | def test_any_match_empty 283 | stream = FromArray.new([]) 284 | assert( 285 | !stream.any_match { raise ScriptError, 'Should not be called' }, 286 | 'Stream is empty, any_match should be false!' 287 | ) 288 | end 289 | end 290 | end 291 | --------------------------------------------------------------------------------