chronic 0.2.2 → 0.10.2

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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.travis.yml +8 -0
  4. data/HISTORY.md +243 -0
  5. data/LICENSE +21 -0
  6. data/README.md +182 -0
  7. data/Rakefile +62 -15
  8. data/chronic.gemspec +23 -0
  9. data/lib/chronic/date.rb +82 -0
  10. data/lib/chronic/grabber.rb +24 -17
  11. data/lib/chronic/handler.rb +97 -0
  12. data/lib/chronic/handlers.rb +490 -312
  13. data/lib/chronic/mini_date.rb +38 -0
  14. data/lib/chronic/numerizer.rb +130 -0
  15. data/lib/chronic/ordinal.rb +33 -24
  16. data/lib/chronic/parser.rb +268 -0
  17. data/lib/chronic/pointer.rb +22 -17
  18. data/lib/chronic/repeater.rb +137 -107
  19. data/lib/chronic/repeaters/repeater_day.rb +51 -44
  20. data/lib/chronic/repeaters/repeater_day_name.rb +50 -43
  21. data/lib/chronic/repeaters/repeater_day_portion.rb +97 -81
  22. data/lib/chronic/repeaters/repeater_fortnight.rb +61 -54
  23. data/lib/chronic/repeaters/repeater_hour.rb +51 -44
  24. data/lib/chronic/repeaters/repeater_minute.rb +51 -44
  25. data/lib/chronic/repeaters/repeater_month.rb +79 -60
  26. data/lib/chronic/repeaters/repeater_month_name.rb +85 -83
  27. data/lib/chronic/repeaters/repeater_season.rb +110 -22
  28. data/lib/chronic/repeaters/repeater_season_name.rb +41 -22
  29. data/lib/chronic/repeaters/repeater_second.rb +41 -34
  30. data/lib/chronic/repeaters/repeater_time.rb +128 -104
  31. data/lib/chronic/repeaters/repeater_week.rb +65 -58
  32. data/lib/chronic/repeaters/repeater_weekday.rb +86 -0
  33. data/lib/chronic/repeaters/repeater_weekend.rb +59 -52
  34. data/lib/chronic/repeaters/repeater_year.rb +72 -52
  35. data/lib/chronic/scalar.rb +53 -46
  36. data/lib/chronic/season.rb +26 -0
  37. data/lib/chronic/separator.rb +174 -43
  38. data/lib/chronic/sign.rb +49 -0
  39. data/lib/chronic/span.rb +31 -0
  40. data/lib/chronic/tag.rb +37 -0
  41. data/lib/chronic/time.rb +40 -0
  42. data/lib/chronic/time_zone.rb +21 -11
  43. data/lib/chronic/token.rb +51 -0
  44. data/lib/chronic.rb +94 -69
  45. data/test/helper.rb +12 -0
  46. data/test/test_chronic.rb +183 -0
  47. data/test/test_daylight_savings.rb +118 -0
  48. data/test/{test_Handler.rb → test_handler.rb} +73 -55
  49. data/test/test_mini_date.rb +32 -0
  50. data/test/test_numerizer.rb +86 -0
  51. data/test/test_parsing.rb +891 -248
  52. data/test/{test_RepeaterDayName.rb → test_repeater_day_name.rb} +16 -17
  53. data/test/test_repeater_day_portion.rb +254 -0
  54. data/test/{test_RepeaterFortnight.rb → test_repeater_fortnight.rb} +17 -18
  55. data/test/{test_RepeaterHour.rb → test_repeater_hour.rb} +23 -20
  56. data/test/test_repeater_minute.rb +34 -0
  57. data/test/{test_RepeaterMonth.rb → test_repeater_month.rb} +21 -18
  58. data/test/{test_RepeaterMonthName.rb → test_repeater_month_name.rb} +18 -19
  59. data/test/test_repeater_season.rb +40 -0
  60. data/test/{test_RepeaterTime.rb → test_repeater_time.rb} +39 -23
  61. data/test/{test_RepeaterWeek.rb → test_repeater_week.rb} +17 -18
  62. data/test/test_repeater_weekday.rb +55 -0
  63. data/test/{test_RepeaterWeekend.rb → test_repeater_weekend.rb} +22 -23
  64. data/test/{test_RepeaterYear.rb → test_repeater_year.rb} +25 -19
  65. data/test/{test_Span.rb → test_span.rb} +7 -8
  66. data/test/{test_Token.rb → test_token.rb} +6 -7
  67. metadata +163 -85
  68. data/History.txt +0 -49
  69. data/Manifest.txt +0 -47
  70. data/README.txt +0 -149
  71. data/lib/chronic/chronic.rb +0 -239
  72. data/lib/numerizer/numerizer.rb +0 -103
  73. data/test/suite.rb +0 -9
  74. data/test/test_Chronic.rb +0 -50
  75. data/test/test_Numerizer.rb +0 -48
  76. data/test/test_Time.rb +0 -50
@@ -1,239 +0,0 @@
1
- module Chronic
2
- class << self
3
-
4
- # Parses a string containing a natural language date or time. If the parser
5
- # can find a date or time, either a Time or Chronic::Span will be returned
6
- # (depending on the value of <tt>:guess</tt>). If no date or time can be found,
7
- # +nil+ will be returned.
8
- #
9
- # Options are:
10
- #
11
- # [<tt>:context</tt>]
12
- # <tt>:past</tt> or <tt>:future</tt> (defaults to <tt>:future</tt>)
13
- #
14
- # If your string represents a birthday, you can set <tt>:context</tt> to <tt>:past</tt>
15
- # and if an ambiguous string is given, it will assume it is in the
16
- # past. Specify <tt>:future</tt> or omit to set a future context.
17
- #
18
- # [<tt>:now</tt>]
19
- # Time (defaults to Time.now)
20
- #
21
- # By setting <tt>:now</tt> to a Time, all computations will be based off
22
- # of that time instead of Time.now
23
- #
24
- # [<tt>:guess</tt>]
25
- # +true+ or +false+ (defaults to +true+)
26
- #
27
- # By default, the parser will guess a single point in time for the
28
- # given date or time. If you'd rather have the entire time span returned,
29
- # set <tt>:guess</tt> to +false+ and a Chronic::Span will be returned.
30
- #
31
- # [<tt>:ambiguous_time_range</tt>]
32
- # Integer or <tt>:none</tt> (defaults to <tt>6</tt> (6am-6pm))
33
- #
34
- # If an Integer is given, ambiguous times (like 5:00) will be
35
- # assumed to be within the range of that time in the AM to that time
36
- # in the PM. For example, if you set it to <tt>7</tt>, then the parser will
37
- # look for the time between 7am and 7pm. In the case of 5:00, it would
38
- # assume that means 5:00pm. If <tt>:none</tt> is given, no assumption
39
- # will be made, and the first matching instance of that time will
40
- # be used.
41
- def parse(text, specified_options = {})
42
- # get options and set defaults if necessary
43
- default_options = {:context => :future,
44
- :now => Time.now,
45
- :guess => true,
46
- :ambiguous_time_range => 6}
47
- options = default_options.merge specified_options
48
-
49
- # ensure the specified options are valid
50
- specified_options.keys.each do |key|
51
- default_options.keys.include?(key) || raise(InvalidArgumentException, "#{key} is not a valid option key.")
52
- end
53
- [:past, :future, :none].include?(options[:context]) || raise(InvalidArgumentException, "Invalid value ':#{options[:context]}' for :context specified. Valid values are :past and :future.")
54
-
55
- # store now for later =)
56
- @now = options[:now]
57
-
58
- # put the text into a normal format to ease scanning
59
- text = self.pre_normalize(text)
60
-
61
- # get base tokens for each word
62
- @tokens = self.base_tokenize(text)
63
-
64
- # scan the tokens with each token scanner
65
- [Repeater].each do |tokenizer|
66
- @tokens = tokenizer.scan(@tokens, options)
67
- end
68
-
69
- [Grabber, Pointer, Scalar, Ordinal, Separator, TimeZone].each do |tokenizer|
70
- @tokens = tokenizer.scan(@tokens)
71
- end
72
-
73
- # strip any non-tagged tokens
74
- @tokens = @tokens.select { |token| token.tagged? }
75
-
76
- if Chronic.debug
77
- puts "+---------------------------------------------------"
78
- puts "| " + @tokens.to_s
79
- puts "+---------------------------------------------------"
80
- end
81
-
82
- # do the heavy lifting
83
- begin
84
- span = self.tokens_to_span(@tokens, options)
85
- rescue
86
- raise
87
- return nil
88
- end
89
-
90
- # guess a time within a span if required
91
- if options[:guess]
92
- return self.guess(span)
93
- else
94
- return span
95
- end
96
- end
97
-
98
- # Clean up the specified input text by stripping unwanted characters,
99
- # converting idioms to their canonical form, converting number words
100
- # to numbers (three => 3), and converting ordinal words to numeric
101
- # ordinals (third => 3rd)
102
- def pre_normalize(text) #:nodoc:
103
- normalized_text = text.to_s.downcase
104
- normalized_text = numericize_numbers(normalized_text)
105
- normalized_text.gsub!(/['"\.]/, '')
106
- normalized_text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
107
- normalized_text.gsub!(/\btoday\b/, 'this day')
108
- normalized_text.gsub!(/\btomm?orr?ow\b/, 'next day')
109
- normalized_text.gsub!(/\byesterday\b/, 'last day')
110
- normalized_text.gsub!(/\bnoon\b/, '12:00')
111
- normalized_text.gsub!(/\bmidnight\b/, '24:00')
112
- normalized_text.gsub!(/\bbefore now\b/, 'past')
113
- normalized_text.gsub!(/\bnow\b/, 'this second')
114
- normalized_text.gsub!(/\b(ago|before)\b/, 'past')
115
- normalized_text.gsub!(/\bthis past\b/, 'last')
116
- normalized_text.gsub!(/\bthis last\b/, 'last')
117
- normalized_text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
118
- normalized_text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
119
- normalized_text.gsub!(/\btonight\b/, 'this night')
120
- normalized_text.gsub!(/(?=\w)([ap]m|oclock)\b/, ' \1')
121
- normalized_text.gsub!(/\b(hence|after|from)\b/, 'future')
122
- normalized_text = numericize_ordinals(normalized_text)
123
- end
124
-
125
- # Convert number words to numbers (three => 3)
126
- def numericize_numbers(text) #:nodoc:
127
- Numerizer.numerize(text)
128
- end
129
-
130
- # Convert ordinal words to numeric ordinals (third => 3rd)
131
- def numericize_ordinals(text) #:nodoc:
132
- text
133
- end
134
-
135
- # Split the text on spaces and convert each word into
136
- # a Token
137
- def base_tokenize(text) #:nodoc:
138
- text.split(' ').map { |word| Token.new(word) }
139
- end
140
-
141
- # Guess a specific time within the given span
142
- def guess(span) #:nodoc:
143
- return nil if span.nil?
144
- if span.width > 1
145
- span.begin + (span.width / 2)
146
- else
147
- span.begin
148
- end
149
- end
150
- end
151
-
152
- class Token #:nodoc:
153
- attr_accessor :word, :tags
154
-
155
- def initialize(word)
156
- @word = word
157
- @tags = []
158
- end
159
-
160
- # Tag this token with the specified tag
161
- def tag(new_tag)
162
- @tags << new_tag
163
- end
164
-
165
- # Remove all tags of the given class
166
- def untag(tag_class)
167
- @tags = @tags.select { |m| !m.kind_of? tag_class }
168
- end
169
-
170
- # Return true if this token has any tags
171
- def tagged?
172
- @tags.size > 0
173
- end
174
-
175
- # Return the Tag that matches the given class
176
- def get_tag(tag_class)
177
- matches = @tags.select { |m| m.kind_of? tag_class }
178
- #matches.size < 2 || raise("Multiple identical tags found")
179
- return matches.first
180
- end
181
-
182
- # Print this Token in a pretty way
183
- def to_s
184
- @word << '(' << @tags.join(', ') << ') '
185
- end
186
- end
187
-
188
- # A Span represents a range of time. Since this class extends
189
- # Range, you can use #begin and #end to get the beginning and
190
- # ending times of the span (they will be of class Time)
191
- class Span < Range
192
- # Returns the width of this span in seconds
193
- def width
194
- (self.end - self.begin).to_i
195
- end
196
-
197
- # Add a number of seconds to this span, returning the
198
- # resulting Span
199
- def +(seconds)
200
- Span.new(self.begin + seconds, self.end + seconds)
201
- end
202
-
203
- # Subtract a number of seconds to this span, returning the
204
- # resulting Span
205
- def -(seconds)
206
- self + -seconds
207
- end
208
-
209
- # Prints this span in a nice fashion
210
- def to_s
211
- '(' << self.begin.to_s << '..' << self.end.to_s << ')'
212
- end
213
- end
214
-
215
- # Tokens are tagged with subclassed instances of this class when
216
- # they match specific criteria
217
- class Tag #:nodoc:
218
- attr_accessor :type
219
-
220
- def initialize(type)
221
- @type = type
222
- end
223
-
224
- def start=(s)
225
- @now = s
226
- end
227
- end
228
-
229
- # Internal exception
230
- class ChronicPain < Exception #:nodoc:
231
-
232
- end
233
-
234
- # This exception is raised if an invalid argument is provided to
235
- # any of Chronic's methods
236
- class InvalidArgumentException < Exception
237
-
238
- end
239
- end
@@ -1,103 +0,0 @@
1
- require 'strscan'
2
-
3
- class Numerizer
4
-
5
- DIRECT_NUMS = [
6
- ['eleven', '11'],
7
- ['twelve', '12'],
8
- ['thirteen', '13'],
9
- ['fourteen', '14'],
10
- ['fifteen', '15'],
11
- ['sixteen', '16'],
12
- ['seventeen', '17'],
13
- ['eighteen', '18'],
14
- ['nineteen', '19'],
15
- ['ninteen', '19'], # Common mis-spelling
16
- ['zero', '0'],
17
- ['one', '1'],
18
- ['two', '2'],
19
- ['three', '3'],
20
- ['four(\W|$)', '4\1'], # The weird regex is so that it matches four but not fourty
21
- ['five', '5'],
22
- ['six(\W|$)', '6\1'],
23
- ['seven(\W|$)', '7\1'],
24
- ['eight(\W|$)', '8\1'],
25
- ['nine(\W|$)', '9\1'],
26
- ['ten', '10'],
27
- ['\ba[\b^$]', '1'] # doesn't make sense for an 'a' at the end to be a 1
28
- ]
29
-
30
- TEN_PREFIXES = [ ['twenty', 20],
31
- ['thirty', 30],
32
- ['fourty', 40],
33
- ['fifty', 50],
34
- ['sixty', 60],
35
- ['seventy', 70],
36
- ['eighty', 80],
37
- ['ninety', 90]
38
- ]
39
-
40
- BIG_PREFIXES = [ ['hundred', 100],
41
- ['thousand', 1000],
42
- ['million', 1_000_000],
43
- ['billion', 1_000_000_000],
44
- ['trillion', 1_000_000_000_000],
45
- ]
46
-
47
- class << self
48
- def numerize(string)
49
- string = string.dup
50
-
51
- # preprocess
52
- string.gsub!(/ +|([^\d])-([^d])/, '\1 \2') # will mutilate hyphenated-words but shouldn't matter for date extraction
53
- string.gsub!(/a half/, 'haAlf') # take the 'a' out so it doesn't turn into a 1, save the half for the end
54
-
55
- # easy/direct replacements
56
-
57
- DIRECT_NUMS.each do |dn|
58
- string.gsub!(/#{dn[0]}/i, dn[1])
59
- end
60
-
61
- # ten, twenty, etc.
62
-
63
- TEN_PREFIXES.each do |tp|
64
- string.gsub!(/(?:#{tp[0]})( *\d(?=[^\d]|$))*/i) { (tp[1] + $1.to_i).to_s }
65
- end
66
-
67
- # hundreds, thousands, millions, etc.
68
-
69
- BIG_PREFIXES.each do |bp|
70
- string.gsub!(/(\d*) *#{bp[0]}/i) { (bp[1] * $1.to_i).to_s}
71
- andition(string)
72
- #combine_numbers(string) # Should to be more efficient way to do this
73
- end
74
-
75
- # fractional addition
76
- # I'm not combining this with the previous block as using float addition complicates the strings
77
- # (with extraneous .0's and such )
78
- string.gsub!(/(\d+)(?: | and |-)*haAlf/i) { ($1.to_f + 0.5).to_s }
79
-
80
- string
81
- end
82
-
83
- private
84
- def andition(string)
85
- sc = StringScanner.new(string)
86
- while(sc.scan_until(/(\d+)( | and )(\d+)(?=[^\w]|$)/i))
87
- if sc[2] =~ /and/ || sc[1].size > sc[3].size
88
- string[(sc.pos - sc.matched_size)..(sc.pos-1)] = (sc[1].to_i + sc[3].to_i).to_s
89
- sc.reset
90
- end
91
- end
92
- end
93
-
94
- # def combine_numbers(string)
95
- # sc = StringScanner.new(string)
96
- # while(sc.scan_until(/(\d+)(?: | and |-)(\d+)(?=[^\w]|$)/i))
97
- # string[(sc.pos - sc.matched_size)..(sc.pos-1)] = (sc[1].to_i + sc[2].to_i).to_s
98
- # sc.reset
99
- # end
100
- # end
101
-
102
- end
103
- end
data/test/suite.rb DELETED
@@ -1,9 +0,0 @@
1
- require 'test/unit'
2
-
3
- tests = Dir["#{File.dirname(__FILE__)}/test_*.rb"]
4
- tests.delete_if { |o| o =~ /test_parsing/ }
5
- tests.each do |file|
6
- require file
7
- end
8
-
9
- require File.dirname(__FILE__) + '/test_parsing.rb'
data/test/test_Chronic.rb DELETED
@@ -1,50 +0,0 @@
1
- require 'chronic'
2
- require 'test/unit'
3
-
4
- class TestChronic < Test::Unit::TestCase
5
-
6
- def setup
7
- # Wed Aug 16 14:00:00 UTC 2006
8
- @now = Time.local(2006, 8, 16, 14, 0, 0, 0)
9
- end
10
-
11
- def test_post_normalize_am_pm_aliases
12
- # affect wanted patterns
13
-
14
- tokens = [Chronic::Token.new("5:00"), Chronic::Token.new("morning")]
15
- tokens[0].tag(Chronic::RepeaterTime.new("5:00"))
16
- tokens[1].tag(Chronic::RepeaterDayPortion.new(:morning))
17
-
18
- assert_equal :morning, tokens[1].tags[0].type
19
-
20
- tokens = Chronic.dealias_and_disambiguate_times(tokens, {})
21
-
22
- assert_equal :am, tokens[1].tags[0].type
23
- assert_equal 2, tokens.size
24
-
25
- # don't affect unwanted patterns
26
-
27
- tokens = [Chronic::Token.new("friday"), Chronic::Token.new("morning")]
28
- tokens[0].tag(Chronic::RepeaterDayName.new(:friday))
29
- tokens[1].tag(Chronic::RepeaterDayPortion.new(:morning))
30
-
31
- assert_equal :morning, tokens[1].tags[0].type
32
-
33
- tokens = Chronic.dealias_and_disambiguate_times(tokens, {})
34
-
35
- assert_equal :morning, tokens[1].tags[0].type
36
- assert_equal 2, tokens.size
37
- end
38
-
39
- def test_guess
40
- span = Chronic::Span.new(Time.local(2006, 8, 16, 0), Time.local(2006, 8, 17, 0))
41
- assert_equal Time.local(2006, 8, 16, 12), Chronic.guess(span)
42
-
43
- span = Chronic::Span.new(Time.local(2006, 8, 16, 0), Time.local(2006, 8, 17, 0, 0, 1))
44
- assert_equal Time.local(2006, 8, 16, 12), Chronic.guess(span)
45
-
46
- span = Chronic::Span.new(Time.local(2006, 11), Time.local(2006, 12))
47
- assert_equal Time.local(2006, 11, 16), Chronic.guess(span)
48
- end
49
-
50
- end
@@ -1,48 +0,0 @@
1
- require 'test/unit'
2
- require 'chronic'
3
-
4
- class ParseNumbersTest < Test::Unit::TestCase
5
-
6
- def test_straight_parsing
7
- strings = { 1 => 'one',
8
- 5 => 'five',
9
- 10 => 'ten',
10
- 11 => 'eleven',
11
- 12 => 'twelve',
12
- 13 => 'thirteen',
13
- 14 => 'fourteen',
14
- 15 => 'fifteen',
15
- 16 => 'sixteen',
16
- 17 => 'seventeen',
17
- 18 => 'eighteen',
18
- 19 => 'nineteen',
19
- 20 => 'twenty',
20
- 27 => 'twenty seven',
21
- 31 => 'thirty-one',
22
- 59 => 'fifty nine',
23
- 100 => 'a hundred',
24
- 100 => 'one hundred',
25
- 150 => 'one hundred and fifty',
26
- # 150 => 'one fifty',
27
- 200 => 'two-hundred',
28
- 500 => '5 hundred',
29
- 999 => 'nine hundred and ninety nine',
30
- 1_000 => 'one thousand',
31
- 1_200 => 'twelve hundred',
32
- 1_200 => 'one thousand two hundred',
33
- 17_000 => 'seventeen thousand',
34
- 21_473 => 'twentyone-thousand-four-hundred-and-seventy-three',
35
- 74_002 => 'seventy four thousand and two',
36
- 99_999 => 'ninety nine thousand nine hundred ninety nine',
37
- 100_000 => '100 thousand',
38
- 250_000 => 'two hundred fifty thousand',
39
- 1_000_000 => 'one million',
40
- 1_250_007 => 'one million two hundred fifty thousand and seven',
41
- 1_000_000_000 => 'one billion',
42
- 1_000_000_001 => 'one billion and one' }
43
-
44
- strings.keys.sort.each do |key|
45
- assert_equal key, Numerizer.numerize(strings[key]).to_i
46
- end
47
- end
48
- end
data/test/test_Time.rb DELETED
@@ -1,50 +0,0 @@
1
- require 'chronic'
2
- require 'test/unit'
3
-
4
- class TestTime < Test::Unit::TestCase
5
-
6
- def setup
7
- end
8
-
9
- def test_normal
10
- assert_equal Time.local(2006, 1, 2, 0, 0, 0), Time.construct(2006, 1, 2, 0, 0, 0)
11
- assert_equal Time.local(2006, 1, 2, 3, 0, 0), Time.construct(2006, 1, 2, 3, 0, 0)
12
- assert_equal Time.local(2006, 1, 2, 3, 4, 0), Time.construct(2006, 1, 2, 3, 4, 0)
13
- assert_equal Time.local(2006, 1, 2, 3, 4, 5), Time.construct(2006, 1, 2, 3, 4, 5)
14
- end
15
-
16
- def test_second_overflow
17
- assert_equal Time.local(2006, 1, 1, 0, 1, 30), Time.construct(2006, 1, 1, 0, 0, 90)
18
- assert_equal Time.local(2006, 1, 1, 0, 5, 0), Time.construct(2006, 1, 1, 0, 0, 300)
19
- end
20
-
21
- def test_minute_overflow
22
- assert_equal Time.local(2006, 1, 1, 1, 30), Time.construct(2006, 1, 1, 0, 90)
23
- assert_equal Time.local(2006, 1, 1, 5), Time.construct(2006, 1, 1, 0, 300)
24
- end
25
-
26
- def test_hour_overflow
27
- assert_equal Time.local(2006, 1, 2, 12), Time.construct(2006, 1, 1, 36)
28
- assert_equal Time.local(2006, 1, 7), Time.construct(2006, 1, 1, 144)
29
- end
30
-
31
- def test_day_overflow
32
- assert_equal Time.local(2006, 2, 1), Time.construct(2006, 1, 32)
33
- assert_equal Time.local(2006, 3, 5), Time.construct(2006, 2, 33)
34
- assert_equal Time.local(2004, 3, 4), Time.construct(2004, 2, 33)
35
- assert_equal Time.local(2000, 3, 5), Time.construct(2000, 2, 33)
36
-
37
- assert_nothing_raised do
38
- Time.construct(2006, 1, 56)
39
- end
40
-
41
- assert_raise(RuntimeError) do
42
- Time.construct(2006, 1, 57)
43
- end
44
- end
45
-
46
- def test_month_overflow
47
- assert_equal Time.local(2006, 1), Time.construct(2005, 13)
48
- assert_equal Time.local(2005, 12), Time.construct(2000, 72)
49
- end
50
- end