timerage 2.2.0 → 2.4.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: '07846226e71bd754087bd3b54c49d85a70e9d2f9b809d678351fbd685546fcb4'
4
- data.tar.gz: e3be843301a7d7a7ea7cec902c2f4fd8152314a5cbbf4e74d8aad65ede5196ec
3
+ metadata.gz: dd5297580375d5da0f4f8e0c5565cbe0b5e87bb27e01a3f6ab7b53e322f0836b
4
+ data.tar.gz: 1095aa363d5652855d905dfeeecc74291cdb4399c5769ac115c2bed2ad889043
5
5
  SHA512:
6
- metadata.gz: 76acc33ddaa358897021f20b82c4f3faffb1867469e410da6566480880c78c9524a84e39edb6c3468bfd5d49cba4d72dfe526dbd749ad6ecff66be839fcb6bc0
7
- data.tar.gz: 96e281a1b857a002653629b4289574f4488068609f40f74b85832966a402311c88ea16f9db91080a74566ac502a7bd1468d496bb73b5095dcfbe572d09f2ec06
6
+ metadata.gz: c8114be85281560a2d7c08fad17ab9db72809b3a992cf2fff1433e6e4fe3e04673e9fa53bfa93070c4c1b3643bad913c550ca68d11f1ff2f04c2c4b04a9f8188
7
+ data.tar.gz: 701cd90ba28da27218c23f0346bc15d33c63646bf0a96d8f79ff39dc6a8fe55974fc848b5bca7266bfdf4fe6709fc70448242930e6ffd284f9519b04e40da4bf
@@ -5,11 +5,34 @@ module Timerage
5
5
  class TimeInterval < Range
6
6
 
7
7
  class << self
8
+ # Returns a new TimeInterval
9
+ #
10
+ # start_time - the beginning of the interval
11
+ # end_time - the end of the interval
12
+ # exclude_end - whether the end time is excluded from the interval
8
13
  def new(*args)
9
- args = [args.first.begin, args.first.end, args.first.exclude_end?] if args.first.respond_to?(:exclude_end?)
10
- new_obj = allocate
11
- new_obj.send(:initialize, *args)
12
- new_obj
14
+ return from_range(*args) if args.first.respond_to?(:exclude_end?)
15
+
16
+ super
17
+ end
18
+
19
+ # Returns a new TimeInterval
20
+ #
21
+ # time - the beginning or end of the interval
22
+ # duration - the duration of the interval, if negative the
23
+ # interval will start before`time`
24
+ # exclude_end - whether the end time is excluded from the interval
25
+ def from_time_and_duration(time, duration, exclude_end: true)
26
+ if duration >= 0
27
+ new(time, time + duration, exclude_end)
28
+ else
29
+ new(time + duration, time, exclude_end)
30
+ end
31
+ end
32
+
33
+ # Returns a new TimeInterval based on the specified range
34
+ def from_range(range)
35
+ new(range.begin, range.end, range.exclude_end?)
13
36
  end
14
37
  end
15
38
 
@@ -32,12 +55,12 @@ module Timerage
32
55
  end
33
56
  end
34
57
 
35
- def slice(seconds)
36
- time_enumerator(seconds)
37
- .map{|t|
38
- end_time = [t+seconds, self.end].min
39
- inclusive = (t == end_time || t+seconds > self.end) && !exclude_end?
40
- TimeInterval.new(t, end_time, !inclusive) }
58
+ def slice(duration)
59
+ time_enumerator(duration)
60
+ .each_cons(2).map { |s_begin, s_end| TimeInterval.new(s_begin, s_end, exclusive_end_slice?(s_end)) }
61
+ .then do |slices|
62
+ slices << TimeInterval.new(slices.last.end, self.end, exclusive_end_slice?(slices.last.end + duration)) if slices.present?
63
+ end
41
64
  end
42
65
 
43
66
  # Return new TimeInterval that is the concatenation of self and
@@ -54,7 +77,7 @@ module Timerage
54
77
  # Returns an ISO8601 interval representation of self
55
78
  # Takes same args as Time#iso8601
56
79
  def iso8601(*args)
57
- "#{self.begin.iso8601(*args)}/#{self.end.iso8601(*args)}"
80
+ "#{self.begin&.iso8601(*args) || ".."}/#{self.end&.iso8601(*args) || ".."}"
58
81
  end
59
82
 
60
83
  def getutc
@@ -134,6 +157,10 @@ module Timerage
134
157
  an_obj.respond_to?(:end)
135
158
  end
136
159
 
160
+ def exclusive_end_slice?(slice_end)
161
+ !((slice_end > self.end) && !exclude_end?)
162
+ end
163
+
137
164
  def time_enumerator(step)
138
165
  next_offset = step * 0
139
166
 
@@ -155,7 +182,17 @@ module Timerage
155
182
  #
156
183
  # Currently this only supports `<begin>/<end>` style time intervals.
157
184
  def self.iso8601(str, exclusive_end: true)
158
- new *str.split("/", 2).map{|s| Time.iso8601(s)}, exclusive_end
185
+ str
186
+ .split("/", 2)
187
+ .tap { |it| fail ArgumentError, "Invalid iso8601 interval: #{str.inspect}" unless it.size == 2 }
188
+ .map {|s|
189
+ if s.strip == ".."
190
+ nil
191
+ else
192
+ Time.iso8601(s)
193
+ end
194
+ }
195
+ .then { |b, e| new(b, e, exclusive_end) }
159
196
 
160
197
  rescue ArgumentError
161
198
  raise ArgumentError, "Invalid iso8601 interval: #{str.inspect}"
@@ -1,3 +1,3 @@
1
1
  module Timerage
2
- VERSION = "2.2.0"
2
+ VERSION = "2.4.0"
3
3
  end
data/lib/timerage.rb CHANGED
@@ -16,17 +16,30 @@ module Timerage
16
16
  Time.iso8601(str)
17
17
  end
18
18
 
19
+ # Returns a new TimeInterval from a time and duration or two times.
20
+ #
21
+ # signature:
22
+ # Interval(begin_time, end_time, exclude_end: true)
23
+ # Interval(begin_or_end_time, duration, exclude_end: true)
24
+ def self.Interval(begin_or_end_time, end_time_or_duration, exclude_end: true)
25
+ if end_time_or_duration.respond_to?(:since)
26
+ TimeInterval.from_time_and_duration(begin_or_end_time, end_time_or_duration, exclude_end: exclude_end)
27
+ else
28
+ TimeInterval.new(begin_or_end_time, end_time_or_duration, exclude_end)
29
+ end
30
+ end
31
+
19
32
  refine Range do
20
33
  def step(n, &blk)
21
34
  if self.begin.kind_of?(Time) || self.begin.kind_of?(Date)
22
- Timerage::TimeInterval.new(self).step(n, &blk)
35
+ Timerage::TimeInterval.from_range(self).step(n, &blk)
23
36
  else
24
37
  super
25
38
  end
26
39
  end
27
40
 
28
41
  def to_time_interval
29
- Timerage::TimeInterval.new(self)
42
+ Timerage::TimeInterval.from_range(self)
30
43
  end
31
44
  end
32
45
  end
@@ -40,7 +53,7 @@ module Kernel
40
53
  thing
41
54
 
42
55
  when ->(x) { x.respond_to? :exclude_end? }
43
- Timerage::TimeInterval.new(thing)
56
+ Timerage::TimeInterval.from_range(thing)
44
57
 
45
58
  when ->(x) { x.respond_to? :to_str }
46
59
  Timerage.parse_iso8601(thing.to_str)
@@ -6,18 +6,64 @@ describe Timerage::TimeInterval do
6
6
  let(:duration) { 3600 }
7
7
 
8
8
  describe "creation" do
9
- specify { expect(described_class.new(now-1..now)).to be_kind_of described_class }
10
- specify { expect(described_class.new(now-1, now)).to be_kind_of described_class }
11
- specify { expect(described_class.new(now-1, now, true)).to be_kind_of described_class }
12
- specify { expect(described_class.new(now-1, now, false )).to be_kind_of described_class }
9
+ specify { expect(described_class.new(now-1.day, now))
10
+ .to be_kind_of(described_class).and eq now-1.day..now }
11
+ specify { expect(described_class.new(now-1.day, now, true))
12
+ .to be_kind_of(described_class).and eq now-1.day...now }
13
+ specify { expect(described_class.new(now-1.day, now, false ))
14
+ .to be_kind_of(described_class).and eq now-1.day..now }
15
+
16
+ it "support new(range) for backwards compatibility" do
17
+ expect(described_class.new((now-1.day)..now)).to eq (now-1.day)..now
18
+ end
13
19
  end
14
20
 
15
- describe ".iso8601" do
16
- specify { expect(described_class
17
- .iso8601("2001-01-01T00:00:00Z/2001-01-02T00:00:00-06:00"))
18
- .to be_kind_of described_class }
19
- specify { expect{described_class.iso8601("2001-01-01T00:00:00Z")}
20
- .to raise_error ArgumentError }
21
+ describe ".iso8601" do
22
+ specify do
23
+ expect(described_class.iso8601("2001-01-01T00:00:00Z/2001-01-02T00:00:00-06:00"))
24
+ .to be_kind_of described_class
25
+ end
26
+
27
+ specify do
28
+ expect{described_class.iso8601("2001-01-01T00:00:00Z")}
29
+ .to raise_error ArgumentError
30
+ end
31
+
32
+ specify do
33
+ expect(described_class.iso8601("2001-01-01T00:00:00Z/.."))
34
+ .to (be_kind_of described_class)
35
+ .and (have_attributes(begin: Time.parse("2001-01-01T00:00:00Z"), end: nil))
36
+ end
37
+
38
+ specify do
39
+ expect(described_class.iso8601("\t2001-01-01T00:00:00Z/..\t"))
40
+ .to (be_kind_of described_class)
41
+ .and (have_attributes(begin: Time.parse("2001-01-01T00:00:00Z"), end: nil))
42
+ end
43
+
44
+ specify do
45
+ expect(described_class.iso8601(" 2001-01-01T00:00:00Z/.. "))
46
+ .to (be_kind_of described_class)
47
+ .and (have_attributes(begin: Time.parse("2001-01-01T00:00:00Z"), end: nil))
48
+ end
49
+
50
+ specify do
51
+ expect(described_class.iso8601("../2001-01-01T00:00:00Z"))
52
+ .to (be_kind_of described_class)
53
+ .and (have_attributes(begin: nil, end: Time.parse("2001-01-01T00:00:00Z")))
54
+ end
55
+
56
+ specify do
57
+ expect(described_class.iso8601("\t../2001-01-01T00:00:00Z\t"))
58
+ .to (be_kind_of described_class)
59
+ .and (have_attributes(begin: nil, end: Time.parse("2001-01-01T00:00:00Z")))
60
+ end
61
+
62
+ specify do
63
+ expect(described_class.iso8601(" ../2001-01-01T00:00:00Z "))
64
+ .to (be_kind_of described_class)
65
+ .and (have_attributes(begin: nil, end: Time.parse("2001-01-01T00:00:00Z")))
66
+ end
21
67
  end
22
68
 
23
69
  describe "#getutc" do
@@ -60,6 +106,18 @@ describe Timerage::TimeInterval do
60
106
  .to eq "#{interval.begin.iso8601(3)}/#{interval.end.iso8601(3)}" }
61
107
  specify { expect( interval.duration).to eq duration }
62
108
 
109
+ context "open-ended intervals" do
110
+ subject(:interval) { described_class.new(now-duration, nil) }
111
+ specify { expect(interval.iso8601).to eq "#{interval.begin.iso8601}/.." }
112
+ specify { expect(interval.iso8601(3)).to eq "#{interval.begin.iso8601(3)}/.." }
113
+ end
114
+
115
+ context "open-start intervals" do
116
+ subject(:interval) { described_class.new(nil, now) }
117
+ specify { expect(interval.iso8601).to eq "../#{interval.end.iso8601}" }
118
+ specify { expect(interval.iso8601(3)).to eq "../#{interval.end.iso8601(3)}" }
119
+ end
120
+
63
121
  specify { expect(interval.cover? interval.begin+1..interval.end-1).to be_truthy }
64
122
  specify { expect(interval.cover? interval.begin...interval.end).to be_truthy }
65
123
  specify { expect(interval.cover? interval.begin-1..interval.end-1).to be_falsy }
@@ -86,12 +144,12 @@ describe Timerage::TimeInterval do
86
144
  specify { expect( interval & (interval.begin...interval.end) ).to eq interval.begin...interval.end }
87
145
  specify { expect( interval & (interval.begin...interval.end-1) )
88
146
  .to eq interval.begin...interval.end-1 }
89
- specify { expect( described_class.new(interval.begin...interval.end-1) & interval )
90
- .to eq described_class.new(interval.begin...interval.end-1) }
147
+ specify { expect( described_class.new(interval.begin, interval.end-1) & interval )
148
+ .to eq described_class.new(interval.begin,interval.end-1) }
91
149
  end
92
150
 
93
151
  describe "==" do
94
- specify { expect( described_class.new(interval) == interval ).to be true }
152
+ specify { expect( described_class.from_range(interval) == interval ).to be true }
95
153
  specify { expect( described_class.new(interval.begin, interval.end) == interval ).to be true }
96
154
  end
97
155
 
@@ -159,7 +217,7 @@ describe Timerage::TimeInterval do
159
217
  end
160
218
 
161
219
  context "exclusive end" do
162
- subject(:interval) { described_class.new(now-duration...now) }
220
+ subject(:interval) { described_class.from_range(now-duration...now) }
163
221
 
164
222
  specify { expect(interval.exclude_end?).to be true }
165
223
  specify { expect(interval.cover? now).to be false }
@@ -187,29 +245,29 @@ describe Timerage::TimeInterval do
187
245
  end
188
246
 
189
247
  context "exclusive 0 length interval" do
190
- subject(:interval) { described_class.new(now...now) }
248
+ subject(:interval) { described_class.from_range(now...now) }
191
249
  specify { expect{ |b| subject.step(1, &b) }.not_to yield_control }
192
250
  end
193
251
 
194
252
  context "inclusive 0 length interval" do
195
- subject(:interval) { described_class.new(now..now) }
253
+ subject(:interval) { described_class.from_range(now..now) }
196
254
  specify { expect{ |b| subject.step(1, &b) }.to yield_control.once }
197
255
  end
198
256
 
199
257
  context "includes leap day" do
200
- subject(:interval) { described_class.new(before_leap_day..after_leap_day) }
258
+ subject(:interval) { described_class.new(before_leap_day, after_leap_day) }
201
259
  specify { expect{ |b| subject.step(1.day, &b) }.to yield_control.exactly(3).times }
202
260
  specify { expect{ |b| subject.step(86_400, &b) }.to yield_control.exactly(3).times }
203
261
  end
204
262
 
205
263
  context "transition into dst with explicit time zone" do
206
- subject(:interval) { described_class.new(before_dst..after_dst) }
264
+ subject(:interval) { described_class.new(before_dst, after_dst) }
207
265
  specify { expect{ |b| subject.step(1.hour, &b) }.to yield_control.exactly(2).times }
208
266
  specify { expect{ |b| subject.step(3_600, &b) }.to yield_control.exactly(2).times }
209
267
  end
210
268
 
211
269
  context "transition into dst without explicit time zone" do
212
- subject(:interval) { described_class.new(before_dst..(before_dst + 1.hour)) }
270
+ subject(:interval) { described_class.new(before_dst, (before_dst + 1.hour)) }
213
271
  specify { expect{ |b| subject.step(1.hour, &b) }.to yield_control.exactly(2).times }
214
272
  specify { expect{ |b| subject.step(3_600, &b) }.to yield_control.exactly(2).times }
215
273
  end
@@ -230,6 +288,14 @@ describe Timerage::TimeInterval do
230
288
  end
231
289
  end
232
290
 
291
+ context "produces complete slices" do
292
+ it "slices a range into intervals without missing any time inbetween" do
293
+ range = Timerage(Time.parse("2023-10-31T00:00:00+00:00")...Time.parse("2024-06-30 00:00:00 UTC"))
294
+ slices = range.slice(1.month)
295
+ expect(slices.map(&:duration).sum).to eq range.duration
296
+ end
297
+ end
298
+
233
299
  let(:leap_day) { Time.parse("2016-02-29 12:00:00 UTC") }
234
300
  let(:before_leap_day) { leap_day - 1.day }
235
301
  let(:after_leap_day) { leap_day + 1.day}
@@ -17,6 +17,22 @@ describe Timerage do
17
17
  .to be_kind_of Time }
18
18
  end
19
19
 
20
+ describe ".Interval" do
21
+ let(:t) { Time.now }
22
+ let(:one_day) { 1.day }
23
+
24
+ specify { expect(described_class.Interval(t, 1.day)).to eq t...(t+1.day) }
25
+ specify { expect(described_class.Interval(t, 1.day, exclude_end: false)).to eq(t..(t+1.day)) }
26
+
27
+ specify { expect(described_class.Interval(t, -1.day)).to eq((t-1.day)...t) }
28
+ specify { expect(described_class.Interval(t, -1.day, exclude_end: false)).to eq((t+-1.day)..t) }
29
+
30
+ specify { expect{ described_class.Interval(t, (t+1.day)).to eq(t...(t+1.day)) } }
31
+ specify { expect{ described_class.Interval(t, (t+1.day), exclude_end: false).to eq(t..(t+1.day)) } }
32
+
33
+ specify { expect{ described_class.Interval(t..(t+1.day)).to eq(t..(t+1.day)) } }
34
+ end
35
+
20
36
  context "interval include end" do
21
37
  specify { expect{|b| range.step(1200, &b) }
22
38
  .to yield_successive_args now-duration, now-(duration-1200), now-(duration-2400), now }
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timerage
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Williams
8
8
  - Chris Schneider
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2023-11-10 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: bundler
@@ -96,7 +95,6 @@ homepage: ''
96
95
  licenses:
97
96
  - MIT
98
97
  metadata: {}
99
- post_install_message:
100
98
  rdoc_options: []
101
99
  require_paths:
102
100
  - lib
@@ -111,8 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
109
  - !ruby/object:Gem::Version
112
110
  version: '0'
113
111
  requirements: []
114
- rubygems_version: 3.2.33
115
- signing_key:
112
+ rubygems_version: 3.7.2
116
113
  specification_version: 4
117
114
  summary: Simple refinement to Range to allow Time or Date as arguments
118
115
  test_files: