├── .gitignore ├── lib ├── black_friday │ └── version.rb └── black_friday.rb ├── .standard.yml ├── sig └── black_friday.rbs ├── bin ├── setup └── console ├── test ├── test_helper.rb └── test_black_friday.rb ├── Rakefile ├── Gemfile ├── .github ├── dependabot.yml ├── workflows │ └── main.yml ├── FUNDING.yml ├── pull_request_template.md └── ISSUE_TEMPLATE │ ├── config.yml │ └── issue_template.md ├── CHANGELOG.md ├── LICENSE.txt ├── black_friday.gemspec ├── Gemfile.lock ├── README.md └── CODE_OF_CONDUCT.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /lib/black_friday/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module BlackFriday 4 | VERSION = "1.0.4" 5 | end 6 | -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | # For available configuration options, see: 2 | # https://github.com/standardrb/standard 3 | ruby_version: 3.0 4 | -------------------------------------------------------------------------------- /sig/black_friday.rbs: -------------------------------------------------------------------------------- 1 | module BlackFriday 2 | VERSION: String 3 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides 4 | end 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__) 4 | require "black_friday" 5 | 6 | require "minitest/autorun" 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "minitest/test_task" 5 | 6 | Minitest::TestTask.create 7 | 8 | require "standard/rake" 9 | 10 | task default: %i[test standard] 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in black_friday.gemspec 6 | gemspec 7 | 8 | gem "rake", "~> 13.0" 9 | 10 | gem "minitest", "~> 5.16" 11 | 12 | gem "standard", "~> 1.3" 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "black_friday" 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 | require "irb" 11 | IRB.start(__FILE__) 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ## [1.0.4] - 2024-11-27 4 | 5 | - Fix `current_sales` to return an array of names, not a hash. 6 | 7 | ## [1.0.3] - 2024-11-22 8 | 9 | - Add Canadian Thanksgiving 10 | - Add `range_for(name)` to retrieve a range easily 11 | 12 | ## [1.0.2] - 2024-11-22 13 | 14 | - Add `BlackFriday.range_for(:name)` to retrieve a range 15 | 16 | ## [1.0.1] - 2024-11-22 17 | 18 | - Add support for `ActiveSupport::TimeWithZone` by switching to `cover?` 19 | 20 | ## [1.0.0] - 2024-11-22 21 | 22 | - Initial release 23 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | name: Ruby ${{ matrix.ruby }} 14 | strategy: 15 | matrix: 16 | ruby: 17 | - '3.3.6' 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Ruby 22 | uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true 26 | - name: Run the default task 27 | run: bundle exec rake 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [excid3] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Pull Request 2 | 3 | **Summary:** 4 | 5 | 6 | **Related Issue:** 7 | 8 | 9 | **Description:** 10 | 11 | 12 | **Testing:** 13 | 14 | 15 | **Screenshots (if applicable):** 16 | 17 | 18 | **Checklist:** 19 | 20 | 21 | - [ ] Code follows the project's coding standards 22 | - [ ] Tests have been added or updated to cover the changes 23 | - [ ] Documentation has been updated (if applicable) 24 | - [ ] All existing tests pass 25 | - [ ] Conforms to the contributing guidelines 26 | 27 | **Additional Notes:** 28 | 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Chris Oliver 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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Get Help 4 | url: https://github.com/excid3/refer/discussions/new?category=help 5 | about: If you can't get something to work the way you expect, open a question in our discussion forums. 6 | - name: Feature Request 7 | url: https://github.com/excid3/refer/discussions/new?category=ideas 8 | about: 'Suggest any ideas you have using our discussion forums.' 9 | - name: Bug Report 10 | url: https://github.com/excid3/refer/issues/new?body=%3C%21--%20Please%20provide%20all%20of%20the%20information%20requested%20below.%20We%27re%20a%20small%20team%20and%20without%20all%20of%20this%20information%20it%27s%20not%20possible%20for%20us%20to%20help%20and%20your%20bug%20report%20will%20be%20closed.%20--%3E%0A%0A%2A%2AWhat%20version%20of%20Noticed%20are%20you%20using%3F%2A%2A%0A%0AFor%20example%3A%20v2.0.4%0A%0A%2A%2AWhat%20version%20of%20Rails%20are%20you%20using%3F%2A%2A%0A%0AFor%20example%3A%20v7.1.1%0A%0A%2A%2ADescribe%20your%20issue%2A%2A%0A%0ADescribe%20the%20problem%20you%27re%20seeing%2C%20any%20important%20steps%20to%20reproduce%20and%20what%20behavior%20you%20expect%20instead. 11 | about: If you've already asked for help with a problem and confirmed something is broken with Noticed itself, create a bug report. 12 | -------------------------------------------------------------------------------- /black_friday.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/black_friday/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "black_friday" 7 | spec.version = BlackFriday::VERSION 8 | spec.authors = ["Chris Oliver"] 9 | spec.email = ["excid3@gmail.com"] 10 | 11 | spec.summary = "Black Friday sales in your Rails apps" 12 | spec.description = "Black Friday sales in your Rails apps" 13 | spec.homepage = "https://github.com/excid3/black_friday" 14 | spec.license = "MIT" 15 | spec.required_ruby_version = ">= 3.0.0" 16 | 17 | spec.metadata["homepage_uri"] = spec.homepage 18 | spec.metadata["source_code_uri"] = spec.homepage 19 | spec.metadata["changelog_uri"] = spec.homepage + "/blob/main/CHANGELOG.md" 20 | 21 | # Specify which files should be added to the gem when it is released. 22 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 23 | gemspec = File.basename(__FILE__) 24 | spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls| 25 | ls.readlines("\x0", chomp: true).reject do |f| 26 | (f == gemspec) || 27 | f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile]) 28 | end 29 | end 30 | spec.bindir = "exe" 31 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 32 | spec.require_paths = ["lib"] 33 | 34 | spec.add_dependency "activesupport" 35 | end 36 | -------------------------------------------------------------------------------- /lib/black_friday.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "black_friday/version" 4 | require "active_support/isolated_execution_state" 5 | require "active_support/core_ext/array/wrap" 6 | require "active_support/core_ext/numeric/time" 7 | 8 | module BlackFriday 9 | extend self 10 | 11 | class Error < StandardError; end 12 | 13 | mattr_accessor :sales, default: {} 14 | 15 | def add_sale(name = :black_friday, &block) 16 | sales[name] = block 17 | end 18 | 19 | def active?(*sale_names) 20 | blocks = sale_names.empty? ? sales.values : sales.values_at(*sale_names).compact 21 | Array.wrap(blocks).any? do 22 | in_range? instance_eval(&_1) 23 | end 24 | end 25 | 26 | def current_sales 27 | sales.filter_map do |name, block| 28 | name if in_range? instance_eval(&block) 29 | end 30 | end 31 | 32 | def current_sale 33 | current_sales.first 34 | end 35 | 36 | def in_range?(range) 37 | range.cover?(range.first.is_a?(Date) ? Date.today : Time.current) 38 | end 39 | 40 | def range_for(sale_name) 41 | instance_eval(&sales.fetch(sale_name)) 42 | end 43 | 44 | # Date helpers 45 | 46 | def thanksgiving(year = Date.today.year) 47 | nov_1st = Date.new(year, 11, 1) 48 | first_thursday = nov_1st.thursday? ? nov_1st : nov_1st.next_occurring(:thursday) 49 | first_thursday + 3.weeks 50 | end 51 | 52 | def canadian_thanksgiving(year = Date.today.year) 53 | oct_1st = Date.new(year, 10, 1) 54 | first_monday = oct_1st.monday? ? oct_1st : oct_1st.next_occurring(:monday) 55 | first_monday + 1.weeks 56 | end 57 | 58 | def black_friday(year = Date.today.year) 59 | thanksgiving(year) + 1.day 60 | end 61 | 62 | def cyber_monday(year = Date.today.year) 63 | thanksgiving(year) + 4.days 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐞 Bug 3 | about: File a bug/issue 4 | title: '[BUG] ' 5 | labels: Bug, Needs Triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Bug Report 11 | 12 | **Describe the Bug:** 13 | <!-- A clear and concise description of the bug --> 14 | 15 | **To Reproduce:** 16 | <!-- Steps to reproduce the behavior --> 17 | 18 | 1. Step 1 19 | 2. Step 2 20 | 3. ... 21 | 22 | **Expected Behavior:** 23 | <!-- A clear and concise description of what you expected to happen --> 24 | 25 | **Actual Behavior:** 26 | <!-- A clear and concise description of what actually happened --> 27 | 28 | **Screenshots (if applicable):** 29 | <!-- If applicable, add screenshots to help explain your problem --> 30 | 31 | **Environment:** 32 | - Gem version: <!-- Specify the version of the Noticed gem where the bug occurred --> 33 | - Ruby version: <!-- Specify the version of Ruby you are using --> 34 | - Rails version: <!-- Specify the version of Rails you are using --> 35 | - Operating System: <!-- Specify your operating system --> 36 | 37 | **Additional Context:** 38 | <!-- Add any other context about the problem here --> 39 | 40 | **Possible Fix:** 41 | <!-- If you have suggestions on how to fix the bug, you can provide them here --> 42 | 43 | **Steps to Reproduce with Fix (if available):** 44 | <!-- If you have a fix, outline the steps to reproduce the bug using your fix --> 45 | 46 | **Related Issues:** 47 | <!-- If applicable, reference any related GitHub issues or pull requests --> 48 | 49 | **Labels to Apply:** 50 | <!-- Suggest labels that should be applied to this issue --> 51 | 52 | **Checklist:** 53 | <!-- Make sure all of these items are completed before submitting the issue --> 54 | 55 | - [ ] I have searched for similar issues and couldn't find any 56 | - [ ] I have checked the documentation for relevant information 57 | - [ ] I have included all the required information 58 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | black_friday (1.0.4) 5 | activesupport 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | activesupport (8.1.1) 11 | base64 12 | bigdecimal 13 | concurrent-ruby (~> 1.0, >= 1.3.1) 14 | connection_pool (>= 2.2.5) 15 | drb 16 | i18n (>= 1.6, < 2) 17 | json 18 | logger (>= 1.4.2) 19 | minitest (>= 5.1) 20 | securerandom (>= 0.3) 21 | tzinfo (~> 2.0, >= 2.0.5) 22 | uri (>= 0.13.1) 23 | ast (2.4.3) 24 | base64 (0.3.0) 25 | bigdecimal (3.3.1) 26 | concurrent-ruby (1.3.5) 27 | connection_pool (2.5.5) 28 | drb (2.2.3) 29 | i18n (1.14.7) 30 | concurrent-ruby (~> 1.0) 31 | json (2.16.0) 32 | language_server-protocol (3.17.0.5) 33 | lint_roller (1.1.0) 34 | logger (1.7.0) 35 | minitest (5.26.2) 36 | parallel (1.27.0) 37 | parser (3.3.10.0) 38 | ast (~> 2.4.1) 39 | racc 40 | prism (1.6.0) 41 | racc (1.8.1) 42 | rainbow (3.1.1) 43 | rake (13.3.1) 44 | regexp_parser (2.11.3) 45 | rubocop (1.81.7) 46 | json (~> 2.3) 47 | language_server-protocol (~> 3.17.0.2) 48 | lint_roller (~> 1.1.0) 49 | parallel (~> 1.10) 50 | parser (>= 3.3.0.2) 51 | rainbow (>= 2.2.2, < 4.0) 52 | regexp_parser (>= 2.9.3, < 3.0) 53 | rubocop-ast (>= 1.47.1, < 2.0) 54 | ruby-progressbar (~> 1.7) 55 | unicode-display_width (>= 2.4.0, < 4.0) 56 | rubocop-ast (1.48.0) 57 | parser (>= 3.3.7.2) 58 | prism (~> 1.4) 59 | rubocop-performance (1.25.0) 60 | lint_roller (~> 1.1) 61 | rubocop (>= 1.75.0, < 2.0) 62 | rubocop-ast (>= 1.38.0, < 2.0) 63 | ruby-progressbar (1.13.0) 64 | securerandom (0.4.1) 65 | standard (1.52.0) 66 | language_server-protocol (~> 3.17.0.2) 67 | lint_roller (~> 1.0) 68 | rubocop (~> 1.81.7) 69 | standard-custom (~> 1.0.0) 70 | standard-performance (~> 1.8) 71 | standard-custom (1.0.2) 72 | lint_roller (~> 1.0) 73 | rubocop (~> 1.50) 74 | standard-performance (1.8.0) 75 | lint_roller (~> 1.1) 76 | rubocop-performance (~> 1.25.0) 77 | tzinfo (2.0.6) 78 | concurrent-ruby (~> 1.0) 79 | unicode-display_width (3.2.0) 80 | unicode-emoji (~> 4.1) 81 | unicode-emoji (4.1.0) 82 | uri (1.1.1) 83 | 84 | PLATFORMS 85 | arm64-darwin-24 86 | ruby 87 | 88 | DEPENDENCIES 89 | black_friday! 90 | minitest (~> 5.16) 91 | rake (~> 13.0) 92 | standard (~> 1.3) 93 | 94 | BUNDLED WITH 95 | 2.7.2 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlackFriday 2 | 3 | Add Black Friday sales to your Rails app. 4 | 5 | [![Ruby](https://github.com/excid3/black_friday/actions/workflows/main.yml/badge.svg)](https://github.com/excid3/black_friday/actions/workflows/main.yml) 6 | 7 | ## Installation 8 | 9 | Install the gem and add to the application's Gemfile by executing: 10 | 11 | ```bash 12 | bundle add black_friday 13 | ``` 14 | 15 | ## Usage 16 | 17 | ### Date helpers 18 | 19 | Black Friday provides some helpers for dates. You can also pass in the year which can be helpful for making reports for previous years. 20 | 21 | ```ruby 22 | BlackFriday.thanksgiving #=> Thu, 28 Nov 2024 23 | BlackFriday.black_friday #=> Fri, 29 Nov 2024 24 | BlackFriday.cyber_monday #=> Mon, 2 Dec 2024 25 | 26 | BlackFriday.thanksgiving(2029) #=> Thu, 22 Nov 2029 27 | ``` 28 | 29 | ### Adding Sales 30 | 31 | Sales are handy to set the date/time range for a sale. The `add_sale` block should return a `Range`. 32 | 33 | ```ruby 34 | BlackFriday.add_sale do 35 | thanksgiving.monday.beginning_of_day..cyber_monday.end_of_day 36 | end 37 | 38 | BlackFriday.add_sale :labor_day do 39 | # First Monday in September 40 | sep_1st = Date.new(Date.today.year, 9, 1) 41 | labor_day = sep_1st.monday? ? sep_1st : sep_1st.next_occurring(:monday) 42 | start_day = labor_day - 3.days 43 | 44 | start_day.beginning_of_day..labor_day.end_of_day 45 | end 46 | ``` 47 | 48 | You can then check if a sale is active: 49 | 50 | ```ruby 51 | BlackFriday.active? 52 | #=> true/false 53 | 54 | # Or specific sale(s) 55 | BlackFriday.active?(:black_friday, :labor_day) 56 | #=> true/false 57 | ``` 58 | 59 | Render a banner when a sale is active: 60 | 61 | ```erb 62 | <%= render "banners/black_friday" if BlackFriday.active?(:black_friday) %> 63 | ``` 64 | 65 | To check the active sales: 66 | 67 | ```ruby 68 | BlackFriday.current_sale #=> :black_friday 69 | 70 | # For multiple sales at once 71 | BlackFriday.current_sales #=> [:black_friday] 72 | ``` 73 | 74 | ## Development 75 | 76 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 77 | 78 | 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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). 79 | 80 | ## Contributing 81 | 82 | Bug reports and pull requests are welcome on GitHub at https://github.com/excid3/black_friday. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/excid3/black_friday/blob/main/CODE_OF_CONDUCT.md). 83 | 84 | ## License 85 | 86 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 87 | 88 | ## Code of Conduct 89 | 90 | Everyone interacting in the BlackFriday project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/excid3/black_friday/blob/main/CODE_OF_CONDUCT.md). 91 | -------------------------------------------------------------------------------- /test/test_black_friday.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class TestBlackFriday < Minitest::Test 6 | def setup 7 | BlackFriday.sales.clear 8 | end 9 | 10 | def test_that_it_has_a_version_number 11 | refute_nil ::BlackFriday::VERSION 12 | end 13 | 14 | def test_thanksgiving 15 | assert_equal Date.new(2024, 11, 28), BlackFriday.thanksgiving(2024) 16 | assert_equal Date.new(2025, 11, 27), BlackFriday.thanksgiving(2025) 17 | assert_equal Date.new(2026, 11, 26), BlackFriday.thanksgiving(2026) 18 | assert_equal Date.new(2029, 11, 22), BlackFriday.thanksgiving(2029) 19 | end 20 | 21 | def test_canadian_thanksgiving 22 | assert_equal Date.new(2024, 10, 14), BlackFriday.canadian_thanksgiving(2024) 23 | assert_equal Date.new(2025, 10, 13), BlackFriday.canadian_thanksgiving(2025) 24 | assert_equal Date.new(2026, 10, 12), BlackFriday.canadian_thanksgiving(2026) 25 | assert_equal Date.new(2029, 10, 8), BlackFriday.canadian_thanksgiving(2029) 26 | end 27 | 28 | def test_black_friday 29 | assert_equal Date.new(2024, 11, 29), BlackFriday.black_friday(2024) 30 | assert_equal Date.new(2025, 11, 28), BlackFriday.black_friday(2025) 31 | assert_equal Date.new(2026, 11, 27), BlackFriday.black_friday(2026) 32 | assert_equal Date.new(2029, 11, 23), BlackFriday.black_friday(2029) 33 | end 34 | 35 | def test_cyber_monday 36 | assert_equal Date.new(2024, 12, 2), BlackFriday.cyber_monday(2024) 37 | assert_equal Date.new(2025, 12, 1), BlackFriday.cyber_monday(2025) 38 | assert_equal Date.new(2026, 11, 30), BlackFriday.cyber_monday(2026) 39 | assert_equal Date.new(2029, 11, 26), BlackFriday.cyber_monday(2029) 40 | end 41 | 42 | def test_add_sale 43 | block = -> {} 44 | BlackFriday.add_sale(&block) 45 | assert_equal block, BlackFriday.sales[:black_friday] 46 | end 47 | 48 | def test_active_sale 49 | BlackFriday.add_sale { Date.yesterday..Date.tomorrow } 50 | assert BlackFriday.active? 51 | end 52 | 53 | def test_active_sale_by_name 54 | BlackFriday.add_sale { Date.yesterday..Date.tomorrow } 55 | assert BlackFriday.active?(:black_friday) 56 | refute BlackFriday.active?(:invalid) 57 | end 58 | 59 | def test_inactive_sale 60 | BlackFriday.add_sale { Date.yesterday..Date.yesterday } 61 | refute BlackFriday.active? 62 | refute BlackFriday.active?(:black_friday) 63 | refute BlackFriday.active?(:invalid) 64 | end 65 | 66 | def test_current_sales 67 | BlackFriday.add_sale { Date.yesterday..Date.tomorrow } 68 | BlackFriday.add_sale(:extra) { Date.yesterday..Date.tomorrow } 69 | BlackFriday.add_sale(:tomorrow) { Date.tomorrow..Date.tomorrow } 70 | assert_equal [:black_friday, :extra], BlackFriday.current_sales 71 | end 72 | 73 | def test_current_sale 74 | BlackFriday.add_sale { Date.yesterday..Date.tomorrow } 75 | assert_equal :black_friday, BlackFriday.current_sale 76 | end 77 | 78 | def test_in_range_with_time_with_zone 79 | Time.zone = "Central Time (US & Canada)" 80 | BlackFriday.add_sale { Time.current.yesterday..Time.current.tomorrow } 81 | assert BlackFriday.active? 82 | ensure 83 | Time.zone = nil 84 | end 85 | 86 | def test_range_for 87 | range = Date.yesterday..Date.tomorrow 88 | BlackFriday.add_sale { range } 89 | assert_equal range, BlackFriday.range_for(:black_friday) 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official email address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | [INSERT CONTACT METHOD]. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | --------------------------------------------------------------------------------