├── .gitignore ├── .rspec ├── .rubocop.yml ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── circular_array.gemspec ├── lib └── circular_array.rb └── spec ├── circular_array_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.6 3 | 4 | Style/Documentation: 5 | Enabled: false 6 | 7 | Metrics/LineLength: 8 | Max: 120 9 | 10 | Metrics/BlockLength: 11 | ExcludedMethods: [ 12 | 'describe', 'xdescribe', 'context', 'xcontext', 'it', 'xit', 'let', 'before', 'after', 'guard' 13 | ] 14 | 15 | Layout/AlignHash: 16 | EnforcedColonStyle: table 17 | EnforcedHashRocketStyle: table 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: false 3 | language: ruby 4 | cache: bundler 5 | rvm: 6 | - 2.6.3 7 | 8 | before_install: gem install bundler -v 2.0.2 9 | 10 | script: 11 | - bundle exec rubocop 12 | - bundle exec rspec spec 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 0.1.0 4 | Initial gem release 5 | -------------------------------------------------------------------------------- /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 volodymyr.sveredyuk@innocode.no. 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 [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in circular_array.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | circular_array (0.2.1) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | ast (2.4.0) 10 | coderay (1.1.2) 11 | diff-lcs (1.3) 12 | ffi (1.11.1) 13 | formatador (0.2.5) 14 | guard (2.15.1) 15 | formatador (>= 0.2.4) 16 | listen (>= 2.7, < 4.0) 17 | lumberjack (>= 1.0.12, < 2.0) 18 | nenv (~> 0.1) 19 | notiffany (~> 0.0) 20 | pry (>= 0.9.12) 21 | shellany (~> 0.0) 22 | thor (>= 0.18.1) 23 | guard-compat (1.2.1) 24 | guard-rspec (4.7.3) 25 | guard (~> 2.1) 26 | guard-compat (~> 1.1) 27 | rspec (>= 2.99.0, < 4.0) 28 | jaro_winkler (1.5.3) 29 | listen (3.1.5) 30 | rb-fsevent (~> 0.9, >= 0.9.4) 31 | rb-inotify (~> 0.9, >= 0.9.7) 32 | ruby_dep (~> 1.2) 33 | lumberjack (1.0.13) 34 | method_source (0.9.2) 35 | nenv (0.3.0) 36 | notiffany (0.1.3) 37 | nenv (~> 0.1) 38 | shellany (~> 0.0) 39 | parallel (1.17.0) 40 | parser (2.6.4.0) 41 | ast (~> 2.4.0) 42 | pry (0.12.2) 43 | coderay (~> 1.1.0) 44 | method_source (~> 0.9.0) 45 | rainbow (3.0.0) 46 | rake (13.0.1) 47 | rb-fsevent (0.10.3) 48 | rb-inotify (0.10.0) 49 | ffi (~> 1.0) 50 | rspec (3.8.0) 51 | rspec-core (~> 3.8.0) 52 | rspec-expectations (~> 3.8.0) 53 | rspec-mocks (~> 3.8.0) 54 | rspec-core (3.8.2) 55 | rspec-support (~> 3.8.0) 56 | rspec-expectations (3.8.4) 57 | diff-lcs (>= 1.2.0, < 2.0) 58 | rspec-support (~> 3.8.0) 59 | rspec-mocks (3.8.1) 60 | diff-lcs (>= 1.2.0, < 2.0) 61 | rspec-support (~> 3.8.0) 62 | rspec-support (3.8.2) 63 | rubocop (0.74.0) 64 | jaro_winkler (~> 1.5.1) 65 | parallel (~> 1.10) 66 | parser (>= 2.6) 67 | rainbow (>= 2.2.2, < 4.0) 68 | ruby-progressbar (~> 1.7) 69 | unicode-display_width (>= 1.4.0, < 1.7) 70 | ruby-progressbar (1.10.1) 71 | ruby_dep (1.5.0) 72 | shellany (0.0.1) 73 | thor (0.20.3) 74 | unicode-display_width (1.6.0) 75 | 76 | PLATFORMS 77 | ruby 78 | 79 | DEPENDENCIES 80 | bundler (~> 2.0) 81 | circular_array! 82 | guard-rspec (~> 4.7.3) 83 | pry (~> 0.12) 84 | rake (~> 13.0) 85 | rspec (~> 3.0) 86 | rubocop (~> 0.74) 87 | 88 | BUNDLED WITH 89 | 2.0.2 90 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # A sample Guardfile 4 | # More info at https://github.com/guard/guard#readme 5 | 6 | ## Uncomment and set this to only include directories you want to watch 7 | # directories %w(app lib config test spec features) \ 8 | # .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")} 9 | 10 | ## Note: if you are using the `directories` clause above and you are not 11 | ## watching the project directory ('.'), then you will want to move 12 | ## the Guardfile to a watched dir and symlink it back, e.g. 13 | # 14 | # $ mkdir config 15 | # $ mv Guardfile config/ 16 | # $ ln -s config/Guardfile . 17 | # 18 | # and, you'll have to watch "config/Guardfile" instead of "Guardfile" 19 | 20 | # Note: The cmd option is now required due to the increasing number of ways 21 | # rspec may be run, below are examples of the most common uses. 22 | # * bundler: 'bundle exec rspec' 23 | # * bundler binstubs: 'bin/rspec' 24 | # * spring: 'bin/rspec' (This will use spring if running and you have 25 | # installed the spring binstubs per the docs) 26 | # * zeus: 'zeus rspec' (requires the server to be started separately) 27 | # * 'just' rspec: 'rspec' 28 | 29 | guard :rspec, cmd: 'bundle exec rspec' do 30 | require 'guard/rspec/dsl' 31 | dsl = Guard::RSpec::Dsl.new(self) 32 | 33 | # Feel free to open issues for suggestions and improvements 34 | 35 | # RSpec files 36 | rspec = dsl.rspec 37 | watch(rspec.spec_helper) { rspec.spec_dir } 38 | watch(rspec.spec_support) { rspec.spec_dir } 39 | watch(rspec.spec_files) 40 | 41 | # Ruby files 42 | ruby = dsl.ruby 43 | dsl.watch_spec_files_for(ruby.lib_files) 44 | 45 | # Rails files 46 | rails = dsl.rails(view_extensions: %w[erb haml slim]) 47 | dsl.watch_spec_files_for(rails.app_files) 48 | dsl.watch_spec_files_for(rails.views) 49 | 50 | watch(rails.controllers) do |m| 51 | [ 52 | rspec.spec.call("routing/#{m[1]}_routing"), 53 | rspec.spec.call("controllers/#{m[1]}_controller"), 54 | rspec.spec.call("acceptance/#{m[1]}") 55 | ] 56 | end 57 | 58 | # Rails config changes 59 | watch(rails.spec_helper) { rspec.spec_dir } 60 | watch(rails.routes) { "#{rspec.spec_dir}/routing" } 61 | watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" } 62 | 63 | # Capybara features specs 64 | watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") } 65 | watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") } 66 | 67 | # Turnip features and steps 68 | watch(%r{^spec/acceptance/(.+)\.feature$}) 69 | watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m| 70 | Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Volodymyr Sveredyuk 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CircularArray 2 | 3 | [![Build Status](https://travis-ci.com/sveredyuk/ruby-circular-array.svg?branch=master)](https://travis-ci.com/sveredyuk/ruby-circular-array) 4 | 5 | You have a week with days and you want to iterate over the week and never met `nil`, but: 6 | ```ruby 7 | week = [:mon, :tue, :wed, :thu, :fri, :sat, :sun] 8 | week[0] # => :mon 9 | week[3] # => :thu 10 | week[6] # => :sun 11 | week[7] # => nil... stop what?! I need monday! 12 | ``` 13 | 14 | But it's possible with `CircularArray` 15 | ```ruby 16 | require 'circular_array' 17 | 18 | circular_week = CircularArray[:mon, :tue, :wed, :thu, :fri, :sat, :sun] 19 | 20 | # It behaves like Array. Basically it inherits Array: 21 | circular_week.kind_of? Array # => true 22 | 23 | # But it is endless: 24 | circular_week[0] # => :mon 25 | circular_week[3] # => :thu 26 | circular_week[6] # => :sun 27 | circular_week[7] # => :mon 28 | circular_week[8] # => :tue 29 | circular_week[9] # => :wed 30 | circular_week[10] # => :thu 31 | circular_week[11] # => :fri 32 | circular_week[12] # => :sat 33 | circular_week[13] # => :sun 34 | circular_week[14] # => :mon 35 | # great! 36 | ``` 37 | 38 | **You can use it for recursive matching, but do not forget to add anti-infinity loop clause** 39 | 40 | Only for empty collection in returns `nil` 41 | ```ruby 42 | empty_circular_array = CircularArray.new([]) 43 | empty_circular_array[1] # => nil 44 | ``` 45 | 46 | ## Use cases 47 | 48 | 1. Round-robin selection algorithm 49 | 2. Matching over a list of possible solutions 50 | 3. Cycling dates for complex delivery logic 51 | 52 | ## Installation 53 | 54 | Add this line to your application's Gemfile: 55 | 56 | ```ruby 57 | gem 'circular_array' 58 | ``` 59 | 60 | And then execute: 61 | 62 | $ bundle 63 | 64 | Or install it yourself as: 65 | 66 | $ gem install circular_array 67 | 68 | 69 | ## Development 70 | 71 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 72 | 73 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 74 | 75 | ## Contributing 76 | 77 | Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/circular_array. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 78 | 79 | ## License 80 | 81 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 82 | 83 | ## Code of Conduct 84 | 85 | Everyone interacting in the CircularArray project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/circular_array/blob/master/CODE_OF_CONDUCT.md). 86 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'circular_array' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /circular_array.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 | 6 | require 'circular_array' 7 | 8 | Gem::Specification.new do |spec| 9 | spec.name = 'circular_array' 10 | spec.version = CircularArray::VERSION 11 | spec.authors = ['Volodya Sveredyuk'] 12 | spec.email = ['sveredyuk@gmail.com'] 13 | 14 | spec.summary = 'CircularArray' 15 | spec.description = 'Endless array' 16 | spec.homepage = 'https://github.com/sveredyuk/ruby-circular-array' 17 | spec.license = 'MIT' 18 | 19 | spec.metadata['allowed_push_host'] = 'https://rubygems.org' 20 | 21 | spec.metadata['homepage_uri'] = spec.homepage 22 | spec.metadata['source_code_uri'] = 'https://github.com/sveredyuk/ruby-circular-array' 23 | spec.metadata['changelog_uri'] = 'https://github.com/sveredyuk/ruby-circular-array/CHANGELOG.md' 24 | 25 | # Specify which files should be added to the gem when it is released. 26 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 27 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 28 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 29 | end 30 | spec.bindir = 'exe' 31 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 32 | spec.require_paths = ['lib'] 33 | 34 | spec.add_development_dependency 'bundler', '~> 2.0' 35 | spec.add_development_dependency 'guard-rspec', '~> 4.7.3' 36 | spec.add_development_dependency 'pry', '~> 0.12' 37 | spec.add_development_dependency 'rake', '~> 13.0' 38 | spec.add_development_dependency 'rspec', '~> 3.0' 39 | spec.add_development_dependency 'rubocop', '~> 0.74' 40 | end 41 | -------------------------------------------------------------------------------- /lib/circular_array.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CircularArray < Array 4 | VERSION = '0.2.1' 5 | 6 | def [](index) 7 | return nil if empty? 8 | 9 | super(index % size) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/circular_array_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe CircularArray do 4 | it 'has a version number' do 5 | expect(CircularArray::VERSION).not_to be nil 6 | end 7 | 8 | describe '#[]' do 9 | context 'simple case' do 10 | let(:circular_array) { CircularArray[:a, :b, :c] } 11 | 12 | it 'behaives like array' do 13 | expect(circular_array).to be_kind_of Array 14 | 15 | expect(circular_array[-3]).to eq :a 16 | expect(circular_array[-2]).to eq :b 17 | expect(circular_array[-1]).to eq :c 18 | expect(circular_array[0]).to eq :a 19 | expect(circular_array[1]).to eq :b 20 | expect(circular_array[2]).to eq :c 21 | end 22 | 23 | it 'endless' do 24 | expect(circular_array[-7]).to eq :c 25 | expect(circular_array[-6]).to eq :a 26 | expect(circular_array[-5]).to eq :b 27 | expect(circular_array[-4]).to eq :c 28 | # for indexes -3..2, see test above 29 | expect(circular_array[3]).to eq :a 30 | expect(circular_array[4]).to eq :b 31 | expect(circular_array[5]).to eq :c 32 | expect(circular_array[6]).to eq :a 33 | expect(circular_array[7]).to eq :b 34 | expect(circular_array[8]).to eq :c 35 | expect(circular_array[9]).to eq :a 36 | expect(circular_array[10]).to eq :b 37 | expect(circular_array[11]).to eq :c 38 | expect(circular_array[12]).to eq :a 39 | expect(circular_array[13]).to eq :b 40 | expect(circular_array[14]).to eq :c 41 | expect(circular_array[15]).to eq :a 42 | expect(circular_array[16]).to eq :b 43 | expect(circular_array[17]).to eq :c 44 | expect(circular_array[18]).to eq :a 45 | end 46 | 47 | it 'no recursion' do 48 | # this will detect accidentally introduced recursion 49 | allow(circular_array).to receive(:[]).and_call_original 50 | expect(circular_array).to receive(:[]).exactly(2).times 51 | expect(circular_array[1]).to eq :b 52 | expect(circular_array[10]).to eq :b 53 | end 54 | end 55 | 56 | context 'empty' do 57 | let(:circular_array) { CircularArray.new([]) } 58 | 59 | it 'returns nil' do 60 | expect(circular_array[0]).to eq nil 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'circular_array' 5 | require 'pry' 6 | 7 | RSpec.configure do |config| 8 | # Enable flags like --only-failures and --next-failure 9 | config.example_status_persistence_file_path = '.rspec_status' 10 | 11 | # Disable RSpec exposing methods globally on `Module` and `main` 12 | config.disable_monkey_patching! 13 | 14 | config.filter_run_when_matching :focus 15 | 16 | config.around do |example| 17 | aggregate_failures do 18 | example.run 19 | end 20 | end 21 | 22 | config.expect_with :rspec do |c| 23 | c.syntax = :expect 24 | end 25 | end 26 | --------------------------------------------------------------------------------