active_date_range 0.1.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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: []