timeboss 1.0.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/gem-push.yml +31 -0
- data/.github/workflows/ruby.yml +6 -4
- data/.travis.yml +11 -1
- data/Gemfile +2 -1
- data/README.md +7 -2
- data/Rakefile +3 -1
- data/bin/tbsh +7 -6
- data/lib/tasks/calendars.rake +4 -4
- data/lib/tasks/timeboss.rake +2 -2
- data/lib/timeboss/calendar/day.rb +3 -2
- data/lib/timeboss/calendar/half.rb +2 -1
- data/lib/timeboss/calendar/month.rb +2 -1
- data/lib/timeboss/calendar/parser.rb +9 -8
- data/lib/timeboss/calendar/period.rb +9 -6
- data/lib/timeboss/calendar/quarter.rb +2 -1
- data/lib/timeboss/calendar/support/formatter.rb +5 -4
- data/lib/timeboss/calendar/support/has_fiscal_weeks.rb +17 -0
- data/lib/timeboss/calendar/support/has_iso_weeks.rb +30 -0
- data/lib/timeboss/calendar/support/month_basis.rb +1 -1
- data/lib/timeboss/calendar/support/monthly_unit.rb +8 -8
- data/lib/timeboss/calendar/support/navigable.rb +2 -1
- data/lib/timeboss/calendar/support/shiftable.rb +12 -11
- data/lib/timeboss/calendar/support/translatable.rb +3 -2
- data/lib/timeboss/calendar/support/unit.rb +20 -13
- data/lib/timeboss/calendar/waypoints/absolute.rb +4 -3
- data/lib/timeboss/calendar/waypoints/relative.rb +14 -13
- data/lib/timeboss/calendar/week.rb +3 -2
- data/lib/timeboss/calendar/year.rb +2 -1
- data/lib/timeboss/calendar.rb +8 -7
- data/lib/timeboss/calendars/broadcast.rb +10 -7
- data/lib/timeboss/calendars/gregorian.rb +4 -5
- data/lib/timeboss/calendars.rb +3 -2
- data/lib/timeboss/version.rb +2 -1
- data/lib/timeboss.rb +2 -0
- data/spec/{calendar → lib/timeboss/calendar}/day_spec.rb +14 -14
- data/spec/{calendar → lib/timeboss/calendar}/quarter_spec.rb +9 -9
- data/spec/lib/timeboss/calendar/support/monthly_unit_spec.rb +91 -0
- data/spec/{calendar → lib/timeboss/calendar}/support/unit_spec.rb +23 -22
- data/spec/{calendar → lib/timeboss/calendar}/week_spec.rb +20 -20
- data/spec/lib/timeboss/calendars/broadcast_spec.rb +796 -0
- data/spec/lib/timeboss/calendars/gregorian_spec.rb +793 -0
- data/spec/{calendars_spec.rb → lib/timeboss/calendars_spec.rb} +19 -19
- data/spec/spec_helper.rb +2 -2
- data/timeboss.gemspec +17 -14
- metadata +53 -23
- data/spec/calendar/support/monthly_unit_spec.rb +0 -85
- data/spec/calendars/broadcast_spec.rb +0 -796
- data/spec/calendars/gregorian_spec.rb +0 -684
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 | 
            -
             | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: b856796bb4e37e5776aeed86c19dd42832af3091b189f01878d907de5cbf1864
         | 
| 4 | 
            +
              data.tar.gz: 824c18de591069e4c46d01dc14dc2c1a2ee54881b670218014adf3d4d220a0da
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 24bc5de8281c36802fb79b92b79a2381794710d320d2890d022c40f40aef4cedb1e3fe963085e3e1ea39fe9f51715994ccca4e5844c0e8bfb2784cbb65c2a339
         | 
| 7 | 
            +
              data.tar.gz: a1fb6c4825ad05f9dd21a8514c6e6dc24a4904cb235a2a1d16b888e3664fa5f92c6ea8b4ccef3c09198c8e2aea272fb662610bd25fc5ed210f2b095feb90fab3
         | 
| @@ -0,0 +1,31 @@ | |
| 1 | 
            +
            name: Ruby Gem
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            on:
         | 
| 4 | 
            +
              release:
         | 
| 5 | 
            +
                types: [published]
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            jobs:
         | 
| 8 | 
            +
              build:
         | 
| 9 | 
            +
                name: Build + Publish
         | 
| 10 | 
            +
                runs-on: ubuntu-latest
         | 
| 11 | 
            +
                permissions:
         | 
| 12 | 
            +
                  contents: read
         | 
| 13 | 
            +
                  packages: write
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                steps:
         | 
| 16 | 
            +
                - uses: actions/checkout@v2
         | 
| 17 | 
            +
                - name: Set up Ruby 2.6
         | 
| 18 | 
            +
                  uses: actions/setup-ruby@v1
         | 
| 19 | 
            +
                  with:
         | 
| 20 | 
            +
                    ruby-version: 2.6.x
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                - name: Publish to RubyGems
         | 
| 23 | 
            +
                  run: |
         | 
| 24 | 
            +
                    mkdir -p $HOME/.gem
         | 
| 25 | 
            +
                    touch $HOME/.gem/credentials
         | 
| 26 | 
            +
                    chmod 0600 $HOME/.gem/credentials
         | 
| 27 | 
            +
                    printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
         | 
| 28 | 
            +
                    gem build *.gemspec
         | 
| 29 | 
            +
                    gem push *.gem
         | 
| 30 | 
            +
                  env:
         | 
| 31 | 
            +
                    GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
         | 
    
        data/.github/workflows/ruby.yml
    CHANGED
    
    | @@ -17,6 +17,9 @@ jobs: | |
| 17 17 | 
             
              test:
         | 
| 18 18 |  | 
| 19 19 | 
             
                runs-on: ubuntu-latest
         | 
| 20 | 
            +
                strategy:
         | 
| 21 | 
            +
                  matrix:
         | 
| 22 | 
            +
                    ruby-version: ['2.7', '3.0']
         | 
| 20 23 |  | 
| 21 24 | 
             
                steps:
         | 
| 22 25 | 
             
                - uses: actions/checkout@v2
         | 
| @@ -24,10 +27,9 @@ jobs: | |
| 24 27 | 
             
                # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
         | 
| 25 28 | 
             
                # change this to (see https://github.com/ruby/setup-ruby#versioning):
         | 
| 26 29 | 
             
                # uses: ruby/setup-ruby@v1
         | 
| 27 | 
            -
                  uses: ruby/setup-ruby@ | 
| 30 | 
            +
                  uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
         | 
| 28 31 | 
             
                  with:
         | 
| 29 | 
            -
                    ruby-version:  | 
| 30 | 
            -
             | 
| 31 | 
            -
                  run: bundle install
         | 
| 32 | 
            +
                    ruby-version: ${{ matrix.ruby-version }}
         | 
| 33 | 
            +
                    bundler-cache: true # runs 'bundle install' and caches installed gems automatically
         | 
| 32 34 | 
             
                - name: Run tests
         | 
| 33 35 | 
             
                  run: bundle exec rspec
         | 
    
        data/.travis.yml
    CHANGED
    
    | @@ -1,7 +1,17 @@ | |
| 1 1 | 
             
            language: ruby
         | 
| 2 2 | 
             
            rvm:
         | 
| 3 | 
            -
            - 2.4
         | 
| 4 3 | 
             
            - 2.5
         | 
| 5 4 | 
             
            - 2.6
         | 
| 5 | 
            +
            - 2.7
         | 
| 6 | 
            +
            - 3.0
         | 
| 6 7 | 
             
            script:
         | 
| 7 8 | 
             
            - bundle exec rspec
         | 
| 9 | 
            +
            deploy:
         | 
| 10 | 
            +
              provider: rubygems
         | 
| 11 | 
            +
              api_key:
         | 
| 12 | 
            +
                secure: f7s86k3ZRgaao32goumx0EFSquj8v8vwLBQP0uPd5lZ5OlDjUIbzAGCLpP4IARFf7MhYUUxWqwU7A0Z2MJHgmf4hT2VdVco4kGC4WztMgSI2JwY8Uo21/QJgI82jIfZ6yfSjF8OC3eh9irqJxXfhzspO9DY4p+nJkMJnpG5Y1e5FjS9zM3gS80TD9fauIMEi3fOLDNYEZ95SgjrkX2MHDYQWN1nfFlkRtybSHJ2u2Ad3Ulr5c/1gIoJviJCm8l5Bwo3MnvBtSuHHjFOaH9UTcmDUGpBjr7GMoqn3m053aB1F3ImYwL9+il0rtj+PE1lNaVbUM/QDKp8gDcbo433m8oMiGRpouz0fdIi95fqsshZmSU9sZX6HPiOuURXXwrjW7n3bj71+qZ7zWPTyZB8p3Y6ocp/r6Aj0ewELJksjnbYqSuyYv0o5sKTh2AUMawcqWAnDlZWgMq4UqKQiaWlhZMN1guQIWO6Xq9xdoiIxcqRUTJ7dUAGsfv+GIs2iPLvh20DHudYN70J5b4xzZLFgPQOJbTGlQQtC18m2PaYvcdsZ1qzttQIs0fcgeKno1Ltcman6/yqbAdKsSjifLUcdqHiWOUk5Dh5l4S+iSVazILVFFHwV89JI1+ipuS1nnIaRcmfIkV3GB+aXcbwwYc89mLkXBVmezs+scygK0KUhoyU=
         | 
| 13 | 
            +
              gem: timeboss
         | 
| 14 | 
            +
              on:
         | 
| 15 | 
            +
                tags: true
         | 
| 16 | 
            +
                repo: kevinstuffandthings/timeboss
         | 
| 17 | 
            +
              skip_cleanup: 'true'
         | 
    
        data/Gemfile
    CHANGED
    
    
    
        data/README.md
    CHANGED
    
    | @@ -1,4 +1,4 @@ | |
| 1 | 
            -
            # TimeBoss  | 
| 1 | 
            +
            # TimeBoss  [](https://badge.fury.io/rb/timeboss)
         | 
| 2 2 |  | 
| 3 3 | 
             
            A gem providing convenient navigation of the [Broadcast Calendar](https://en.wikipedia.org/wiki/Broadcast_calendar), the standard Gregorian calendar, and is easily extensible to support multiple financial calendars.
         | 
| 4 4 |  | 
| @@ -160,7 +160,7 @@ $ tbsh | |
| 160 160 | 
             
            If you want to try things out locally without installing the gem or updating your ruby environment, you can use [Docker](https://docker.com):
         | 
| 161 161 |  | 
| 162 162 | 
             
            ```bash
         | 
| 163 | 
            -
            $ docker run --rm -it ruby: | 
| 163 | 
            +
            $ docker run --rm -it ruby:3.0-slim /bin/bash -c "gem install timeboss shellable >/dev/null && tbsh"
         | 
| 164 164 | 
             
            ```
         | 
| 165 165 |  | 
| 166 166 | 
             
            _Having trouble with the REPL? If you are using the examples from the [Usage](#Usage) section, keep in mind that the REPL is already in the context of the calendar -- so you don't need to specify the receiver!_
         | 
| @@ -170,9 +170,14 @@ To create a custom calendar, simply extend the `TimeBoss::Calendar` class, and i | |
| 170 170 |  | 
| 171 171 | 
             
            ```ruby
         | 
| 172 172 | 
             
            require 'timeboss/calendar'
         | 
| 173 | 
            +
            require 'timeboss/calendar/support/has_fiscal_weeks'
         | 
| 173 174 |  | 
| 174 175 | 
             
            module MyCalendars
         | 
| 175 176 | 
             
              class AugustFiscal < TimeBoss::Calendar
         | 
| 177 | 
            +
                # The calendar we wish to implement has "fiscal weeks", meaning that the weeks start on
         | 
| 178 | 
            +
                # the day of the containing period.
         | 
| 179 | 
            +
                include TimeBoss::Calendar::Support::HasFiscalWeeks
         | 
| 180 | 
            +
             | 
| 176 181 | 
             
                def initialize
         | 
| 177 182 | 
             
                  # For each calendar, operation, the class will be instantiated with an ordinal value
         | 
| 178 183 | 
             
                  # for `year` and `month`. It is the instance's job to translate those ordinals into
         | 
    
        data/Rakefile
    CHANGED
    
    
    
        data/bin/tbsh
    CHANGED
    
    | @@ -1,13 +1,14 @@ | |
| 1 1 | 
             
            #!/usr/bin/env ruby
         | 
| 2 2 |  | 
| 3 | 
            -
            require  | 
| 4 | 
            -
            require  | 
| 3 | 
            +
            require "timeboss"
         | 
| 4 | 
            +
            require "timeboss/calendars"
         | 
| 5 | 
            +
            require "shellable"
         | 
| 5 6 |  | 
| 6 7 | 
             
            calendar = if ARGV.length == 1
         | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 8 | 
            +
              TimeBoss::Calendars[ARGV.first]
         | 
| 9 | 
            +
            else
         | 
| 10 | 
            +
              TimeBoss::Calendars.first.calendar
         | 
| 11 | 
            +
            end
         | 
| 11 12 |  | 
| 12 13 | 
             
            abort "Unknown calendar" if calendar.nil?
         | 
| 13 14 |  | 
    
        data/lib/tasks/calendars.rake
    CHANGED
    
    | @@ -1,17 +1,17 @@ | |
| 1 | 
            -
            require  | 
| 1 | 
            +
            require "./lib/timeboss/calendars"
         | 
| 2 2 |  | 
| 3 3 | 
             
            namespace :timeboss do
         | 
| 4 4 | 
             
              namespace :calendars do
         | 
| 5 5 | 
             
                TimeBoss::Calendars.each do |entry|
         | 
| 6 6 | 
             
                  namespace entry.name do
         | 
| 7 7 | 
             
                    desc "Evaluate an expression for the #{entry.name} calendar"
         | 
| 8 | 
            -
                    task :evaluate, %i[expression] => [ | 
| 8 | 
            +
                    task :evaluate, %i[expression] => ["timeboss:init"] do |_, args|
         | 
| 9 9 | 
             
                      puts entry.calendar.parse(args[:expression])
         | 
| 10 10 | 
             
                    end
         | 
| 11 11 |  | 
| 12 12 | 
             
                    desc "Open a REPL with the #{entry.name} calendar"
         | 
| 13 | 
            -
                    task repl: [ | 
| 14 | 
            -
                      require  | 
| 13 | 
            +
                    task repl: ["timeboss:init"] do
         | 
| 14 | 
            +
                      require "shellable"
         | 
| 15 15 | 
             
                      Shellable.open(entry.calendar)
         | 
| 16 16 | 
             
                    end
         | 
| 17 17 |  | 
    
        data/lib/tasks/timeboss.rake
    CHANGED
    
    
| @@ -1,5 +1,6 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative "./support/unit"
         | 
| 3 4 |  | 
| 4 5 | 
             
            module TimeBoss
         | 
| 5 6 | 
             
              class Calendar
         | 
| @@ -17,7 +18,7 @@ module TimeBoss | |
| 17 18 | 
             
                  # Get a "pretty" representation of this day.
         | 
| 18 19 | 
             
                  # @return [String] (e.g. "August 3, 2020")
         | 
| 19 20 | 
             
                  def title
         | 
| 20 | 
            -
                    start_date.strftime( | 
| 21 | 
            +
                    start_date.strftime("%B %-d, %Y")
         | 
| 21 22 | 
             
                  end
         | 
| 22 23 |  | 
| 23 24 | 
             
                  alias_method :to_s, :name
         | 
| @@ -1,8 +1,9 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 2 3 | 
             
            module TimeBoss
         | 
| 3 4 | 
             
              class Calendar
         | 
| 4 5 | 
             
                class Parser
         | 
| 5 | 
            -
                  RANGE_DELIMITER =  | 
| 6 | 
            +
                  RANGE_DELIMITER = ".."
         | 
| 6 7 | 
             
                  InvalidPeriodIdentifierError = Class.new(StandardError)
         | 
| 7 8 | 
             
                  attr_reader :calendar
         | 
| 8 9 |  | 
| @@ -11,7 +12,7 @@ module TimeBoss | |
| 11 12 | 
             
                  end
         | 
| 12 13 |  | 
| 13 14 | 
             
                  def parse(identifier = nil)
         | 
| 14 | 
            -
                    return nil unless (identifier ||  | 
| 15 | 
            +
                    return nil unless (identifier || "").strip.length > 0
         | 
| 15 16 | 
             
                    return parse_identifier(identifier) unless identifier&.include?(RANGE_DELIMITER)
         | 
| 16 17 | 
             
                    bases = identifier.split(RANGE_DELIMITER).map { |i| parse_identifier(i.strip) } unless identifier.nil?
         | 
| 17 18 | 
             
                    bases ||= [parse_identifier(nil)]
         | 
| @@ -24,13 +25,13 @@ module TimeBoss | |
| 24 25 |  | 
| 25 26 | 
             
                  def parse_identifier(identifier)
         | 
| 26 27 | 
             
                    captures = identifier&.match(/^([^-]+)(\s*[+-]\s*[0-9]+)$/)&.captures
         | 
| 27 | 
            -
                    base, offset = captures || [identifier,  | 
| 28 | 
            -
                    period = parse_period(base&.strip)  | 
| 29 | 
            -
                    period.offset(offset.gsub(/\s+/,  | 
| 28 | 
            +
                    base, offset = captures || [identifier, "0"]
         | 
| 29 | 
            +
                    (period = parse_period(base&.strip)) || raise(InvalidPeriodIdentifierError)
         | 
| 30 | 
            +
                    period.offset(offset.gsub(/\s+/, "").to_i)
         | 
| 30 31 | 
             
                  end
         | 
| 31 32 |  | 
| 32 33 | 
             
                  def parse_period(identifier)
         | 
| 33 | 
            -
                    return calendar. | 
| 34 | 
            +
                    return calendar.public_send(identifier) if calendar.respond_to?(identifier.to_s)
         | 
| 34 35 | 
             
                    parse_term(identifier || Date.today.year.to_s)
         | 
| 35 36 | 
             
                  end
         | 
| 36 37 |  | 
| @@ -38,13 +39,13 @@ module TimeBoss | |
| 38 39 | 
             
                    return Day.new(calendar, Date.parse(identifier)) if identifier.match?(/^[0-9]{4}-?[01][0-9]-?[0-3][0-9]$/)
         | 
| 39 40 |  | 
| 40 41 | 
             
                    raise InvalidPeriodIdentifierError unless identifier.match?(/^[HQMWD0-9]+$/)
         | 
| 41 | 
            -
                    period =  | 
| 42 | 
            +
                    period = identifier.to_i == 0 ? calendar.this_year : calendar.year(identifier.to_i)
         | 
| 42 43 | 
             
                    %w[half quarter month week day].each do |size|
         | 
| 43 44 | 
             
                      prefix = size[0].upcase
         | 
| 44 45 | 
             
                      next unless identifier.include?(prefix)
         | 
| 45 46 | 
             
                      junk, identifier = identifier.split(prefix)
         | 
| 46 47 | 
             
                      raise InvalidPeriodIdentifierError if junk.match?(/\D/)
         | 
| 47 | 
            -
                      period = period. | 
| 48 | 
            +
                      (period = period.public_send(size.pluralize)[identifier.to_i - 1]) || raise(InvalidPeriodIdentifierError)
         | 
| 48 49 | 
             
                    end
         | 
| 49 50 | 
             
                    period
         | 
| 50 51 | 
             
                  end
         | 
| @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 2 3 | 
             
            module TimeBoss
         | 
| 3 4 | 
             
              class Calendar
         | 
| 4 5 | 
             
                class Period
         | 
| @@ -28,8 +29,8 @@ module TimeBoss | |
| 28 29 |  | 
| 29 30 | 
             
                  %i[name title to_s].each do |message|
         | 
| 30 31 | 
             
                    define_method(message) do
         | 
| 31 | 
            -
                      text = self.begin. | 
| 32 | 
            -
                      text = "#{text} #{Parser::RANGE_DELIMITER} #{self.end. | 
| 32 | 
            +
                      text = self.begin.public_send(message)
         | 
| 33 | 
            +
                      text = "#{text} #{Parser::RANGE_DELIMITER} #{self.end.public_send(message)}" unless self.end == self.begin
         | 
| 33 34 | 
             
                      text
         | 
| 34 35 | 
             
                    end
         | 
| 35 36 | 
             
                  end
         | 
| @@ -112,12 +113,14 @@ module TimeBoss | |
| 112 113 |  | 
| 113 114 | 
             
                  %w[day week month quarter half year].each do |size|
         | 
| 114 115 | 
             
                    define_method(size.pluralize) do
         | 
| 115 | 
            -
                      entry = calendar. | 
| 116 | 
            -
                      build_entries | 
| 116 | 
            +
                      entry = calendar.public_send("#{size}_for", self.begin.start_date) || self.begin.public_send(size, 1)
         | 
| 117 | 
            +
                      entries = build_entries(entry)
         | 
| 118 | 
            +
                      entries.pop if size == "week" && self.end.next.public_send(size, 1) == entries.last
         | 
| 119 | 
            +
                      entries
         | 
| 117 120 | 
             
                    end
         | 
| 118 121 |  | 
| 119 122 | 
             
                    define_method(size) do |index = nil|
         | 
| 120 | 
            -
                      entries =  | 
| 123 | 
            +
                      entries = public_send(size.pluralize)
         | 
| 121 124 | 
             
                      return entries[index - 1] unless index.nil?
         | 
| 122 125 | 
             
                      return nil unless entries.length == 1
         | 
| 123 126 | 
             
                      entries.first
         | 
| @@ -127,7 +130,7 @@ module TimeBoss | |
| 127 130 | 
             
                  # Express this period as a date range.
         | 
| 128 131 | 
             
                  # @return [Range<Date, Date>]
         | 
| 129 132 | 
             
                  def to_range
         | 
| 130 | 
            -
                    @_to_range ||= start_date | 
| 133 | 
            +
                    @_to_range ||= start_date..end_date
         | 
| 131 134 | 
             
                  end
         | 
| 132 135 |  | 
| 133 136 | 
             
                  def inspect
         | 
| @@ -1,5 +1,6 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative "./translatable"
         | 
| 3 4 |  | 
| 4 5 | 
             
            module TimeBoss
         | 
| 5 6 | 
             
              class Calendar
         | 
| @@ -18,10 +19,10 @@ module TimeBoss | |
| 18 19 | 
             
                    end
         | 
| 19 20 |  | 
| 20 21 | 
             
                    def to_s
         | 
| 21 | 
            -
                      base, text =  | 
| 22 | 
            +
                      base, text = "year", unit.year.name
         | 
| 22 23 | 
             
                      periods.each do |period|
         | 
| 23 | 
            -
                        sub = unit. | 
| 24 | 
            -
                        index = sub. | 
| 24 | 
            +
                        (sub = unit.public_send(period)) || break
         | 
| 25 | 
            +
                        index = sub.public_send("in_#{base}")
         | 
| 25 26 | 
             
                        text += "#{period[0].upcase}#{index}"
         | 
| 26 27 | 
             
                        base = period
         | 
| 27 28 | 
             
                      end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module TimeBoss
         | 
| 4 | 
            +
              class Calendar
         | 
| 5 | 
            +
                module Support
         | 
| 6 | 
            +
                  module HasFiscalWeeks
         | 
| 7 | 
            +
                    def weeks_in(year:)
         | 
| 8 | 
            +
                      num_weeks = (((year.end_date - year.start_date) + 1) / 7.0).to_i
         | 
| 9 | 
            +
                      num_weeks.times.map do |i|
         | 
| 10 | 
            +
                        start_date = year.start_date + (i * 7).days
         | 
| 11 | 
            +
                        Week.new(self, start_date, start_date + 6.days)
         | 
| 12 | 
            +
                      end
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,30 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module TimeBoss
         | 
| 4 | 
            +
              class Calendar
         | 
| 5 | 
            +
                module Support
         | 
| 6 | 
            +
                  module HasIsoWeeks
         | 
| 7 | 
            +
                    def weeks_in(year:)
         | 
| 8 | 
            +
                      weeks = []
         | 
| 9 | 
            +
                      start_date = Date.commercial(year.year_index)
         | 
| 10 | 
            +
                      end_date = Date.commercial(year.next.year_index)
         | 
| 11 | 
            +
                      while start_date < end_date
         | 
| 12 | 
            +
                        weeks << Week.new(self, start_date, start_date + 6.days)
         | 
| 13 | 
            +
                        start_date += 7.days
         | 
| 14 | 
            +
                      end
         | 
| 15 | 
            +
                      weeks
         | 
| 16 | 
            +
                    end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    class Week < Calendar::Week
         | 
| 19 | 
            +
                      def index
         | 
| 20 | 
            +
                        start_date.cweek
         | 
| 21 | 
            +
                      end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                      def year
         | 
| 24 | 
            +
                        calendar.year(start_date.cwyear)
         | 
| 25 | 
            +
                      end
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
            end
         | 
| @@ -1,5 +1,6 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative "./unit"
         | 
| 3 4 |  | 
| 4 5 | 
             
            module TimeBoss
         | 
| 5 6 | 
             
              class Calendar
         | 
| @@ -22,10 +23,9 @@ module TimeBoss | |
| 22 23 | 
             
                    # Get a list of weeks contained within this period.
         | 
| 23 24 | 
             
                    # @return [Array<Week>]
         | 
| 24 25 | 
             
                    def weeks
         | 
| 26 | 
            +
                      raise UnsupportedUnitError unless calendar.supports_weeks?
         | 
| 25 27 | 
             
                      base = calendar.year(year_index)
         | 
| 26 | 
            -
                       | 
| 27 | 
            -
                      num_weeks.times.map { |i| Week.new(calendar, base.start_date + (i * 7).days, base.start_date + ((i * 7) + 6).days) }
         | 
| 28 | 
            -
                                     .select { |w| w.start_date.between?(start_date, end_date) }
         | 
| 28 | 
            +
                      calendar.weeks_in(year: base).select { |w| (w.dates & dates).count >= 4 }
         | 
| 29 29 | 
             
                    end
         | 
| 30 30 |  | 
| 31 31 | 
             
                    private
         | 
| @@ -36,17 +36,17 @@ module TimeBoss | |
| 36 36 |  | 
| 37 37 | 
             
                    def up
         | 
| 38 38 | 
             
                      if index == max_index
         | 
| 39 | 
            -
                        calendar. | 
| 39 | 
            +
                        calendar.public_send(self.class.type, year_index + 1, 1)
         | 
| 40 40 | 
             
                      else
         | 
| 41 | 
            -
                        calendar. | 
| 41 | 
            +
                        calendar.public_send(self.class.type, year_index, index + 1)
         | 
| 42 42 | 
             
                      end
         | 
| 43 43 | 
             
                    end
         | 
| 44 44 |  | 
| 45 45 | 
             
                    def down
         | 
| 46 46 | 
             
                      if index == 1
         | 
| 47 | 
            -
                        calendar. | 
| 47 | 
            +
                        calendar.public_send(self.class.type, year_index - 1, max_index)
         | 
| 48 48 | 
             
                      else
         | 
| 49 | 
            -
                        calendar. | 
| 49 | 
            +
                        calendar.public_send(self.class.type, year_index, index - 1)
         | 
| 50 50 | 
             
                      end
         | 
| 51 51 | 
             
                    end
         | 
| 52 52 | 
             
                  end
         | 
| @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 2 3 | 
             
            module TimeBoss
         | 
| 3 4 | 
             
              class Calendar
         | 
| 4 5 | 
             
                module Support
         | 
| @@ -61,7 +62,7 @@ module TimeBoss | |
| 61 62 | 
             
                        entry = self
         | 
| 62 63 | 
             
                        while quantity > 0
         | 
| 63 64 | 
             
                          entries << entry
         | 
| 64 | 
            -
                          entry = entry. | 
| 65 | 
            +
                          entry = entry.public_send(navigator)
         | 
| 65 66 | 
             
                          quantity -= 1
         | 
| 66 67 | 
             
                        end
         | 
| 67 68 | 
             
                      end
         | 
| @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 2 3 | 
             
            module TimeBoss
         | 
| 3 4 | 
             
              class Calendar
         | 
| 4 5 | 
             
                module Support
         | 
| @@ -7,21 +8,21 @@ module TimeBoss | |
| 7 8 | 
             
                      periods = period.pluralize
         | 
| 8 9 |  | 
| 9 10 | 
             
                      define_method("in_#{period}") do
         | 
| 10 | 
            -
                        base =  | 
| 11 | 
            +
                        base = public_send(periods)
         | 
| 11 12 | 
             
                        return unless base.length == 1
         | 
| 12 | 
            -
                        base.first. | 
| 13 | 
            +
                        base.first.public_send(self.class.type.to_s.pluralize).find_index { |p| p == self } + 1
         | 
| 13 14 | 
             
                      end
         | 
| 14 15 |  | 
| 15 16 | 
             
                      define_method("#{periods}_ago") do |offset|
         | 
| 16 | 
            -
                        base_offset =  | 
| 17 | 
            -
                        (calendar. | 
| 17 | 
            +
                        (base_offset = public_send("in_#{period}")) || return
         | 
| 18 | 
            +
                        (calendar.public_send("this_#{period}") - offset).public_send(self.class.type.to_s.pluralize)[base_offset - 1]
         | 
| 18 19 | 
             
                      end
         | 
| 19 20 |  | 
| 20 | 
            -
                      define_method("#{periods}_ahead") { |o|  | 
| 21 | 
            +
                      define_method("#{periods}_ahead") { |o| public_send("#{periods}_ago", o * -1) }
         | 
| 21 22 |  | 
| 22 | 
            -
                      define_method("last_#{period}") {  | 
| 23 | 
            -
                      define_method("this_#{period}") {  | 
| 24 | 
            -
                      define_method("next_#{period}") {  | 
| 23 | 
            +
                      define_method("last_#{period}") { public_send("#{periods}_ago", 1) }
         | 
| 24 | 
            +
                      define_method("this_#{period}") { public_send("#{periods}_ago", 0) }
         | 
| 25 | 
            +
                      define_method("next_#{period}") { public_send("#{periods}_ahead", 1) }
         | 
| 25 26 | 
             
                    end
         | 
| 26 27 |  | 
| 27 28 | 
             
                    alias_method :yesterday, :last_day
         | 
| @@ -133,7 +134,7 @@ module TimeBoss | |
| 133 134 | 
             
                    # Get the index-relative month 1 month forward.
         | 
| 134 135 | 
             
                    # Returns nil if no single month can be identified.
         | 
| 135 136 | 
             
                    # @return [Calendar::Month, nil]
         | 
| 136 | 
            -
             | 
| 137 | 
            +
             | 
| 137 138 | 
             
                    ### Quarters
         | 
| 138 139 |  | 
| 139 140 | 
             
                    # @!method in_quarter
         | 
| @@ -167,7 +168,7 @@ module TimeBoss | |
| 167 168 | 
             
                    # Get the index-relative quarter 1 quarter forward.
         | 
| 168 169 | 
             
                    # Returns nil if no single quarter can be identified.
         | 
| 169 170 | 
             
                    # @return [Calendar::Quarter, nil]
         | 
| 170 | 
            -
             | 
| 171 | 
            +
             | 
| 171 172 | 
             
                    ### Halves
         | 
| 172 173 |  | 
| 173 174 | 
             
                    # @!method in_half
         | 
| @@ -201,7 +202,7 @@ module TimeBoss | |
| 201 202 | 
             
                    # Get the index-relative half 1 half forward.
         | 
| 202 203 | 
             
                    # Returns nil if no single half can be identified.
         | 
| 203 204 | 
             
                    # @return [Calendar::Half, nil]
         | 
| 204 | 
            -
             | 
| 205 | 
            +
             | 
| 205 206 | 
             
                    ### Years
         | 
| 206 207 |  | 
| 207 208 | 
             
                    # @!method in_year
         | 
| @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 2 3 | 
             
            module TimeBoss
         | 
| 3 4 | 
             
              class Calendar
         | 
| 4 5 | 
             
                module Support
         | 
| @@ -8,10 +9,10 @@ module TimeBoss | |
| 8 9 | 
             
                    PERIODS.each do |period|
         | 
| 9 10 | 
             
                      periods = period.pluralize
         | 
| 10 11 |  | 
| 11 | 
            -
                      define_method(periods) { calendar. | 
| 12 | 
            +
                      define_method(periods) { calendar.public_send("#{periods}_for", self) }
         | 
| 12 13 |  | 
| 13 14 | 
             
                      define_method(period) do |index = nil|
         | 
| 14 | 
            -
                        entries =  | 
| 15 | 
            +
                        entries = public_send(periods)
         | 
| 15 16 | 
             
                        return entries[index - 1] unless index.nil?
         | 
| 16 17 | 
             
                        return nil unless entries.length == 1
         | 
| 17 18 | 
             
                        entries.first
         | 
| @@ -1,8 +1,9 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            require_relative  | 
| 4 | 
            -
            require_relative  | 
| 5 | 
            -
            require_relative  | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative "./navigable"
         | 
| 4 | 
            +
            require_relative "./translatable"
         | 
| 5 | 
            +
            require_relative "./shiftable"
         | 
| 6 | 
            +
            require_relative "./formatter"
         | 
| 6 7 |  | 
| 7 8 | 
             
            module TimeBoss
         | 
| 8 9 | 
             
              class Calendar
         | 
| @@ -16,7 +17,7 @@ module TimeBoss | |
| 16 17 | 
             
                    UnsupportedUnitError = Class.new(StandardError)
         | 
| 17 18 |  | 
| 18 19 | 
             
                    def self.type
         | 
| 19 | 
            -
                       | 
| 20 | 
            +
                      name.demodulize.underscore
         | 
| 20 21 | 
             
                    end
         | 
| 21 22 |  | 
| 22 23 | 
             
                    def initialize(calendar, start_date, end_date)
         | 
| @@ -28,8 +29,8 @@ module TimeBoss | |
| 28 29 | 
             
                    # Is the specified unit equal to this one, based on its unit type and date range?
         | 
| 29 30 | 
             
                    # @param entry [Unit] the unit to compare
         | 
| 30 31 | 
             
                    # @return [Boolean] true when periods are equal
         | 
| 31 | 
            -
                    def ==( | 
| 32 | 
            -
                      self.class ==  | 
| 32 | 
            +
                    def ==(other)
         | 
| 33 | 
            +
                      self.class == other.class && start_date == other.start_date && end_date == other.end_date
         | 
| 33 34 | 
             
                    end
         | 
| 34 35 |  | 
| 35 36 | 
             
                    # Format this period based on specified granularities.
         | 
| @@ -59,33 +60,39 @@ module TimeBoss | |
| 59 60 | 
             
                    def offset(value)
         | 
| 60 61 | 
             
                      method = value.negative? ? :previous : :next
         | 
| 61 62 | 
             
                      base = self
         | 
| 62 | 
            -
                      value.abs.times { base = base. | 
| 63 | 
            +
                      value.abs.times { base = base.public_send(method) }
         | 
| 63 64 | 
             
                      base
         | 
| 64 65 | 
             
                    end
         | 
| 65 66 |  | 
| 66 67 | 
             
                    # Move some number of units forward from this unit.
         | 
| 67 68 | 
             
                    # @param value [Integer]
         | 
| 68 69 | 
             
                    # @return [Unit]
         | 
| 69 | 
            -
                    def +( | 
| 70 | 
            -
                      offset( | 
| 70 | 
            +
                    def +(other)
         | 
| 71 | 
            +
                      offset(other)
         | 
| 71 72 | 
             
                    end
         | 
| 72 73 |  | 
| 73 74 | 
             
                    # Move some number of units backward from this unit.
         | 
| 74 75 | 
             
                    # @param value [Integer]
         | 
| 75 76 | 
             
                    # @return [Unit]
         | 
| 76 | 
            -
                    def -( | 
| 77 | 
            -
                      offset(- | 
| 77 | 
            +
                    def -(other)
         | 
| 78 | 
            +
                      offset(-other)
         | 
| 78 79 | 
             
                    end
         | 
| 79 80 |  | 
| 80 81 | 
             
                    # Express this period as a date range.
         | 
| 81 82 | 
             
                    # @return [Range<Date, Date>]
         | 
| 82 83 | 
             
                    def to_range
         | 
| 83 | 
            -
                      @_to_range ||= start_date | 
| 84 | 
            +
                      @_to_range ||= start_date..end_date
         | 
| 84 85 | 
             
                    end
         | 
| 85 86 |  | 
| 86 87 | 
             
                    def inspect
         | 
| 87 88 | 
             
                      "#<#{self.class.name} start_date=#{start_date}, end_date=#{end_date}>"
         | 
| 88 89 | 
             
                    end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                    protected
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                    def dates
         | 
| 94 | 
            +
                      @_dates ||= to_range.to_a
         | 
| 95 | 
            +
                    end
         | 
| 89 96 | 
             
                  end
         | 
| 90 97 | 
             
                end
         | 
| 91 98 | 
             
              end
         | 
| @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 2 3 | 
             
            module TimeBoss
         | 
| 3 4 | 
             
              class Calendar
         | 
| 4 5 | 
             
                module Waypoints
         | 
| @@ -9,13 +10,13 @@ module TimeBoss | |
| 9 10 |  | 
| 10 11 | 
             
                      define_method type do |year_index, index = 1|
         | 
| 11 12 | 
             
                        month = (index * size) - size + 1
         | 
| 12 | 
            -
                        months = (month | 
| 13 | 
            +
                        months = (month..month + size - 1).map { |i| basis.new(year_index, i) }
         | 
| 13 14 | 
             
                        klass.new(self, year_index, index, months.first.start_date, months.last.end_date)
         | 
| 14 15 | 
             
                      end
         | 
| 15 16 |  | 
| 16 17 | 
             
                      define_method "#{type}_for" do |date|
         | 
| 17 | 
            -
                        window =  | 
| 18 | 
            -
                         | 
| 18 | 
            +
                        window = public_send(type, date.year - 1, 1)
         | 
| 19 | 
            +
                        loop do
         | 
| 19 20 | 
             
                          break window if window.to_range.include?(date)
         | 
| 20 21 | 
             
                          window = window.next
         | 
| 21 22 | 
             
                        end
         |