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,4 +1,6 @@
1
1
 
2
+ autoload :TZInfo, "tzinfo"
3
+
2
4
  class TimeStep
3
5
 
4
6
  class Calendar
@@ -21,9 +23,13 @@ class TimeStep
21
23
  SYM_360_day => DateTime::Fixed360Day,
22
24
  }
23
25
 
24
- def initialize (calendar = "standard", bc: false)
26
+ # Construct the object
27
+ #
28
+ def initialize (calendar = "standard", tz: nil)
29
+ unless calendar.is_a?(String)
30
+ raise ArgumentError, "argument calendar '#{calendar}' should be string"
31
+ end
25
32
  @name = calendar
26
- @bc = bc
27
33
  case @name.downcase.intern
28
34
  when :standard, :gregorian
29
35
  @calendar = :standard
@@ -38,22 +44,30 @@ class TimeStep
38
44
  when SYM_360_day
39
45
  @calendar = SYM_360_day
40
46
  end
47
+ if tz
48
+ raise "'standard' calendar needed for tz" unless @calendar == :standard
49
+ case tz
50
+ when nil
51
+ when String
52
+ @tz = TZInfo::Timezone.get(tz)
53
+ when TZInfo::Timezone
54
+ @tz = tz
55
+ else
56
+ raise "invalid tz specification"
57
+ end
58
+ end
41
59
  end
42
60
 
43
- attr_reader :name, :calendar
44
-
61
+ attr_reader :name, :calendar, :tz
62
+
45
63
  def inspect
46
- "#<TimeStep::Calendar calendar='#{@calendar.to_s}' bc='#{@bc}'>"
64
+ "#<TimeStep::Calendar calendar='#{@calendar.to_s}'>"
47
65
  end
48
66
 
49
67
  def == (other)
50
- return @calendar == other.calendar && bc? == other.bc?
68
+ return @calendar == other.calendar
51
69
  end
52
-
53
- def bc?
54
- return @bc ? true : false
55
- end
56
-
70
+
57
71
  def valid_datetime_type? (time)
58
72
  return false unless time.is_a?(DateTimeType[@calendar])
59
73
  case @calendar
@@ -72,10 +86,31 @@ class TimeStep
72
86
  # and creates an date time instance.
73
87
  #
74
88
  # @return [DateTime object]
75
- def parse (timespec, format: nil)
76
- return DateTime.parse_timestamp(timespec, calendar: @calendar.to_s, bc: @bc, format: format)
89
+ def parse (timespec, format: nil, offset: nil)
90
+ return DateTime.parse_timestamp(timespec, calendar: @calendar.to_s, format: format, tz: @tz, offset: offset)
77
91
  end
78
-
92
+
93
+ def time_new (year, month, day, hour=0, min=0, sec=0, offset=0)
94
+ case @calendar
95
+ when :standard
96
+ time = DateTime.new(year, month, day, hour, min, sec, offset, Date::ITALY)
97
+ when :proleptic_gregorian
98
+ time = DateTime.new(year, month, day, hour, min, sec, offset, Date::GREGORIAN)
99
+ when :proleptic_julian
100
+ time = DateTime.new(year, month, day, hour, min, sec, offset, Date::JULIAN)
101
+ when :noleap
102
+ time = DateTime::NoLeap.new(year, month, day, hour, min, sec, offset)
103
+ when :allleap
104
+ time = DateTime::AllLeap.new(year, month, day, hour, min, sec, offset)
105
+ when SYM_360_day
106
+ time = DateTime::Fixed360Day.new(year, month, day, hour, min, sec, offset)
107
+ end
108
+ if @tz
109
+ time = @tz.to_local(time)
110
+ end
111
+ return time
112
+ end
113
+
79
114
  def jday2date (jday)
80
115
  case @calendar
81
116
  when :standard
@@ -91,27 +126,17 @@ class TimeStep
91
126
  when SYM_360_day
92
127
  time = DateTime::Fixed360Day.jd(jday, 0, 0, 0, 0)
93
128
  end
129
+ if @tz
130
+ time = @tz.to_local(time)
131
+ end
94
132
  return time
95
133
  end
96
134
 
97
135
  def date2jday (year, month, day)
98
- case @calendar
99
- when :standard
100
- time = DateTime.new(year, month, day, 0, 0, 0, 0, Date::ITALY)
101
- when :proleptic_gregorian
102
- time = DateTime.new(year, month, day, 0, 0, 0, 0, Date::GREGORIAN)
103
- when :proleptic_julian
104
- time = DateTime.new(year, month, day, 0, 0, 0, 0, Date::JULIAN)
105
- when :noleap
106
- time = DateTime::NoLeap.new(year, month, day, 0, 0, 0, 0)
107
- when :allleap
108
- time = DateTime::AllLeap.new(year, month, day, 0, 0, 0, 0)
109
- when SYM_360_day
110
- time = DateTime::Fixed360Day.new(year, month, day, 0, 0, 0, 0)
111
- end
112
- return time.jd
136
+ return time_new(year, month, day).jd
113
137
  end
114
138
 
115
139
  end
116
140
 
117
141
  end
142
+
@@ -2,16 +2,15 @@ require "timesteps"
2
2
 
3
3
  class TimeStep::Converter
4
4
 
5
- def initialize (reference, name: "reference", calendar: "standard", bc: false)
5
+ def initialize (reference, name: "reference", calendar: "standard")
6
6
  @calendar = calendar
7
- @bc = bc
8
7
  case reference
9
8
  when String
10
- @reference = TimeStep.new(reference, calendar: calendar, bc: bc)
9
+ @reference = TimeStep.new(reference, calendar: calendar)
11
10
  when TimeStep
12
11
  @reference = reference
13
12
  else
14
- raise "first argument should be TimeStep or string"
13
+ raise ArgumentError, "reference argument '#{reference}' should be time-step or string"
15
14
  end
16
15
  @pair = {}
17
16
  @target = {}
@@ -21,9 +20,9 @@ class TimeStep::Converter
21
20
  # Append or reset new target time step for the given name.
22
21
  #
23
22
  def add_timestep (spec, name:)
24
- @pair[name] = TimeStep::Pair.new(@reference, spec, calendar: @calendar, bc: @bc)
23
+ @pair[name] = TimeStep::Pair.new(@reference, spec, calendar: @calendar)
25
24
  @target[name] = @pair[name].to
26
- return @target[name]
25
+ return @pair[name]
27
26
  end
28
27
 
29
28
  # Append or reset new target time step for the given name.
@@ -35,14 +34,14 @@ class TimeStep::Converter
35
34
  # Return target time step of the given name.
36
35
  #
37
36
  def [] (name)
38
- return @target[name]
37
+ return @pair[name]
39
38
  end
40
39
 
41
40
  # Relete target of the given name.
42
41
  #
43
42
  def delete (name)
44
- @pair.delete(name)
45
43
  @target.delete(name)
44
+ @pair.delete(name)
46
45
  end
47
46
 
48
47
  # Returns the time represented by the given index as DateTime object
@@ -56,11 +55,18 @@ class TimeStep::Converter
56
55
 
57
56
  # Converts index refering `from` timestep to index refering `to`timestep.
58
57
  #
59
- def forward (*indices, with_time: false)
58
+ def forward (*indices, with_time: nil)
60
59
  if with_time
61
- hash = {
62
- "time" => time_at(*indices)
63
- }
60
+ case with_time
61
+ when String
62
+ hash = {
63
+ "time" => time_at(*indices).map{|t| t.strftime(with_time) }
64
+ }
65
+ else
66
+ hash = {
67
+ "time" => time_at(*indices)
68
+ }
69
+ end
64
70
  else
65
71
  hash = {}
66
72
  end
@@ -70,8 +76,8 @@ class TimeStep::Converter
70
76
  return hash
71
77
  end
72
78
 
73
- def range (ge: nil, gt: nil, lt: nil, le: nil, with_time: false)
74
- indices = @reference.range(ge: ge, gt: gt, lt: lt, le: le)
79
+ def range (other, with_time: false)
80
+ indices = @reference.range(other)
75
81
  return forward(*indices, with_time: with_time)
76
82
  end
77
83
 
@@ -80,14 +86,5 @@ class TimeStep::Converter
80
86
  return forward(*indices, with_time: with_time)
81
87
  end
82
88
 
83
- def valid? (*indices)
84
- hash = {}
85
- @pair.each do |name, conv|
86
- hash[name] = conv.to.valid?(*conv.forward(*indices))
87
- end
88
- return hash
89
- end
90
-
91
-
92
89
  end
93
90
 
@@ -46,28 +46,18 @@ class TimeStep
46
46
  #
47
47
  # @return [Integer]
48
48
  def difference_in_years (other)
49
- extra = 0
50
- if self.year > other.year && self.compare_md(other) < 0
51
- extra = -1
52
- end
53
- if self.year < other.year && self.compare_md(other) > 0
54
- extra = 1
55
- end
56
- return self.year - other.year + extra
49
+ my = self
50
+ other = other.new_offset(self.offset)
51
+ return my.year - other.year + my.compare_md(other).quo(2)
57
52
  end
58
53
 
59
54
  # Calculate difference between the object and other object in months.
60
55
  #
61
56
  # @return [Integer]
62
57
  def difference_in_months (other)
63
- extra = 0
64
- if self.month > other.month && self.compare_d(other) < 0
65
- extra = -1
66
- end
67
- if self.month < other.month && self.compare_d(other) > 0
68
- extra = 1
69
- end
70
- return 12*(self.year - other.year) + self.month - other.month + extra
58
+ my = self
59
+ other = other.new_offset(self.offset)
60
+ return 12*(my.year - other.year) + my.month - other.month + my.compare_d(other).quo(2)
71
61
  end
72
62
 
73
63
  end
@@ -8,34 +8,32 @@ class TimeStep::Pair
8
8
  # The `from` and `to` arguments are either TimeStep or a string representing
9
9
  # the time step definition ("<INTERVAL> since <TIME>").
10
10
  # If a string representing the time step definition is given as an argument
11
- # `from` or `to`, the options `calendar` and `bc` are used to construct
11
+ # `from` or `to`, the options `calendar` are used to construct
12
12
  # TimeStep. The option `calendar` specifies the calendar for datetime
13
- # calculation. The option `bc` is a flag whether AD/BC or EC when parsing
14
- # timestamp for negative year.
13
+ # calculation.
15
14
  #
16
15
  # @param from [TimeStep, String]
17
16
  # @param to [TimeStep, String]
18
17
  # @option calendar [String, TimeStep::Calendar]
19
- # @option bc [Boolean]
20
18
  #
21
- def initialize (from, to, calendar: "standard", bc: false)
19
+ def initialize (from, to, calendar: "standard")
22
20
 
23
21
  case from
24
22
  when String
25
- @from = TimeStep.new(from, calendar: calendar, bc: bc)
23
+ @from = TimeStep.new(from, calendar: calendar)
26
24
  when TimeStep
27
25
  @from = from
28
26
  else
29
- raise "first argument should be TimeStep or string"
27
+ raise ArgumentError, "from argument '#{from}' should be a time-step or a string"
30
28
  end
31
29
 
32
30
  case to
33
31
  when String
34
- @to = TimeStep.new(to, calendar: calendar, bc: bc)
32
+ @to = TimeStep.new(to, calendar: calendar)
35
33
  when TimeStep
36
34
  @to = to
37
35
  else
38
- raise "second argument should be TimeStep or string"
36
+ raise ArgumentError, "to argument '#{to}' should be a time-step or a string"
39
37
  end
40
38
 
41
39
  @from_origin = @from.origin
@@ -66,13 +64,17 @@ class TimeStep::Pair
66
64
  "#<TimeStep::Pair from='#{from.inspect}' to='#{to.inspect}'>"
67
65
  end
68
66
 
67
+ def invert
68
+ return TimeStep::Pair.new(@to, @from)
69
+ end
70
+
69
71
  # Returns true if other has same contents of `from` and `to` as self has.
70
72
  #
71
73
  # @return [Boolean]
72
74
  def == (other)
73
75
  return @from == other.from && @to == other.to
74
76
  end
75
-
77
+
76
78
  # Returns the time represented by the given index as DateTime object
77
79
  #
78
80
  # @param indices [Numeric,Array<Numeric>]
@@ -85,10 +87,16 @@ class TimeStep::Pair
85
87
  # Converts index refering `from` timestep to index refering `to`timestep.
86
88
  #
87
89
  def forward (*indices, &block)
88
- if indices.size == 1
90
+ if indices.empty? || indices.size == 1 && indices.first.is_a?(Range)
91
+ return forward(*@from.range(*indices), &block)
92
+ elsif indices.size == 1
89
93
  index = indices.first.to_r
90
94
  if @numeric_conversion
91
- value = (index*@from_interval + @difference).quo(@to_interval)
95
+ if @to_interval == 0
96
+ value = 0
97
+ else
98
+ value = (index*@from_interval + @difference).quo(@to_interval)
99
+ end
92
100
  else
93
101
  case @from_symbol
94
102
  when :years, :months
@@ -97,12 +105,14 @@ class TimeStep::Pair
97
105
  target = @from_origin + index*(@from_interval.quo(86400))
98
106
  end
99
107
  case @to_symbol
100
- when :years
101
- value = target.difference_in_years(@to_origin).quo(@to.numeric)
108
+ when :years
109
+ diff = target.difference_in_years(@to_origin)
110
+ value = diff.quo(@to.numeric)
102
111
  when :months
103
- value = target.difference_in_months(@to_origin).quo(@to.numeric)
112
+ diff = target.difference_in_months(@to_origin)
113
+ value = diff.quo(@to.numeric)
104
114
  else
105
- value = (target.ajd - @to_origin.ajd).quo(@to_interval)
115
+ value = (target.ajd - @to_origin.ajd).quo(@to_interval)*86400
106
116
  end
107
117
  end
108
118
  value = value.to_i if value.denominator == 1
@@ -126,18 +136,19 @@ class TimeStep::Pair
126
136
  return forward(*indices) {|index| @to.time_at(index) }
127
137
  end
128
138
 
129
- def range (ge: nil, gt: nil, lt: nil, le: nil)
130
- indices = @from.range(ge: ge, gt: gt, lt: lt, le: le)
131
- return forward(*indices)
132
- end
133
-
134
139
  # Converts index refering `to` timestep to index refering `from` timestep.
135
140
  #
136
141
  def inverse (*indices, &block)
137
- if indices.size == 1
142
+ if indices.empty? || indices.size == 1 && indices.first.is_a?(Range)
143
+ return inverse(*@to.range(*indices), &block)
144
+ elsif indices.size == 1
138
145
  index = indices.first.to_r
139
146
  if @numeric_conversion
140
- value = (index*@to_interval - @difference).quo(@from_interval)
147
+ if @from_interval == 0
148
+ value = 0
149
+ else
150
+ value = (index*@to_interval - @difference).quo(@from_interval)
151
+ end
141
152
  else
142
153
  case @to_symbol
143
154
  when :years, :months
@@ -147,11 +158,13 @@ class TimeStep::Pair
147
158
  end
148
159
  case @from_symbol
149
160
  when :years
150
- value = target.difference_in_years(@from_origin).quo(@from.numeric)
161
+ diff = target.difference_in_years(@from_origin)
162
+ value = diff.quo(@from.numeric)
151
163
  when :months
152
- value = target.difference_in_months(@from_origin).quo(@from.numeric)
164
+ diff = target.difference_in_months(@from_origin)
165
+ value = diff.quo(@from.numeric)
153
166
  else
154
- value = (target.ajd - @from_origin.ajd).quo(@from_interval)
167
+ value = (target.ajd - @from_origin.ajd).quo(@from_interval)*86400
155
168
  end
156
169
  end
157
170
  value = value.to_i if value.denominator == 1
@@ -0,0 +1,57 @@
1
+ require "timesteps"
2
+
3
+ class TimeStep::Query
4
+
5
+ def initialize (timestep, format: nil)
6
+ @timestep = timestep
7
+ @format = format
8
+ end
9
+
10
+ def __format__ (time)
11
+ return ( @format.nil? ) ? time : time.strftime(@format)
12
+ end
13
+
14
+ private :__format__
15
+
16
+ def just_time? (time)
17
+ return @timestep.index_at(time).denominator == 1
18
+ end
19
+
20
+ def just_before_time_of (time)
21
+ idx = @timestep.index_at(time)
22
+ if idx.denominator == 1
23
+ time0 = time
24
+ else
25
+ time0 = @timestep.time_at(idx.floor)
26
+ end
27
+ return __format__(time0)
28
+ end
29
+
30
+ def just_after_time_of (time)
31
+ idx = @timestep.index_at(time)
32
+ if idx.denominator == 1
33
+ time0 = time
34
+ else
35
+ time0 = @timestep.time_at(idx.ceil)
36
+ end
37
+ return __format__(time0)
38
+ end
39
+
40
+ def prev_time_of (time)
41
+ return __format__(@timestep.prev_time_of(time))
42
+ end
43
+
44
+ def next_time_of (time)
45
+ return __format__(@timestep.next_time_of(time))
46
+ end
47
+
48
+ end
49
+
50
+ class TimeStep
51
+
52
+ def query (format = nil)
53
+ return TimeStep::Query.new(self, format: format)
54
+ end
55
+
56
+ end
57
+
@@ -0,0 +1,153 @@
1
+ require "forwardable"
2
+
3
+ class TimeStep::Range
4
+
5
+ extend Forwardable
6
+
7
+ def initialize (timestep, start = nil, last = nil, count: nil, ends: "[]")
8
+
9
+ raise "'ends' option should be one of '[]', '()', '(]', '[)" unless ends =~ /\A[\[\(][\]\)]\z/
10
+ include_start = ends[0] == "["
11
+ include_last = ends[1] == "]"
12
+
13
+ if last
14
+ case start
15
+ when Numeric
16
+ when Time
17
+ start = timestep.index_at(start.to_datetime)
18
+ when DateTime, DateTimeLike, String
19
+ start = timestep.index_at(start)
20
+ else
21
+ raise "unknown type of argument"
22
+ end
23
+ case last
24
+ when Numeric
25
+ when Time
26
+ last = timestep.index_at(last.to_datetime)
27
+ when DateTime, DateTimeLike, String
28
+ last = timestep.index_at(last)
29
+ else
30
+ raise "unknown argument"
31
+ end
32
+ else
33
+ case start
34
+ when Integer
35
+ count = start
36
+ start = 0
37
+ last = count - 1
38
+ when String
39
+ raise "count should be set" unless count.is_a?(Integer)
40
+ start = timestep.index_at(start)
41
+ last = start + count - 1
42
+ when TimePeriod
43
+ period = range
44
+ return initialize(timestep, period.origin..period.last, ends: period.ends)
45
+ else
46
+ raise "unknown argument"
47
+ end
48
+ end
49
+
50
+ if include_start
51
+ @start = start.ceil
52
+ else
53
+ @start = start.floor + 1
54
+ end
55
+
56
+ if include_last
57
+ @last = last.floor
58
+ else
59
+ @last = last.ceil - 1
60
+ end
61
+
62
+ @timestep = timestep.new_origin(timestep.time_at(@start))
63
+ @count = @last - @start + 1
64
+
65
+ @start_time = @timestep.time_at(@start)
66
+ @last_time = @timestep.time_at(@last)
67
+ end
68
+
69
+ attr_reader :timestep, :count, :start, :last, :start_time, :last_time
70
+
71
+ def_delegators :@timestep, :numeric, :symbol, :interval,
72
+ :origin, :offset, :calendar, :definition,
73
+ :parse, :time_at, :index_at, :duration_at, :[]
74
+
75
+
76
+ # FIXME
77
+ def valid? (*indices)
78
+ if @count
79
+ if indices.size == 1
80
+ index = indices.first
81
+ if index >= 0 and index < @count
82
+ return true
83
+ else
84
+ return false
85
+ end
86
+ else
87
+ return indices.map{|index| valid?(index) }
88
+ end
89
+ else
90
+ if indices.size == 1
91
+ return true
92
+ else
93
+ return indices.map{|index| true }
94
+ end
95
+ end
96
+ end
97
+
98
+ # Returns TimeStep object which has origin time determined
99
+ # by the given `index`.
100
+ #
101
+ # @param index [Numeric]
102
+ #
103
+ # @return [TimeStep]
104
+ def shift_origin (index)
105
+ case with
106
+ when :index, "index"
107
+ timestep = @timestep.shift_origin(index)
108
+ return TimeStep::Range.new(timestep, count: @count)
109
+ when :duration, "duration", :days, "days"
110
+ timestep = @timestep.shift_origin(index, with: "duration")
111
+ return TimeStep::Range.new(timestep, count: @count)
112
+ end
113
+ end
114
+
115
+ # Returns TimeStep object which has origin time specified
116
+ # by the given `time`.
117
+ #
118
+ # @param time [DateTime]
119
+ #
120
+ # @return [TimeStep]
121
+ def new_origin (time)
122
+ timestep = @timestep.new_origin(time)
123
+ return TimeStep::Range.new(timestep, count: @count)
124
+ end
125
+
126
+ include Enumerable
127
+
128
+ def each_time (&block)
129
+ if block
130
+ @start.upto(@last) do |k|
131
+ block.call(@timestep.time_at(k))
132
+ end
133
+ else
134
+ return Enumerator.new {|y|
135
+ @start.upto(@last) do |k|
136
+ y << @timestep.time_at(k)
137
+ end
138
+ }
139
+ end
140
+ end
141
+
142
+ alias each each_time
143
+
144
+ def each_index (&block)
145
+ (@start..@last).each(&block)
146
+ end
147
+
148
+ def map_index (&block)
149
+ (@start..@last).map(&block)
150
+ end
151
+
152
+ end
153
+
data/lib/timesteps.rb CHANGED
@@ -1,13 +1,18 @@
1
1
 
2
2
  require "date"
3
+ require "timesteps/time"
3
4
  require "timesteps/datetimelike"
5
+ require "timesteps/datetimelike_format"
4
6
  require "timesteps/datetime_noleap"
5
7
  require "timesteps/datetime_allleap"
6
8
  require "timesteps/datetime_360day"
7
- require "timesteps/parse_timestamp"
8
- require "timesteps/format"
9
+ require "timesteps/datetime_parse_timestamp"
9
10
  require "timesteps/timestep_datetime_ext"
10
- require "timesteps/calendar"
11
+ require "timesteps/timestep_calendar"
11
12
  require "timesteps/timestep"
12
13
  require "timesteps/timestep_pair"
13
14
  require "timesteps/timestep_converter"
15
+ require "timesteps/timeperiod"
16
+ require "timesteps/timestep_range"
17
+ require "timesteps/datetime_timestep"
18
+ require "timesteps/timestep_query"
data/spec/allleap_spec.rb CHANGED
@@ -123,17 +123,6 @@ describe "AllLeap.parse_timestamp" do
123
123
 
124
124
  end
125
125
 
126
- example "with bc option" do
127
-
128
- d1 = DateTime::AllLeap.new(0, 1, 1, 0, 0, 0, 0)
129
- d2 = DateTime.parse_timestamp('BC 0001-1-1 00:00:00', calendar: "allleap")
130
- d3 = DateTime.parse_timestamp('-0001-1-1 00:00:00', calendar: "allleap", bc: true)
131
-
132
- is_asserted_by { d2 == d1 }
133
- is_asserted_by { d3 == d1 }
134
-
135
- end
136
-
137
126
  example '2000-12-31 24:00:00' do
138
127
 
139
128
  d1 = DateTime::AllLeap.new(2001, 1, 1, 0, 0, 0, 0)
@@ -301,8 +290,8 @@ describe "AllLeap#difference_in_years" do
301
290
  d3 = DateTime::AllLeap.new(2002, 3, 5, 9, 35, 11.5, 9.quo(24))
302
291
  d4 = DateTime::AllLeap.new(2002, 3, 5, 9, 35, 12.5, 9.quo(24))
303
292
 
304
- is_asserted_by { d2.difference_in_years(d1) == 0 }
305
- is_asserted_by { d3.difference_in_years(d1) == 0 }
293
+ is_asserted_by { d2.difference_in_years(d1) == 1.quo(2) }
294
+ is_asserted_by { d3.difference_in_years(d1) == 1.quo(2) }
306
295
  is_asserted_by { d4.difference_in_years(d1) == 1 }
307
296
 
308
297
  end
@@ -319,8 +308,8 @@ describe "AllLeap#difference_in_months" do
319
308
  d4 = DateTime::AllLeap.new(2001, 4, 5, 9, 35, 12.5, 9.quo(24))
320
309
  d5 = DateTime::AllLeap.new(2002, 3, 5, 9, 35, 12.5, 9.quo(24))
321
310
 
322
- is_asserted_by { d2.difference_in_months(d1) == 0 }
323
- is_asserted_by { d3.difference_in_months(d1) == 0 }
311
+ is_asserted_by { d2.difference_in_months(d1) == 1.quo(2) }
312
+ is_asserted_by { d3.difference_in_months(d1) == 1.quo(2) }
324
313
  is_asserted_by { d4.difference_in_months(d1) == 1 }
325
314
  is_asserted_by { d5.difference_in_months(d1) == 12 }
326
315