timesteps 0.9.5 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardopts +1 -0
- data/{LICENSES → LICENSE} +0 -0
- data/NEWS.md +8 -0
- data/Note.ja.md +9 -0
- data/README.md +20 -29
- data/lib/timesteps/{parse_timestamp.rb → datetime_parse_timestamp.rb} +25 -13
- data/lib/timesteps/datetime_timestep.rb +64 -0
- data/lib/timesteps/datetimelike.rb +12 -23
- data/lib/timesteps/{format.rb → datetimelike_format.rb} +0 -0
- data/lib/timesteps/grads.rb +39 -18
- data/lib/timesteps/time.rb +14 -0
- data/lib/timesteps/timeperiod.rb +205 -0
- data/lib/timesteps/timestep.rb +505 -268
- data/lib/timesteps/{calendar.rb → timestep_calendar.rb} +54 -29
- data/lib/timesteps/timestep_converter.rb +20 -23
- data/lib/timesteps/timestep_datetime_ext.rb +6 -16
- data/lib/timesteps/timestep_pair.rb +39 -26
- data/lib/timesteps/timestep_query.rb +57 -0
- data/lib/timesteps/timestep_range.rb +153 -0
- data/lib/timesteps.rb +8 -3
- data/spec/allleap_spec.rb +4 -15
- data/spec/fixed360day_spec.rb +4 -15
- data/spec/noleap_spec.rb +2 -13
- data/spec/timestep_spec.rb +24 -47
- data/spec/timesteppair_spec.rb +2 -2
- data/timesteps.gemspec +3 -3
- metadata +14 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8ec2401e1d32acce3ddb048864c34b37de56ad5f83066fa617baeea8c65abc3a
|
4
|
+
data.tar.gz: '083c51a6c6fd1f295e7d2359960f5fe893ccc0847bcf014b7928f629c21e1a8e'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e242891a7aed17ea1a3c0c138e80382b55422b5d6c9cbeec0c32e0b5725d5979a1e7631b110eb136ca4942c16023b93b325390f28be30792aa15f637a736c09b
|
7
|
+
data.tar.gz: 4da60d62efce29eeb13b1fe93d2621cdc616c6d8fe4d421922257bbb5e28c5695504d6804806430feb1dd42adbba7870de37b41758cf3df121dae59b57c1d3bf
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private
|
data/{LICENSES → LICENSE}
RENAMED
File without changes
|
data/NEWS.md
ADDED
data/Note.ja.md
CHANGED
data/README.md
CHANGED
@@ -2,16 +2,16 @@ timesteps
|
|
2
2
|
================
|
3
3
|
|
4
4
|
A library for handling discrete time series in constant increments.
|
5
|
-
The
|
5
|
+
The primary purpose is to describe the time axis
|
6
6
|
when dealing with time series of observational data and climate data.
|
7
7
|
|
8
8
|
Features
|
9
9
|
--------
|
10
10
|
|
11
|
-
* TimeStep
|
11
|
+
* TimeStep holds an origin time and a unit time interval.
|
12
12
|
* Parsing a time step expression like "hours since 2001-01-01 00:00:00" (originate from udunits library)
|
13
|
-
* Obtaining time value for index value (0 for the origin time)
|
14
|
-
* Obtaining index value for time value
|
13
|
+
* Obtaining time value for the index value (0 for the origin time)
|
14
|
+
* Obtaining index value for the time value
|
15
15
|
* Treating non-standard calendar-type such as 'noleap', 'allleap', and '360_day'
|
16
16
|
* Comparing the index values between different time step definitions
|
17
17
|
|
@@ -29,27 +29,26 @@ require "timesteps"
|
|
29
29
|
Description
|
30
30
|
-----------
|
31
31
|
|
32
|
-
This library
|
32
|
+
This library is for time conversion and time index comparison of multiple time series. This library treats the time series data of the type that specifies the time using indexes of time steps since origin time.
|
33
33
|
|
34
34
|
#### Time steps
|
35
35
|
|
36
|
-
The main class of this library is the TimeStep class, which holds the origin time and the interval representing the unit time step.
|
36
|
+
The main class of this library is the TimeStep class, which holds the origin time and the interval representing the unit time step. For the initialization of the TimeStep object, the following notation is used.
|
37
37
|
|
38
38
|
* "second since 1970-01-01 00:00:00 +00:00"
|
39
39
|
* "hour since 2001-01-01 00:00:00 JST"
|
40
40
|
* "3 days since 2001-01-01 00:00:00 +00:00"
|
41
41
|
* "10 years since 1901-01-01 00:00:00 +00:00"
|
42
42
|
|
43
|
-
|
43
|
+
These notations are imported from Unidata's [UDUNITS](https://www.unidata.ucar.edu/software/udunits/) library. These are also used in [CF conventions](http://cfconventions.org) of NetCDF. However, note that there are some differences between our library and the udunits library about the date-time expressions.
|
44
44
|
|
45
|
-
In this library, the elapsed time from the origin is expressed as an index value. For the case of "3 hours since 2001-01-01 00:00:00", the index values
|
46
|
-
are expressed as,
|
45
|
+
In this library, the elapsed time from the origin is expressed as an index value. For the case of "3 hours since 2001-01-01 00:00:00", the index values are expressed as,
|
47
46
|
|
48
47
|
* "2001-01-01 00:00:00" => 0 (0 / 3 hours) ( 0 days)
|
49
48
|
* "2001-01-01 03:00:00" => 1 (1 / 3 hours) ((1/8) days)
|
50
49
|
* "2001-01-02 00:00:00" => 8 (8 / 3 hours) ( 1 days)
|
51
50
|
|
52
|
-
|
51
|
+
These are expressed as a Ruby script.
|
53
52
|
|
54
53
|
```ruby
|
55
54
|
ts = TimeStep.new("3 hours since 2001-01-01 00:00:00")
|
@@ -59,18 +58,15 @@ ts.index_at("2001-01-02 00:00:00") ### => 8
|
|
59
58
|
ts.time_at(0) ### => #<DateTime 2001-01-01T00:00:00 ...>
|
60
59
|
ts.time_at(1) ### => #<DateTime 2001-01-01T03:00:00 ...>
|
61
60
|
ts.time_at(8) ### => #<DateTime 2001-01-02T00:00:00 ...>
|
62
|
-
ts.
|
63
|
-
ts.
|
64
|
-
ts.
|
61
|
+
ts.duration_at(0) ### => 0 [Integer]
|
62
|
+
ts.duration_at(1) ### => (1/8) [Rational]
|
63
|
+
ts.duration_at(8) ### => 1 [Integer]
|
65
64
|
```
|
66
65
|
|
67
66
|
#### Treatment of year and month units
|
68
67
|
|
69
68
|
The time units like day, hour, minute, second have constant intervals,
|
70
|
-
but the time units like years and months are not. So, the year and
|
71
|
-
month units are given special treatment in this library.
|
72
|
-
One year is counted at the same month and day as the origin time, and
|
73
|
-
one month is counted at the same day as the origin time.
|
69
|
+
but the time units like years and months are not. So, the year and month units are given special treatment in this library. One year is counted at the same month and day as the origin time, and one month is counted on the same day as the origin time.
|
74
70
|
|
75
71
|
```ruby
|
76
72
|
ts = TimeStep.new("year since 2000-01-15 00:00:00")
|
@@ -106,7 +102,7 @@ The following calendars, including non-standard calendars, can be handled.
|
|
106
102
|
* allleap, 366_day
|
107
103
|
* 360_day
|
108
104
|
|
109
|
-
You can find the description for these
|
105
|
+
You can find the description for these calendars at the document of CF-Conventions ([4.4.1 Calendar](http://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#calendar)).
|
110
106
|
|
111
107
|
```ruby
|
112
108
|
ts = TimeStep.new("day since 2000-01-01", calendar: "standard")
|
@@ -119,13 +115,11 @@ ts = TimeStep.new("day since 2000-01-01", calendar: "360_day")
|
|
119
115
|
ts.time_at(59) ### => #<DateTime::Fixed360Day: 2000-02-30T00:00:00+00:00 ...>
|
120
116
|
```
|
121
117
|
|
122
|
-
In this library, DateTime class is adopted as the object that represents date and time (not Time class). Non-standard calendars ("proleptic_gregorian", "
|
118
|
+
In this library, the DateTime class is adopted as the object that represents date and time (not Time class). Non-standard calendars ("proleptic_gregorian", "julian") can be handled by the DateTime class, with appropriate use of the start parameter. But, other non-standard calendars ("noleap", "allleap", "360_day") can not. So, DateTimeLike class and its subclasses DateTime::NoLeap, DateTime::AllLeap, DateTime::Fixed360Day are introduced. Since many (not all) of methods in the DateTime class are also implemented in DateTimeLike class, users don't need to be too aware of these class differences.
|
123
119
|
|
124
120
|
#### Parsing datetime string
|
125
121
|
|
126
|
-
In `standard` calendar, use DateTime.parse as usual.
|
127
|
-
In other calendar, you can use DateTime.parse_timestamp, which is
|
128
|
-
special method to parse date time string with specification of calendar.
|
122
|
+
In `standard` calendar, use DateTime.parse as usual. In other calendars, you can use DateTime.parse_timestamp, which is a particular method to parse a date-time string with a specification of the calendar.
|
129
123
|
|
130
124
|
```ruby
|
131
125
|
DateTime.parse_timestamp("1200-01-01") ### "standard"
|
@@ -143,14 +137,14 @@ ts = TimeStep.new("days since 2001-01-01", calendar: "allleap")
|
|
143
137
|
ts.parse("2001-02-29") ### => #<DateTime::AllLeap 2001-02-29T ...>
|
144
138
|
```
|
145
139
|
|
146
|
-
In the UDUNITS library, negative years are treated as BCs and A.D. 0 is treated as non-existent. This is different from how it is handled in Ruby's DateTime class.
|
140
|
+
In the UDUNITS library, negative years are treated as BCs, and A.D. 0 is treated as non-existent. This is different from how it is handled in Ruby's DateTime class.
|
147
141
|
|
148
142
|
```ruby
|
149
143
|
DateTime.parse_timestamp("-0001-01-01")
|
150
144
|
# => #<DateTime: -0001-01-01T00:00:00+00:00 ...>
|
151
145
|
# B.C. 2
|
152
146
|
|
153
|
-
DateTime.parse_timestamp("-
|
147
|
+
DateTime.parse_timestamp("0000-01-01")
|
154
148
|
# => #<DateTime: 0000-01-01T00:00:00+00:00 ...>
|
155
149
|
# B.C. 1
|
156
150
|
|
@@ -159,12 +153,9 @@ DateTime.parse_timestamp("BC 0001-01-01")
|
|
159
153
|
# B.C. 1
|
160
154
|
```
|
161
155
|
|
162
|
-
#### Comparing
|
156
|
+
#### Comparing multiple time series
|
163
157
|
|
164
|
-
TimeStep::Pair is a class to compare indices of two time series,
|
165
|
-
which is initialized by two time step object. It is possible to
|
166
|
-
compute the other index corresponding to the time represented
|
167
|
-
by one of the indices using TimeStep::Pair.
|
158
|
+
TimeStep::Pair is a class to compare indices of two time series, which is initialized by two time step object. It is possible to compute the other index corresponding to the time represented by one of the indices using TimeStep::Pair.
|
168
159
|
|
169
160
|
|
170
161
|
```ruby
|
@@ -11,12 +11,10 @@ class DateTime
|
|
11
11
|
# `DateTime._parse()` is called internally.
|
12
12
|
#
|
13
13
|
# @param spec [String]
|
14
|
-
# @option bc [Boolean]
|
15
14
|
#
|
16
15
|
# @return [DateTimeFixedDPY]
|
17
16
|
|
18
|
-
def self.parse_timestamp (spec, calendar: "standard",
|
19
|
-
raise "invalid option 'calendar'" if self != DateTime
|
17
|
+
def self.parse_timestamp (spec, format: nil, offset: nil, calendar: "standard", tz: nil)
|
20
18
|
case calendar.downcase.intern
|
21
19
|
when :standard, :gregorian
|
22
20
|
klass = DateTime
|
@@ -39,25 +37,39 @@ class DateTime
|
|
39
37
|
end
|
40
38
|
if format
|
41
39
|
hash = DateTime._strptime(spec, format)
|
42
|
-
raise "
|
40
|
+
raise "date-time string '#{spec}' doesn't match with the given format '#{format}'" unless hash
|
43
41
|
else
|
44
|
-
|
42
|
+
if spec =~ /\A([+\-]?\d{1,4})(\-(\d{1,2}))?(\s+(\w{3}|[+\-]\d{1,2}(:?\d{1,2})))?\z/
|
43
|
+
year = $1.to_i
|
44
|
+
month = $3 ? $3.to_i : 1
|
45
|
+
mday = 1
|
46
|
+
rest = $4
|
47
|
+
hash = DateTime._parse("#{year}-#{month}-#{mday} 00:00:00 #{rest}")
|
48
|
+
else
|
49
|
+
hash = DateTime._parse(spec)
|
50
|
+
end
|
45
51
|
end
|
46
|
-
year, month, day, hour, minute, second, sec_fraction,
|
52
|
+
year, month, day, hour, minute, second, sec_fraction, offset_ =
|
47
53
|
hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)
|
48
|
-
if bc and year < 0
|
49
|
-
year = year + 1
|
50
|
-
end
|
51
54
|
hour ||= 0
|
52
55
|
minute ||= 0
|
53
56
|
second ||= 0.0
|
54
57
|
sec_fraction ||= 0.0
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
+
if offset_
|
59
|
+
offset = offset_.quo(86400)
|
60
|
+
else
|
61
|
+
offset ||= 0
|
62
|
+
end
|
63
|
+
if tz
|
64
|
+
time = tz.local_datetime(year, month, day, hour, minute, second.to_i, sec_fraction.to_r)
|
58
65
|
else
|
59
|
-
|
66
|
+
if hour == 24 && minute == 0 && second == 0.0
|
67
|
+
time = klass.new(year, month, day, 23, minute, second + sec_fraction, offset, start) + 1.quo(24)
|
68
|
+
else
|
69
|
+
time = klass.new(year, month, day, hour, minute, second + sec_fraction, offset, start)
|
70
|
+
end
|
60
71
|
end
|
72
|
+
return time
|
61
73
|
end
|
62
74
|
|
63
75
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
|
2
|
+
class DateTime
|
3
|
+
|
4
|
+
def timestep (interval_spec, tz: nil)
|
5
|
+
case start
|
6
|
+
when Date::ITALY
|
7
|
+
calendar = "standard"
|
8
|
+
when Date::GREGORIAN
|
9
|
+
calendar = "proleptic_gregorian"
|
10
|
+
when Date::JULIAN
|
11
|
+
calendar = "proleptic_julian"
|
12
|
+
end
|
13
|
+
return TimeStep.new(interval_spec, since: self, calendar: calendar, tz: tz)
|
14
|
+
end
|
15
|
+
|
16
|
+
def timeperiod (interval_spec, tz: nil, ends: "[]")
|
17
|
+
case start
|
18
|
+
when Date::ITALY
|
19
|
+
calendar = "standard"
|
20
|
+
when Date::GREGORIAN
|
21
|
+
calendar = "proleptic_gregorian"
|
22
|
+
when Date::JULIAN
|
23
|
+
calendar = "proleptic_julian"
|
24
|
+
end
|
25
|
+
return TimePeriod.new(interval_spec, since: self, calendar: calendar, ends: ends, tz: tz)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
class DateTime::NoLeap
|
31
|
+
|
32
|
+
def timestep (interval_spec, tz: nil)
|
33
|
+
return TimeStep.new(interval_spec, since: self, clanedar: "noleap", tz: tz)
|
34
|
+
end
|
35
|
+
|
36
|
+
def timeperiod (interval_spec, tz: nil, ends: "[]")
|
37
|
+
return TimePeriod.new(interval_spec, since: self, calendar: "noleap", ends: ends, tz: tz)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
class DateTime::AllLeap
|
43
|
+
|
44
|
+
def timestep (interval_spec, tz: nil)
|
45
|
+
return TimeStep.new(interval_spec, since: self, clanedar: "allleap", tz: tz)
|
46
|
+
end
|
47
|
+
|
48
|
+
def timeperiod (interval_spec, tz: nil, ends: "[]")
|
49
|
+
return TimePeriod.new(interval_spec, since: self, calendar: "allleap", ends: ends, tz: tz)
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
class DateTime::Fixed360Day
|
55
|
+
|
56
|
+
def timestep (interval_spec, tz: nil)
|
57
|
+
return TimeStep.new(interval_spec, since: self, clanedar: "360day", tz: tz)
|
58
|
+
end
|
59
|
+
|
60
|
+
def timeperiod (interval_spec, tz: nil, ends: "[]")
|
61
|
+
return TimePeriod.new(interval_spec, since: self, calendar: "360day", ends: ends, tz: tz)
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -75,16 +75,16 @@ class DateTimeLike
|
|
75
75
|
|
76
76
|
def check_valid_datetime
|
77
77
|
unless valid_date?
|
78
|
-
raise "invalid date"
|
78
|
+
raise "invalid date for #{self.class} [year: #{@year}, month: #{@month}, day: #{@day}]"
|
79
79
|
end
|
80
80
|
unless valid_time?
|
81
|
-
raise "invalid time"
|
82
|
-
end
|
81
|
+
raise "invalid time for #{self.class} [hour: #{@hour}, minute: #{@minute}, second: #{@second}]"
|
82
|
+
end
|
83
83
|
end
|
84
84
|
|
85
85
|
def valid_time?
|
86
86
|
begin
|
87
|
-
DateTime.new(2001,1,1
|
87
|
+
DateTime.new(2001, 1, 1, @hour, @minute, @second)
|
88
88
|
true
|
89
89
|
rescue
|
90
90
|
false
|
@@ -199,7 +199,7 @@ class DateTimeLike
|
|
199
199
|
end
|
200
200
|
|
201
201
|
# Returns the difference between the two dates
|
202
|
-
# if the other is a
|
202
|
+
# if the other is a datetime object.
|
203
203
|
# If the other is a numeric value,
|
204
204
|
# returns a date object pointing other days before self.
|
205
205
|
def - (other_or_days)
|
@@ -209,7 +209,7 @@ class DateTimeLike
|
|
209
209
|
when self.class
|
210
210
|
return self.jd - other_or_days.jd
|
211
211
|
else
|
212
|
-
raise "
|
212
|
+
raise "other shoud be date-time object or numeric value"
|
213
213
|
end
|
214
214
|
end
|
215
215
|
|
@@ -398,29 +398,18 @@ class DateTimeLike
|
|
398
398
|
#
|
399
399
|
# @return [Integer]
|
400
400
|
def difference_in_years (other)
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
end
|
405
|
-
if self.year < other.year && self.compare_md(other) > 0
|
406
|
-
extra = 1
|
407
|
-
end
|
408
|
-
return self.year - other.year + extra
|
401
|
+
my = self.new_offset(0)
|
402
|
+
other = other.new_offset(0)
|
403
|
+
return my.year - other.year + my.compare_md(other).quo(2)
|
409
404
|
end
|
410
405
|
|
411
406
|
# Calculate difference between the object and other object in months.
|
412
407
|
#
|
413
408
|
# @return [Integer]
|
414
409
|
def difference_in_months (other)
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
end
|
419
|
-
if self.month < other.month && self.compare_d(other) > 0
|
420
|
-
extra = 1
|
421
|
-
end
|
422
|
-
return 12*(self.year - other.year) + self.month - other.month + extra
|
410
|
+
my = self.new_offset(0)
|
411
|
+
other = other.new_offset(0)
|
412
|
+
return 12*(my.year - other.year) + my.month - other.month + my.compare_d(other).quo(2)
|
423
413
|
end
|
424
|
-
|
425
414
|
end
|
426
415
|
|
File without changes
|
data/lib/timesteps/grads.rb
CHANGED
@@ -1,6 +1,24 @@
|
|
1
|
-
|
1
|
+
require "timesteps"
|
2
2
|
|
3
|
-
class
|
3
|
+
class DateTime
|
4
|
+
|
5
|
+
def self.parse_grads_time (time_string)
|
6
|
+
if time_string.strip =~ /\A(((\d{2})?(:(\d{2}))?Z)?(\d{2}))?(\w{3})(\d{4})\z/i
|
7
|
+
hour = $3 || "00"
|
8
|
+
min = $5 || "00"
|
9
|
+
day = $6 || "01"
|
10
|
+
mon = $7
|
11
|
+
year = $8
|
12
|
+
time = DateTime.parse("#{year}#{mon}#{day} #{hour}:#{min}:00")
|
13
|
+
else
|
14
|
+
raise "invalid time format"
|
15
|
+
end
|
16
|
+
return time
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
class TimeStep::Range
|
4
22
|
|
5
23
|
# @private
|
6
24
|
REGEXP_GRADS_TDEF = /\A(?:TDEF\s+)?(\d+)\s+LINEAR\s+([^\s]*?)\s+([^\s]*?)\s*\z/i
|
@@ -13,8 +31,17 @@ class TimeStep
|
|
13
31
|
"mo" => "months",
|
14
32
|
"yr" => "years",
|
15
33
|
}
|
16
|
-
|
17
|
-
|
34
|
+
|
35
|
+
module GrADSMixin
|
36
|
+
|
37
|
+
def parse_grads_time (time_string)
|
38
|
+
return DateTime.parse_grads_time(time_string)
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.from_grads_tdef (tdef_string)
|
44
|
+
|
18
45
|
if tdef_string.strip =~ REGEXP_GRADS_TDEF
|
19
46
|
count = $1.to_i
|
20
47
|
time = $2
|
@@ -22,22 +49,16 @@ class TimeStep
|
|
22
49
|
else
|
23
50
|
raise "invalid grads tdef string"
|
24
51
|
end
|
25
|
-
|
26
|
-
|
27
|
-
min = $5 || "00"
|
28
|
-
day = $6 || "01"
|
29
|
-
mon = $7
|
30
|
-
year = $8
|
31
|
-
origin = DateTime.parse("#{year}#{mon}#{day} #{hour}:#{min}:00")
|
32
|
-
else
|
33
|
-
raise "invalid time format"
|
34
|
-
end
|
52
|
+
|
53
|
+
origin = DateTime.parse_grads_time(time)
|
35
54
|
|
36
55
|
increment = increment.sub(/(mn|hr|dy|mo|yr)/i) {|s| GRADS_INCREMENT[s.downcase]}
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
56
|
+
|
57
|
+
range = TimeStep.new(increment, since: origin).range(count)
|
58
|
+
range.extend GrADSMixin
|
59
|
+
|
60
|
+
return range
|
41
61
|
end
|
42
62
|
|
63
|
+
|
43
64
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
class Time
|
4
|
+
def to_datetime
|
5
|
+
# Convert seconds + microseconds into a fractional number of seconds
|
6
|
+
seconds = sec + Rational(usec, 10**6)
|
7
|
+
|
8
|
+
# Convert a UTC offset measured in minutes to one measured in a
|
9
|
+
# fraction of a day.
|
10
|
+
offset = Rational(utc_offset, 60 * 60 * 24)
|
11
|
+
|
12
|
+
DateTime.new(year, month, day, hour, min, seconds, offset)
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
class TimePeriod < TimeStep
|
4
|
+
|
5
|
+
def initialize (spec, since: nil, format: nil, offset: nil, calendar: "standard", tz: nil, ends: "[]" )
|
6
|
+
super(spec, since: since, format: format, offset: offset, calendar: calendar, tz: tz)
|
7
|
+
raise "invalid ends specification" unless ends =~ /\A[\[\(][\]\)]\z/
|
8
|
+
@ends = ends
|
9
|
+
@include_start = ( ends[0] == "[" ) ? true : false
|
10
|
+
@include_last = ( ends[1] == "]" ) ? true : false
|
11
|
+
@last = time_at(1)
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :last, :ends
|
15
|
+
|
16
|
+
def start
|
17
|
+
return @origin
|
18
|
+
end
|
19
|
+
|
20
|
+
def include_start?
|
21
|
+
return @include_start
|
22
|
+
end
|
23
|
+
|
24
|
+
def include_last?
|
25
|
+
return @include_last
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the value as a string for inspection.
|
29
|
+
#
|
30
|
+
def inspect
|
31
|
+
options = ""
|
32
|
+
case @calendar.name
|
33
|
+
when "standard", "gregorian"
|
34
|
+
else
|
35
|
+
options << " calendar='#{calendar.name}'"
|
36
|
+
end
|
37
|
+
left_paren = @include_start ? "[" : "("
|
38
|
+
right_paren = @include_last ? "]" : ")"
|
39
|
+
"#<TimePeriod '#{interval_spec}' #{left_paren}#{start.to_s}, #{last.to_s}#{right_paren} #{options}>"
|
40
|
+
end
|
41
|
+
|
42
|
+
def include? (other)
|
43
|
+
case other
|
44
|
+
when TimePeriod
|
45
|
+
if ( other.origin < @origin ) ||
|
46
|
+
( other.origin == @origin && ( not @include_start ) && other.include_start? )
|
47
|
+
left = false
|
48
|
+
else
|
49
|
+
left = true
|
50
|
+
end
|
51
|
+
if ( @last < other.last ) ||
|
52
|
+
( @last == other.last && ( not @include_last ) && other.include_last? )
|
53
|
+
right = false
|
54
|
+
else
|
55
|
+
right = true
|
56
|
+
end
|
57
|
+
return left & right
|
58
|
+
else
|
59
|
+
if @include_start
|
60
|
+
left = @origin <= other
|
61
|
+
else
|
62
|
+
left = @origin < other
|
63
|
+
end
|
64
|
+
if @include_last
|
65
|
+
right = other <= @last
|
66
|
+
else
|
67
|
+
right = other < @last
|
68
|
+
end
|
69
|
+
return left & right
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def overlap? (other)
|
74
|
+
case other
|
75
|
+
when TimePeriod
|
76
|
+
if ( @origin < other.last ) ||
|
77
|
+
( @origin == other.last && @include_start && other.include_last? )
|
78
|
+
left = true
|
79
|
+
else
|
80
|
+
left = false
|
81
|
+
end
|
82
|
+
if ( other.origin < @last ) ||
|
83
|
+
( other.origin == @last && @include_last && other.include_start? )
|
84
|
+
right = true
|
85
|
+
else
|
86
|
+
right = false
|
87
|
+
end
|
88
|
+
return left && right
|
89
|
+
else
|
90
|
+
return include?(other)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def contact? (other)
|
95
|
+
case other
|
96
|
+
when TimePeriod
|
97
|
+
if @origin <= other.last
|
98
|
+
left = true
|
99
|
+
else
|
100
|
+
left = false
|
101
|
+
end
|
102
|
+
if other.origin <= @last
|
103
|
+
right = true
|
104
|
+
else
|
105
|
+
right = false
|
106
|
+
end
|
107
|
+
return left && right
|
108
|
+
else
|
109
|
+
left = @origin <= other
|
110
|
+
right = other <= @last
|
111
|
+
return left & right
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def merge (other)
|
116
|
+
raise "can't merge period without contacted period" unless contact?(other)
|
117
|
+
if ( other.origin < @origin ) ||
|
118
|
+
( other.origin == @origin && ( not include_start? ) && other.include_start? )
|
119
|
+
left = other.origin
|
120
|
+
left_end = other.include_start? ? "[" : "("
|
121
|
+
else
|
122
|
+
left = @origin
|
123
|
+
left_end = include_start? ? "[" : "("
|
124
|
+
end
|
125
|
+
if ( @last < other.last ) ||
|
126
|
+
( @last == other.last && ( not include_last? ) && other.include_last? )
|
127
|
+
right = other.last
|
128
|
+
right_end = other.include_last? ? "]" : ")"
|
129
|
+
else
|
130
|
+
right = @last
|
131
|
+
right_end = include_last? ? ")" : "]"
|
132
|
+
end
|
133
|
+
ridx = index_at(right)
|
134
|
+
lidx = index_at(left)
|
135
|
+
numeric = (ridx - lidx) * @numeric
|
136
|
+
origin = left
|
137
|
+
interval_spec = format("%g %s", numeric, @symbol)
|
138
|
+
ends = left_end + right_end
|
139
|
+
return TimePeriod.new(interval_spec, since: origin, calendar: @calendar, ends: ends)
|
140
|
+
end
|
141
|
+
|
142
|
+
def clip (other)
|
143
|
+
raise "can't clip period without contacted period" unless contact?(other)
|
144
|
+
if ( @origin < other.origin ) ||
|
145
|
+
( @origin == other.origin && include_start? && ( not other.include_start? ) )
|
146
|
+
left = other.origin
|
147
|
+
left_end = other.include_start? ? "[" : "("
|
148
|
+
else
|
149
|
+
left = @origin
|
150
|
+
left_end = include_start? ? "[" : "("
|
151
|
+
end
|
152
|
+
if ( other.last < @last ) ||
|
153
|
+
( other.last == @last && include_last? && ( not other.include_last? ) )
|
154
|
+
right = other.last
|
155
|
+
right_end = other.include_last? ? "]" : ")"
|
156
|
+
else
|
157
|
+
right = @last
|
158
|
+
right_end = include_last? ? ")" : "]"
|
159
|
+
end
|
160
|
+
ridx = index_at(right)
|
161
|
+
lidx = index_at(left)
|
162
|
+
numeric = (ridx - lidx) * @numeric
|
163
|
+
origin = left
|
164
|
+
interval_spec = format("%g %s", numeric, @symbol)
|
165
|
+
ends = left_end + right_end
|
166
|
+
return TimePeriod.new(interval_spec, since: origin, calendar: @calendar, ends: ends)
|
167
|
+
end
|
168
|
+
|
169
|
+
def split (time, ends: ")[")
|
170
|
+
raise "can't split period without contacted time" unless contact?(time)
|
171
|
+
# left
|
172
|
+
numeric = index_at(time) * @numeric
|
173
|
+
interval_spec = format("%g %s", numeric, @symbol)
|
174
|
+
ends = (@include_start ? "[" : "(") + ends[0]
|
175
|
+
left_period = TimePeriod.new(interval_spec, since: @origin, calendar: @calendar, ends: ends)
|
176
|
+
# right
|
177
|
+
numeric = (1 - index_at(time)) * @numeric
|
178
|
+
interval_spec = format("%g %s", numeric, @symbol)
|
179
|
+
ends = ends[1] + (@include_last ? "]" : ")")
|
180
|
+
right_period = TimePeriod.new(interval_spec, since: @origin, calendar: @calendar, ends: ends)
|
181
|
+
return left_period, right_period
|
182
|
+
end
|
183
|
+
|
184
|
+
def next
|
185
|
+
return TimePeriod.new(interval_spec, since: @last, calendar: @calendar, ends: ends)
|
186
|
+
end
|
187
|
+
|
188
|
+
def prev
|
189
|
+
origin = index_at(-1)
|
190
|
+
return TimePeriod.new(interval_spec, since: origin, calendar: @calendar, ends: ends)
|
191
|
+
end
|
192
|
+
|
193
|
+
def shift_origin (index, with: "index")
|
194
|
+
case with
|
195
|
+
when :index, "index"
|
196
|
+
return TimePeriod.new(interval_spec, since: time_at(index), calendar: @calendar, ends: ends)
|
197
|
+
when :duration, "duration", :days, "days"
|
198
|
+
time = @origin + index
|
199
|
+
return TimePeriod.new(interval_spec, since: time, calendar: @calendar, ends: ends)
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
|