chronic 0.4.0 → 0.5.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 (47) hide show
  1. data/.gemtest +0 -0
  2. data/.gitignore +5 -0
  3. data/.yardopts +3 -0
  4. data/HISTORY.md +39 -1
  5. data/Manifest.txt +7 -2
  6. data/README.md +8 -2
  7. data/Rakefile +9 -101
  8. data/chronic.gemspec +12 -77
  9. data/lib/chronic/chronic.rb +225 -91
  10. data/lib/chronic/grabber.rb +12 -3
  11. data/lib/chronic/handlers.rb +95 -199
  12. data/lib/chronic/mini_date.rb +8 -3
  13. data/lib/chronic/ordinal.rb +14 -5
  14. data/lib/chronic/pointer.rb +11 -5
  15. data/lib/chronic/repeater.rb +31 -19
  16. data/lib/chronic/repeaters/repeater_day.rb +0 -1
  17. data/lib/chronic/repeaters/repeater_day_name.rb +0 -1
  18. data/lib/chronic/repeaters/repeater_day_portion.rb +0 -1
  19. data/lib/chronic/repeaters/repeater_fortnight.rb +0 -1
  20. data/lib/chronic/repeaters/repeater_hour.rb +0 -1
  21. data/lib/chronic/repeaters/repeater_minute.rb +0 -1
  22. data/lib/chronic/repeaters/repeater_month.rb +13 -2
  23. data/lib/chronic/repeaters/repeater_month_name.rb +13 -21
  24. data/lib/chronic/repeaters/repeater_season.rb +0 -19
  25. data/lib/chronic/repeaters/repeater_season_name.rb +0 -2
  26. data/lib/chronic/repeaters/repeater_second.rb +0 -1
  27. data/lib/chronic/repeaters/repeater_time.rb +1 -2
  28. data/lib/chronic/repeaters/repeater_week.rb +0 -1
  29. data/lib/chronic/repeaters/repeater_weekday.rb +0 -1
  30. data/lib/chronic/repeaters/repeater_weekend.rb +0 -1
  31. data/lib/chronic/repeaters/repeater_year.rb +20 -9
  32. data/lib/chronic/scalar.rb +30 -5
  33. data/lib/chronic/season.rb +37 -0
  34. data/lib/chronic/separator.rb +24 -9
  35. data/lib/chronic/tag.rb +17 -3
  36. data/lib/chronic/time_zone.rb +12 -3
  37. data/lib/chronic/token.rb +14 -4
  38. data/lib/chronic.rb +62 -49
  39. data/test/test_Chronic.rb +52 -2
  40. data/test/test_MiniDate.rb +32 -0
  41. data/test/test_RepeaterMonth.rb +4 -0
  42. data/test/test_RepeaterSeason.rb +40 -0
  43. data/test/test_RepeaterYear.rb +7 -0
  44. data/test/test_Time.rb +1 -1
  45. data/test/test_parsing.rb +159 -113
  46. metadata +33 -29
  47. data/benchmark/benchmark.rb +0 -13
@@ -4,7 +4,6 @@ module Chronic
4
4
 
5
5
  def initialize(type)
6
6
  super
7
- @current_day_start = nil
8
7
  end
9
8
 
10
9
  def next(pointer)
@@ -11,7 +11,6 @@ module Chronic
11
11
 
12
12
  def initialize(type)
13
13
  super
14
- @current_span = nil
15
14
 
16
15
  if type.kind_of? Integer
17
16
  @range = (@type * 60 * 60)..((@type + 12) * 60 * 60)
@@ -4,7 +4,6 @@ module Chronic
4
4
 
5
5
  def initialize(type)
6
6
  super
7
- @current_fortnight_start = nil
8
7
  end
9
8
 
10
9
  def next(pointer)
@@ -4,7 +4,6 @@ module Chronic
4
4
 
5
5
  def initialize(type)
6
6
  super
7
- @current_hour_start = nil
8
7
  end
9
8
 
10
9
  def next(pointer)
@@ -4,7 +4,6 @@ module Chronic
4
4
 
5
5
  def initialize(type)
6
6
  super
7
- @current_minute_start = nil
8
7
  end
9
8
 
10
9
  def next(pointer = :future)
@@ -2,10 +2,11 @@ module Chronic
2
2
  class RepeaterMonth < Repeater #:nodoc:
3
3
  MONTH_SECONDS = 2_592_000 # 30 * 24 * 60 * 60
4
4
  YEAR_MONTHS = 12
5
+ MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
6
+ MONTH_DAYS_LEAP = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
5
7
 
6
8
  def initialize(type)
7
9
  super
8
- @current_month_start = nil
9
10
  end
10
11
 
11
12
  def next(pointer)
@@ -54,7 +55,11 @@ module Chronic
54
55
  new_year += 1
55
56
  new_month -= YEAR_MONTHS
56
57
  end
57
- Time.construct(new_year, new_month, time.day, time.hour, time.min, time.sec)
58
+
59
+ days = month_days(new_year, new_month)
60
+ new_day = time.day > days ? days : time.day
61
+
62
+ Time.construct(new_year, new_month, new_day, time.hour, time.min, time.sec)
58
63
  end
59
64
 
60
65
  def width
@@ -64,5 +69,11 @@ module Chronic
64
69
  def to_s
65
70
  super << '-month'
66
71
  end
72
+
73
+ private
74
+
75
+ def month_days(year, month)
76
+ Date.leap?(year) ? MONTH_DAYS_LEAP[month - 1] : MONTH_DAYS[month - 1]
77
+ end
67
78
  end
68
79
  end
@@ -18,32 +18,30 @@ module Chronic
18
18
 
19
19
  def initialize(type)
20
20
  super
21
- @current_month_begin = nil
22
21
  end
23
22
 
24
23
  def next(pointer)
25
24
  super
26
25
 
27
26
  if !@current_month_begin
28
- target_month = symbol_to_number(@type)
29
27
  case pointer
30
28
  when :future
31
- if @now.month < target_month
32
- @current_month_begin = Time.construct(@now.year, target_month)
33
- elsif @now.month > target_month
34
- @current_month_begin = Time.construct(@now.year + 1, target_month)
29
+ if @now.month < index
30
+ @current_month_begin = Time.construct(@now.year, index)
31
+ elsif @now.month > index
32
+ @current_month_begin = Time.construct(@now.year + 1, index)
35
33
  end
36
34
  when :none
37
- if @now.month <= target_month
38
- @current_month_begin = Time.construct(@now.year, target_month)
39
- elsif @now.month > target_month
40
- @current_month_begin = Time.construct(@now.year + 1, target_month)
35
+ if @now.month <= index
36
+ @current_month_begin = Time.construct(@now.year, index)
37
+ elsif @now.month > index
38
+ @current_month_begin = Time.construct(@now.year + 1, index)
41
39
  end
42
40
  when :past
43
- if @now.month >= target_month
44
- @current_month_begin = Time.construct(@now.year, target_month)
45
- elsif @now.month < target_month
46
- @current_month_begin = Time.construct(@now.year - 1, target_month)
41
+ if @now.month >= index
42
+ @current_month_begin = Time.construct(@now.year, index)
43
+ elsif @now.month < index
44
+ @current_month_begin = Time.construct(@now.year - 1, index)
47
45
  end
48
46
  end
49
47
  @current_month_begin || raise("Current month should be set by now")
@@ -86,17 +84,11 @@ module Chronic
86
84
  end
87
85
 
88
86
  def index
89
- symbol_to_number(@type)
87
+ @index ||= MONTHS[@type]
90
88
  end
91
89
 
92
90
  def to_s
93
91
  super << '-monthname-' << @type.to_s
94
92
  end
95
-
96
- private
97
-
98
- def symbol_to_number(sym)
99
- MONTHS[sym] || raise("Invalid symbol specified")
100
- end
101
93
  end
102
94
  end
@@ -1,22 +1,4 @@
1
1
  module Chronic
2
- class Season
3
- attr_reader :start, :end
4
-
5
- def initialize(myStart, myEnd)
6
- @start = myStart
7
- @end = myEnd
8
- end
9
-
10
- def self.find_next_season(season, pointer)
11
- lookup = {:spring => 0, :summer => 1, :autumn => 2, :winter => 3}
12
- next_season_num = (lookup[season]+1*pointer) % 4
13
- lookup.invert[next_season_num]
14
- end
15
-
16
- def self.season_after(season); find_next_season(season, +1); end
17
- def self.season_before(season); find_next_season(season, -1); end
18
- end
19
-
20
2
  class RepeaterSeason < Repeater #:nodoc:
21
3
  SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60
22
4
  SEASONS = {
@@ -28,7 +10,6 @@ module Chronic
28
10
 
29
11
  def initialize(type)
30
12
  super
31
- @next_season_start = nil
32
13
  end
33
14
 
34
15
  def next(pointer)
@@ -9,8 +9,6 @@ module Chronic
9
9
  end
10
10
 
11
11
  def this(pointer = :future)
12
- # super
13
-
14
12
  direction = pointer == :future ? 1 : -1
15
13
 
16
14
  today = Time.construct(@now.year, @now.month, @now.day)
@@ -4,7 +4,6 @@ module Chronic
4
4
 
5
5
  def initialize(type)
6
6
  super
7
- @second_start = nil
8
7
  end
9
8
 
10
9
  def next(pointer = :future)
@@ -27,7 +27,6 @@ module Chronic
27
27
  end
28
28
 
29
29
  def initialize(time)
30
- @current_time = nil
31
30
  t = time.gsub(/\:/, '')
32
31
 
33
32
  @type =
@@ -37,7 +36,7 @@ module Chronic
37
36
  hours == 12 ? Tick.new(0 * 60 * 60, true) : Tick.new(hours * 60 * 60, true)
38
37
  when 3
39
38
  hours = t[0..0].to_i
40
- ambiguous = hours > 1
39
+ ambiguous = hours > 0
41
40
  Tick.new((hours * 60 * 60) + (t[1..2].to_i * 60), ambiguous)
42
41
  when 4
43
42
  ambiguous = time =~ /:/ && t[0..0].to_i != 0 && t[0..1].to_i <= 12
@@ -4,7 +4,6 @@ module Chronic
4
4
 
5
5
  def initialize(type)
6
6
  super
7
- @current_week_start = nil
8
7
  end
9
8
 
10
9
  def next(pointer)
@@ -13,7 +13,6 @@ module Chronic
13
13
 
14
14
  def initialize(type)
15
15
  super
16
- @current_weekday_start = nil
17
16
  end
18
17
 
19
18
  def next(pointer)
@@ -4,7 +4,6 @@ module Chronic
4
4
 
5
5
  def initialize(type)
6
6
  super
7
- @current_week_start = nil
8
7
  end
9
8
 
10
9
  def next(pointer)
@@ -4,7 +4,6 @@ module Chronic
4
4
 
5
5
  def initialize(type)
6
6
  super
7
- @current_year_start = nil
8
7
  end
9
8
 
10
9
  def next(pointer)
@@ -45,22 +44,34 @@ module Chronic
45
44
 
46
45
  def offset(span, amount, pointer)
47
46
  direction = pointer == :future ? 1 : -1
48
-
49
- sb = span.begin
50
- new_begin = Time.construct(sb.year + (amount * direction), sb.month, sb.day, sb.hour, sb.min, sb.sec)
51
-
52
- se = span.end
53
- new_end = Time.construct(se.year + (amount * direction), se.month, se.day, se.hour, se.min, se.sec)
54
-
47
+ new_begin = build_offset_time(span.begin, amount, direction)
48
+ new_end = build_offset_time(span.end, amount, direction)
55
49
  Span.new(new_begin, new_end)
56
50
  end
57
51
 
58
52
  def width
59
- (365 * 24 * 60 * 60)
53
+ YEAR_SECONDS
60
54
  end
61
55
 
62
56
  def to_s
63
57
  super << '-year'
64
58
  end
59
+
60
+ private
61
+
62
+ def build_offset_time(time, amount, direction)
63
+ year = time.year + (amount * direction)
64
+ days = month_days(year, time.month)
65
+ day = time.day > days ? days : time.day
66
+ Time.construct(year, time.month, day, time.hour, time.min, time.sec)
67
+ end
68
+
69
+ def month_days(year, month)
70
+ if Date.leap?(year)
71
+ RepeaterMonth::MONTH_DAYS_LEAP[month - 1]
72
+ else
73
+ RepeaterMonth::MONTH_DAYS[month - 1]
74
+ end
75
+ end
65
76
  end
66
77
  end
@@ -1,10 +1,14 @@
1
1
  module Chronic
2
-
3
- class Scalar < Tag #:nodoc:
2
+ class Scalar < Tag
4
3
  DAY_PORTIONS = %w( am pm morning afternoon evening night )
5
4
 
5
+ # Scan an Array of {Token}s and apply any necessary Scalar tags to
6
+ # each token
7
+ #
8
+ # @param [Array<Token>] tokens Array of tokens to scan
9
+ # @param [Hash] options Options specified in {Chronic.parse}
10
+ # @return [Array] list of tokens
6
11
  def self.scan(tokens, options)
7
- # for each token
8
12
  tokens.each_index do |i|
9
13
  if t = scan_for_scalars(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
10
14
  if t = scan_for_days(tokens[i], tokens[i + 1]) then tokens[i].tag(t) end
@@ -13,6 +17,9 @@ module Chronic
13
17
  end
14
18
  end
15
19
 
20
+ # @param [Token] token
21
+ # @param [Token] post_token
22
+ # @return [Scalar, nil]
16
23
  def self.scan_for_scalars(token, post_token)
17
24
  if token.word =~ /^\d*$/
18
25
  unless post_token && DAY_PORTIONS.include?(post_token.word)
@@ -21,6 +28,9 @@ module Chronic
21
28
  end
22
29
  end
23
30
 
31
+ # @param [Token] token
32
+ # @param [Token] post_token
33
+ # @return [ScalarDay, nil]
24
34
  def self.scan_for_days(token, post_token)
25
35
  if token.word =~ /^\d\d?$/
26
36
  toi = token.word.to_i
@@ -30,6 +40,9 @@ module Chronic
30
40
  end
31
41
  end
32
42
 
43
+ # @param [Token] token
44
+ # @param [Token] post_token
45
+ # @return [ScalarMonth, nil]
33
46
  def self.scan_for_months(token, post_token)
34
47
  if token.word =~ /^\d\d?$/
35
48
  toi = token.word.to_i
@@ -39,6 +52,10 @@ module Chronic
39
52
  end
40
53
  end
41
54
 
55
+ # @param [Token] token
56
+ # @param [Token] post_token
57
+ # @param [Hash] options Options specified in {Chronic.parse}
58
+ # @return [ScalarYear, nil]
42
59
  def self.scan_for_years(token, post_token, options)
43
60
  if token.word =~ /^([1-9]\d)?\d\d?$/
44
61
  unless post_token && DAY_PORTIONS.include?(post_token.word)
@@ -49,6 +66,15 @@ module Chronic
49
66
  end
50
67
 
51
68
  # Build a year from a 2 digit suffix
69
+ #
70
+ # @example
71
+ # make_year(96, 50) #=> 1996
72
+ # make_year(79, 20) #=> 2079
73
+ # make_year(00, 50) #=> 2000
74
+ #
75
+ # @param [Integer] year The two digit year to build from
76
+ # @param [Integer] bias The amount of future years to bias
77
+ # @return [Integer] The 4 digit year
52
78
  def self.make_year(year, bias)
53
79
  return year if year.to_s.size > 2
54
80
  start_year = Chronic.time_class.now.year - bias
@@ -80,5 +106,4 @@ module Chronic
80
106
  super << '-year-' << @type.to_s
81
107
  end
82
108
  end
83
-
84
- end
109
+ end
@@ -0,0 +1,37 @@
1
+ module Chronic
2
+ class Season
3
+ # @return [MiniDate]
4
+ attr_reader :start
5
+
6
+ # @return [MiniDate]
7
+ attr_reader :end
8
+
9
+ # @param [MiniDate] start_date
10
+ # @param [MiniDate] end_date
11
+ def initialize(start_date, end_date)
12
+ @start = start_date
13
+ @end = end_date
14
+ end
15
+
16
+ # @param [Symbol] season The season name
17
+ # @param [Integer] pointer The direction (-1 for past, 1 for future)
18
+ # @return [Symbol] The new season name
19
+ def self.find_next_season(season, pointer)
20
+ lookup = {:spring => 0, :summer => 1, :autumn => 2, :winter => 3}
21
+ next_season_num = (lookup[season] + 1 * pointer) % 4
22
+ lookup.invert[next_season_num]
23
+ end
24
+
25
+ # @param [Symbol] season The season name
26
+ # @return [Symbol] The new season name
27
+ def self.season_after(season)
28
+ find_next_season(season, +1)
29
+ end
30
+
31
+ # @param [Symbol] season The season name
32
+ # @return [Symbol] The new season name
33
+ def self.season_before(season)
34
+ find_next_season(season, -1)
35
+ end
36
+ end
37
+ end
@@ -1,20 +1,30 @@
1
1
  module Chronic
2
+ class Separator < Tag
2
3
 
3
- class Separator < Tag #:nodoc:
4
+ # Scan an Array of {Token}s and apply any necessary Separator tags to
5
+ # each token
6
+ #
7
+ # @param [Array<Token>] tokens Array of tokens to scan
8
+ # @param [Hash] options Options specified in {Chronic.parse}
9
+ # @return [Array] list of tokens
4
10
  def self.scan(tokens, options)
5
- tokens.each_index do |i|
6
- if t = scan_for_commas(tokens[i]) then tokens[i].tag(t); next end
7
- if t = scan_for_slash_or_dash(tokens[i]) then tokens[i].tag(t); next end
8
- if t = scan_for_at(tokens[i]) then tokens[i].tag(t); next end
9
- if t = scan_for_in(tokens[i]) then tokens[i].tag(t); next end
10
- if t = scan_for_on(tokens[i]) then tokens[i].tag(t); next end
11
+ tokens.each do |token|
12
+ if t = scan_for_commas(token) then token.tag(t); next end
13
+ if t = scan_for_slash_or_dash(token) then token.tag(t); next end
14
+ if t = scan_for_at(token) then token.tag(t); next end
15
+ if t = scan_for_in(token) then token.tag(t); next end
16
+ if t = scan_for_on(token) then token.tag(t); next end
11
17
  end
12
18
  end
13
19
 
20
+ # @param [Token] token
21
+ # @return [SeparatorComma, nil]
14
22
  def self.scan_for_commas(token)
15
23
  scan_for token, SeparatorComma, { /^,$/ => :comma }
16
24
  end
17
25
 
26
+ # @param [Token] token
27
+ # @return [SeparatorSlashOrDash, nil]
18
28
  def self.scan_for_slash_or_dash(token)
19
29
  scan_for token, SeparatorSlashOrDash,
20
30
  {
@@ -23,14 +33,20 @@ module Chronic
23
33
  }
24
34
  end
25
35
 
36
+ # @param [Token] token
37
+ # @return [SeparatorAt, nil]
26
38
  def self.scan_for_at(token)
27
39
  scan_for token, SeparatorAt, { /^(at|@)$/ => :at }
28
40
  end
29
41
 
42
+ # @param [Token] token
43
+ # @return [SeparatorIn, nil]
30
44
  def self.scan_for_in(token)
31
45
  scan_for token, SeparatorIn, { /^in$/ => :in }
32
46
  end
33
47
 
48
+ # @param [Token] token
49
+ # @return [SeparatorOn, nil]
34
50
  def self.scan_for_on(token)
35
51
  scan_for token, SeparatorOn, { /^on$/ => :on }
36
52
  end
@@ -69,5 +85,4 @@ module Chronic
69
85
  super << '-on'
70
86
  end
71
87
  end
72
-
73
- end
88
+ end
data/lib/chronic/tag.rb CHANGED
@@ -1,13 +1,17 @@
1
1
  module Chronic
2
2
  # Tokens are tagged with subclassed instances of this class when
3
3
  # they match specific criteria
4
- class Tag #:nodoc:
4
+ class Tag
5
+
6
+ # @return [Symbol]
5
7
  attr_accessor :type
6
8
 
9
+ # @param [Symbol] type
7
10
  def initialize(type)
8
11
  @type = type
9
12
  end
10
13
 
14
+ # @param [Time] s Set the start timestamp for this Tag
11
15
  def start=(s)
12
16
  @now = s
13
17
  end
@@ -15,9 +19,19 @@ module Chronic
15
19
  class << self
16
20
  private
17
21
 
22
+ # @param [Token] token
23
+ # @param [Class] klass The class instance to create
24
+ # @param [Regexp, Hash] items
25
+ # @return [Object, nil] either a new instance of `klass` or `nil` if
26
+ # no match is found
18
27
  def scan_for(token, klass, items={})
19
- items.each do |item, symbol|
20
- return klass.new(symbol) if item =~ token.word
28
+ case items
29
+ when Regexp
30
+ return klass.new(token.word) if items =~ token.word
31
+ when Hash
32
+ items.each do |item, symbol|
33
+ return klass.new(symbol) if item =~ token.word
34
+ end
21
35
  end
22
36
  nil
23
37
  end
@@ -1,11 +1,20 @@
1
1
  module Chronic
2
- class TimeZone < Tag #:nodoc:
2
+ class TimeZone < Tag
3
+
4
+ # Scan an Array of {Token}s and apply any necessary TimeZone tags to
5
+ # each token
6
+ #
7
+ # @param [Array<Token>] tokens Array of tokens to scan
8
+ # @param [Hash] options Options specified in {Chronic.parse}
9
+ # @return [Array] list of tokens
3
10
  def self.scan(tokens, options)
4
- tokens.each_index do |i|
5
- if t = scan_for_all(tokens[i]) then tokens[i].tag(t); next end
11
+ tokens.each do |token|
12
+ if t = scan_for_all(token) then token.tag(t); next end
6
13
  end
7
14
  end
8
15
 
16
+ # @param [Token] token
17
+ # @return [TimeZone, nil]
9
18
  def self.scan_for_all(token)
10
19
  scan_for token, self,
11
20
  {
data/lib/chronic/token.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  module Chronic
2
- class Token #:nodoc:
3
- attr_accessor :word, :tags
2
+ class Token
3
+
4
+ # @return [String] The word this Token represents
5
+ attr_accessor :word
6
+
7
+ # @return [Array] A list of tag associated with this Token
8
+ attr_accessor :tags
4
9
 
5
10
  def initialize(word)
6
11
  @word = word
@@ -8,21 +13,26 @@ module Chronic
8
13
  end
9
14
 
10
15
  # Tag this token with the specified tag
16
+ #
17
+ # @param [Tag] new_tag An instance of {Tag} or one of its subclasses
11
18
  def tag(new_tag)
12
19
  @tags << new_tag
13
20
  end
14
21
 
15
22
  # Remove all tags of the given class
23
+ #
24
+ # @param [Class] The tag class to remove
16
25
  def untag(tag_class)
17
26
  @tags.delete_if { |m| m.kind_of? tag_class }
18
27
  end
19
28
 
20
- # Return true if this token has any tags
29
+ # @return [Boolean] true if this token has any tags
21
30
  def tagged?
22
31
  @tags.size > 0
23
32
  end
24
33
 
25
- # Return the Tag that matches the given class
34
+ # @param [Class] tag_class The tag class to search for
35
+ # @return [Tag] The first Tag that matches the given class
26
36
  def get_tag(tag_class)
27
37
  @tags.find { |m| m.kind_of? tag_class }
28
38
  end