chronic 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -4
  3. data/HISTORY.md +12 -0
  4. data/README.md +8 -0
  5. data/Rakefile +28 -8
  6. data/chronic.gemspec +7 -5
  7. data/lib/chronic.rb +10 -10
  8. data/lib/chronic/date.rb +82 -0
  9. data/lib/chronic/handler.rb +34 -25
  10. data/lib/chronic/handlers.rb +22 -3
  11. data/lib/chronic/ordinal.rb +22 -20
  12. data/lib/chronic/parser.rb +31 -26
  13. data/lib/chronic/repeater.rb +18 -18
  14. data/lib/chronic/repeaters/repeater_day.rb +4 -3
  15. data/lib/chronic/repeaters/repeater_day_name.rb +5 -4
  16. data/lib/chronic/repeaters/repeater_day_portion.rb +4 -3
  17. data/lib/chronic/repeaters/repeater_fortnight.rb +4 -3
  18. data/lib/chronic/repeaters/repeater_hour.rb +4 -3
  19. data/lib/chronic/repeaters/repeater_minute.rb +4 -3
  20. data/lib/chronic/repeaters/repeater_month.rb +5 -4
  21. data/lib/chronic/repeaters/repeater_month_name.rb +4 -3
  22. data/lib/chronic/repeaters/repeater_season.rb +5 -3
  23. data/lib/chronic/repeaters/repeater_second.rb +4 -3
  24. data/lib/chronic/repeaters/repeater_time.rb +35 -25
  25. data/lib/chronic/repeaters/repeater_week.rb +4 -3
  26. data/lib/chronic/repeaters/repeater_weekday.rb +4 -3
  27. data/lib/chronic/repeaters/repeater_weekend.rb +4 -3
  28. data/lib/chronic/repeaters/repeater_year.rb +5 -4
  29. data/lib/chronic/scalar.rb +34 -69
  30. data/lib/chronic/separator.rb +107 -8
  31. data/lib/chronic/sign.rb +49 -0
  32. data/lib/chronic/tag.rb +5 -4
  33. data/lib/chronic/time.rb +40 -0
  34. data/test/helper.rb +2 -2
  35. data/test/test_chronic.rb +4 -4
  36. data/test/test_handler.rb +20 -0
  37. data/test/test_parsing.rb +72 -3
  38. data/test/test_repeater_time.rb +18 -0
  39. metadata +24 -6
@@ -8,8 +8,10 @@ module Chronic
8
8
  :winter => Season.new(MiniDate.new(12,22), MiniDate.new(3,19))
9
9
  }
10
10
 
11
- def initialize(type)
11
+ def initialize(type, options = {})
12
12
  super
13
+ @next_season_start = nil
14
+ @next_season_end = nil
13
15
  end
14
16
 
15
17
  def next(pointer)
@@ -63,7 +65,7 @@ module Chronic
63
65
  private
64
66
 
65
67
  def find_next_season_span(direction, next_season)
66
- unless @next_season_start or @next_season_end
68
+ unless @next_season_start || @next_season_end
67
69
  @next_season_start = Chronic.construct(@now.year, @now.month, @now.day)
68
70
  @next_season_end = Chronic.construct(@now.year, @now.month, @now.day)
69
71
  end
@@ -106,4 +108,4 @@ module Chronic
106
108
  )
107
109
  end
108
110
  end
109
- end
111
+ end
@@ -2,8 +2,9 @@ module Chronic
2
2
  class RepeaterSecond < Repeater #:nodoc:
3
3
  SECOND_SECONDS = 1 # haha, awesome
4
4
 
5
- def initialize(type)
5
+ def initialize(type, options = {})
6
6
  super
7
+ @second_start = nil
7
8
  end
8
9
 
9
10
  def next(pointer = :future)
@@ -11,7 +12,7 @@ module Chronic
11
12
 
12
13
  direction = pointer == :future ? 1 : -1
13
14
 
14
- if !@second_start
15
+ unless @second_start
15
16
  @second_start = @now + (direction * SECOND_SECONDS)
16
17
  else
17
18
  @second_start += SECOND_SECONDS * direction
@@ -39,4 +40,4 @@ module Chronic
39
40
  super << '-second'
40
41
  end
41
42
  end
42
- end
43
+ end
@@ -26,31 +26,41 @@ module Chronic
26
26
 
27
27
  end
28
28
 
29
- def initialize(time)
30
- t = time.gsub(/\:/, '')
31
-
32
- @type =
33
- case t.size
34
- when 1..2
35
- hours = t.to_i
36
- Tick.new((hours == 12 ? 0 : hours) * 60 * 60, true)
37
- when 3
38
- hours = t[0..0].to_i
39
- ambiguous = hours > 0
40
- Tick.new((hours * 60 * 60) + (t[1..2].to_i * 60), ambiguous)
41
- when 4
42
- ambiguous = time =~ /:/ && t[0..0].to_i != 0 && t[0..1].to_i <= 12
43
- hours = t[0..1].to_i
44
- hours == 12 ? Tick.new(0 * 60 * 60 + t[2..3].to_i * 60, ambiguous) : Tick.new(hours * 60 * 60 + t[2..3].to_i * 60, ambiguous)
45
- when 5
46
- Tick.new(t[0..0].to_i * 60 * 60 + t[1..2].to_i * 60 + t[3..4].to_i, true)
47
- when 6
48
- ambiguous = time =~ /:/ && t[0..0].to_i != 0 && t[0..1].to_i <= 12
49
- hours = t[0..1].to_i
50
- hours == 12 ? Tick.new(0 * 60 * 60 + t[2..3].to_i * 60 + t[4..5].to_i, ambiguous) : Tick.new(hours * 60 * 60 + t[2..3].to_i * 60 + t[4..5].to_i, ambiguous)
51
- else
52
- raise("Time cannot exceed six digits")
29
+ def initialize(time, options = {})
30
+ @current_time = nil
31
+ @options = options
32
+ time_parts = time.split(':')
33
+ raise ArgumentError, "Time cannot have more than 4 groups of ':'" if time_parts.count > 4
34
+
35
+ if time_parts.first.length > 2 and time_parts.count == 1
36
+ if time_parts.first.length > 4
37
+ second_index = time_parts.first.length - 2
38
+ time_parts.insert(1, time_parts.first[second_index..time_parts.first.length])
39
+ time_parts[0] = time_parts.first[0..second_index - 1]
40
+ end
41
+ minute_index = time_parts.first.length - 2
42
+ time_parts.insert(1, time_parts.first[minute_index..time_parts.first.length])
43
+ time_parts[0] = time_parts.first[0..minute_index - 1]
44
+ end
45
+
46
+ ambiguous = false
47
+ hours = time_parts.first.to_i
48
+
49
+ if @options[:hours24].nil? or (not @options[:hours24].nil? and @options[:hours24] != true)
50
+ ambiguous = true if (time_parts.first.length == 1 and hours > 0) or (hours >= 10 and hours <= 12) or (@options[:hours24] == false and hours > 0)
51
+ hours = 0 if hours == 12 and ambiguous
53
52
  end
53
+
54
+ hours *= 60 * 60
55
+ minutes = 0
56
+ seconds = 0
57
+ subseconds = 0
58
+
59
+ minutes = time_parts[1].to_i * 60 if time_parts.count > 1
60
+ seconds = time_parts[2].to_i if time_parts.count > 2
61
+ subseconds = time_parts[3].to_f / (10 ** time_parts[3].length) if time_parts.count > 3
62
+
63
+ @type = Tick.new(hours + minutes + seconds + subseconds, ambiguous)
54
64
  end
55
65
 
56
66
  # Return the next past or future Span for the time that this Repeater represents
@@ -125,4 +135,4 @@ module Chronic
125
135
  super << '-time-' << @type.to_s
126
136
  end
127
137
  end
128
- end
138
+ end
@@ -2,14 +2,15 @@ module Chronic
2
2
  class RepeaterWeek < Repeater #:nodoc:
3
3
  WEEK_SECONDS = 604800 # (7 * 24 * 60 * 60)
4
4
 
5
- def initialize(type)
5
+ def initialize(type, options = {})
6
6
  super
7
+ @current_week_start = nil
7
8
  end
8
9
 
9
10
  def next(pointer)
10
11
  super
11
12
 
12
- if !@current_week_start
13
+ unless @current_week_start
13
14
  case pointer
14
15
  when :future
15
16
  sunday_repeater = RepeaterDayName.new(:sunday)
@@ -71,4 +72,4 @@ module Chronic
71
72
  super << '-week'
72
73
  end
73
74
  end
74
- end
75
+ end
@@ -11,8 +11,9 @@ module Chronic
11
11
  :saturday => 6
12
12
  }
13
13
 
14
- def initialize(type)
14
+ def initialize(type, options = {})
15
15
  super
16
+ @current_weekday_start = nil
16
17
  end
17
18
 
18
19
  def next(pointer)
@@ -20,7 +21,7 @@ module Chronic
20
21
 
21
22
  direction = pointer == :future ? 1 : -1
22
23
 
23
- if !@current_weekday_start
24
+ unless @current_weekday_start
24
25
  @current_weekday_start = Chronic.construct(@now.year, @now.month, @now.day)
25
26
  @current_weekday_start += direction * DAY_SECONDS
26
27
 
@@ -82,4 +83,4 @@ module Chronic
82
83
  DAYS[sym] || raise("Invalid symbol specified")
83
84
  end
84
85
  end
85
- end
86
+ end
@@ -2,14 +2,15 @@ module Chronic
2
2
  class RepeaterWeekend < Repeater #:nodoc:
3
3
  WEEKEND_SECONDS = 172_800 # (2 * 24 * 60 * 60)
4
4
 
5
- def initialize(type)
5
+ def initialize(type, options = {})
6
6
  super
7
+ @current_week_start = nil
7
8
  end
8
9
 
9
10
  def next(pointer)
10
11
  super
11
12
 
12
- if !@current_week_start
13
+ unless @current_week_start
13
14
  case pointer
14
15
  when :future
15
16
  saturday_repeater = RepeaterDayName.new(:saturday)
@@ -63,4 +64,4 @@ module Chronic
63
64
  super << '-weekend'
64
65
  end
65
66
  end
66
- end
67
+ end
@@ -2,14 +2,15 @@ module Chronic
2
2
  class RepeaterYear < Repeater #:nodoc:
3
3
  YEAR_SECONDS = 31536000 # 365 * 24 * 60 * 60
4
4
 
5
- def initialize(type)
5
+ def initialize(type, options = {})
6
6
  super
7
+ @current_year_start = nil
7
8
  end
8
9
 
9
10
  def next(pointer)
10
11
  super
11
12
 
12
- if !@current_year_start
13
+ unless @current_year_start
13
14
  case pointer
14
15
  when :future
15
16
  @current_year_start = Chronic.construct(@now.year + 1)
@@ -67,11 +68,11 @@ module Chronic
67
68
  end
68
69
 
69
70
  def month_days(year, month)
70
- if Date.leap?(year)
71
+ if ::Date.leap?(year)
71
72
  RepeaterMonth::MONTH_DAYS_LEAP[month - 1]
72
73
  else
73
74
  RepeaterMonth::MONTH_DAYS[month - 1]
74
75
  end
75
76
  end
76
77
  end
77
- end
78
+ end
@@ -11,88 +11,53 @@ module Chronic
11
11
  # Returns an Array of tokens.
12
12
  def self.scan(tokens, options)
13
13
  tokens.each_index do |i|
14
- if t = scan_for_scalars(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
15
- if t = scan_for_days(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
16
- if t = scan_for_months(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
17
- if t = scan_for_years(tokens[i], tokens[i + 1], options) then tokens[i].tag(t) end
18
- end
19
- end
20
-
21
- # token - The Token object we want to scan.
22
- # post_token - The next Token object.
23
- #
24
- # Returns a new Scalar object.
25
- def self.scan_for_scalars(token, post_token)
26
- if token.word =~ /^\d*$/
27
- unless post_token && DAY_PORTIONS.include?(post_token.word)
28
- return Scalar.new(token.word.to_i)
14
+ token = tokens[i]
15
+ post_token = tokens[i + 1]
16
+ if token.word =~ /^\d+$/
17
+ scalar = token.word.to_i
18
+ token.tag(Scalar.new(scalar))
19
+ token.tag(ScalarSubsecond.new(scalar)) if Chronic::Time::could_be_subsecond?(scalar)
20
+ token.tag(ScalarSecond.new(scalar)) if Chronic::Time::could_be_second?(scalar)
21
+ token.tag(ScalarMinute.new(scalar)) if Chronic::Time::could_be_minute?(scalar)
22
+ token.tag(ScalarHour.new(scalar)) if Chronic::Time::could_be_hour?(scalar)
23
+ unless post_token and DAY_PORTIONS.include?(post_token.word)
24
+ token.tag(ScalarDay.new(scalar)) if Chronic::Date::could_be_day?(scalar)
25
+ token.tag(ScalarMonth.new(scalar)) if Chronic::Date::could_be_month?(scalar)
26
+ if Chronic::Date::could_be_year?(scalar)
27
+ year = Chronic::Date::make_year(scalar, options[:ambiguous_year_future_bias])
28
+ token.tag(ScalarYear.new(year.to_i))
29
+ end
30
+ end
29
31
  end
30
32
  end
31
33
  end
32
34
 
33
- # token - The Token object we want to scan.
34
- # post_token - The next Token object.
35
- #
36
- # Returns a new Scalar object.
37
- def self.scan_for_days(token, post_token)
38
- if token.word =~ /^\d\d?$/
39
- toi = token.word.to_i
40
- unless toi > 31 || toi < 1 || (post_token && DAY_PORTIONS.include?(post_token.word))
41
- return ScalarDay.new(toi)
42
- end
43
- end
35
+ def to_s
36
+ 'scalar'
44
37
  end
38
+ end
45
39
 
46
- # token - The Token object we want to scan.
47
- # post_token - The next Token object.
48
- #
49
- # Returns a new Scalar object.
50
- def self.scan_for_months(token, post_token)
51
- if token.word =~ /^\d\d?$/
52
- toi = token.word.to_i
53
- unless toi > 12 || toi < 1 || (post_token && DAY_PORTIONS.include?(post_token.word))
54
- return ScalarMonth.new(toi)
55
- end
56
- end
40
+ class ScalarSubsecond < Scalar #:nodoc:
41
+ def to_s
42
+ super << '-subsecond-' << @type.to_s
57
43
  end
44
+ end
58
45
 
59
- # token - The Token object we want to scan.
60
- # post_token - The next Token object.
61
- # options - The Hash of options specified in Chronic::parse.
62
- #
63
- # Returns a new Scalar object.
64
- def self.scan_for_years(token, post_token, options)
65
- if token.word =~ /^([1-9]\d)?\d\d?$/
66
- unless post_token && DAY_PORTIONS.include?(post_token.word)
67
- year = make_year(token.word.to_i, options[:ambiguous_year_future_bias])
68
- return ScalarYear.new(year.to_i)
69
- end
70
- end
46
+ class ScalarSecond < Scalar #:nodoc:
47
+ def to_s
48
+ super << '-second-' << @type.to_s
71
49
  end
50
+ end
72
51
 
73
- # Build a year from a 2 digit suffix.
74
- #
75
- # year - The two digit Integer year to build from.
76
- # bias - The Integer amount of future years to bias.
77
- #
78
- # Examples:
79
- #
80
- # make_year(96, 50) #=> 1996
81
- # make_year(79, 20) #=> 2079
82
- # make_year(00, 50) #=> 2000
83
- #
84
- # Returns The Integer 4 digit year.
85
- def self.make_year(year, bias)
86
- return year if year.to_s.size > 2
87
- start_year = Chronic.time_class.now.year - bias
88
- century = (start_year / 100) * 100
89
- full_year = century + year
90
- full_year += 100 if full_year < start_year
91
- full_year
52
+ class ScalarMinute < Scalar #:nodoc:
53
+ def to_s
54
+ super << '-minute-' << @type.to_s
92
55
  end
56
+ end
93
57
 
58
+ class ScalarHour < Scalar #:nodoc:
94
59
  def to_s
95
- 'scalar'
60
+ super << '-hour-' << @type.to_s
96
61
  end
97
62
  end
98
63
 
@@ -11,11 +11,18 @@ module Chronic
11
11
  def self.scan(tokens, options)
12
12
  tokens.each do |token|
13
13
  if t = scan_for_commas(token) then token.tag(t); next end
14
- if t = scan_for_slash_or_dash(token) then token.tag(t); next end
14
+ if t = scan_for_dots(token) then token.tag(t); next end
15
+ if t = scan_for_colon(token) then token.tag(t); next end
16
+ if t = scan_for_space(token) then token.tag(t); next end
17
+ if t = scan_for_slash(token) then token.tag(t); next end
18
+ if t = scan_for_dash(token) then token.tag(t); next end
19
+ if t = scan_for_quote(token) then token.tag(t); next end
15
20
  if t = scan_for_at(token) then token.tag(t); next end
16
21
  if t = scan_for_in(token) then token.tag(t); next end
17
22
  if t = scan_for_on(token) then token.tag(t); next end
18
23
  if t = scan_for_and(token) then token.tag(t); next end
24
+ if t = scan_for_t(token) then token.tag(t); next end
25
+ if t = scan_for_w(token) then token.tag(t); next end
19
26
  end
20
27
  end
21
28
 
@@ -28,12 +35,47 @@ module Chronic
28
35
 
29
36
  # token - The Token object we want to scan.
30
37
  #
31
- # Returns a new SeparatorSlashOrDash object.
32
- def self.scan_for_slash_or_dash(token)
33
- scan_for token, SeparatorSlashOrDash,
38
+ # Returns a new SeparatorDot object.
39
+ def self.scan_for_dots(token)
40
+ scan_for token, SeparatorDot, { /^\.$/ => :dot }
41
+ end
42
+
43
+ # token - The Token object we want to scan.
44
+ #
45
+ # Returns a new SeparatorColon object.
46
+ def self.scan_for_colon(token)
47
+ scan_for token, SeparatorColon, { /^:$/ => :colon }
48
+ end
49
+
50
+ # token - The Token object we want to scan.
51
+ #
52
+ # Returns a new SeparatorSpace object.
53
+ def self.scan_for_space(token)
54
+ scan_for token, SeparatorSpace, { /^ $/ => :space }
55
+ end
56
+
57
+ # token - The Token object we want to scan.
58
+ #
59
+ # Returns a new SeparatorSlash object.
60
+ def self.scan_for_slash(token)
61
+ scan_for token, SeparatorSlash, { /^\/$/ => :slash }
62
+ end
63
+
64
+ # token - The Token object we want to scan.
65
+ #
66
+ # Returns a new SeparatorDash object.
67
+ def self.scan_for_dash(token)
68
+ scan_for token, SeparatorDash, { /^-$/ => :dash }
69
+ end
70
+
71
+ # token - The Token object we want to scan.
72
+ #
73
+ # Returns a new SeparatorQuote object.
74
+ def self.scan_for_quote(token)
75
+ scan_for token, SeparatorQuote,
34
76
  {
35
- /^-$/ => :dash,
36
- /^\/$/ => :slash
77
+ /^'$/ => :single_quote,
78
+ /^"$/ => :double_quote
37
79
  }
38
80
  end
39
81
 
@@ -65,6 +107,20 @@ module Chronic
65
107
  scan_for token, SeparatorAnd, { /^and$/ => :and }
66
108
  end
67
109
 
110
+ # token - The Token object we want to scan.
111
+ #
112
+ # Returns a new SeperatorT Object object.
113
+ def self.scan_for_t(token)
114
+ scan_for token, SeparatorT, { /^t$/ => :T }
115
+ end
116
+
117
+ # token - The Token object we want to scan.
118
+ #
119
+ # Returns a new SeperatorW Object object.
120
+ def self.scan_for_w(token)
121
+ scan_for token, SeparatorW, { /^w$/ => :W }
122
+ end
123
+
68
124
  def to_s
69
125
  'separator'
70
126
  end
@@ -76,9 +132,39 @@ module Chronic
76
132
  end
77
133
  end
78
134
 
79
- class SeparatorSlashOrDash < Separator #:nodoc:
135
+ class SeparatorDot < Separator #:nodoc:
80
136
  def to_s
81
- super << '-slashordash-' << @type.to_s
137
+ super << '-dot'
138
+ end
139
+ end
140
+
141
+ class SeparatorColon < Separator #:nodoc:
142
+ def to_s
143
+ super << '-colon'
144
+ end
145
+ end
146
+
147
+ class SeparatorSpace < Separator #:nodoc:
148
+ def to_s
149
+ super << '-space'
150
+ end
151
+ end
152
+
153
+ class SeparatorSlash < Separator #:nodoc:
154
+ def to_s
155
+ super << '-slash'
156
+ end
157
+ end
158
+
159
+ class SeparatorDash < Separator #:nodoc:
160
+ def to_s
161
+ super << '-dash'
162
+ end
163
+ end
164
+
165
+ class SeparatorQuote < Separator #:nodoc:
166
+ def to_s
167
+ super << '-quote-' << @type.to_s
82
168
  end
83
169
  end
84
170
 
@@ -105,4 +191,17 @@ module Chronic
105
191
  super << '-and'
106
192
  end
107
193
  end
194
+
195
+ class SeparatorT < Separator #:nodoc:
196
+ def to_s
197
+ super << '-T'
198
+ end
199
+ end
200
+
201
+ class SeparatorW < Separator #:nodoc:
202
+ def to_s
203
+ super << '-W'
204
+ end
205
+ end
206
+
108
207
  end