timesteps 0.9.5 → 1.0.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.
@@ -1,8 +1,104 @@
1
1
 
2
+ # TimeStep class
3
+ #
4
+ #
2
5
  class TimeStep
3
6
 
4
7
  using DateTimeExt
5
8
 
9
+ # @private
10
+ CALENDARS = {}
11
+
12
+ [
13
+ "standard",
14
+ "gregorian",
15
+ "proleptic_gregorian",
16
+ "proleptic_julian",
17
+ "julian",
18
+ "noleap",
19
+ "365_day",
20
+ "allleap",
21
+ "366_day",
22
+ "360_day",
23
+ ].each do |calendar|
24
+ CALENDARS[calendar] = Calendar.new(calendar)
25
+ end
26
+
27
+ # @private
28
+ PATTERN_NUMERIC = '[\+\-]?\d*(?:\.\d+)?(?:[eE][\+\-]?\d+)?'
29
+
30
+ # @private
31
+ PATTERN_UNITS = [
32
+ 'years?',
33
+ 'months?',
34
+ 'days?',
35
+ 'hours?',
36
+ 'minutes?',
37
+ 'seconds?',
38
+ 'd',
39
+ 'hrs?',
40
+ 'h',
41
+ 'mins?',
42
+ 'secs?',
43
+ 's',
44
+ 'milliseconds?',
45
+ 'msecs?',
46
+ 'ms',
47
+ 'microseconds?',
48
+ '(?:weeks?|w)(?:\-(?:sun|mon|tue|wed|thu|fri|sat))?',
49
+ 'sundays?',
50
+ 'mondays?',
51
+ 'tuesdays?',
52
+ 'wednesdays?',
53
+ 'thursdays?',
54
+ 'fridays?',
55
+ 'saturdays?',
56
+ 'ayears?',
57
+ ].join("|")
58
+
59
+ # @private
60
+ WDAY = {
61
+ sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6,
62
+ }
63
+
64
+ # @private
65
+ WDAY_NAME = {
66
+ sun: "sundays",
67
+ mon: "mondays",
68
+ tue: "tuesdays",
69
+ wed: "wednesdays",
70
+ thu: "thursdays",
71
+ fri: "fridays",
72
+ sat: "saturdays",
73
+ }
74
+
75
+ # Extracts numeric part and symbol part from the given interval specification.
76
+ #
77
+ # @example
78
+ # TimeStep.split_interval_spec("12 months")
79
+ # # => [12, "months"]
80
+ #
81
+ # TimeStep.split_interval_spec("month-end")
82
+ # # => [1, "month-end"]
83
+ #
84
+ # @param [String] interval specification (ex. "12 months", "3 hours", "year")
85
+ # @return [Array(Numeric, String)] A pair of `numeric` and `symbol`
86
+ #
87
+ def self.split_interval_spec (spec)
88
+ if spec.strip =~ /\A(#{PATTERN_NUMERIC}|)\s*((?:#{PATTERN_UNITS}).*)\z/i
89
+ numeric = if $1 == ""
90
+ 1
91
+ else
92
+ Float($1)
93
+ end
94
+ numeric = numeric.to_i if numeric.denominator == 1
95
+ symbol = $2
96
+ else
97
+ raise "the interval specification '#{spec}' is invalid."
98
+ end
99
+ return numeric, symbol
100
+ end
101
+
6
102
  # Constructs the object.
7
103
  #
8
104
  # The argument `spec` specifies the time step definition, which has the form,
@@ -12,6 +108,7 @@ class TimeStep
12
108
  # * "hour since 2001-01-01 00:00:00 JST"
13
109
  # * "3 days since 2001-01-01 00:00:00 +00:00"
14
110
  # * "10 years since 1901-01-01 00:00:00 +00:00"
111
+ #
15
112
  # The symbol for time unit symbols should be one of
16
113
  # * ayears, ayear (astronomical year: 365.242198781 day)
17
114
  # * years, year
@@ -21,236 +118,268 @@ class TimeStep
21
118
  # * minutes, minute, mins, min
22
119
  # * seconds, second, secs, sec, s
23
120
  # * milliseconds, millisecond, msecs, msec, ms
24
- # * microseconds, microsecond, msecs, msec, ms
121
+ # * microseconds, microsecond
122
+ #
25
123
  # If you have already origin time object or general date string,
26
124
  # you can use `since` option,
27
125
  # TimeStep.new("3 hours", since: time)
28
126
  # TimeStep.new("3 hours", since: "2001010121", format: '%Y%m%d%H')
29
- # The option `calendar` specifies the calendar for datetime calculation,
30
- # * standard, gregorian -> DateTime with Date::ITALY as start
31
- # * proleptic_gregorian -> DateTime with Date::GREGORIAN as start
32
- # * proleptic_julian, julian -> DateTime with Date::JULIAN as start
33
- # * noleap, 365_day -> DateTimeNoLeap
34
- # * allleap, 366_day -> DateTimeAllLeap
35
- # * 360_day -> DateTimeFixed360Day
36
- # The option `bc` is a flag whether AD/BC or EC when parsing timestamp for
37
- # negative year.
38
- # The option count specifies the number of time steps, which is hint for
39
- # some methods (#each etc).
40
- #
41
- # @param spec [String]
42
- # @option since [DateTime, DateTimeLike, String]
43
- # @option format [String]
44
- # @option calendar [String, TimeStep::Calendar]
45
- # @option bc [Boolean]
46
- # @option count [Integer] number of time steps (as hint for some methods)
127
+ # When origin time is specified in both 'spec' and 'since' option,
128
+ # the origin time in 'spec' has priority.
129
+ # If origin time is not specified in neither 'spec' and 'since' option,
130
+ # the default value is set to the origin time ("0000-01-01 00:00:00" for date and "1970-01-01 00:00:00" for time).
131
+ # The time offset from UTC can be set by 'offset' option.
132
+ # The option `calendar` specifies the name of calendar for datetime calculation,
133
+ # * "standard", "gregorian" -> DateTime with Date::ITALY as start
134
+ # * "proleptic_gregorian" -> DateTime with Date::GREGORIAN as start
135
+ # * "proleptic_julian", "julian" -> DateTime with Date::JULIAN as start
136
+ # * "noleap", "365_day" -> DateTimeNoLeap
137
+ # * "allleap", "366_day" -> DateTimeAllLeap
138
+ # * "360_day" -> DateTimeFixed360Day
139
+ #
140
+ # @param spec [String] timestep specification
141
+ # @param since [DateTime, String]
142
+ # @param offset [Numeric, String] offset in origin time
143
+ # @param format [String] template string for strptime for parsing time
144
+ # @param calendar [String, TimeStep::Calendar]
47
145
  #
48
- def initialize (spec, since: nil, format: nil, count: nil, calendar: "standard", bc: false)
146
+ def initialize (spec, since: nil, offset: nil, format: nil, calendar: "standard", tz: nil)
147
+
49
148
  case calendar
50
149
  when String
51
- @calendar = Calendar.new(calendar, bc: bc)
52
- else
53
- @calendar = calendar
54
- end
55
- if since
56
- parse_interval(spec)
57
- case since
58
- when String
59
- @origin = @calendar.parse(since, format: format)
150
+ if tz
151
+ raise "tz option can be used only with 'standard' calendar type" if calendar != "standard"
152
+ @calendar = Calendar.new(calendar, tz: tz)
60
153
  else
61
- raise "mismatch time object with calendar" unless @calendar.valid_datetime_type?(since)
62
- @origin = since
154
+ @calendar = CALENDARS[calendar]
155
+ raise "specified calendar type '#{calendar}' is invalid" unless @calendar
63
156
  end
157
+ when TimeStep::Calendar
158
+ @calendar = calendar
159
+ else
160
+ raise "invalid object for option 'calendar'"
161
+ end
162
+
163
+ if spec =~ /\s+since\s+/
164
+ interval_spec, time_spec = $~.pre_match, $~.post_match
165
+ parse_interval(interval_spec)
166
+ @origin = @calendar.parse(time_spec, offset: offset)
64
167
  else
65
- intervalspec, originspec = spec.split(/\s+since\s+/)
66
- parse_interval(intervalspec)
67
- @origin = @calendar.parse(originspec)
168
+ parse_interval(spec)
169
+ @origin = case since
170
+ when nil
171
+ case @symbol
172
+ when :hours, :minutes, :seconds
173
+ @calendar.parse("1970-1-1", offset: offset)
174
+ else
175
+ @calendar.parse("0000-1-1", offset: offset)
176
+ end
177
+ when String
178
+ @calendar.parse(since, format: format, offset: offset)
179
+ when Time
180
+ since.to_datetime
181
+ else
182
+ raise "datetime mismatched with calendar type" unless @calendar.valid_datetime_type?(since)
183
+ since
184
+ end
185
+ end
186
+
187
+ if @wday
188
+ origin = @origin - @origin.wday + WDAY[@wday]
189
+ origin -= 7 unless @origin >= origin
190
+ @origin = origin
68
191
  end
69
- @intervalspec = format("%g %s", @numeric, @symbol)
70
- @originspec = @origin.strftime("%Y-%m-%d %H:%M:%S.%N %:z")
71
- @definition = format("%s since %s", @intervalspec, @originspec)
72
- @count = count
192
+
73
193
  end
74
194
 
75
195
  # @private
76
- PATTERN_NUMERIC = '[\+\-]?\d*(?:\.\d+)?(?:[eE][\+\-]?\d+)?'
77
- # @private
78
- PATTERN_UNITS = 'ayears?|years?|months?|days?|hours?|minutes?|seconds?|d|hrs?|hrs?|h|mins?|secs?|s|milliseconds?|msecs?|ms|microseconds?'
79
-
80
196
  def parse_interval (spec)
81
- if spec.strip =~ /(#{PATTERN_NUMERIC}|)\s*(#{PATTERN_UNITS})/
82
- if $1 == ""
83
- @numeric = 1.to_r
84
- else
85
- @numeric = Float($1).to_r
86
- end
197
+ if spec.strip =~ /\A(#{PATTERN_NUMERIC}|)\s*(#{PATTERN_UNITS})\z/i
198
+ @numeric = if $1 == ""
199
+ 1.to_r
200
+ else
201
+ Float($1).to_r
202
+ end
87
203
  symbol = $2
88
204
  else
89
- raise "invalid interval specification"
205
+ raise "the interval specification '#{spec}' is invalid."
90
206
  end
91
207
  @interval = @numeric
92
208
  case symbol
93
- when /\Aayears?/
94
- @symbol = :days
95
- @numeric *= 365.242198781.to_r
96
- @interval = @numeric * 86400
97
- when /\Ayears?/
209
+ when /\Ayears?\z/i
98
210
  unless numeric.denominator == 1
99
211
  raise "numeric factor for year should be an integer"
100
212
  end
101
213
  @symbol = :years
102
214
  @interval *= 356.242*86400
103
- when /\Amonths?/
215
+ when /\Amonths?\z/i
104
216
  unless numeric.denominator == 1
105
217
  raise "numeric factor for month should be an integer"
106
218
  end
107
219
  @symbol = :months
108
220
  @interval *= 30.4368*86400
109
- when /\A(days?|d)/
221
+ when /\A(days?|d)\z/i
110
222
  @symbol = :days
111
223
  @interval *= 86400
112
- when /\A(hours?|hrs?|h)/
224
+ when /\A(hours?|hrs?|h)\z/i
113
225
  @symbol = :hours
114
226
  @interval *= 3600
115
- when /\A(minutes?|mins?)/
227
+ when /\A(minutes?|mins?)\z/i
116
228
  @symbol = :minutes
117
229
  @interval *= 60
118
- when /\A(seconds?|secs?|s)/
230
+ when /\A(seconds?|secs?|s)\z/i
119
231
  @symbol = :seconds
120
- when /\A(milliseconds?|msecs?|ms)/
232
+ when /\A(milliseconds?|msecs?|ms)\z/i
121
233
  @symbol = :seconds
122
234
  @interval *= 1.quo(1000)
123
- when /\Amicroseconds?/
235
+ when /\Amicroseconds?\z/i
124
236
  @symbol = :seconds
125
237
  @interval *= 1.quo(1000000)
238
+ when /\A(weeks?|w)\-(sun|mon|tue|wed|thu|fri|sat)\z/i
239
+ symbol =~ /\A(weeks?|w)\-(\w{3})/i
240
+ @wday = $2.downcase.intern
241
+ @symbol = WDAY_NAME[@wday].intern ### :sundays, :mondays, ...
242
+ @interval *= 7*86400
243
+ when /\A(weeks?|w)\z/i
244
+ @symbol = :weeks
245
+ @interval *= 7*86400
246
+ when /\Asundays?|mondays?|tuesdays?|wednesdays?|thursday?|fridays?|saturdays?\z/i
247
+ symbol =~ /\A(\w{3})/i
248
+ @wday = $1.downcase.intern
249
+ @symbol = WDAY_NAME[@wday].intern ### :sundays, :mondays, ...
250
+ @interval *= 7*86400
251
+ when /\Aayears?\z/i
252
+ @symbol = :days
253
+ @numeric *= 365.242198781.to_r
254
+ @interval = @numeric * 86400
255
+ end
256
+ if @numeric.denominator == 1
257
+ @numeric = @numeric.to_i
126
258
  end
127
259
  end
128
260
 
129
261
  private :parse_interval
130
262
 
131
- attr_reader :definition,
132
- :intervalspec,
133
- :originspec,
134
- :numeric,
263
+ attr_reader :numeric,
135
264
  :symbol,
136
265
  :interval,
137
266
  :origin,
138
267
  :calendar
139
268
 
140
- attr_accessor :count
141
-
142
- # Returns limit time if the object has `count`, otherwise returns nil.
143
- #
144
- # @return [DateTime, nil]
145
- def limit
146
- if @count
147
- return time_at(@count - 1)
269
+ # Returns a string expression for interval section in timestem spec.
270
+ #
271
+ # @return [String]
272
+ def interval_spec
273
+ if @wday
274
+ return format("%g %s", @numeric, WDAY_NAME[@wday])
148
275
  else
149
- return nil
276
+ return format("%g %s", @numeric, @symbol)
150
277
  end
151
278
  end
152
279
 
153
- # Sets count by giving maximum time.
154
- #
155
- # @return [DateTime, nil]
156
- def limit= (time)
157
- if time
158
- @count = (prev_index_of(time) + 1).to_i
280
+ # Returns a string expression for origin time section in timestep spec.
281
+ #
282
+ # @return [String]
283
+ def origin_spec
284
+ if @calendar.tz
285
+ return @origin.strftime("%Y-%m-%d %H:%M:%S.%N %:z %Z")
159
286
  else
160
- @count = nil
287
+ return @origin.strftime("%Y-%m-%d %H:%M:%S.%N %:z")
161
288
  end
162
- return limit
163
- end
164
-
165
- def set_limit (time, format: nil)
166
- case time
167
- when String
168
- time = @calendar.parse(time, format: format)
169
- end
170
- self.limit = time
171
289
  end
172
290
 
173
- def valid? (*indices)
174
- if @count
175
- if indices.size == 1
176
- index = indices.first
177
- if index >= 0 and index < @count
178
- return true
179
- else
180
- return false
181
- end
182
- else
183
- return indices.map{|index| valid?(index) }
184
- end
185
- else
186
- if indices.size == 1
187
- return true
188
- else
189
- return indices.map{|index| true }
190
- end
191
- end
291
+ # Returns a string expression of definition of timestep.
292
+ # The return value can be used for constructs other TimeStep object.
293
+ #
294
+ # @return [String]
295
+ def definition
296
+ format("%s since %s", interval_spec, origin_spec)
192
297
  end
193
298
 
194
- def debug_info
195
- return {
196
- "definition" => @definition.clone,
197
- "intervalspec" => @intervalspec.clone,
198
- "originspec" => @originspec.clone,
199
- "calendar" => @calendar.name,
200
- "numeric" => @numeric,
201
- "symbol" => @symbol.to_s,
202
- "interval" => @interval,
203
- "origin" => @origin.clone,
204
- "count" => @count,
205
- "bc" => @calendar.bc?,
206
- }
299
+ # Returns the time offset of origin time.
300
+ #
301
+ # @return [Rational]
302
+ def offset
303
+ return @origin.offset
207
304
  end
208
305
 
209
- private :debug_info
306
+ # Parses datetime string and return datetime object.
307
+ # In the parsing, the calendar of the object is used.
308
+ # If `format` option is given, `strptime` method is
309
+ # used for the parsing. Otherwise, the `parse` is used.
310
+ #
311
+ # @param time [String] string to be parsed
312
+ # @param format [String] template string for strptime for parsing time
313
+ #
314
+ # @return [DateTime]
315
+ def parse (time, format: nil)
316
+ return @calendar.parse(time, format: format, offset: @origin.offset)
317
+ end
210
318
 
211
- # Returns the value as a string for inspection.
319
+ # Returns a string for inspection.
212
320
  #
321
+ # @return [String]
213
322
  def inspect
214
- "#<TimeStep definition='#{definition}' calendar='#{calendar.name}'>"
323
+ options = ""
324
+ case @calendar.name
325
+ when "standard", "gregorian"
326
+ else
327
+ options << " calendar='#{calendar.name}'"
328
+ end
329
+ "#<TimeStep definition='#{definition}'#{options}>"
215
330
  end
216
331
 
217
332
  # Returns true if other has same contents of `definition` and `calendar`
218
333
  # as self has.
219
334
  #
335
+ # @param other [TimeStep]
336
+ #
220
337
  # @return [Boolean]
221
338
  def == (other)
222
- return @definition == other.definition && @calendar == other.calendar
339
+ return definition == other.definition && @calendar == other.calendar
223
340
  end
224
341
 
342
+ # @private
225
343
  def user_to_days (index)
226
344
  return ( @interval * index.to_r ).quo(86400)
227
345
  end
228
346
 
347
+ # @private
229
348
  def days_to_user (index)
230
- return ( 86400 * index.to_r ).quo(@interval)
349
+ if @interval == 0
350
+ return 0
351
+ else
352
+ return ( 86400 * index.to_r ).quo(@interval)
353
+ end
231
354
  end
232
355
 
233
356
  private :user_to_days, :days_to_user
234
357
 
235
- # Returns the time represented by the given amount as DateTime object
358
+ # Returns the datetime object (array) for the given index (indices).
236
359
  #
237
- # @param indices [Numeric,Array<Numeric>]
360
+ # @example
361
+ # ts = TimeStep.new("days since 2001-01-01 00:00:00")
362
+ # ts.time_at(0)
363
+ # # => #<DateTime: 2001-01-01T00:00:00+00:00 ...>
364
+ # ts.time_at(14)
365
+ # # => #<DateTime: 2001-01-15T00:00:00+00:00 ...>
238
366
  #
239
- # @return [DateTime]
367
+ # @param indices [Array<Numeric>]
368
+ #
369
+ # @return [DateTime, Array<DateTime>]
240
370
  def time_at (*indices)
241
371
  if indices.size == 1
242
372
  index = indices.first
243
- raise ArgumentError, "index should be numeric" unless index.is_a?(Numeric)
244
- index = index.to_r
373
+ raise ArgumentError, "index argument should be a numeric" unless index.is_a?(Numeric)
245
374
  case @symbol
246
375
  when :years
247
- unless index.denominator == 1
248
- raise "index for years should be an integer"
376
+ unless (index*@numeric).denominator == 1
377
+ raise ArgumentError, "index argument should be an integer for years"
249
378
  end
250
379
  return @origin.next_year(index*@numeric)
251
380
  when :months
252
- unless index.denominator == 1
253
- raise "index for years should be an integer"
381
+ unless (index*@numeric).denominator == 1
382
+ raise ArgumentError, "index argument should be an integer for months"
254
383
  end
255
384
  return @origin.next_month(index*@numeric)
256
385
  else
@@ -264,48 +393,75 @@ class TimeStep
264
393
  end
265
394
  end
266
395
 
267
- # Calculate the time difference in days from origin at the index.
396
+ alias [] time_at
397
+
398
+ # Calculate the duration (array) in day unit since origin time at the given index (indices).
399
+ #
400
+ # @example
401
+ # ts = TimeStep.new("hours since 2001-01-01 00:00:00")
402
+ # ts.duration_at(0)
403
+ # # => 0 ### 0 days
404
+ # ts.duration_at(12)
405
+ # # => (1/2) ### half of a day
406
+ # ts.duration_at(14*24)
407
+ # # => 14 ### 14 days
408
+ #
409
+ # @param indices [Array<Numeric>]
268
410
  #
269
- # @param indices [Array]
270
411
  # @return [DateTime, Array<DateTime>]
271
- def days_at (*indices)
412
+ def duration_at (*indices)
272
413
  if indices.size == 1
273
414
  index = indices.first
274
- case @symbol
275
- when :years
276
- unless index.denominator == 1
277
- raise "index for years should be an integer"
278
- end
279
- days = @origin.next_year(@numeric*index) - @origin
280
- when :months
281
- unless index.denominator == 1
282
- raise "index for years should be an integer"
283
- end
284
- days = @origin.next_month(@numeric*index) - @origin
285
- else
286
- days = user_to_days(index)
287
- end
415
+ days = case @symbol
416
+ when :years
417
+ unless (index*@numeric).denominator == 1
418
+ raise ArgumentError, "index argument should be an integer for years"
419
+ end
420
+ @origin.next_year(@numeric*index) - @origin
421
+ when :months
422
+ unless (index*@numeric).denominator == 1
423
+ raise ArgumentError, "index argument should be an integer for months"
424
+ end
425
+ @origin.next_month(@numeric*index) - @origin
426
+ else
427
+ user_to_days(index)
428
+ end
288
429
  days = days.to_i if days.denominator == 1
289
430
  return days
290
431
  else
291
- return indices.map{ |index| days_at(index) }
432
+ return indices.map{ |index| duration_at(index) }
292
433
  end
293
434
  end
294
435
 
295
- # Returns the index for the given time in the unit represented by the object
436
+ # Returns the index (indices) for the given time (array).
296
437
  #
297
- # @param times [DateTime] time object
438
+ # @example
439
+ # ts = TimeStep.new("days since 2001-01-01 00:00:00")
440
+ # ts.index_at(ts.parse("2001-01-01 00:00:00"))
441
+ # # => 0
442
+ # ts.index_at("2001-01-15 00:00:00")
443
+ # # => 14
444
+ # ts.index_at("2002")
445
+ # # => 365
298
446
  #
299
- # @return [Numeric]
447
+ # @param times [Array<DateTime>]
448
+ # @param format [String] template string for strptime for parsing time
449
+ #
450
+ # @return [Numeric, Array<Numeric>]
300
451
  def index_at (*times, format: nil)
301
452
  if times.size == 1
302
453
  time = times.first
303
- time = @calendar.parse(time, format: format) if time.is_a?(String)
454
+ time = time.to_datetime if time.is_a?(Time)
455
+ time = @calendar.parse(time, format: format, offset: @origin.offset) if time.is_a?(String)
304
456
  case @symbol
305
457
  when :years
306
- index = time.difference_in_years(@origin).quo(@numeric.to_i)
458
+ diff = time.difference_in_years(@origin)
459
+ frac = diff - diff.floor
460
+ index = diff.floor.quo(@numeric.to_i) + frac
307
461
  when :months
308
- index = time.difference_in_months(@origin).quo(@numeric.to_i)
462
+ diff = time.difference_in_months(@origin)
463
+ frac = diff - diff.floor
464
+ index = diff.floor.quo(@numeric.to_i) + frac
309
465
  else
310
466
  jday = @calendar.date2jday(time.year, time.month, time.day)
311
467
  fday = time.fraction
@@ -319,145 +475,226 @@ class TimeStep
319
475
  return times.map{|time| index_at(time, format: format) }
320
476
  end
321
477
  end
322
-
323
- def range (ge: nil, gt: nil, lt: nil, le: nil)
324
- raise "lower limit is not given" if ge.nil? and gt.nil?
325
- raise "upper limit is not given" if lt.nil? and le.nil?
326
- raise "lower limits are duplicated" if ge and gt
327
- raise "upper limits are duplicated" if lt and le
328
- if ge
329
- case ge
330
- when Numeric
331
- min = ge.floor
332
- else
333
- min = prev_index_of(ge)
334
- end
335
- end
336
- if gt
337
- case gt
338
- when Numeric
339
- if lt.to_r.denominator == 1
340
- min = gt.to_i + 1
341
- else
342
- min = gt.ceil
343
- end
344
- else
345
- min = next_index_of(gt)
346
- end
347
- end
348
- if lt
349
- case lt
350
- when Numeric
351
- if lt.to_r.denominator == 1
352
- max = lt.to_i - 1
353
- else
354
- max = lt.floor
355
- end
356
- else
357
- max = prev_index_of(lt)
358
- end
359
- end
360
- if le
361
- case le
362
- when Numeric
363
- max = le.ceil
364
- else
365
- max = next_index_of(le)
366
- end
367
- end
368
- return (min..max).to_a
369
- end
370
-
371
- # Returns TimeStep object which has origin time determined
372
- # by the given `index`.
478
+
479
+ # Returns new timestep object which has new origin time specified by `index`.
480
+ #
481
+ # @example
482
+ # ts = TimeStep.new("days since 2001-01-01 00:00:00")
483
+ # # => #<TimeStep definition='1 days since 2001-01-01 00:00:00.000000000 +00:00'>
484
+ # ts.shift_origin(14)
485
+ # # => #<TimeStep definition='1 days since 2001-01-15 00:00:00.000000000 +00:00'>
486
+ # ts.shift_origin(365)
487
+ # # => #<TimeStep definition='1 days since 2002-01-01 00:00:00.000000000 +00:00'>
373
488
  #
374
489
  # @param index [Numeric]
490
+ # @param with [String, Symbol] "index" : shift by index , "duration" : shift by duration
375
491
  #
376
492
  # @return [TimeStep]
377
- def shift_origin (index)
378
- time = time_at(index)
379
- return TimeStep.new(@intervalspec, since: time, calendar: @calendar)
380
- end
381
-
382
- def shift_origin_with_days (days)
383
- time = @origin + days
384
- return TimeStep.new(@intervalspec, since: time, calendar: @calendar)
493
+ def shift_origin (index, with: "index")
494
+ case with
495
+ when :index, "index"
496
+ time = time_at(index)
497
+ return TimeStep.new(interval_spec, since: time, calendar: @calendar)
498
+ when :duration, "duration", :days, "days"
499
+ time = @origin + index
500
+ return TimeStep.new(interval_spec, since: time, calendar: @calendar)
501
+ end
385
502
  end
386
503
 
387
- # Returns TimeStep object which has origin time specified
388
- # by the given `time`.
504
+ # Returns new timestep object which holds the given time as origin.
505
+ #
506
+ # @example
507
+ # ts = TimeStep.new("days since 2001-01-01 00:00:00")
508
+ # # => #<TimeStep definition='1 days since 2001-01-01 00:00:00.000000000 +00:00'>
509
+ # ts.new_origin(ts.parse("2001-01-15"))
510
+ # # => #<TimeStep definition='1 days since 2001-01-15 00:00:00.000000000 +00:00'>
511
+ # ts.new_origin("2001-01-15")
512
+ # # => #<TimeStep definition='1 days since 2001-01-15 00:00:00.000000000 +00:00'>
513
+ # ts.new_origin("2002")
514
+ # # => #<TimeStep definition='1 days since 2002-01-01 00:00:00.000000000 +00:00'>
389
515
  #
390
- # @param time [DateTime]
516
+ # @param time [DateTime, String]
391
517
  #
392
518
  # @return [TimeStep]
393
519
  def new_origin (time)
394
- case time
395
- when String
396
- time = @calendar.parse(time)
520
+ time = @calendar.parse(time, offset: @origin.offset) if time.is_a?(String)
521
+ if @wday
522
+ origin = time - time.wday + WDAY[@wday]
523
+ origin -= 7 unless time >= origin
524
+ time = origin
397
525
  end
398
- return TimeStep.new(@intervalspec + " since " + time.strftime("%Y-%m-%d %H:%M:%S.%N %:z"), calendar: @calendar)
526
+ return TimeStep.new(interval_spec, since: time, calendar: @calendar)
399
527
  end
400
528
 
401
- include Enumerable
402
-
403
- def each (limit = nil, incr = 1, &block)
404
- if limit.nil?
405
- raise "step method require count" unless @count
406
- limit = @count - 1
407
- end
408
- if block
409
- 0.step(limit, incr) do |k|
410
- block.call(time_at(k))
411
- end
412
- else
413
- return Enumerator.new {|y|
414
- 0.step(limit, incr) do |k|
415
- y << time_at(k)
416
- end
417
- }
418
- end
419
- end
420
-
421
- def times (&block)
422
- raise "step method require count" unless @count
423
- (0...@count).each(&block)
424
- end
425
-
426
- # Creates TimeStep::Pair
529
+ # Truncate the given datetime to the unit of the object.
427
530
  #
531
+ # @example
532
+ # hours = TimeStep.new("hours since 2001-01-01 00:00:00")
533
+ # hours.truncate("2001-01-15 12:35:00")
534
+ # # => #<DateTime: 2001-01-15T12:00:00+00:00 ...>
428
535
  #
429
- def in (unit)
430
- return TimeStep::Pair.new(self, format("%s since %s", unit, @originspec), calendar: @calendar)
536
+ # days = TimeStep.new("days since 2001-01-01 00:00:00")
537
+ # days.truncate("2001-01-15 12:00:00")
538
+ # # => #<DateTime: 2001-01-15T00:00:00+00:00 ...>
539
+ #
540
+ # months = TimeStep.new("months since 2001-01-01 00:00:00")
541
+ # months.truncate("2001-05-15 12:00:00")
542
+ # # => #<DateTime: 2001-05-01T00:00:00+00:00 ...>
543
+ #
544
+ # @param time [DateTime, String]
545
+ #
546
+ # @return [DateTime]
547
+ def truncate (time)
548
+ return time_at(index_at(time).floor)
431
549
  end
432
-
550
+
551
+ # Returns next integer index of the given time
552
+ #
553
+ # @example
554
+ # ts = TimeStep.new("days since 2001-01-01 00:00:00")
555
+ # ts.next_index_of("2001-01-14 12:00:00")
556
+ # #=> 14
557
+ # ts.next_index_of("2001-01-15 00:00:00")
558
+ # #=> 15
559
+ #
560
+ # @param time [DateTime, String]
561
+ #
562
+ # @return [Numeric]
433
563
  def next_index_of (time)
434
- case time
435
- when String
436
- time = @calendar.parse(time)
437
- end
438
- index = index_at(time).to_r
439
- if index.denominator == 1
440
- return index.to_i + 1
441
- else
442
- return index.ceil
443
- end
564
+ time = @calendar.parse(time, offset: @origin.offset) if time.is_a?(String)
565
+ return index_at(time).floor + 1
444
566
  end
445
567
 
568
+ # Returns previous integer index of the given time
569
+ #
570
+ # @example
571
+ # ts = TimeStep.new("days since 2001-01-01 00:00:00")
572
+ # ts.prev_index_of("2001-01-14 12:00:00")
573
+ # #=> 13
574
+ # ts.prev_index_of("2001-01-15 00:00:00")
575
+ # #=> 13
576
+ #
577
+ # @param time [DateTime, String]
578
+ #
579
+ # @return [Numeric]
446
580
  def prev_index_of (time)
447
- return next_index_of(time) - 1
581
+ time = @calendar.parse(time, offset: @origin.offset) if time.is_a?(String)
582
+ return index_at(time).ceil - 1
448
583
  end
449
584
 
585
+ # Returns next time of the given time
586
+ #
587
+ # @example
588
+ # ts = TimeStep.new("days since 2001-01-01 00:00:00")
589
+ # ts.next_time_of("2001-01-14 12:00:00")
590
+ # #=> #<DateTime: 2001-01-15T00:00:00+00:00 ...>
591
+ # ts.next_time_of("2001-01-15 00:00:00")
592
+ # #=> #<DateTime: 2001-01-16T00:00:00+00:00 ...>
593
+ #
594
+ # @param time [DateTime, String]
595
+ #
596
+ # @return [DateTime]
450
597
  def next_time_of (time)
451
598
  return time_at(next_index_of(time))
452
599
  end
453
600
 
601
+ # Returns previous time of the given time
602
+ #
603
+ # @example
604
+ # ts = TimeStep.new("days since 2001-01-01 00:00:00")
605
+ # ts.prev_time_of("2001-01-14 12:00:00")
606
+ # #=> #<DateTime: 2001-01-14T00:00:00+00:00 ...>
607
+ # ts.prev_time_of("2001-01-15 00:00:00")
608
+ # #=> #<DateTime: 2001-01-14T00:00:00+00:00 ...>
609
+ #
610
+ # @param time [String, DateTime]
611
+ #
612
+ # @return [DateTime]
454
613
  def prev_time_of (time)
455
614
  return time_at(prev_index_of(time))
456
615
  end
457
616
 
458
- def parse (time, format: nil)
459
- return @calendar.parse(time, format: format)
617
+ # Creates new timestep pair object which refers `other` as other unit
618
+ #
619
+ # @example
620
+ # days = TimeStep.new("days since 2001-01-01 00:00:00")
621
+ # pair = days.in("hours")
622
+ # pair.forward(1)
623
+ # # => 24
624
+ #
625
+ # @param other [String, TimeStep]
626
+ #
627
+ # @return [TimeStep::Pair]
628
+ def in (unit)
629
+ other = TimeStep.new(unit, since: @origin, calendar: @calendar)
630
+ return Pair.new(self, other)
631
+ end
632
+
633
+ # Creates new timestep pair object which refers `other` from `self`
634
+ #
635
+ # @example
636
+ # days = TimeStep.new("days since 2001-01-01 00:00:00")
637
+ # hours = TimeStep.new("hours since 2001-01-01 00:00:00")
638
+ # pair = days.to(hours)
639
+ # pair.forward(1)
640
+ # # => 24
641
+ #
642
+ # @param other [String, TimeStep]
643
+ #
644
+ # @return [TimeStep::Pair]
645
+ def to (other)
646
+ return Pair.new(self, other)
460
647
  end
461
648
 
649
+ # Creates new timeperiod object corresponding given time or index for
650
+ # start and last.
651
+ #
652
+ # @example
653
+ # ts = TimeStep.new("days since 2001-01-01 00:00:00")
654
+ # ts.period(0, 1)
655
+ # #=> #<TimePeriod '1 days' [2001-01-01T00:00:00+00:00, 2001-01-02T00:00:00+00:00] >
656
+ # ts.period("2001", "2002", ends: "[)")
657
+ # #=> #<TimePeriod '365 days' [2001-01-01T00:00:00+00:00, 2002-01-01T00:00:00+00:00) >
658
+ #
659
+ # @param start [Numeric, DateTime]
660
+ # @param last [Numeric, DateTime]
661
+ # @param ends [String] one of "[]", "()", "[)", "(]"
662
+ #
663
+ # @return [TimePeriod]
664
+ def period (start, last, ends: "[]")
665
+ idx1 = if start.kind_of?(Numeric)
666
+ start
667
+ else
668
+ index_at(start)
669
+ end
670
+ idx2 = if last.kind_of?(Numeric)
671
+ last
672
+ else
673
+ index_at(last)
674
+ end
675
+ origin = time_at(idx1)
676
+ numeric = (idx2 - idx1) * @numeric
677
+ interval_spec = format("%g %s", numeric, @symbol)
678
+ return TimePeriod.new(interval_spec, since: origin, calendar: @calendar, ends: ends)
679
+ end
680
+
681
+ # Creates new timestep range object.
682
+ #
683
+ # @example
684
+ # ts = TimeStep.new("days since 2001-01-01 00:00:00")
685
+ # ts.range(0, 1)
686
+ # ts.range("2001", "2002", ends: "[)")
687
+ # ts.range("2001", 1)
688
+ #
689
+ # @param start [Numeric, DateTime]
690
+ # @param last [Numeric, DateTime]
691
+ # @param count [Integer]
692
+ # @param ends [String] one of "[]", "()", "[)", "(]"
693
+ #
694
+ # @return [TimeStep::Range]
695
+ def range (start, last = nil, count: nil, ends: "[]")
696
+ return TimeStep::Range.new(self, start, last, count: count, ends: ends)
697
+ end
698
+
462
699
  end
463
700