├── .rspec ├── lib ├── scan_left │ ├── version.rb │ └── enumerable_with_scan_left.rb └── scan_left.rb ├── bin ├── setup └── console ├── spec ├── scan_left_spec.rb ├── spec_helper.rb ├── enumerable_with_scan_left_spec.rb └── scan_left_examples.rb ├── .rubocop.yml ├── Gemfile ├── .github ├── dependabot.yml └── workflows │ ├── dependency-review.yml │ ├── tests.yml │ └── dependabot-prs.yml ├── .gitignore ├── LICENSE ├── scan_left.gemspec ├── Gemfile.lock ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/scan_left/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ScanLeft 4 | VERSION = "0.3.1" 5 | end 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/scan_left_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "scan_left_examples" 4 | 5 | RSpec.describe ScanLeft do 6 | subject { described_class.new(enumerable).scan_left(initial, &block) } 7 | 8 | include_examples "scan_left_examples" 9 | end 10 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | panolint-ruby: panolint-ruby-rubocop.yml 3 | Metrics/BlockLength: 4 | Exclude: 5 | - scan_left.gemspec 6 | - spec/*.rb 7 | Style/FrozenStringLiteralComment: 8 | Exclude: 9 | - Gemfile 10 | - Rakefile 11 | - bin/* 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 4 | 5 | group :development do 6 | gem "rspec", "~> 3.13" 7 | end 8 | 9 | group :checks do 10 | gem "panolint-ruby", github: "panorama-ed/panolint-ruby", branch: "main" 11 | end 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "00:00" 8 | timezone: America/New_York 9 | open-pull-requests-limit: 99 10 | labels: 11 | - dependencies 12 | - Needs QA 13 | allow: 14 | - dependency-type: direct 15 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "scan_left" 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 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require "scan_left" 5 | 6 | RSpec.configure do |config| 7 | # Enable flags like --only-failures and --next-failure 8 | config.example_status_persistence_file_path = ".rspec_status" 9 | 10 | # Disable RSpec exposing methods globally on `Module` and `main` 11 | config.disable_monkey_patching! 12 | 13 | config.expect_with :rspec do |c| 14 | c.syntax = :expect 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/enumerable_with_scan_left_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "scan_left_examples" 4 | 5 | RSpec.describe EnumerableWithScanLeft do 6 | using described_class 7 | 8 | subject { enumerable.scan_left(initial, &block) } 9 | 10 | # There is a bug in Ruby 2.7 that prevents these specs from passing. See: 11 | # * https://github.com/rspec/rspec-core/issues/2727 12 | # * https://bugs.ruby-lang.org/issues/16852 13 | # 14 | # Until this is resolved, we skip these specs on Ruby 2.7. 15 | pending_msg = "Known bug in Ruby 2.7" if RUBY_VERSION.start_with?("2.7") 16 | include_examples "scan_left_examples", skip: pending_msg 17 | end 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | # Ignore Byebug command history file. 17 | .byebug_history 18 | 19 | ## Documentation cache and generated files: 20 | /.yardoc/ 21 | /_yardoc/ 22 | /doc/ 23 | /rdoc/ 24 | 25 | ## Environment normalization: 26 | /.bundle/ 27 | /vendor/bundle 28 | /lib/bundler/man/ 29 | 30 | # for a library or gem, you might want to ignore these files since the code is 31 | # intended to run in multiple environments; otherwise, check them in: 32 | # Gemfile.lock 33 | .ruby-version 34 | .ruby-gemset 35 | .rspec_status 36 | 37 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 38 | .rvmrc 39 | 40 | # Used by RuboCop. Remote config files pulled in from inherit_from directive. 41 | # .rubocop-https?--* 42 | -------------------------------------------------------------------------------- /lib/scan_left/enumerable_with_scan_left.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # *Optional* 4 | # {refinement}[https://docs.ruby-lang.org/en/2.7.0/syntax/refinements_rdoc.html] 5 | # which refines +Enumerable+ to add a +#scan_left+ method, in order to provide 6 | # a more natural syntax in comparison to explicitly creating instances of the 7 | # +ScanLeft+ class. 8 | # 9 | # Without using this refinement, we wrap Enumerables in +ScanLeft+ instances: 10 | # 11 | # ScanLeft.new([1,2,3]).scan_left(0, &:+) # => [0, 1, 3, 6] 12 | # 13 | # Using this refinement, we can call +#scan_left+ directly on any Enumerable: 14 | # 15 | # [1,2,3].scan_left(0, &:+) # => [0, 1, 3, 6] 16 | # 17 | # @example 18 | # class Foo 19 | # using EnumerableWithScanLeft 20 | # 21 | # def bar(x) 22 | # [1,2,3].scan_left(x, &:+) 23 | # end 24 | # end 25 | # 26 | # Foo.new.bar(10) # => [10, 11, 13, 16] 27 | # 28 | module EnumerableWithScanLeft 29 | refine Enumerable do 30 | def scan_left(initial, &block) 31 | ScanLeft.new(self).scan_left(initial, &block) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v3 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v3 21 | with: 22 | # Possible values: "critical", "high", "moderate", "low" 23 | fail-on-severity: high -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2022 Panorama Education 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | ci: 11 | name: CI 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | # Due to https://github.com/actions/runner/issues/849, we have to use 16 | # quotes for '3.0'. Without quotes, CI runs 3.1. 17 | ruby: [ jruby, truffleruby, 2.5, 2.6, 2.7, '3.0', 3.1 ] 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | # Conditionally configure bundler via environment variables as advised 23 | # * https://github.com/ruby/setup-ruby#bundle-config 24 | - name: Set bundler environment variables 25 | run: | 26 | echo "BUNDLE_WITHOUT=checks" >> $GITHUB_ENV 27 | if: matrix.ruby != 3.1 28 | 29 | # Use 'bundler-cache: true' instead of actions/cache as advised: 30 | # * https://github.com/actions/cache/blob/main/examples.md#ruby---bundler 31 | - uses: ruby/setup-ruby@v1 32 | with: 33 | ruby-version: ${{ matrix.ruby }} 34 | bundler-cache: true 35 | 36 | - run: bundle exec rspec 37 | 38 | - run: bundle exec rubocop 39 | if: matrix.ruby == 3.1 40 | -------------------------------------------------------------------------------- /scan_left.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("lib", __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require "scan_left/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "scan_left" 9 | spec.version = ScanLeft::VERSION 10 | spec.authors = ["Marc Siegel", "Parker Finch"] 11 | spec.email = ["marc@usainnov.com", "msiegel@panoramaed.com", 12 | "pfinch@panoramaed.com"] 13 | 14 | spec.summary = "A tiny Ruby gem to provide the 'scan_left' operation on "\ 15 | "any Ruby Enumerable." 16 | spec.description = spec.summary 17 | spec.homepage = "https://github.com/panorama-ed/scan_left" 18 | spec.license = "MIT" 19 | 20 | if spec.respond_to?(:metadata) 21 | spec.metadata["allowed_push_host"] = "https://rubygems.org/" # allow pushes 22 | spec.metadata["homepage_uri"] = spec.homepage 23 | spec.metadata["source_code_uri"] = spec.homepage 24 | spec.metadata["changelog_uri"] = 25 | "https://github.com/panorama-ed/scan_left/blob/main/CHANGELOG.md" 26 | spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/scan_left" 27 | else 28 | raise "RubyGems 2.0 or newer is required to protect against "\ 29 | "public gem pushes." 30 | end 31 | 32 | # Specify which files should be added to the gem when it is released. 33 | # The `git ls-files -z` loads the files in the RubyGem that have been added 34 | # into git. 35 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 36 | `git ls-files -z`. 37 | split("\x0"). 38 | reject { |f| f.match(%r{^(test|spec|features)/}) } 39 | end 40 | spec.bindir = "exe" 41 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 42 | spec.require_paths = ["lib"] 43 | 44 | spec.metadata["rubygems_mfa_required"] = "true" 45 | end 46 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: https://github.com/panorama-ed/panolint-ruby 3 | revision: d19a2a048fcd61e5bdbaed5c1ebf4641fef79a9c 4 | branch: main 5 | specs: 6 | panolint-ruby (0) 7 | rubocop (= 1.72.1) 8 | rubocop-performance (= 1.21.1) 9 | rubocop-rspec (= 3.5.0) 10 | 11 | GEM 12 | remote: https://rubygems.org/ 13 | specs: 14 | ast (2.4.3) 15 | diff-lcs (1.5.1) 16 | json (2.10.2) 17 | language_server-protocol (3.17.0.4) 18 | lint_roller (1.1.0) 19 | parallel (1.26.3) 20 | parser (3.3.7.2) 21 | ast (~> 2.4.1) 22 | racc 23 | racc (1.8.1) 24 | rainbow (3.1.1) 25 | regexp_parser (2.10.0) 26 | rspec (3.13.0) 27 | rspec-core (~> 3.13.0) 28 | rspec-expectations (~> 3.13.0) 29 | rspec-mocks (~> 3.13.0) 30 | rspec-core (3.13.0) 31 | rspec-support (~> 3.13.0) 32 | rspec-expectations (3.13.0) 33 | diff-lcs (>= 1.2.0, < 2.0) 34 | rspec-support (~> 3.13.0) 35 | rspec-mocks (3.13.0) 36 | diff-lcs (>= 1.2.0, < 2.0) 37 | rspec-support (~> 3.13.0) 38 | rspec-support (3.13.0) 39 | rubocop (1.72.1) 40 | json (~> 2.3) 41 | language_server-protocol (~> 3.17.0.2) 42 | lint_roller (~> 1.1.0) 43 | parallel (~> 1.10) 44 | parser (>= 3.3.0.2) 45 | rainbow (>= 2.2.2, < 4.0) 46 | regexp_parser (>= 2.9.3, < 3.0) 47 | rubocop-ast (>= 1.38.0, < 2.0) 48 | ruby-progressbar (~> 1.7) 49 | unicode-display_width (>= 2.4.0, < 4.0) 50 | rubocop-ast (1.41.0) 51 | parser (>= 3.3.7.2) 52 | rubocop-performance (1.21.1) 53 | rubocop (>= 1.48.1, < 2.0) 54 | rubocop-ast (>= 1.31.1, < 2.0) 55 | rubocop-rspec (3.5.0) 56 | lint_roller (~> 1.1) 57 | rubocop (~> 1.72, >= 1.72.1) 58 | ruby-progressbar (1.13.0) 59 | unicode-display_width (3.1.4) 60 | unicode-emoji (~> 4.0, >= 4.0.4) 61 | unicode-emoji (4.0.4) 62 | 63 | PLATFORMS 64 | ruby 65 | 66 | DEPENDENCIES 67 | panolint-ruby! 68 | rspec (~> 3.13) 69 | 70 | BUNDLED WITH 71 | 2.3.22 72 | -------------------------------------------------------------------------------- /spec/scan_left_examples.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.shared_examples "scan_left_examples" do |skip: false| 4 | # Uses the following lets: 5 | # - enumerable The stream to transform 6 | # - initial The initial value to use 7 | # - block The binary operation to use 8 | shared_examples "a_series_of_injects" do 9 | let(:series_of_intermediate_inject_results) do 10 | slices = (0..enumerable.size).map { |n| enumerable.take(n) } 11 | slices.map { |slice| slice.inject(initial, &block) } 12 | end 13 | 14 | it "equals a series of intermediate results from #inject" do 15 | expect(subject.to_a).to eq series_of_intermediate_inject_results 16 | end 17 | 18 | it "preserves laziness" do 19 | if enumerable.is_a?(Enumerator::Lazy) 20 | is_expected.to be_a(Enumerator::Lazy) 21 | else 22 | is_expected.not_to be_a(Enumerator::Lazy) 23 | end 24 | end 25 | end 26 | 27 | context "when summing ints", skip: skip do 28 | let(:initial) { 0 } 29 | let(:block) { ->(memo, obj) { memo + obj } } 30 | 31 | context "with zero elements" do 32 | let(:enumerable) { [] } 33 | 34 | it_behaves_like "a_series_of_injects" 35 | end 36 | 37 | context "with a single element" do 38 | let(:enumerable) { [1] } 39 | 40 | it_behaves_like "a_series_of_injects" 41 | end 42 | 43 | context "with multiple elements" do 44 | let(:enumerable) { (1..5).to_a } 45 | 46 | it_behaves_like "a_series_of_injects" 47 | 48 | context "when lazy" do 49 | let(:enumerable) { super().lazy } 50 | 51 | it_behaves_like "a_series_of_injects" 52 | end 53 | end 54 | end 55 | 56 | context "when using #with_index", skip: skip do 57 | let(:initial) { 0 } 58 | let(:enumerable) { [1, 2, 3].each.with_index } 59 | let(:block) { ->(memo, obj, index) { memo + obj + index } } 60 | 61 | it "passes the correct number of arguments to produce the correct result" do 62 | expect(subject).to eq([0, 1, 4, 9]) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.3.1] - 2023-06-05 10 | ### Changed 11 | - Changed linting development dependency from `panolint` to `panolint-ruby`. 12 | 13 | ## [0.3.0] - 2020-06-30 14 | ### Added 15 | - Dependabot configuration for automatic dependency update PRs 16 | - Added Parker Finch as a gem author in the gemspec! 17 | 18 | ### Changed 19 | - Moved panolint to *development* dependency (oops!) 20 | - Fixed YARD docs link and other typos 21 | 22 | ## [0.2.1] - 2020-05-15 23 | ### Added 24 | - Additional documentation and examples 25 | 26 | ### Changed 27 | - Fixed formatting inside of YARD documentation comments 28 | - Fixed syntax highlighting in examples in YARD doc comments 29 | 30 | ## [0.2.0] - 2020-05-14 31 | ### Added 32 | - Optional refinement to add a `#scan_left` directly to `Enumerable` 33 | - Badges in README.md for tests, gem version, and docs 34 | - Documentation link in gemspec to rubydoc.info 35 | 36 | ### Changed 37 | - Improved YARD doc formatting for RubyDoc.info 38 | 39 | ## [0.1.0] - 2020-05-08 40 | ### Added 41 | - Initial implementation of `ScanLeft` 42 | - Code of conduct 43 | - Rubocop configuration 44 | - Tests run via Github Actions configuration 45 | 46 | ### Changed 47 | - Bumped bundler version to 2.x 48 | 49 | ### Removed 50 | - Scaffolding from the gem skeleton structure 51 | 52 | ## [0.0.1] - 2020-05-06 53 | ### Added 54 | - Initial gem project structure 55 | - Initial CHANGELOG.md based on keepachangelog.com 56 | 57 | [Unreleased]: https://github.com/panorama-ed/scan_left/compare/v0.3.0...HEAD 58 | [0.3.0]: https://github.com/panorama-ed/scan_left/compare/v0.2.1...v0.3.0 59 | [0.2.1]: https://github.com/panorama-ed/scan_left/compare/v0.2.0...v0.2.1 60 | [0.2.0]: https://github.com/panorama-ed/scan_left/compare/v0.1.0...v0.2.0 61 | [0.1.0]: https://github.com/panorama-ed/scan_left/compare/v0.0.1...v0.1.0 62 | [0.0.1]: https://github.com/panorama-ed/scan_left/releases/tag/v0.0.1 63 | -------------------------------------------------------------------------------- /lib/scan_left.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Original author: {Marc Siegel}[https://github.com/ms-ati]. 4 | 5 | require "scan_left/enumerable_with_scan_left" 6 | require "scan_left/version" 7 | 8 | # Wraps any +Enumerable+ to provide the {#scan_left} operation. 9 | # 10 | # Please see {file:README.md} for details, examples, background, and further 11 | # reading about this operation. 12 | # 13 | # *Note:* if you'd prefer to use the {#scan_left} method directly on any 14 | # +Enumerable+ instance, please see the optional refinement 15 | # {EnumerableWithScanLeft}. 16 | # 17 | # @example 18 | # ScanLeft.new([1, 2, 3]).scan_left(0) { |s, x| s + x } == [0, 1, 3, 6] 19 | # 20 | # @see EnumerableWithScanLeft 21 | class ScanLeft 22 | # @return [Enumerable] Enumerable to transform via {#scan_left} 23 | attr_reader :enumerable 24 | 25 | # @param enumerable [Enumerable] Enumerable to transform via {#scan_left} 26 | def initialize(enumerable) 27 | @enumerable = enumerable 28 | end 29 | 30 | # Map the enumerable to the incremental state of a calculation by 31 | # applying the given block, in turn, to each element and the 32 | # previous state of the calculation, resulting in an enumerable of 33 | # the state after each iteration. 34 | # 35 | # @return [Enumerable] Generate a stream of intermediate states 36 | # resulting from applying a binary operator. Equivalent to a 37 | # stream of +#inject+ calls on first N elements, increasing N from 38 | # zero to the size of the stream. NOTE: Preserves laziness if 39 | # +enumerable+ is lazy. 40 | # 41 | # @param initial [Object] Initial state value to yield. 42 | # 43 | # @yield [memo, obj] Invokes given block with previous state value 44 | # +memo+ and next element of the stream +obj+. 45 | # 46 | # @yieldreturn [Object] The next state value for +memo+. 47 | def scan_left(initial) 48 | memo = initial 49 | outs = enumerable.map { |*obj| memo = yield(memo, *obj) } 50 | prepend_preserving_laziness(value: initial, enum: outs) 51 | end 52 | 53 | private 54 | 55 | def prepend_preserving_laziness(value:, enum:) 56 | case enum 57 | when Enumerator::Lazy 58 | [[value], enum].lazy.flat_map { |x| x } 59 | else 60 | [value] + enum 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-prs.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Pull Request 2 | on: 3 | pull_request_target: 4 | types: [opened, synchronize, reopened, labeled] 5 | jobs: 6 | build: 7 | if: startsWith(github.head_ref, 'dependabot/') 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Get unique committers 11 | id: unique-committers 12 | run: echo "::set-output name=committers::$(gh pr view $PR_URL --json commits --jq '[.commits.[] | .authors.[] | .login] | unique')" 13 | env: 14 | PR_URL: ${{github.event.pull_request.html_url}} 15 | GITHUB_TOKEN: ${{secrets.PANORAMA_BOT_RW_TOKEN}} 16 | # The last step enables auto-merge in certain situations, but we don't want dependabots that require 17 | # additional work to accidentally get merged before code review so we turn it off here. 18 | - name: Disable auto-merge if there are commits from someone other than Dependabot 19 | if: steps.unique-committers.outputs.committers != '["dependabot[bot]"]' 20 | run: gh pr merge --disable-auto --merge "$PR_URL" 21 | env: 22 | PR_URL: ${{github.event.pull_request.html_url}} 23 | GITHUB_TOKEN: ${{secrets.PANORAMA_BOT_RW_TOKEN}} 24 | - name: Add the Needs QA label to dependabots after any change by someone other than the dependabot bot 25 | # Need to avoid the situation where someone removes the "Needs QA" label and we are adding it back. 26 | if: ${{ github.actor != 'dependabot[bot]' && github.event.action != 'labeled' }} 27 | run: gh pr edit "$PR_URL" --add-label "Needs QA" 28 | env: 29 | PR_URL: ${{github.event.pull_request.html_url}} 30 | GITHUB_TOKEN: ${{secrets.PANORAMA_BOT_RW_TOKEN}} 31 | - name: Fetch Dependabot metadata 32 | if: ${{ github.actor == 'dependabot[bot]' }} 33 | id: dependabot-metadata 34 | uses: dependabot/fetch-metadata@v1.1.0 35 | with: 36 | github-token: "${{ secrets.GITHUB_TOKEN }}" 37 | - name: Approve and merge Dependabot PRs for development dependencies 38 | # Auto-merge the PR if either: 39 | # a) it has the `development-dependencies` label, which we add for certain 40 | # categories of PRs (see `.github/dependabot.yml`), OR 41 | # b) Dependabot has categorized it as a `direct:development` dependency, 42 | # meaning it's in the Gemfile in a `development` or `test` group, OR 43 | # c) our scripts have flagged the PR as an automergeable dependency (i.e 44 | # a stable dependency with good unit test coverage) that has passed 45 | # the waiting period. 46 | if: ${{ (github.actor == 'dependabot[bot]' || github.actor == 'panorama-bot-r') && steps.unique-committers.outputs.committers == '["dependabot[bot]"]' && (contains(github.event.pull_request.labels.*.name, 'development-dependencies') || steps.dependabot-metadata.outputs.dependency-type == 'direct:development' || contains(github.event.pull_request.labels.*.name, 'automerge-dependencies')) }} 47 | run: gh pr merge --auto --merge "$PR_URL" && gh pr edit "$PR_URL" --remove-label "Needs QA" && gh pr review --approve "$PR_URL" 48 | env: 49 | PR_URL: ${{github.event.pull_request.html_url}} 50 | GITHUB_TOKEN: ${{secrets.PANORAMA_BOT_RW_TOKEN}} 51 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at engineering@panoramaed.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [https://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scan_left 2 | [![Tests](https://github.com/panorama-ed/scan_left/workflows/Tests/badge.svg)](https://github.com/panorama-ed/scan_left/actions?query=workflow%3ATests) 3 | 4 | [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://rubydoc.info/github/panorama-ed/scan_left) 5 | [![Docs Coverage](http://inch-ci.org/github/panorama-ed/scan_left.png)](http://inch-ci.org/github/panorama-ed/scan_left) 6 | 7 | [![Gem Version](https://img.shields.io/gem/v/scan_left.svg)](https://rubygems.org/gems/scan_left) 8 | [![Gem Downloads](https://img.shields.io/gem/dt/scan_left.svg)](https://rubygems.org/gems/scan_left) 9 | 10 | A tiny Ruby gem to provide the `#scan_left` operation on any Ruby 11 | `Enumerable`. 12 | 13 | ## What does it do? 14 | 15 | Imagine a series of numbers which you want to *sum*. You accomplish 16 | this by processing all elements, adding each to the previous sum, 17 | returning the final result. 18 | 19 | Now imagine that, rather than just the final sum at the end of the 20 | series, you want *a series* of the partial sums after processing each 21 | element. This is called a ["Prefix 22 | Sum"](https://en.wikipedia.org/wiki/Prefix_sum). 23 | 24 | In functional programming (FP), the *sum* is generalized as the *fold* 25 | operation, in which an initial state and a binary operation are 26 | combined to "fold" a series of values into a single result. The 27 | closely related *prefix sum*, which produces a series of intermediate 28 | results, is generalized as the *scan* operation. Adding "left" to 29 | these operation names indicates that calculation is to proceed 30 | left-to-right. 31 | 32 | ## Compare / Contrast with #inject 33 | 34 | Ruby's standard library operation `Enumerable#inject` implements the 35 | FP fold operation. It also implements the simpler reduce operation, 36 | which is a fold whose initial state is the first element of the 37 | series. 38 | 39 | The key differences between `#inject` and `#scan_left` are: 40 | 41 | 1. **Incremental results**: `#scan_left` returns a series of results 42 | after processing each element of the input series. `#inject` 43 | returns a single value, which equals the final result in the 44 | series returned by `#scan_left`. 45 | 46 | 2. **Laziness**: `#scan_left` can preserve the laziness of the input 47 | series. As each incremental result is read from the output 48 | series, the actual calculation is lazily performed against the 49 | input. `#inject` cannot be a lazy operation in general, as its 50 | single result reflects a calculation across every element of the 51 | input series. 52 | 53 | ## Examples 54 | 55 | ```ruby 56 | require "scan_left" 57 | 58 | # For comparison, results from #inject are shown as well: 59 | 60 | ScanLeft.new([]).scan_left(0) { |s, x| s + x } == [0] 61 | [].inject(0) { |s, x| s + x } == 0 62 | 63 | ScanLeft.new([1]).scan_left(0) { |s, x| s + x } == [0, 1] 64 | [1].inject(0) { |s, x| s + x } == 1 65 | 66 | ScanLeft.new([1, 2, 3]).scan_left(0) { |s, x| s + x } == [0, 1, 3, 6] 67 | [1, 2, 3].inject(0) { |s, x| s + x } == 6 68 | 69 | # OPTIONAL: To avoid explicitly using the `ScanLeft` class, you may 70 | # choose to use the provided refinement on Enumerable. 71 | # 72 | # This refinement adds a `#scan_left` method directly to Enumerable 73 | # for a more concise syntax. 74 | 75 | using EnumerableWithScanleft 76 | 77 | [].scan_left(0) { |s, x| s + x } => [0] 78 | [1].scan_left(0) { |s, x| s + x } => [0, 1] 79 | [1, 2, 3].scan_left(0) { |s, x| s + x } => [0, 1, 3, 6] 80 | ``` 81 | 82 | ## Further Reading 83 | 84 | * https://en.wikipedia.org/wiki/Fold_(higher-order_function) 85 | * https://en.wikipedia.org/wiki/Prefix_sum#Scan_higher_order_function 86 | * http://alvinalexander.com/scala/how-to-walk-scala-collections-reduceleft-foldright-cookbook#scanleft-and-scanright 87 | --------------------------------------------------------------------------------