chronic 0.1.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 (41) hide show
  1. data/README +119 -0
  2. data/lib/chronic.rb +30 -0
  3. data/lib/chronic/chronic.rb +242 -0
  4. data/lib/chronic/grabber.rb +26 -0
  5. data/lib/chronic/handlers.rb +405 -0
  6. data/lib/chronic/ordinal.rb +40 -0
  7. data/lib/chronic/pointer.rb +27 -0
  8. data/lib/chronic/repeater.rb +114 -0
  9. data/lib/chronic/repeaters/repeater_day.rb +40 -0
  10. data/lib/chronic/repeaters/repeater_day_name.rb +41 -0
  11. data/lib/chronic/repeaters/repeater_day_portion.rb +93 -0
  12. data/lib/chronic/repeaters/repeater_fortnight.rb +64 -0
  13. data/lib/chronic/repeaters/repeater_hour.rb +52 -0
  14. data/lib/chronic/repeaters/repeater_minute.rb +21 -0
  15. data/lib/chronic/repeaters/repeater_month.rb +54 -0
  16. data/lib/chronic/repeaters/repeater_month_name.rb +82 -0
  17. data/lib/chronic/repeaters/repeater_season.rb +23 -0
  18. data/lib/chronic/repeaters/repeater_season_name.rb +24 -0
  19. data/lib/chronic/repeaters/repeater_second.rb +34 -0
  20. data/lib/chronic/repeaters/repeater_time.rb +106 -0
  21. data/lib/chronic/repeaters/repeater_week.rb +62 -0
  22. data/lib/chronic/repeaters/repeater_weekend.rb +11 -0
  23. data/lib/chronic/repeaters/repeater_year.rb +55 -0
  24. data/lib/chronic/scalar.rb +74 -0
  25. data/lib/chronic/separator.rb +76 -0
  26. data/test/parse_numbers.rb +50 -0
  27. data/test/suite.rb +9 -0
  28. data/test/test_Chronic.rb +50 -0
  29. data/test/test_Handler.rb +110 -0
  30. data/test/test_RepeaterDayName.rb +52 -0
  31. data/test/test_RepeaterFortnight.rb +63 -0
  32. data/test/test_RepeaterHour.rb +65 -0
  33. data/test/test_RepeaterMonth.rb +47 -0
  34. data/test/test_RepeaterMonthName.rb +57 -0
  35. data/test/test_RepeaterTime.rb +72 -0
  36. data/test/test_RepeaterWeek.rb +63 -0
  37. data/test/test_RepeaterYear.rb +63 -0
  38. data/test/test_Span.rb +24 -0
  39. data/test/test_Token.rb +26 -0
  40. data/test/test_parsing.rb +472 -0
  41. metadata +87 -0
@@ -0,0 +1,40 @@
1
+ module Chronic
2
+
3
+ class Ordinal < Tag #:nodoc:
4
+ def self.scan(tokens)
5
+ # for each token
6
+ tokens.each_index do |i|
7
+ if t = self.scan_for_ordinals(tokens[i]) then tokens[i].tag(t) end
8
+ if t = self.scan_for_days(tokens[i]) then tokens[i].tag(t) end
9
+ end
10
+ tokens
11
+ end
12
+
13
+ def self.scan_for_ordinals(token)
14
+ if token.word =~ /^(\d*)(st|nd|rd|th)$/
15
+ return Ordinal.new($1.to_i)
16
+ end
17
+ return nil
18
+ end
19
+
20
+ def self.scan_for_days(token)
21
+ if token.word =~ /^(\d*)(st|nd|rd|th)$/
22
+ unless $1.to_i > 31
23
+ return OrdinalDay.new(token.word.to_i)
24
+ end
25
+ end
26
+ return nil
27
+ end
28
+
29
+ def to_s
30
+ 'ordinal'
31
+ end
32
+ end
33
+
34
+ class OrdinalDay < Ordinal #:nodoc:
35
+ def to_s
36
+ super << '-day-' << @type.to_s
37
+ end
38
+ end
39
+
40
+ end
@@ -0,0 +1,27 @@
1
+ module Chronic
2
+
3
+ class Pointer < Tag #:nodoc:
4
+ def self.scan(tokens)
5
+ # for each token
6
+ tokens.each_index do |i|
7
+ if t = self.scan_for_all(tokens[i]) then tokens[i].tag(t) end
8
+ end
9
+ tokens
10
+ end
11
+
12
+ def self.scan_for_all(token)
13
+ scanner = {/past/ => :past,
14
+ /future/ => :future,
15
+ /in/ => :future}
16
+ scanner.keys.each do |scanner_item|
17
+ return self.new(scanner[scanner_item]) if scanner_item =~ token.word
18
+ end
19
+ return nil
20
+ end
21
+
22
+ def to_s
23
+ 'pointer-' << @type.to_s
24
+ end
25
+ end
26
+
27
+ end
@@ -0,0 +1,114 @@
1
+ class Chronic::Repeater < Chronic::Tag #:nodoc:
2
+ def self.scan(tokens, options)
3
+ # for each token
4
+ tokens.each_index do |i|
5
+ if t = self.scan_for_month_names(tokens[i]) then tokens[i].tag(t); next end
6
+ if t = self.scan_for_day_names(tokens[i]) then tokens[i].tag(t); next end
7
+ if t = self.scan_for_day_portions(tokens[i]) then tokens[i].tag(t); next end
8
+ if t = self.scan_for_times(tokens[i], options) then tokens[i].tag(t); next end
9
+ if t = self.scan_for_units(tokens[i]) then tokens[i].tag(t); next end
10
+ end
11
+ tokens
12
+ end
13
+
14
+ def self.scan_for_month_names(token)
15
+ scanner = {/^jan\.?(uary)?$/ => :january,
16
+ /^feb\.?(ruary)?$/ => :february,
17
+ /^mar\.?(ch)?$/ => :march,
18
+ /^apr\.?(il)?$/ => :april,
19
+ /^may$/ => :may,
20
+ /^jun\.?e?$/ => :june,
21
+ /^jul\.?y?$/ => :july,
22
+ /^aug\.?(ust)?$/ => :august,
23
+ /^sep\.?(tember)?$/ => :september,
24
+ /^oct\.?(ober)?$/ => :october,
25
+ /^nov\.?(ember)?$/ => :november,
26
+ /^dec\.?(ember)?$/ => :december}
27
+ scanner.keys.each do |scanner_item|
28
+ return Chronic::RepeaterMonthName.new(scanner[scanner_item]) if scanner_item =~ token.word
29
+ end
30
+ return nil
31
+ end
32
+
33
+ def self.scan_for_day_names(token)
34
+ scanner = {/^m[ou]n(day)?$/ => :monday,
35
+ /^t(ue|eu|oo|u|)s(day)?$/ => :tuesday,
36
+ /^we(dnes|nds|nns)day$/ => :wednesday,
37
+ /^wed$/ => :wednesday,
38
+ /^th(urs|ers)day$/ => :thursday,
39
+ /^thu$/ => :thursday,
40
+ /^fr[iy](day)?$/ => :friday,
41
+ /^sat(t?[ue]rday)?$/ => :saturday,
42
+ /^su[nm](day)?$/ => :sunday}
43
+ scanner.keys.each do |scanner_item|
44
+ return Chronic::RepeaterDayName.new(scanner[scanner_item]) if scanner_item =~ token.word
45
+ end
46
+ return nil
47
+ end
48
+
49
+ def self.scan_for_day_portions(token)
50
+ scanner = {/^ams?$/ => :am,
51
+ /^pms?$/ => :pm,
52
+ /^mornings?$/ => :morning,
53
+ /^afternoons?$/ => :afternoon,
54
+ /^evenings?$/ => :evening,
55
+ /^nights?$/ => :night}
56
+ scanner.keys.each do |scanner_item|
57
+ return Chronic::RepeaterDayPortion.new(scanner[scanner_item]) if scanner_item =~ token.word
58
+ end
59
+ return nil
60
+ end
61
+
62
+ def self.scan_for_times(token, options)
63
+ if token.word =~ /^\d{1,2}(:?\d{2})?$/
64
+ return Chronic::RepeaterTime.new(token.word, options)
65
+ end
66
+ return nil
67
+ end
68
+
69
+ def self.scan_for_units(token)
70
+ scanner = {/^years?$/ => :year,
71
+ /^seasons?$/ => :season,
72
+ /^months?$/ => :month,
73
+ /^fortnights?$/ => :fortnight,
74
+ /^weeks?$/ => :week,
75
+ /^weekends?$/ => :weekends,
76
+ /^days?$/ => :day,
77
+ /^hours?$/ => :hour,
78
+ /^minutes?$/ => :minute,
79
+ /^seconds?$/ => :second}
80
+ scanner.keys.each do |scanner_item|
81
+ if scanner_item =~ token.word
82
+ klass_name = 'Chronic::Repeater' + scanner[scanner_item].to_s.capitalize
83
+ klass = eval(klass_name)
84
+ return klass.new(scanner[scanner_item])
85
+ end
86
+ end
87
+ return nil
88
+ end
89
+
90
+ def <=>(other)
91
+ width <=> other.width
92
+ end
93
+
94
+ # returns the width (in seconds or months) of this repeatable.
95
+ def width
96
+ raise("Repeatable#width must be overridden in subclasses")
97
+ end
98
+
99
+ # returns the next occurance of this repeatable.
100
+ def next(pointer)
101
+ !@now.nil? || raise("Start point must be set before calling #next")
102
+ [:future, :past].include?(pointer) || raise("First argument 'pointer' must be one of :past or :future")
103
+ #raise("Repeatable#next must be overridden in subclasses")
104
+ end
105
+
106
+ def this(pointer)
107
+ !@now.nil? || raise("Start point must be set before calling #next")
108
+ [:future, :past].include?(pointer) || raise("First argument 'pointer' must be one of :past or :future")
109
+ end
110
+
111
+ def to_s
112
+ 'repeater'
113
+ end
114
+ end
@@ -0,0 +1,40 @@
1
+ class Chronic::RepeaterDay < Chronic::Repeater #:nodoc:
2
+ DAY_SECONDS = 86_400 # (24 * 60 * 60)
3
+
4
+ def next(pointer)
5
+ super
6
+
7
+ @current_day ||= Date.parse(@now.to_s)
8
+ @current_day += pointer == :future ? 1 : -1
9
+ span_begin = Time.parse(@current_day.to_s)
10
+ Chronic::Span.new(span_begin, span_begin + width)
11
+ end
12
+
13
+ def this(pointer = :future)
14
+ super
15
+
16
+ case pointer
17
+ when :future
18
+ day_begin = Time.local(@now.year, @now.month, @now.day, @now.hour + 1)
19
+ day_end = Time.local(@now.year, @now.month, @now.day) + DAY_SECONDS
20
+ when :past
21
+ day_begin = Time.local(@now.year, @now.month, @now.day)
22
+ day_end = Time.local(@now.year, @now.month, @now.day, @now.hour)
23
+ end
24
+
25
+ Chronic::Span.new(day_begin, day_end)
26
+ end
27
+
28
+ def offset(span, amount, pointer)
29
+ direction = pointer == :future ? 1 : -1
30
+ span + direction * amount * DAY_SECONDS
31
+ end
32
+
33
+ def width
34
+ DAY_SECONDS
35
+ end
36
+
37
+ def to_s
38
+ super << '-day'
39
+ end
40
+ end
@@ -0,0 +1,41 @@
1
+ class Chronic::RepeaterDayName < Chronic::Repeater #:nodoc:
2
+ DAY_SECONDS = 86400 # (24 * 60 * 60)
3
+
4
+ def next(pointer)
5
+ super
6
+
7
+ if !@current_day
8
+ @current_day = Date.parse(@now.to_s)
9
+ @current_day += pointer == :future ? 1 : -1
10
+ day_num = symbol_to_number(@type)
11
+ while @current_day.wday != day_num
12
+ @current_day += pointer == :future ? 1 : -1
13
+ end
14
+ else
15
+ @current_day += pointer == :future ? 7 : -7
16
+ end
17
+ span_begin = Time.parse(@current_day.to_s)
18
+ Chronic::Span.new(span_begin, span_begin + width)
19
+ end
20
+
21
+ def this(pointer = :future)
22
+ super
23
+
24
+ self.next(:future)
25
+ end
26
+
27
+ def width
28
+ DAY_SECONDS
29
+ end
30
+
31
+ def to_s
32
+ super << '-dayofweek-' << @type.to_s
33
+ end
34
+
35
+ private
36
+
37
+ def symbol_to_number(sym)
38
+ lookup = {:sunday => 0, :monday => 1, :tuesday => 2, :wednesday => 3, :thursday => 4, :friday => 5, :saturday => 6}
39
+ lookup[sym] || raise("Invalid symbol specified")
40
+ end
41
+ end
@@ -0,0 +1,93 @@
1
+ class Chronic::RepeaterDayPortion < Chronic::Repeater #:nodoc:
2
+ @@morning = (6 * 60 * 60)..(12 * 60 * 60) # 6am-12am
3
+ @@afternoon = (13 * 60 * 60)..(17 * 60 * 60) # 1pm-5pm
4
+ @@evening = (17 * 60 * 60)..(20 * 60 * 60) # 5pm-8pm
5
+ @@night = (20 * 60 * 60)..(24 * 60 * 60) # 8pm-12pm
6
+
7
+ def initialize(type)
8
+ super
9
+
10
+ if type.kind_of? Integer
11
+ @range = (@type * 60 * 60)..((@type + 12) * 60 * 60)
12
+ else
13
+ lookup = {:am => 1..(12 * 60 * 60),
14
+ :pm => (12 * 60 * 60)..(24 * 60 * 60),
15
+ :morning => @@morning,
16
+ :afternoon => @@afternoon,
17
+ :evening => @@evening,
18
+ :night => @@night}
19
+ @range = lookup[type]
20
+ lookup[type] || raise("Invalid type '#{type}' for RepeaterDayPortion")
21
+ end
22
+ @range || raise("Range should have been set by now")
23
+ end
24
+
25
+ def next(pointer)
26
+ super
27
+
28
+ full_day = 60 * 60 * 24
29
+
30
+ if !@current_span
31
+ now_seconds = @now - Time.local(@now.year, @now.month, @now.day)
32
+ if now_seconds < @range.begin
33
+ case pointer
34
+ when :future
35
+ range_start = Time.local(@now.year, @now.month, @now.day) + @range.begin
36
+ when :past
37
+ range_start = Time.local(@now.year, @now.month, @now.day) - full_day + @range.begin
38
+ end
39
+ elsif now_seconds > @range.end
40
+ case pointer
41
+ when :future
42
+ range_start = Time.local(@now.year, @now.month, @now.day) + full_day + @range.begin
43
+ when :past
44
+ range_start = Time.local(@now.year, @now.month, @now.day) + @range.begin
45
+ end
46
+ else
47
+ case pointer
48
+ when :future
49
+ range_start = Time.local(@now.year, @now.month, @now.day) + full_day + @range.begin
50
+ when :past
51
+ range_start = Time.local(@now.year, @now.month, @now.day) - full_day + @range.begin
52
+ end
53
+ end
54
+
55
+ @current_span = Chronic::Span.new(range_start, range_start + (@range.end - @range.begin))
56
+ else
57
+ case pointer
58
+ when :future
59
+ @current_span += full_day
60
+ when :past
61
+ @current_span -= full_day
62
+ end
63
+ end
64
+ end
65
+
66
+ def this(context = :future)
67
+ super
68
+
69
+ range_start = Time.local(@now.year, @now.month, @now.day) + @range.begin
70
+ @current_span = Chronic::Span.new(range_start, range_start + (@range.end - @range.begin))
71
+ end
72
+
73
+ def offset(span, amount, pointer)
74
+ @now = span.begin
75
+ portion_span = self.next(pointer)
76
+ direction = pointer == :future ? 1 : -1
77
+ portion_span + (direction * (amount - 1) * Chronic::RepeaterDay::DAY_SECONDS)
78
+ end
79
+
80
+ def width
81
+ @range || raise("Range has not been set")
82
+ return @current_span.width if @current_span
83
+ if @type.kind_of? Integer
84
+ return (12 * 60 * 60)
85
+ else
86
+ @range.end - @range.begin
87
+ end
88
+ end
89
+
90
+ def to_s
91
+ super << '-dayportion-' << @type.to_s
92
+ end
93
+ end
@@ -0,0 +1,64 @@
1
+ class Chronic::RepeaterFortnight < Chronic::Repeater #:nodoc:
2
+ FORTNIGHT_SECONDS = 1_209_600 # (14 * 24 * 60 * 60)
3
+
4
+ def next(pointer)
5
+ super
6
+
7
+ if !@current_fortnight_start
8
+ case pointer
9
+ when :future
10
+ sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
11
+ sunday_repeater.start = @now
12
+ next_sunday_span = sunday_repeater.next(:future)
13
+ @current_fortnight_start = next_sunday_span.begin
14
+ when :past
15
+ sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
16
+ sunday_repeater.start = (@now + Chronic::RepeaterDay::DAY_SECONDS)
17
+ 2.times { sunday_repeater.next(:past) }
18
+ last_sunday_span = sunday_repeater.next(:past)
19
+ @current_fortnight_start = last_sunday_span.begin
20
+ end
21
+ else
22
+ direction = pointer == :future ? 1 : -1
23
+ @current_fortnight_start += direction * FORTNIGHT_SECONDS
24
+ end
25
+
26
+ Chronic::Span.new(@current_fortnight_start, @current_fortnight_start + FORTNIGHT_SECONDS)
27
+ end
28
+
29
+ def this(pointer = :future)
30
+ super
31
+
32
+ case pointer
33
+ when :future
34
+ this_fortnight_start = Time.local(@now.year, @now.month, @now.day, @now.hour) + Chronic::RepeaterHour::HOUR_SECONDS
35
+ sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
36
+ sunday_repeater.start = @now
37
+ sunday_repeater.this(:future)
38
+ this_sunday_span = sunday_repeater.this(:future)
39
+ this_fortnight_end = this_sunday_span.begin
40
+ Chronic::Span.new(this_fortnight_start, this_fortnight_end)
41
+ when :past
42
+ this_fortnight_end = Time.local(@now.year, @now.month, @now.day, @now.hour)
43
+ sunday_repeater = Chronic::RepeaterDayName.new(:sunday)
44
+ sunday_repeater.start = @now
45
+ #sunday_repeater.next(:past)
46
+ last_sunday_span = sunday_repeater.next(:past)
47
+ this_fortnight_start = last_sunday_span.begin
48
+ Chronic::Span.new(this_fortnight_start, this_fortnight_end)
49
+ end
50
+ end
51
+
52
+ def offset(span, amount, pointer)
53
+ direction = pointer == :future ? 1 : -1
54
+ span + direction * amount * FORTNIGHT_SECONDS
55
+ end
56
+
57
+ def width
58
+ FORTNIGHT_SECONDS
59
+ end
60
+
61
+ def to_s
62
+ super << '-fortnight'
63
+ end
64
+ end
@@ -0,0 +1,52 @@
1
+ class Chronic::RepeaterHour < Chronic::Repeater #:nodoc:
2
+ HOUR_SECONDS = 3600 # 60 * 60
3
+
4
+ def next(pointer)
5
+ super
6
+
7
+ if !@current_hour_start
8
+ case pointer
9
+ when :future
10
+ @current_hour_start = Time.local(@now.year, @now.month, @now.day, @now.hour + 1)
11
+ when :past
12
+ @current_hour_start = Time.local(@now.year, @now.month, @now.day, @now.hour - 1)
13
+ end
14
+ else
15
+ direction = pointer == :future ? 1 : -1
16
+ @current_hour_start += direction * HOUR_SECONDS
17
+ end
18
+
19
+ Chronic::Span.new(@current_hour_start, @current_hour_start + HOUR_SECONDS)
20
+ end
21
+
22
+ def this(pointer = :future)
23
+ super
24
+
25
+ case pointer
26
+ when :future
27
+ hour_start = Time.local(@now.year, @now.month, @now.day, @now.hour, @now.min + 1)
28
+ hour_end = Time.local(@now.year, @now.month, @now.day, @now.hour + 1)
29
+ when :past
30
+ hour_start = Time.local(@now.year, @now.month, @now.day, @now.hour)
31
+ hour_end = Time.local(@now.year, @now.month, @now.day, @now.hour, @now.min)
32
+ when :none
33
+ hour_start = Time.local(@now.year, @now.month, @now.day, @now.hour)
34
+ hour_end = hour_begin + HOUR_SECONDS
35
+ end
36
+
37
+ Chronic::Span.new(hour_start, hour_end)
38
+ end
39
+
40
+ def offset(span, amount, pointer)
41
+ direction = pointer == :future ? 1 : -1
42
+ span + direction * amount * HOUR_SECONDS
43
+ end
44
+
45
+ def width
46
+ HOUR_SECONDS
47
+ end
48
+
49
+ def to_s
50
+ super << '-hour'
51
+ end
52
+ end