date_continuity 0.1.1 → 0.3.0

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: 3b2c82afe0f2c75341f185beb73d42b342230a42b3a5a0c39c6d8eac0150e390
4
- data.tar.gz: 3cbac97c4ef0e7180a4db12506ef1026ef718cb0b853b5b8b00b36e1b85626f2
3
+ metadata.gz: e1d7577b97382b9ed04948b62531e747b21117cc675443ece300ced7f64b4964
4
+ data.tar.gz: '03418a26403935db94bfe6a341150a24fb26fd3c06000a26387ddd6b3d68d7b7'
5
5
  SHA512:
6
- metadata.gz: b8ebeb68346a77478840e90fe8014cb59f7d6008e77cf98ccc154a346948223b0b3ed468c6380aa6ca01095c6ca406f8cf6a9225e29d9b6065737e62c3124db1
7
- data.tar.gz: 5d4e430a577158306fc7115cac688539e910bb9ed6190ade98507a61d6042493dbf71f38efef0380bff18d092e263e6cf3a9986363b72fb4dab8454fac2221d0
6
+ metadata.gz: ba63e8ae3b2ac8f05f799a0e3c03ce56355fcf163a27a0c430a86d9c1874461f3d3a016ee6b23c032aa7368cdba4026a67392e4f4086a7209b7e86149961b2ee
7
+ data.tar.gz: ac28c092029703ea2ebe515eb7bee03e7c57acd83bde4b27722f2198af82d96a38ff25ef5e65d1bba7085f7a2d10f6892fe889539f432f31dfd2191c147c64d5
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DateContinuity
4
+ class Configuration
5
+ attr_accessor :duration_method, :end_method, :frequency_count_method, :start_method, :time_unit_method
6
+
7
+ def initialize
8
+ @duration_method = :duration
9
+ @end_method = :end_at
10
+ @frequency_count_method = :frequency
11
+ @start_method = :start_at
12
+ @time_unit_method = :time_unit
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DateContinuity
4
+ class UnsupportedTimeUnitError < StandardError
5
+ def initialize(time_unit_value)
6
+ super(
7
+ "Unsupported time unit #{time_unit_value}. \
8
+ Must be one of the following: \
9
+ #{DateContinuity::Model::TIME_UNIT_VALUES.to_sentence(last_word_connector: " or ")}".squish
10
+ )
11
+ end
12
+ end
13
+
14
+ class NotEnoughInformationError < StandardError
15
+ def initialize(missing_columns)
16
+ super("Missing Information: #{missing_columns.to_sentence} must be set.")
17
+ end
18
+ end
19
+
20
+ module Errors
21
+ extend ActiveSupport::Concern
22
+
23
+ def _raise_unsupported_time_unit_error(value)
24
+ raise UnsupportedTimeUnitError.new(value)
25
+ end
26
+
27
+ def _raise_not_enough_information_error(required_columns)
28
+ raise NotEnoughInformationError.new(required_columns)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/all"
4
+ require "date_continuity/errors"
5
+ require "date_continuity/time_between"
6
+ require "date_continuity/occurrences"
7
+
8
+ module DateContinuity
9
+ module Model
10
+ TIME_UNIT_VALUES = %w(second minute hour day week month year).freeze
11
+ DAY_EQUIVALENTS = {
12
+ second: 86_400,
13
+ minute: 1_440,
14
+ hour: 24,
15
+ day: 1,
16
+ week: (1.0 / 7.0)
17
+ }.freeze
18
+
19
+ extend ActiveSupport::Concern
20
+
21
+ delegate :duration_method, :end_method, :frequency_count_method,
22
+ :start_method, :time_unit_method, to: :config
23
+
24
+ included do
25
+ include DateContinuity::Errors
26
+ include DateContinuity::TimeBetween
27
+ include DateContinuity::Occurrences
28
+
29
+ alias_method "calc_#{DateContinuity.configuration.duration_method}".to_sym, :calc_duration
30
+ alias_method "calc_#{DateContinuity.configuration.end_method}".to_sym, :calc_end
31
+ alias_method "calc_#{DateContinuity.configuration.start_method}".to_sym, :calc_start
32
+ end
33
+
34
+ # Calculation Methods
35
+
36
+ def calc_end
37
+ if start_value.present? && duration_value.present?
38
+ start_value + duration_object
39
+ else
40
+ _raise_not_enough_information_error [start_method, duration_method]
41
+ end
42
+ end
43
+
44
+ def calc_start
45
+ if end_value.present? && duration_value.present?
46
+ end_value - duration_object
47
+ else
48
+ _raise_not_enough_information_error [end_method, duration_method]
49
+ end
50
+ end
51
+
52
+ def calc_duration(start_datetime = start_value, end_datetime = end_value)
53
+ # DateTime subtraction returns a fraction representing days
54
+ if start_datetime && end_datetime
55
+ duration = case time_unit_value
56
+ when "month" then months_between(start_datetime, end_datetime)
57
+ when "year" then years_between(start_datetime, end_datetime)
58
+ else
59
+ (end_datetime - start_datetime) * DAY_EQUIVALENTS[time_unit_value.to_sym]
60
+ end + 1
61
+ (duration * frequency_value).to_i
62
+ else
63
+ _raise_not_enough_information_error [start_method, end_method]
64
+ end
65
+ end
66
+
67
+ def duration_object(duration = duration_value - 1)
68
+ (duration.public_send(time_unit_value) / frequency_value)
69
+ end
70
+
71
+ # Setter methods
72
+
73
+ def set_duration
74
+ send("#{duration_method}=", calc_duration)
75
+ self
76
+ end
77
+
78
+ def set_end
79
+ send("#{end_method}=", calc_end)
80
+ self
81
+ end
82
+
83
+ def set_start
84
+ send("#{start_method}=", calc_start)
85
+ self
86
+ end
87
+
88
+ private
89
+
90
+ def config
91
+ @config ||= DateContinuity.configuration
92
+ end
93
+
94
+ def interval_object
95
+ 1.public_send(time_unit_value) / frequency_value
96
+ end
97
+
98
+ def time_unit_less_than_day?
99
+ DAY_EQUIVALENTS[time_unit_value.to_sym] && DAY_EQUIVALENTS[time_unit_value.to_sym] < 1
100
+ end
101
+
102
+ # Getter methods
103
+ def start_value
104
+ public_send(start_method)&.to_datetime
105
+ end
106
+
107
+ def end_value
108
+ public_send(end_method)&.to_datetime
109
+ end
110
+
111
+ def duration_value
112
+ public_send(duration_method).to_i
113
+ end
114
+
115
+ def frequency_value
116
+ if respond_to?(frequency_count_method)
117
+ send(frequency_count_method).to_f
118
+ else
119
+ 1.0
120
+ end
121
+ end
122
+
123
+ def time_unit_value
124
+ send(time_unit_method).downcase.singularize.tap do |value|
125
+ _raise_unsupported_time_unit_error(value) unless TIME_UNIT_VALUES.include?(value)
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DateContinuity
4
+ module Occurrences
5
+ extend ActiveSupport::Concern
6
+
7
+ def all_occurrences
8
+ if start_value.present? && duration_value.present?
9
+ Array.new(duration_value) do |count|
10
+ start_value + duration_object(count)
11
+ end
12
+ else
13
+ _raise_not_enough_information_error [start_method, duration_method]
14
+ end
15
+ end
16
+
17
+ def prev_occurrence(current_time = DateTime.current)
18
+ set_start if start_value.blank?
19
+ current_time = current_time.beginning_of_day unless time_unit_less_than_day?
20
+ return if start_value >= current_time
21
+
22
+ duration_from_start_to_now = calc_duration(start_value, current_time) - 1
23
+ start_value + duration_object(duration_from_start_to_now)
24
+ end
25
+
26
+ def next_occurrence(current_time = DateTime.current)
27
+ set_end if end_value.blank?
28
+ current_time = current_time.beginning_of_day unless time_unit_less_than_day?
29
+ return if current_time >= end_value
30
+ return start_value unless prev_occurrence(current_time)
31
+
32
+ prev_occurrence(current_time) + interval_object
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DateContinuity
4
+ module TimeBetween
5
+ extend ActiveSupport::Concern
6
+
7
+ def months_between(date1, date2)
8
+ earlier, later = [date1, date2].sort
9
+ (later.year - earlier.year) * 12 + later.month - earlier.month
10
+ end
11
+
12
+ def years_between(date1, date2)
13
+ earlier, later = [date1, date2].sort
14
+ diff = (later.year - earlier.year)
15
+
16
+ if earlier.month > later.month
17
+ diff -= 1
18
+ elsif earlier.month == later.month
19
+ if earlier.end_of_month == earlier && later.end_of_month == later
20
+ # no-op, account for leap years
21
+ elsif earlier.day > later.day
22
+ diff -= 1
23
+ end
24
+ end
25
+
26
+ diff
27
+ end
28
+ end
29
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: date_continuity
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Merritt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-23 00:00:00.000000000 Z
11
+ date: 2022-03-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">"
18
18
  - !ruby/object:Gem::Version
19
- version: 5.0.0
19
+ version: '3'
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
- version: 5.0.0
26
+ version: '3'
27
27
  description: Calculate fields based on a minimum set of information.
28
28
  email: andrew.w.merritt@gmail.com
29
29
  executables: []
@@ -31,7 +31,12 @@ extensions: []
31
31
  extra_rdoc_files: []
32
32
  files:
33
33
  - lib/date_continuity.rb
34
- homepage: https://rubygems.org/gems/date_continuity
34
+ - lib/date_continuity/configuration.rb
35
+ - lib/date_continuity/errors.rb
36
+ - lib/date_continuity/model.rb
37
+ - lib/date_continuity/occurrences.rb
38
+ - lib/date_continuity/time_between.rb
39
+ homepage: https://github.com/amerritt14/date_continuity
35
40
  licenses:
36
41
  - MIT
37
42
  metadata: {}