active_date_range 0.1.0 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e93ef4c06d102fab1f4d60e5a6e8ef093f80f73e5d18ae80feb7b3ff952e53d3
4
- data.tar.gz: f3e0b0fcfc7e7ae3a4085ceb6eb719b13c8b03452c7219d9570f6634768327a6
3
+ metadata.gz: 35a7b655ef32c770ab582f7228e59a90091cdba9553de211e1b18d4efbbb016c
4
+ data.tar.gz: 2f8b685dcaa03b9df69a3d17876023c6e0de5dd8d3bc2f2c0a8208ecffd6b1df
5
5
  SHA512:
6
- metadata.gz: 0e6ec69527db1a35890f69111e4b1a00f2d1158b2f7bfaeb5d45b51cc7f0a943876c9a4c2cb0cc6da944e251e836c02c8b3a5ccaee43b9cfb351f2cc9340159a
7
- data.tar.gz: f5405fc96a213e444dd62a3cd4caf97601a0852937204c5261cf7cb9d979bdfca20b8920f4988af8e6ecd30cb7797e28355ff0f71781dd1cb96677d3ad56b464
6
+ metadata.gz: 292d7bc594e974903c6f73e4edf7b4e3775901f1c970ec54623950d019a8c327ba04e25b0d743c36605df2f514ee752471bd3787d86ff25ece1cdf9903b5b157
7
+ data.tar.gz: 156323981ba292945aebe705431e86eb5474dcf2adfc1caea88fa9703a60b6f4b164222312ae14710b825b3ed8e2482fdd8545ae839bb3165dcb666a683b8ae0
@@ -0,0 +1,57 @@
1
+ # This workflow uses actions that are not certified by GitHub.
2
+ # They are provided by a third-party and are governed by
3
+ # separate terms of service, privacy policy, and support
4
+ # documentation.
5
+ # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6
+ # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7
+
8
+ name: Ruby
9
+
10
+ on:
11
+ push:
12
+ branches: [ main ]
13
+ pull_request:
14
+ branches: [ main ]
15
+
16
+ jobs:
17
+ test:
18
+ runs-on: ubuntu-latest
19
+ strategy:
20
+ matrix:
21
+ ruby-version: ['2.7', '3.0']
22
+
23
+ steps:
24
+ - uses: actions/checkout@v2
25
+ - name: Set up Ruby
26
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
27
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
28
+ # uses: ruby/setup-ruby@v1
29
+ uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e
30
+ with:
31
+ ruby-version: ${{ matrix.ruby-version }}
32
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
33
+ - name: Run tests
34
+ run: bundle exec rake
35
+
36
+ lint:
37
+ runs-on: ubuntu-latest
38
+
39
+ steps:
40
+ - uses: actions/checkout@v2
41
+ - name: Set up Ruby 2.7
42
+ uses: ruby/setup-ruby@v1
43
+ with:
44
+ ruby-version: 2.7
45
+ - name: Cache gems
46
+ uses: actions/cache@v1
47
+ with:
48
+ path: vendor/bundle
49
+ key: ${{ runner.os }}-rubocop-${{ hashFiles('**/Gemfile.lock') }}
50
+ restore-keys: |
51
+ ${{ runner.os }}-rubocop-
52
+ - name: Install gems
53
+ run: |
54
+ bundle config path vendor/bundle
55
+ bundle install --jobs 4 --retry 3
56
+ - name: Run RuboCop
57
+ run: bundle exec rubocop --parallel
data/.rubocop.yml CHANGED
@@ -8,7 +8,6 @@ AllCops:
8
8
  # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop
9
9
  # to ignore them, so only the ones explicitly set in this file are enabled.
10
10
  DisabledByDefault: true
11
- SuggestExtensions: false
12
11
  Exclude:
13
12
  - '**/tmp/**/*'
14
13
  - '**/templates/**/*'
data/CHANGELOG.md CHANGED
@@ -1,4 +1,55 @@
1
- == 0.1.0
1
+ ## 0.3.1
2
+
3
+ * Fix issue with `next` not returning a full year when leap years are in the range
4
+
5
+ *Edwin Vlieg*
6
+
7
+ ## 0.3.0
8
+
9
+ * `include?` now behaves like `cover?` for better performance
10
+
11
+ *Edwin Vlieg*
12
+
13
+ * Add intersection support:
14
+
15
+ ```
16
+ date_range.intersection(other_date_range) # => DateRange
17
+ ```
18
+
19
+ *Edwin Vlieg*
20
+
21
+
22
+ * Add support for boundless ranges:
23
+
24
+ ```
25
+ date_range = DateRange.parse('202101..')
26
+ date_range.boundless? # => true
27
+ date_range.in_groups_of(:month) # => Enumerator::Lazy
28
+ Model.where(date: date_range) # => SQL "WHERE date >= 2021-01-01"
29
+ ```
30
+
31
+ *Edwin Vlieg*
32
+
33
+ * Add ActiveModel type for date range:
34
+
35
+ ```
36
+ attribute :period, :date_range
37
+ ```
38
+
39
+ *Edwin Vlieg*
40
+
41
+ ## 0.2.0
42
+
43
+ * Add support for weeks:
44
+
45
+ - Shorthands for `this_week`, `next_week` and `prev_week`
46
+ - `full_week?` and `one_week?`
47
+ - `next` and `previous` now handle weeks correctly
48
+ - Tests for biweekly calculations
49
+
50
+ *Edwin Vlieg*
51
+
52
+ ## 0.1.0
2
53
 
3
54
  * Initial import of the DateRange implementation from the internal implementation in the Moneybird code.
4
55
 
data/Gemfile CHANGED
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
5
  # Specify your gem's dependencies in active_date_range.gemspec
4
6
  gemspec
5
7
 
6
8
  gem "rake", "~> 12.0"
7
- gem "railties", "~> 6.1"
9
+ gem "railties", "> 6.1"
8
10
  gem "minitest", "~> 5.0"
9
11
  gem "guard"
10
12
  gem "guard-minitest"
data/Gemfile.lock CHANGED
@@ -1,76 +1,75 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- active_date_range (0.1.0)
5
- activesupport (~> 6.1)
4
+ active_date_range (0.3.1)
5
+ activesupport (> 6.1)
6
6
  i18n
7
7
 
8
8
  GEM
9
9
  remote: https://rubygems.org/
10
10
  specs:
11
- actionpack (6.1.2.1)
12
- actionview (= 6.1.2.1)
13
- activesupport (= 6.1.2.1)
14
- rack (~> 2.0, >= 2.0.9)
11
+ actionpack (7.0.0.alpha2)
12
+ actionview (= 7.0.0.alpha2)
13
+ activesupport (= 7.0.0.alpha2)
14
+ rack (~> 2.0, >= 2.2.0)
15
15
  rack-test (>= 0.6.3)
16
16
  rails-dom-testing (~> 2.0)
17
17
  rails-html-sanitizer (~> 1.0, >= 1.2.0)
18
- actionview (6.1.2.1)
19
- activesupport (= 6.1.2.1)
18
+ actionview (7.0.0.alpha2)
19
+ activesupport (= 7.0.0.alpha2)
20
20
  builder (~> 3.1)
21
21
  erubi (~> 1.4)
22
22
  rails-dom-testing (~> 2.0)
23
23
  rails-html-sanitizer (~> 1.1, >= 1.2.0)
24
- activesupport (6.1.2.1)
24
+ activemodel (7.0.0.alpha2)
25
+ activesupport (= 7.0.0.alpha2)
26
+ activesupport (7.0.0.alpha2)
25
27
  concurrent-ruby (~> 1.0, >= 1.0.2)
26
28
  i18n (>= 1.6, < 2)
27
29
  minitest (>= 5.1)
28
30
  tzinfo (~> 2.0)
29
- zeitwerk (~> 2.3)
30
- ast (2.4.1)
31
+ ast (2.4.2)
31
32
  builder (3.2.4)
32
33
  coderay (1.1.3)
33
- concurrent-ruby (1.1.8)
34
+ concurrent-ruby (1.1.9)
34
35
  crass (1.0.6)
35
36
  erubi (1.10.0)
36
- ffi (1.14.2)
37
- formatador (0.2.5)
38
- guard (2.16.2)
37
+ ffi (1.15.4)
38
+ formatador (0.3.0)
39
+ guard (2.18.0)
39
40
  formatador (>= 0.2.4)
40
41
  listen (>= 2.7, < 4.0)
41
42
  lumberjack (>= 1.0.12, < 2.0)
42
43
  nenv (~> 0.1)
43
44
  notiffany (~> 0.0)
44
- pry (>= 0.9.12)
45
+ pry (>= 0.13.0)
45
46
  shellany (~> 0.0)
46
47
  thor (>= 0.18.1)
47
48
  guard-compat (1.2.1)
48
49
  guard-minitest (2.4.6)
49
50
  guard-compat (~> 1.2)
50
51
  minitest (>= 3.0)
51
- i18n (1.8.9)
52
+ i18n (1.8.10)
52
53
  concurrent-ruby (~> 1.0)
53
- listen (3.4.1)
54
+ listen (3.7.0)
54
55
  rb-fsevent (~> 0.10, >= 0.10.3)
55
56
  rb-inotify (~> 0.9, >= 0.9.10)
56
- loofah (2.9.0)
57
+ loofah (2.12.0)
57
58
  crass (~> 1.0.2)
58
59
  nokogiri (>= 1.5.9)
59
60
  lumberjack (1.2.8)
60
61
  method_source (1.0.0)
61
- mini_portile2 (2.5.0)
62
- minitest (5.14.2)
62
+ minitest (5.14.4)
63
63
  nenv (0.3.0)
64
- nokogiri (1.11.1)
65
- mini_portile2 (~> 2.5.0)
64
+ nokogiri (1.12.5-x86_64-linux)
66
65
  racc (~> 1.4)
67
66
  notiffany (0.1.3)
68
67
  nenv (~> 0.1)
69
68
  shellany (~> 0.0)
70
- parallel (1.19.2)
71
- parser (2.7.1.4)
69
+ parallel (1.21.0)
70
+ parser (3.0.2.0)
72
71
  ast (~> 2.4.1)
73
- pry (0.14.0)
72
+ pry (0.14.1)
74
73
  coderay (~> 1.1)
75
74
  method_source (~> 1.0)
76
75
  racc (1.5.2)
@@ -80,59 +79,61 @@ GEM
80
79
  rails-dom-testing (2.0.3)
81
80
  activesupport (>= 4.2.0)
82
81
  nokogiri (>= 1.6)
83
- rails-html-sanitizer (1.3.0)
82
+ rails-html-sanitizer (1.4.2)
84
83
  loofah (~> 2.3)
85
- railties (6.1.2.1)
86
- actionpack (= 6.1.2.1)
87
- activesupport (= 6.1.2.1)
84
+ railties (7.0.0.alpha2)
85
+ actionpack (= 7.0.0.alpha2)
86
+ activesupport (= 7.0.0.alpha2)
88
87
  method_source
89
- rake (>= 0.8.7)
88
+ rake (>= 0.13)
90
89
  thor (~> 1.0)
90
+ zeitwerk (~> 2.5.0.beta3)
91
91
  rainbow (3.0.0)
92
92
  rake (12.3.3)
93
- rb-fsevent (0.10.4)
93
+ rb-fsevent (0.11.0)
94
94
  rb-inotify (0.10.1)
95
95
  ffi (~> 1.0)
96
- regexp_parser (1.8.2)
97
- rexml (3.2.4)
98
- rubocop (0.91.1)
96
+ regexp_parser (2.1.1)
97
+ rexml (3.2.5)
98
+ rubocop (1.21.0)
99
99
  parallel (~> 1.10)
100
- parser (>= 2.7.1.1)
100
+ parser (>= 3.0.0.0)
101
101
  rainbow (>= 2.2.2, < 4.0)
102
- regexp_parser (>= 1.7)
102
+ regexp_parser (>= 1.8, < 3.0)
103
103
  rexml
104
- rubocop-ast (>= 0.4.0, < 1.0)
104
+ rubocop-ast (>= 1.9.1, < 2.0)
105
105
  ruby-progressbar (~> 1.7)
106
- unicode-display_width (>= 1.4.0, < 2.0)
107
- rubocop-ast (0.4.2)
108
- parser (>= 2.7.1.4)
106
+ unicode-display_width (>= 1.4.0, < 3.0)
107
+ rubocop-ast (1.11.0)
108
+ parser (>= 3.0.1.1)
109
109
  rubocop-packaging (0.5.1)
110
110
  rubocop (>= 0.89, < 2.0)
111
- rubocop-performance (1.9.2)
112
- rubocop (>= 0.90.0, < 2.0)
111
+ rubocop-performance (1.11.5)
112
+ rubocop (>= 1.7.0, < 2.0)
113
113
  rubocop-ast (>= 0.4.0)
114
- rubocop-rails (2.9.1)
114
+ rubocop-rails (2.12.2)
115
115
  activesupport (>= 4.2.0)
116
116
  rack (>= 1.1)
117
- rubocop (>= 0.90.0, < 2.0)
118
- ruby-progressbar (1.10.1)
117
+ rubocop (>= 1.7.0, < 2.0)
118
+ ruby-progressbar (1.11.0)
119
119
  shellany (0.0.1)
120
120
  thor (1.1.0)
121
121
  tzinfo (2.0.4)
122
122
  concurrent-ruby (~> 1.0)
123
- unicode-display_width (1.7.0)
124
- zeitwerk (2.4.2)
123
+ unicode-display_width (2.1.0)
124
+ zeitwerk (2.5.0.beta3)
125
125
 
126
126
  PLATFORMS
127
- ruby
127
+ x86_64-linux
128
128
 
129
129
  DEPENDENCIES
130
130
  active_date_range!
131
+ activemodel
131
132
  guard
132
133
  guard-minitest
133
134
  minitest (~> 5.0)
134
135
  pry
135
- railties (~> 6.1)
136
+ railties (> 6.1)
136
137
  rake (~> 12.0)
137
138
  rubocop
138
139
  rubocop-packaging
@@ -140,4 +141,4 @@ DEPENDENCIES
140
141
  rubocop-rails
141
142
 
142
143
  BUNDLED WITH
143
- 2.1.4
144
+ 2.3.5
data/Guardfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # A sample Guardfile
2
4
  # More info at https://github.com/guard/guard#readme
3
5
 
@@ -19,11 +21,11 @@ clearing :on
19
21
 
20
22
  guard :minitest do
21
23
  # with Minitest::Unit
22
- watch(%r{^test/(.*)\/?test_(.*)\.rb$})
23
- watch(%r{^test/(.*)\/?(.*)_test\.rb$})
24
+ watch(%r{^test/(.*)/?test_(.*)\.rb$})
25
+ watch(%r{^test/(.*)/?(.*)_test\.rb$})
24
26
  watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
25
27
  watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}#{m[2]}_test.rb" }
26
- watch(%r{^test/test_helper\.rb$}) { 'test' }
28
+ watch(%r{^test/test_helper\.rb$}) { "test" }
27
29
 
28
30
  # with Minitest::Spec
29
31
  # watch(%r{^spec/(.*)_spec\.rb$})
data/README.md CHANGED
@@ -29,18 +29,21 @@ ActiveDateRange::DateRange.new(Date.new(2021, 1, 1), Date.new(2021, 12, 31))
29
29
  ActiveDateRange::DateRange.new(Date.new(2021, 1, 1)..Date.new(2021, 12, 31))
30
30
  ```
31
31
 
32
- You can also use shorthands to initialize a range relative to today. Shorthands are available for `this`, `prev` and `next` for the ranges `month`, `quarter` and `year`:
32
+ You can also use shorthands to initialize a range relative to today. Shorthands are available for `this`, `prev` and `next` for the ranges `month`, `quarter`, `year` and `week`:
33
33
 
34
34
  ```ruby
35
35
  ActiveDateRange::DateRange.this_month
36
36
  ActiveDateRange::DateRange.this_year
37
37
  ActiveDateRange::DateRange.this_quarter
38
+ ActiveDateRange::DateRange.this_week
38
39
  ActiveDateRange::DateRange.prev_month
39
40
  ActiveDateRange::DateRange.prev_year
40
41
  ActiveDateRange::DateRange.prev_quarter
42
+ ActiveDateRange::DateRange.prev_week
41
43
  ActiveDateRange::DateRange.next_month
42
44
  ActiveDateRange::DateRange.next_year
43
45
  ActiveDateRange::DateRange.next_quarter
46
+ ActiveDateRange::DateRange.next_week
44
47
  ```
45
48
 
46
49
  The third option is to use parse:
@@ -71,6 +74,7 @@ date_range.months # => 12
71
74
  date_range.quarters # => 4
72
75
  date_range.years # => 1
73
76
  date_range.one_month? # => false
77
+ date_range.one_week? # => false
74
78
  date_range.one_year? # => true
75
79
  date_range.full_year? # => true
76
80
  date_range.same_year? # => true
@@ -89,6 +93,16 @@ date_range.previous(2) # => DateRange.parse('201901..20
89
93
  date_range.next # => DateRange.parse('202201..202212')
90
94
  date_range + DateRange.parse('202201..202202') # => DateRange.parse('202101..202202')
91
95
  date_range.in_groups_of(:month) # => [DateRange.parse('202101..202101'), ..., DateRange.parse('202112..202112')]
96
+ date_range.intersection(DateRange.parse('202101..202102')) # => DateRange.parse('202101..202102')
97
+ ```
98
+
99
+ Support for boundless ranges is also available:
100
+
101
+ ```ruby
102
+ date_range = DateRange.parse('202101..')
103
+ date_range.boundless? # => true
104
+ date_range.in_groups_of(:month) # => Enumerator::Lazy
105
+ Model.where(date: date_range) # => SQL "WHERE date >= 2021-01-01"
92
106
  ```
93
107
 
94
108
  And lastly you can call `.humanize` to get a localizable human representation of the range for in the user interface:
@@ -100,6 +114,18 @@ date_range.humanize(format: :explicit) # => 'January 1st, 2021 - December 31st
100
114
 
101
115
  See [active_date_range/locale/en.yml](https://github.com/moneybird/active-date-range/blob/main/lib/active_date_range/locale/en.yml) for all the I18n keys you need to translate for your application.
102
116
 
117
+ ### ActiveModel type
118
+
119
+ Date ranges are also available as an ActiveModel type. So you can use a date range attribute and the value will automatically be converted:
120
+
121
+ ```ruby
122
+ class Report
123
+ include ActiveModel::Attributes
124
+
125
+ attribute :period, :date_range
126
+ end
127
+ ```
128
+
103
129
  ### Usage example
104
130
 
105
131
  Use the shorthands to link to a specific period:
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rake/testtask"
3
5
 
@@ -7,4 +9,4 @@ Rake::TestTask.new(:test) do |t|
7
9
  t.test_files = FileList["test/**/*_test.rb"]
8
10
  end
9
11
 
10
- task :default => :test
12
+ task default: :test
@@ -1,4 +1,6 @@
1
- require_relative 'lib/active_date_range/version'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/active_date_range/version"
2
4
 
3
5
  Gem::Specification.new do |spec|
4
6
  spec.name = "active_date_range"
@@ -9,26 +11,27 @@ Gem::Specification.new do |spec|
9
11
  spec.summary = "DateRange for ActiveSupport"
10
12
  spec.description = "ActiveDateRange provides a range of dates with a powerful API to manipulate and use date ranges in your software."
11
13
  spec.homepage = "https://github.com/moneybird/active-date-range"
12
- spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
13
15
 
14
16
  spec.metadata["homepage_uri"] = spec.homepage
15
17
  spec.metadata["source_code_uri"] = "https://github.com/moneybird/active-date-range"
16
- spec.metadata["changelog_uri"] = "https://github.com/moneybird/active-date-range/CHANGELOG.md"
18
+ spec.metadata["changelog_uri"] = "https://github.com/moneybird/active-date-range/blob/main/CHANGELOG.md"
17
19
 
18
20
  # Specify which files should be added to the gem when it is released.
19
21
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
22
+ spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
21
23
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
24
  end
23
25
  spec.bindir = "exe"
24
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
27
  spec.require_paths = ["lib"]
26
28
 
27
- spec.add_dependency "activesupport", "~> 6.1"
29
+ spec.add_dependency "activesupport", "> 6.1"
28
30
  spec.add_dependency "i18n"
29
31
 
30
32
  spec.add_development_dependency "rubocop"
31
33
  spec.add_development_dependency "rubocop-packaging"
32
34
  spec.add_development_dependency "rubocop-performance"
33
35
  spec.add_development_dependency "rubocop-rails"
36
+ spec.add_development_dependency "activemodel"
34
37
  end
data/bin/console CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "bundler/setup"
4
5
  require "active_support"
6
+ require "active_model"
5
7
  require "active_date_range"
6
8
 
7
9
  Time.zone = "UTC"
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveDateRange
4
+ class DateRangeType < ActiveModel::Type::String
5
+ def cast(value)
6
+ ActiveDateRange::DateRange.parse(value)
7
+ end
8
+ end
9
+ end
10
+
11
+ if defined?(ActiveModel)
12
+ ActiveModel::Type.register(:date_range, ActiveDateRange::DateRangeType)
13
+ end
@@ -12,7 +12,10 @@ module ActiveDateRange
12
12
  next_quarter: -> { DateRange.new(3.months.from_now.to_date.all_quarter) },
13
13
  this_year: -> { DateRange.new(Time.zone.today.all_year) },
14
14
  prev_year: -> { DateRange.new(12.months.ago.to_date.all_year) },
15
- next_year: -> { DateRange.new(12.months.from_now.to_date.all_year) }
15
+ next_year: -> { DateRange.new(12.months.from_now.to_date.all_year) },
16
+ this_week: -> { DateRange.new(Time.zone.today.all_week) },
17
+ prev_week: -> { DateRange.new(1.week.ago.to_date.all_week) },
18
+ next_week: -> { DateRange.new(1.week.from_now.to_date.all_week) }
16
19
  }.freeze
17
20
 
18
21
  RANGE_PART_REGEXP = %r{\A(?<year>((1\d|2\d)\d\d))-?(?<month>0[1-9]|1[012])-?(?<day>[0-2]\d|3[01])?\z}
@@ -32,12 +35,14 @@ module ActiveDateRange
32
35
  return SHORTHANDS[input.to_sym].call if SHORTHANDS.key?(input.to_sym)
33
36
 
34
37
  begin_date, end_date = input.split("..")
35
- raise InvalidDateRangeFormat, "#{input} doesn't have a begin..end format" unless begin_date && end_date
38
+ raise InvalidDateRangeFormat, "#{input} doesn't have a begin..end format" if begin_date.blank? && end_date.blank?
36
39
 
37
40
  DateRange.new(parse_date(begin_date), parse_date(end_date, last: true))
38
41
  end
39
42
 
40
43
  def self.parse_date(input, last: false)
44
+ return if input.blank?
45
+
41
46
  match_data = input.match(RANGE_PART_REGEXP)
42
47
  raise InvalidDateRangeFormat, "#{input} isn't a valid date format YYYYMMDD or YYYYMM" unless match_data
43
48
 
@@ -59,12 +64,13 @@ module ActiveDateRange
59
64
  # Make sures the begin date is before the end date.
60
65
  def initialize(begin_date, end_date = nil)
61
66
  begin_date, end_date = begin_date.begin, begin_date.end if begin_date.kind_of?(Range)
67
+ begin_date, end_date = begin_date.first, begin_date.last if begin_date.kind_of?(Array)
62
68
  begin_date = begin_date.to_date if begin_date.kind_of?(Time)
63
69
  end_date = end_date.to_date if end_date.kind_of?(Time)
64
70
 
65
- raise InvalidDateRange, "Date range invalid, begin should be a date" unless begin_date.kind_of?(Date)
66
- raise InvalidDateRange, "Date range invalid, end should be a date" unless end_date.kind_of?(Date)
67
- raise InvalidDateRange, "Date range invalid, begin #{begin_date} is after end #{end_date}" if begin_date > end_date
71
+ raise InvalidDateRange, "Date range invalid, begin should be a date" if begin_date && !begin_date.kind_of?(Date)
72
+ raise InvalidDateRange, "Date range invalid, end should be a date" if end_date && !end_date.kind_of?(Date)
73
+ raise InvalidDateRange, "Date range invalid, begin #{begin_date} is after end #{end_date}" if begin_date && end_date && begin_date > end_date
68
74
 
69
75
  super(begin_date, end_date)
70
76
  end
@@ -81,8 +87,14 @@ module ActiveDateRange
81
87
  self.begin <=> other.begin
82
88
  end
83
89
 
90
+ def boundless?
91
+ self.begin.nil? || self.end.nil?
92
+ end
93
+
84
94
  # Returns the number of days in the range
85
95
  def days
96
+ return if boundless?
97
+
86
98
  @days ||= (self.end - self.begin).to_i + 1
87
99
  end
88
100
 
@@ -107,19 +119,31 @@ module ActiveDateRange
107
119
  months / 12
108
120
  end
109
121
 
122
+ # Returns the number of weeks on the range or nil when range is no full week
123
+ def weeks
124
+ return nil unless full_week?
125
+
126
+ days / 7
127
+ end
128
+
110
129
  # Returns true when begin of the range is at the beginning of the month
111
130
  def begin_at_beginning_of_month?
112
- self.begin.day == 1
131
+ self.begin.present? && self.begin.day == 1
113
132
  end
114
133
 
115
134
  # Returns true when begin of the range is at the beginning of the quarter
116
135
  def begin_at_beginning_of_quarter?
117
- begin_at_beginning_of_month? && [1, 4, 7, 10].include?(self.begin.month)
136
+ self.begin.present? && begin_at_beginning_of_month? && [1, 4, 7, 10].include?(self.begin.month)
118
137
  end
119
138
 
120
139
  # Returns true when begin of the range is at the beginning of the year
121
140
  def begin_at_beginning_of_year?
122
- begin_at_beginning_of_month? && self.begin.month == 1
141
+ self.begin.present? && begin_at_beginning_of_month? && self.begin.month == 1
142
+ end
143
+
144
+ # Returns true when begin of the range is at the beginning of the week
145
+ def begin_at_beginning_of_week?
146
+ self.begin.present? && self.begin == self.begin.at_beginning_of_week
123
147
  end
124
148
 
125
149
  # Returns true when the range is exactly one month long
@@ -143,44 +167,57 @@ module ActiveDateRange
143
167
  self.end == self.begin.at_end_of_year
144
168
  end
145
169
 
170
+ def one_week?
171
+ days == 7 &&
172
+ begin_at_beginning_of_week? &&
173
+ self.end == self.begin.at_end_of_week
174
+ end
175
+
146
176
  # Returns true when the range is exactly one or more months long
147
177
  def full_month?
148
- begin_at_beginning_of_month? && self.end == self.end.at_end_of_month
178
+ begin_at_beginning_of_month? && self.end.present? && self.end == self.end.at_end_of_month
149
179
  end
150
180
 
151
181
  alias :full_months? :full_month?
152
182
 
153
183
  # Returns true when the range is exactly one or more quarters long
154
184
  def full_quarter?
155
- begin_at_beginning_of_quarter? && self.end == self.end.at_end_of_quarter
185
+ begin_at_beginning_of_quarter? && self.end.present? && self.end == self.end.at_end_of_quarter
156
186
  end
157
187
 
158
188
  alias :full_quarters? :full_quarter?
159
189
 
160
190
  # Returns true when the range is exactly one or more years long
161
191
  def full_year?
162
- begin_at_beginning_of_year? && self.end == self.end.at_end_of_year
192
+ begin_at_beginning_of_year? && self.end.present? && self.end == self.end.at_end_of_year
163
193
  end
164
194
 
165
195
  alias :full_years? :full_year?
166
196
 
197
+ # Returns true when the range is exactly one or more weeks long
198
+ def full_week?
199
+ begin_at_beginning_of_week? && self.end.present? && self.end == self.end.at_end_of_week
200
+ end
201
+
202
+ alias :full_weeks? :full_week?
203
+
167
204
  # Returns true when begin and end are in the same year
168
205
  def same_year?
169
- self.begin.year == self.end.year
206
+ !boundless? && self.begin.year == self.end.year
170
207
  end
171
208
 
172
209
  # Returns true when the date range is before the given date. Accepts both a <tt>Date</tt>
173
210
  # and <tt>DateRange</tt> as input.
174
211
  def before?(date)
175
212
  date = date.begin if date.kind_of?(DateRange)
176
- self.end.before?(date)
213
+ self.end.present? && self.end.before?(date)
177
214
  end
178
215
 
179
216
  # Returns true when the date range is after the given date. Accepts both a <tt>Date</tt>
180
217
  # and <tt>DateRange</tt> as input.
181
218
  def after?(date)
182
219
  date = date.end if date.kind_of?(DateRange)
183
- self.begin.after?(date)
220
+ self.begin.present? && self.begin.after?(date)
184
221
  end
185
222
 
186
223
  # Returns the granularity of the range. Returns either <tt>:year</tt>, <tt>:quarter</tt> or
@@ -196,6 +233,8 @@ module ActiveDateRange
196
233
  :quarter
197
234
  elsif one_month?
198
235
  :month
236
+ elsif one_week?
237
+ :week
199
238
  end
200
239
  end
201
240
 
@@ -225,7 +264,7 @@ module ActiveDateRange
225
264
  relative_param
226
265
  else
227
266
  format = full_month? ? "%Y%m" : "%Y%m%d"
228
- "#{self.begin.strftime(format)}..#{self.end.strftime(format)}"
267
+ "#{self.begin&.strftime(format)}..#{self.end&.strftime(format)}"
229
268
  end
230
269
  end
231
270
 
@@ -244,13 +283,19 @@ module ActiveDateRange
244
283
  # DateRange.this_month.previous # => DateRange.prev_month
245
284
  # DateRange.this_month.previous(2) # => DateRange.prev_month.previous + DateRange.prev_month
246
285
  def previous(periods = 1)
247
- if granularity
248
- DateRange.new(self.begin - periods.send(granularity), self.begin - 1.day)
286
+ raise BoundlessRangeError, "Can't calculate previous for boundless range" if boundless?
287
+
288
+ begin_date = if granularity
289
+ self.begin - periods.send(granularity)
249
290
  elsif full_month?
250
- DateRange.new(in_groups_of(:month).first.previous(periods * months).begin, self.begin - 1.day)
291
+ in_groups_of(:month).first.previous(periods * months).begin
251
292
  else
252
- DateRange.new((self.begin - (periods * days).days).at_beginning_of_month, self.begin - 1.day)
293
+ (self.begin - (periods * days).days)
253
294
  end
295
+
296
+ begin_date = begin_date.at_beginning_of_month if full_month?
297
+
298
+ DateRange.new(begin_date, self.begin - 1.day)
254
299
  end
255
300
 
256
301
  # Returns the period next to the current period. `periods` can be raised to return more
@@ -259,11 +304,18 @@ module ActiveDateRange
259
304
  # DateRange.this_month.next # => DateRange.next_month
260
305
  # DateRange.this_month.next(2) # => DateRange.next_month + DateRange.next_month.next
261
306
  def next(periods = 1)
262
- if granularity
263
- DateRange.new(self.end + 1.day, (self.end + periods.send(granularity)).at_end_of_month)
307
+ raise BoundlessRangeError, "Can't calculate next for boundless range" if boundless?
308
+
309
+ end_date = if granularity
310
+ self.end + periods.send(granularity)
311
+ elsif full_month?
312
+ in_groups_of(:month).last.next(periods * months).end
264
313
  else
265
- DateRange.new(self.end + 1.day, (self.end + days.days).at_end_of_month)
314
+ self.end + (periods * days).days
266
315
  end
316
+ end_date = end_date.at_end_of_month if full_month?
317
+
318
+ DateRange.new(self.end + 1.day, end_date)
267
319
  end
268
320
 
269
321
  # Returns an array with date ranges containing full months/quarters/years in the current range.
@@ -277,17 +329,40 @@ module ActiveDateRange
277
329
  # DateRange.parse("202101..202103").in_groups_of(:month) # => [DateRange.parse("202001..202001"), DateRange.parse("202002..202002"), DateRange.parse("202003..202003")]
278
330
  # DateRange.parse("202101..202106").in_groups_of(:month, amount: 2) # => [DateRange.parse("202001..202002"), DateRange.parse("202003..202004"), DateRange.parse("202005..202006")]
279
331
  def in_groups_of(granularity, amount: 1)
280
- raise UnknownGranularity, "Unknown granularity #{granularity}. Valid are: month, quarter and year" unless %w[month quarter year].include?(granularity.to_s)
332
+ raise BoundlessRangeError, "Can't group date range without a begin." if self.begin.nil?
281
333
 
282
- group_by { |d| [d.year, d.send(granularity)] }
283
- .map { |_, group| DateRange.new(group.first..group.last) }
284
- .in_groups_of(amount)
285
- .map { |group| group.inject(:+) }
334
+ if boundless?
335
+ grouped_collection(granularity, amount: amount)
336
+ else
337
+ grouped_collection(granularity, amount: amount).to_a
338
+ end
286
339
  end
287
340
 
288
341
  # Returns a human readable format for the date range. See DateRange::Humanizer for options.
289
342
  def humanize(format: :short)
290
343
  Humanizer.new(self, format: format).humanize
291
344
  end
345
+
346
+ # Returns the intersection of the current and the other date range
347
+ def intersection(other)
348
+ intersection = self.to_a.intersection(other.to_a).sort
349
+ DateRange.new(intersection) if intersection.any?
350
+ end
351
+
352
+ def include?(other)
353
+ cover?(other)
354
+ end
355
+
356
+ private
357
+ def grouped_collection(granularity, amount: 1)
358
+ raise UnknownGranularity, "Unknown granularity #{granularity}. Valid are: month, quarter and year" unless %w[month quarter year].include?(granularity.to_s)
359
+
360
+ lazy
361
+ .chunk { |d| [d.year, d.send(granularity)] }
362
+ .map { |_, group| DateRange.new(group.first..group.last) }
363
+ .with_index
364
+ .slice_before { |_, index| index % amount == 0 }
365
+ .map { |group| group.map(&:first).inject(:+) }
366
+ end
292
367
  end
293
368
  end
@@ -108,12 +108,19 @@ module ActiveDateRange
108
108
  month_format = date_range.full_month? ? "month" : "day_month"
109
109
  abbr = "abbr_" if date_range.same_year?
110
110
 
111
+ begin_formatted = I18n.localize(date_range.begin, format: :"#{abbr}#{format}_#{month_format}") if date_range.begin
112
+ end_formatted = I18n.localize(date_range.end, format: :"#{format}_#{month_format}") if date_range.end
113
+
111
114
  range(
112
- I18n.localize(date_range.begin, format: :"#{abbr}#{format}_#{month_format}"),
113
- I18n.localize(date_range.end, format: :"#{format}_#{month_format}")
115
+ begin_formatted || infinite,
116
+ end_formatted || infinite
114
117
  )
115
118
  end
116
119
 
120
+ def infinite
121
+ "∞"
122
+ end
123
+
117
124
  def range(range_begin, range_end)
118
125
  I18n.translate(:range, scope: :date, begin: range_begin, end: range_end)
119
126
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  begin
2
4
  require "i18n"
3
5
  require "i18n/backend/fallbacks"
@@ -25,12 +25,15 @@ en:
25
25
  relative_range:
26
26
  next_month: "next month"
27
27
  next_quarter: "next quarter"
28
+ next_week: "next week"
28
29
  next_year: "next year"
29
30
  prev_month: "the previous month"
30
31
  prev_quarter: "the previous quarter"
32
+ prev_week: "the previous week"
31
33
  prev_year: "the previous year"
32
34
  this_month: "this month"
33
35
  this_quarter: "this quarter"
36
+ this_week: "this week"
34
37
  this_year: "this year"
35
38
  today: "today"
36
39
  long_quarter: "quarter %{quarter}"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveDateRange
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.1"
5
5
  end
@@ -11,6 +11,7 @@ require "active_date_range/core_ext/date"
11
11
  require "active_date_range/version"
12
12
  require "active_date_range/date_range"
13
13
  require "active_date_range/humanizer"
14
+ require "active_date_range/active_model_type"
14
15
 
15
16
  module ActiveDateRange
16
17
  class Error < StandardError; end
@@ -18,4 +19,5 @@ module ActiveDateRange
18
19
  class InvalidAddition < Error; end
19
20
  class InvalidDateRangeFormat < Error; end
20
21
  class UnknownGranularity < Error; end
22
+ class BoundlessRangeError < Error; end
21
23
  end
metadata CHANGED
@@ -1,27 +1,27 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_date_range
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Edwin Vlieg
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-04-22 00:00:00.000000000 Z
11
+ date: 2022-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">"
18
18
  - !ruby/object:Gem::Version
19
19
  version: '6.1'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '6.1'
27
27
  - !ruby/object:Gem::Dependency
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: activemodel
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
97
111
  description: ActiveDateRange provides a range of dates with a powerful API to manipulate
98
112
  and use date ranges in your software.
99
113
  email:
@@ -102,6 +116,7 @@ executables: []
102
116
  extensions: []
103
117
  extra_rdoc_files: []
104
118
  files:
119
+ - ".github/workflows/ruby.yml"
105
120
  - ".gitignore"
106
121
  - ".rubocop.yml"
107
122
  - ".travis.yml"
@@ -116,6 +131,7 @@ files:
116
131
  - bin/console
117
132
  - bin/setup
118
133
  - lib/active_date_range.rb
134
+ - lib/active_date_range/active_model_type.rb
119
135
  - lib/active_date_range/core_ext/date.rb
120
136
  - lib/active_date_range/core_ext/integer.rb
121
137
  - lib/active_date_range/date_range.rb
@@ -128,8 +144,8 @@ licenses: []
128
144
  metadata:
129
145
  homepage_uri: https://github.com/moneybird/active-date-range
130
146
  source_code_uri: https://github.com/moneybird/active-date-range
131
- changelog_uri: https://github.com/moneybird/active-date-range/CHANGELOG.md
132
- post_install_message:
147
+ changelog_uri: https://github.com/moneybird/active-date-range/blob/main/CHANGELOG.md
148
+ post_install_message:
133
149
  rdoc_options: []
134
150
  require_paths:
135
151
  - lib
@@ -137,15 +153,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
137
153
  requirements:
138
154
  - - ">="
139
155
  - !ruby/object:Gem::Version
140
- version: 2.3.0
156
+ version: 2.7.0
141
157
  required_rubygems_version: !ruby/object:Gem::Requirement
142
158
  requirements:
143
159
  - - ">="
144
160
  - !ruby/object:Gem::Version
145
161
  version: '0'
146
162
  requirements: []
147
- rubygems_version: 3.2.16
148
- signing_key:
163
+ rubygems_version: 3.2.9
164
+ signing_key:
149
165
  specification_version: 4
150
166
  summary: DateRange for ActiveSupport
151
167
  test_files: []