timesteps 0.9.5 → 1.0.0

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