time_calc 0.0.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.
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TimeCalc
4
+ # @private
5
+ # Tries to encapsulate all the differences between Time, Date, DateTime
6
+ module Types
7
+ extend self
8
+
9
+ ATTRS = {
10
+ Time => %i[year month day hour min sec subsec utc_offset],
11
+ Date => %i[year month day],
12
+ DateTime => %i[year month day hour min sec sec_fraction zone]
13
+ }.freeze
14
+
15
+ def compatible?(v1, v2)
16
+ [v1, v2].all?(Date) || [v1, v2].all?(Time)
17
+ end
18
+
19
+ def compare(v1, v2)
20
+ compatible?(v1, v2) ? v1 <=> v2 : v1.to_time <=> v2.to_time
21
+ end
22
+
23
+ def convert(v, klass)
24
+ return v if v.class == klass
25
+
26
+ v.public_send("to_#{klass.name.downcase}")
27
+ end
28
+
29
+ def merge_time(value, **attrs)
30
+ _merge(value, **attrs)
31
+ .tap { |h|
32
+ h[:sec] += h.delete(:subsec)
33
+ h[:utc_offset] = value.zone if value.zone.respond_to?(:utc_to_local) # Ruby 2.6 real timezones
34
+ }
35
+ .values.then { |components| Time.new(*components) }
36
+ end
37
+
38
+ def merge_date(value, **attrs)
39
+ _merge(value, **attrs).values.then { |components| Date.new(*components) }
40
+ end
41
+
42
+ def merge_datetime(value, **attrs)
43
+ # When we truncate, we use :subsec key as a sign to zeroefy second fractions
44
+ attrs[:sec_fraction] ||= attrs.delete(:subsec) if attrs.key?(:subsec)
45
+
46
+ _merge(value, **attrs)
47
+ .tap { |h| h[:sec] += h.delete(:sec_fraction) }
48
+ .values.then { |components| DateTime.new(*components) }
49
+ end
50
+
51
+ private
52
+
53
+ def _merge(value, attrs)
54
+ attr_names = ATTRS.fetch(value.class)
55
+ attr_names.to_h { |u| [u, value.public_send(u)] }.merge(**attrs.slice(*attr_names))
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TimeCalc
4
+ # @private
5
+ # Unit-related constants and utilities for fetching their values
6
+ module Units
7
+ ALL = %i[year month week day hour min sec].freeze
8
+ NATURAL = %i[year month day hour min sec].freeze
9
+ STRUCTURAL = %i[year month day hour min sec subsec].freeze
10
+
11
+ SYNONYMS = {
12
+ second: :sec,
13
+ seconds: :sec,
14
+ minute: :min,
15
+ minutes: :min,
16
+ hours: :hour,
17
+ days: :day,
18
+ weeks: :week,
19
+ months: :month,
20
+ years: :year
21
+ }.freeze
22
+
23
+ DEFAULTS = {
24
+ month: 1,
25
+ day: 1,
26
+ hour: 0,
27
+ min: 0,
28
+ sec: 0,
29
+ subsec: 0
30
+ }.freeze
31
+
32
+ MULTIPLIERS = {
33
+ sec: 1,
34
+ min: 60,
35
+ hour: 60 * 60,
36
+ day: 24 * 60 * 60
37
+ }.freeze
38
+
39
+ def self.call(unit)
40
+ SYNONYMS.fetch(unit, unit)
41
+ .tap { |u| ALL.include?(u) or fail ArgumentError, "Unsupported unit: #{u}" }
42
+ end
43
+
44
+ def self.multiplier_for(klass, unit, precise: false)
45
+ res = MULTIPLIERS.fetch(unit)
46
+ d = MULTIPLIERS.fetch(:day)
47
+ case klass.name
48
+ when 'Time'
49
+ res
50
+ when 'DateTime'
51
+ res / d.to_f
52
+ when 'Date'
53
+ precise ? res / d.to_f : res / d
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'backports/2.6.0/enumerable/to_h'
4
+ require 'backports/2.6.0/array/to_h'
5
+ require 'backports/2.6.0/hash/to_h'
6
+ require 'backports/2.6.0/kernel/then'
7
+ require 'backports/2.5.0/hash/slice'
8
+ require 'backports/2.5.0/enumerable/all'
9
+
10
+ class TimeCalc
11
+ # Wrapper (one can say "monad") around date/time value, allowing to perform several TimeCalc
12
+ # operations in a chain.
13
+ #
14
+ # @example
15
+ # TimeCalc.wrap(Time.parse('2019-06-01 14:50')).+(1, :year).-(1, :month).round(:week).unwrap
16
+ # # => 2020-05-04 00:00:00 +0300
17
+ #
18
+ class Value
19
+ # @private
20
+ TIMEY = proc { |t| t.respond_to?(:to_time) }
21
+
22
+ # @private
23
+ def self.wrap(value)
24
+ case value
25
+ when Time, Date, DateTime
26
+ new(value)
27
+ when Value
28
+ value
29
+ when TIMEY
30
+ wrap(value.to_time)
31
+ else
32
+ fail ArgumentError, "Unsupported value: #{value}"
33
+ end
34
+ end
35
+
36
+ # @private
37
+ attr_reader :internal
38
+
39
+ # @note
40
+ # Prefer {TimeCalc.wrap} to create a Value.
41
+ # @param time_or_date [Time, Date, DateTime]
42
+ def initialize(time_or_date)
43
+ @internal = time_or_date
44
+ end
45
+
46
+ # @return [Time, Date, DateTime] The value of the original type that was wrapped and processed
47
+ def unwrap
48
+ @internal
49
+ end
50
+
51
+ # @private
52
+ def inspect
53
+ '#<%s(%s)>' % [self.class, internal]
54
+ end
55
+
56
+ # @return [1, 0, -1]
57
+ def <=>(other)
58
+ return unless other.is_a?(self.class)
59
+
60
+ Types.compare(internal, other.internal)
61
+ end
62
+
63
+ include Comparable
64
+
65
+ Units::ALL.each { |u| define_method(u) { internal.public_send(u) } }
66
+
67
+ def dst?
68
+ return unless internal.respond_to?(:dst?)
69
+
70
+ internal.dst?
71
+ end
72
+
73
+ # Produces new value with some components of underlying time/date replaced.
74
+ #
75
+ # @example
76
+ # TimeCalc.from(Date.parse('2018-06-01')).merge(year: 1983)
77
+ # # => #<TimeCalc::Value(1983-06-01)>
78
+ #
79
+ # @param attrs [Hash<Symbol => Integer>]
80
+ # @return [Value]
81
+ def merge(**attrs)
82
+ Value.new(Types.public_send("merge_#{internal.class.name.downcase}", internal, **attrs))
83
+ end
84
+
85
+ # Truncates all time components lower than `unit`. In other words, "floors" (rounds down)
86
+ # underlying date/time to nearest `unit`.
87
+ #
88
+ # @example
89
+ # TimeCalc.from(Time.parse('2018-06-23 12:30')).floor(:month)
90
+ # # => #<TimeCalc::Value(2018-06-01 00:00:00 +0300)>
91
+ #
92
+ # @param unit [Symbol]
93
+ # @return Value
94
+ def truncate(unit)
95
+ unit = Units.(unit)
96
+ return floor_week if unit == :week
97
+
98
+ Units::STRUCTURAL
99
+ .drop_while { |u| u != unit }
100
+ .drop(1)
101
+ .then { |keys| Units::DEFAULTS.slice(*keys) }
102
+ .then(&method(:merge))
103
+ end
104
+
105
+ alias floor truncate
106
+
107
+ # Ceils (rounds up) underlying date/time to nearest `unit`.
108
+ #
109
+ # @example
110
+ # TimeCalc.from(Time.parse('2018-06-23 12:30')).ceil(:month)
111
+ # # => #<TimeCalc::Value(2018-07-01 00:00:00 +0300)>
112
+ #
113
+ # @param unit [Symbol]
114
+ # @return [Value]
115
+ def ceil(unit)
116
+ floor(unit).then { |res| res == self ? res : res.+(1, unit) }
117
+ end
118
+
119
+ # Rounds up or down underlying date/time to nearest `unit`.
120
+ #
121
+ # @example
122
+ # TimeCalc.from(Time.parse('2018-06-23 12:30')).round(:month)
123
+ # # => #<TimeCalc::Value(2018-07-01 00:00:00 +0300)>
124
+ #
125
+ # @param unit [Symbol]
126
+ # @return Value
127
+ def round(unit)
128
+ f, c = floor(unit), ceil(unit)
129
+
130
+ (internal - f.internal).abs < (internal - c.internal).abs ? f : c
131
+ end
132
+
133
+ # Add `<span units>` to wrapped value.
134
+ #
135
+ # @param span [Integer]
136
+ # @param unit [Symbol]
137
+ # @return [Value]
138
+ def +(span, unit)
139
+ unit = Units.(unit)
140
+ case unit
141
+ when :sec, :min, :hour, :day
142
+ plus_seconds(span, unit)
143
+ when :week
144
+ self.+(span * 7, :day)
145
+ when :month
146
+ plus_months(span)
147
+ when :year
148
+ merge(year: year + span)
149
+ end
150
+ end
151
+
152
+ # @overload -(span, unit)
153
+ # Subtracts `span units` from wrapped value.
154
+ # @param span [Integer]
155
+ # @param unit [Symbol]
156
+ # @return [Value]
157
+ # @overload -(date_or_time)
158
+ # Produces {Diff}, allowing to calculate structured difference between two points in time.
159
+ # @param date_or_time [Date, Time, DateTime]
160
+ # @return [Diff]
161
+ # Subtracts `span units` from wrapped value.
162
+ def -(span_or_other, unit = nil)
163
+ unit.nil? ? Diff.new(self, span_or_other) : self.+(-span_or_other, unit)
164
+ end
165
+
166
+ # Produces {Sequence} from this value to `date_or_time`
167
+ #
168
+ # @param date_or_time [Date, Time, DateTime]
169
+ # @return [Sequence]
170
+ def to(date_or_time)
171
+ Sequence.new(from: self).to(date_or_time)
172
+ end
173
+
174
+ # Produces endless {Sequence} from this value, with step specified.
175
+ #
176
+ # @overload step(unit)
177
+ # Shortcut for `step(1, unit)`
178
+ # @param unit [Symbol]
179
+ # @overload step(span, unit)
180
+ # @param span [Integer]
181
+ # @param unit [Symbol]
182
+ # @return [Sequence]
183
+ def step(span, unit = nil)
184
+ span, unit = 1, span if unit.nil?
185
+ Sequence.new(from: self).step(span, unit)
186
+ end
187
+
188
+ # Produces {Sequence} from this value to `this + <span units>`
189
+ #
190
+ # @param span [Integer]
191
+ # @param unit [Symbol]
192
+ # @return [Sequence]
193
+ def for(span, unit)
194
+ to(self.+(span, unit))
195
+ end
196
+
197
+ # @private
198
+ def convert(klass)
199
+ return dup if internal.class == klass
200
+
201
+ Value.new(Types.convert(internal, klass))
202
+ end
203
+
204
+ private
205
+
206
+ def floor_week
207
+ extra_days = (internal.wday.nonzero? || 7) - 1
208
+ floor(:day).-(extra_days, :days)
209
+ end
210
+
211
+ def plus_months(span)
212
+ target = month + span.to_i
213
+ m = (target - 1) % 12 + 1
214
+ dy = (target - 1) / 12
215
+ merge(year: year + dy, month: m)
216
+ end
217
+
218
+ def plus_seconds(span, unit)
219
+ Value.new(internal + span * Units.multiplier_for(internal.class, unit))
220
+ .then { |res| unit == :day ? DST.fix_value(res, self) : res }
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TimeCalc
4
+ # @private
5
+ VERSION = '0.0.1'
6
+ end
metadata ADDED
@@ -0,0 +1,211 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: time_calc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Victor Shepelev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-07-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: backports
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 3.15.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 3.15.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.72.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.72.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop-rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 1.17.1
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 1.17.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '3.8'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '3.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec-its
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1'
83
+ - !ruby/object:Gem::Dependency
84
+ name: saharspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.9'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.9'
111
+ - !ruby/object:Gem::Dependency
112
+ name: tzinfo
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rubygems-tasks
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: yard
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ description: |2
168
+ TimeCalc is a library for idiomatic time calculations, like "plus N days", "floor to month start",
169
+ "how many hours between those dates", "sequence of months from this to that". It intends to
170
+ be small and easy to remember without any patching of core classes.
171
+ email: zverok.offline@gmail.com
172
+ executables: []
173
+ extensions: []
174
+ extra_rdoc_files: []
175
+ files:
176
+ - LICENSE.txt
177
+ - README.md
178
+ - lib/time_calc.rb
179
+ - lib/time_calc/diff.rb
180
+ - lib/time_calc/dst.rb
181
+ - lib/time_calc/op.rb
182
+ - lib/time_calc/sequence.rb
183
+ - lib/time_calc/types.rb
184
+ - lib/time_calc/units.rb
185
+ - lib/time_calc/value.rb
186
+ - lib/time_calc/version.rb
187
+ homepage: https://github.com/zverok/time_calc
188
+ licenses:
189
+ - MIT
190
+ metadata: {}
191
+ post_install_message:
192
+ rdoc_options: []
193
+ require_paths:
194
+ - lib
195
+ required_ruby_version: !ruby/object:Gem::Requirement
196
+ requirements:
197
+ - - ">="
198
+ - !ruby/object:Gem::Version
199
+ version: 2.3.0
200
+ required_rubygems_version: !ruby/object:Gem::Requirement
201
+ requirements:
202
+ - - ">="
203
+ - !ruby/object:Gem::Version
204
+ version: '0'
205
+ requirements: []
206
+ rubyforge_project:
207
+ rubygems_version: 2.6.14
208
+ signing_key:
209
+ specification_version: 4
210
+ summary: Easy time math
211
+ test_files: []