chronic_2011 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/.gitignore +6 -0
  2. data/HISTORY.md +4 -0
  3. data/LICENSE +21 -0
  4. data/README.md +180 -0
  5. data/Rakefile +46 -0
  6. data/chronic.gemspec +18 -0
  7. data/lib/chronic.rb +117 -0
  8. data/lib/chronic/chronic.rb +346 -0
  9. data/lib/chronic/grabber.rb +33 -0
  10. data/lib/chronic/handler.rb +88 -0
  11. data/lib/chronic/handlers.rb +553 -0
  12. data/lib/chronic/mini_date.rb +38 -0
  13. data/lib/chronic/numerizer.rb +121 -0
  14. data/lib/chronic/ordinal.rb +47 -0
  15. data/lib/chronic/pointer.rb +32 -0
  16. data/lib/chronic/repeater.rb +142 -0
  17. data/lib/chronic/repeaters/repeater_day.rb +53 -0
  18. data/lib/chronic/repeaters/repeater_day_name.rb +52 -0
  19. data/lib/chronic/repeaters/repeater_day_portion.rb +108 -0
  20. data/lib/chronic/repeaters/repeater_fortnight.rb +71 -0
  21. data/lib/chronic/repeaters/repeater_hour.rb +58 -0
  22. data/lib/chronic/repeaters/repeater_minute.rb +58 -0
  23. data/lib/chronic/repeaters/repeater_month.rb +79 -0
  24. data/lib/chronic/repeaters/repeater_month_name.rb +94 -0
  25. data/lib/chronic/repeaters/repeater_season.rb +109 -0
  26. data/lib/chronic/repeaters/repeater_season_name.rb +43 -0
  27. data/lib/chronic/repeaters/repeater_second.rb +42 -0
  28. data/lib/chronic/repeaters/repeater_time.rb +128 -0
  29. data/lib/chronic/repeaters/repeater_week.rb +74 -0
  30. data/lib/chronic/repeaters/repeater_weekday.rb +85 -0
  31. data/lib/chronic/repeaters/repeater_weekend.rb +66 -0
  32. data/lib/chronic/repeaters/repeater_year.rb +77 -0
  33. data/lib/chronic/scalar.rb +116 -0
  34. data/lib/chronic/season.rb +26 -0
  35. data/lib/chronic/separator.rb +94 -0
  36. data/lib/chronic/span.rb +31 -0
  37. data/lib/chronic/tag.rb +36 -0
  38. data/lib/chronic/time_zone.rb +32 -0
  39. data/lib/chronic/token.rb +47 -0
  40. data/test/helper.rb +12 -0
  41. data/test/test_chronic.rb +148 -0
  42. data/test/test_daylight_savings.rb +118 -0
  43. data/test/test_handler.rb +104 -0
  44. data/test/test_mini_date.rb +32 -0
  45. data/test/test_numerizer.rb +72 -0
  46. data/test/test_parsing.rb +977 -0
  47. data/test/test_repeater_day_name.rb +51 -0
  48. data/test/test_repeater_day_portion.rb +254 -0
  49. data/test/test_repeater_fortnight.rb +62 -0
  50. data/test/test_repeater_hour.rb +68 -0
  51. data/test/test_repeater_minute.rb +34 -0
  52. data/test/test_repeater_month.rb +50 -0
  53. data/test/test_repeater_month_name.rb +56 -0
  54. data/test/test_repeater_season.rb +40 -0
  55. data/test/test_repeater_time.rb +70 -0
  56. data/test/test_repeater_week.rb +62 -0
  57. data/test/test_repeater_weekday.rb +55 -0
  58. data/test/test_repeater_weekend.rb +74 -0
  59. data/test/test_repeater_year.rb +69 -0
  60. data/test/test_span.rb +23 -0
  61. data/test/test_token.rb +25 -0
  62. metadata +156 -0
@@ -0,0 +1,38 @@
1
+ module Chronic
2
+ class MiniDate
3
+ attr_accessor :month, :day
4
+
5
+ def self.from_time(time)
6
+ new(time.month, time.day)
7
+ end
8
+
9
+ def initialize(month, day)
10
+ unless (1..12).include?(month)
11
+ raise ArgumentError, "1..12 are valid months"
12
+ end
13
+
14
+ @month = month
15
+ @day = day
16
+ end
17
+
18
+ def is_between?(md_start, md_end)
19
+ return false if (@month == md_start.month && @month == md_end.month) &&
20
+ (@day < md_start.day || @day > md_end.day)
21
+ return true if (@month == md_start.month && @day >= md_start.day) ||
22
+ (@month == md_end.month && @day <= md_end.day)
23
+
24
+ i = (md_start.month % 12) + 1
25
+
26
+ until i == md_end.month
27
+ return true if @month == i
28
+ i = (i % 12) + 1
29
+ end
30
+
31
+ return false
32
+ end
33
+
34
+ def equals?(other)
35
+ @month == other.month and @day == other.day
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,121 @@
1
+ require 'strscan'
2
+
3
+ module Chronic
4
+ class Numerizer
5
+
6
+ DIRECT_NUMS = [
7
+ ['eleven', '11'],
8
+ ['twelve', '12'],
9
+ ['thirteen', '13'],
10
+ ['fourteen', '14'],
11
+ ['fifteen', '15'],
12
+ ['sixteen', '16'],
13
+ ['seventeen', '17'],
14
+ ['eighteen', '18'],
15
+ ['nineteen', '19'],
16
+ ['ninteen', '19'], # Common mis-spelling
17
+ ['zero', '0'],
18
+ ['one', '1'],
19
+ ['two', '2'],
20
+ ['three', '3'],
21
+ ['four(\W|$)', '4\1'], # The weird regex is so that it matches four but not fourty
22
+ ['five', '5'],
23
+ ['six(\W|$)', '6\1'],
24
+ ['seven(\W|$)', '7\1'],
25
+ ['eight(\W|$)', '8\1'],
26
+ ['nine(\W|$)', '9\1'],
27
+ ['ten', '10'],
28
+ ['\ba[\b^$]', '1'] # doesn't make sense for an 'a' at the end to be a 1
29
+ ]
30
+
31
+ ORDINALS = [
32
+ ['first', '1'],
33
+ ['third', '3'],
34
+ ['fourth', '4'],
35
+ ['fifth', '5'],
36
+ ['sixth', '6'],
37
+ ['seventh', '7'],
38
+ ['eighth', '8'],
39
+ ['ninth', '9'],
40
+ ['tenth', '10']
41
+ ]
42
+
43
+ TEN_PREFIXES = [
44
+ ['twenty', 20],
45
+ ['thirty', 30],
46
+ ['forty', 40],
47
+ ['fourty', 40], # Common mis-spelling
48
+ ['fifty', 50],
49
+ ['sixty', 60],
50
+ ['seventy', 70],
51
+ ['eighty', 80],
52
+ ['ninety', 90]
53
+ ]
54
+
55
+ BIG_PREFIXES = [
56
+ ['hundred', 100],
57
+ ['thousand', 1000],
58
+ ['million', 1_000_000],
59
+ ['billion', 1_000_000_000],
60
+ ['trillion', 1_000_000_000_000],
61
+ ]
62
+
63
+ def self.numerize(string)
64
+ string = string.dup
65
+
66
+ # preprocess
67
+ string.gsub!(/ +|([^\d])-([^\d])/, '\1 \2') # will mutilate hyphenated-words but shouldn't matter for date extraction
68
+ string.gsub!(/a half/, 'haAlf') # take the 'a' out so it doesn't turn into a 1, save the half for the end
69
+
70
+ # easy/direct replacements
71
+
72
+ DIRECT_NUMS.each do |dn|
73
+ string.gsub!(/#{dn[0]}/i, '<num>' + dn[1])
74
+ end
75
+
76
+ ORDINALS.each do |on|
77
+ string.gsub!(/#{on[0]}/i, '<num>' + on[1] + on[0][-2, 2])
78
+ end
79
+
80
+ # ten, twenty, etc.
81
+
82
+ TEN_PREFIXES.each do |tp|
83
+ string.gsub!(/(?:#{tp[0]}) *<num>(\d(?=[^\d]|$))*/i) { '<num>' + (tp[1] + $1.to_i).to_s }
84
+ end
85
+
86
+ TEN_PREFIXES.each do |tp|
87
+ string.gsub!(/#{tp[0]}/i) { '<num>' + tp[1].to_s }
88
+ end
89
+
90
+ # hundreds, thousands, millions, etc.
91
+
92
+ BIG_PREFIXES.each do |bp|
93
+ string.gsub!(/(?:<num>)?(\d*) *#{bp[0]}/i) { '<num>' + (bp[1] * $1.to_i).to_s}
94
+ andition(string)
95
+ end
96
+
97
+ # fractional addition
98
+ # I'm not combining this with the previous block as using float addition complicates the strings
99
+ # (with extraneous .0's and such )
100
+ string.gsub!(/(\d+)(?: | and |-)*haAlf/i) { ($1.to_f + 0.5).to_s }
101
+
102
+ string.gsub(/<num>/, '')
103
+ end
104
+
105
+ class << self
106
+ private
107
+
108
+ def andition(string)
109
+ sc = StringScanner.new(string)
110
+
111
+ while sc.scan_until(/<num>(\d+)( | and )<num>(\d+)(?=[^\w]|$)/i)
112
+ if sc[2] =~ /and/ || sc[1].size > sc[3].size
113
+ string[(sc.pos - sc.matched_size)..(sc.pos-1)] = '<num>' + (sc[1].to_i + sc[3].to_i).to_s
114
+ sc.reset
115
+ end
116
+ end
117
+ end
118
+
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,47 @@
1
+ module Chronic
2
+ class Ordinal < Tag
3
+
4
+ # Scan an Array of Token objects and apply any necessary Ordinal
5
+ # tags to each token.
6
+ #
7
+ # tokens - An Array of tokens to scan.
8
+ # options - The Hash of options specified in Chronic::parse.
9
+ #
10
+ # Returns an Array of tokens.
11
+ def self.scan(tokens, options)
12
+ tokens.each do |token|
13
+ if t = scan_for_ordinals(token) then token.tag(t) end
14
+ if t = scan_for_days(token) then token.tag(t) end
15
+ end
16
+ end
17
+
18
+ # token - The Token object we want to scan.
19
+ #
20
+ # Returns a new Ordinal object.
21
+ def self.scan_for_ordinals(token)
22
+ Ordinal.new($1.to_i) if token.word =~ /^(\d*)(st|nd|rd|th)$/
23
+ end
24
+
25
+ # token - The Token object we want to scan.
26
+ #
27
+ # Returns a new Ordinal object.
28
+ def self.scan_for_days(token)
29
+ if token.word =~ /^(\d*)(st|nd|rd|th)$/
30
+ unless $1.to_i > 31 || $1.to_i < 1
31
+ OrdinalDay.new(token.word.to_i)
32
+ end
33
+ end
34
+ end
35
+
36
+ def to_s
37
+ 'ordinal'
38
+ end
39
+ end
40
+
41
+ class OrdinalDay < Ordinal #:nodoc:
42
+ def to_s
43
+ super << '-day-' << @type.to_s
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,32 @@
1
+ module Chronic
2
+ class Pointer < Tag
3
+
4
+ # Scan an Array of Token objects and apply any necessary Pointer
5
+ # tags to each token.
6
+ #
7
+ # tokens - An Array of tokens to scan.
8
+ # options - The Hash of options specified in Chronic::parse.
9
+ #
10
+ # Returns an Array of tokens.
11
+ def self.scan(tokens, options)
12
+ tokens.each do |token|
13
+ if t = scan_for_all(token) then token.tag(t) end
14
+ end
15
+ end
16
+
17
+ # token - The Token object we want to scan.
18
+ #
19
+ # Returns a new Pointer object.
20
+ def self.scan_for_all(token)
21
+ scan_for token, self,
22
+ {
23
+ /\bpast\b/ => :past,
24
+ /\b(?:future|in)\b/ => :future,
25
+ }
26
+ end
27
+
28
+ def to_s
29
+ 'pointer-' << @type.to_s
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,142 @@
1
+ module Chronic
2
+ class Repeater < Tag
3
+
4
+ # Scan an Array of Token objects and apply any necessary Repeater
5
+ # tags to each token.
6
+ #
7
+ # tokens - An Array of tokens to scan.
8
+ # options - The Hash of options specified in Chronic::parse.
9
+ #
10
+ # Returns an Array of tokens.
11
+ def self.scan(tokens, options)
12
+ tokens.each do |token|
13
+ if t = scan_for_season_names(token) then token.tag(t); next end
14
+ if t = scan_for_month_names(token) then token.tag(t); next end
15
+ if t = scan_for_day_names(token) then token.tag(t); next end
16
+ if t = scan_for_day_portions(token) then token.tag(t); next end
17
+ if t = scan_for_times(token) then token.tag(t); next end
18
+ if t = scan_for_units(token) then token.tag(t); next end
19
+ end
20
+ end
21
+
22
+ # token - The Token object we want to scan.
23
+ #
24
+ # Returns a new Repeater object.
25
+ def self.scan_for_season_names(token)
26
+ scan_for token, RepeaterSeasonName,
27
+ {
28
+ /^springs?$/ => :spring,
29
+ /^summers?$/ => :summer,
30
+ /^(autumn)|(fall)s?$/ => :autumn,
31
+ /^winters?$/ => :winter
32
+ }
33
+ end
34
+
35
+ # token - The Token object we want to scan.
36
+ #
37
+ # Returns a new Repeater object.
38
+ def self.scan_for_month_names(token)
39
+ scan_for token, RepeaterMonthName,
40
+ {
41
+ /^jan\.?(uary)?$/ => :january,
42
+ /^feb\.?(ruary)?$/ => :february,
43
+ /^mar\.?(ch)?$/ => :march,
44
+ /^apr\.?(il)?$/ => :april,
45
+ /^may$/ => :may,
46
+ /^jun\.?e?$/ => :june,
47
+ /^jul\.?y?$/ => :july,
48
+ /^aug\.?(ust)?$/ => :august,
49
+ /^sep\.?(t\.?|tember)?$/ => :september,
50
+ /^oct\.?(ober)?$/ => :october,
51
+ /^nov\.?(ember)?$/ => :november,
52
+ /^dec\.?(ember)?$/ => :december
53
+ }
54
+ end
55
+
56
+ # token - The Token object we want to scan.
57
+ #
58
+ # Returns a new Repeater object.
59
+ def self.scan_for_day_names(token)
60
+ scan_for token, RepeaterDayName,
61
+ {
62
+ /^m[ou]n(day)?$/ => :monday,
63
+ /^t(ue|eu|oo|u|)s?(day)?$/ => :tuesday,
64
+ /^we(d|dnes|nds|nns)(day)?$/ => :wednesday,
65
+ /^th(u|ur|urs|ers)(day)?$/ => :thursday,
66
+ /^fr[iy](day)?$/ => :friday,
67
+ /^sat(t?[ue]rday)?$/ => :saturday,
68
+ /^su[nm](day)?$/ => :sunday
69
+ }
70
+ end
71
+
72
+ # token - The Token object we want to scan.
73
+ #
74
+ # Returns a new Repeater object.
75
+ def self.scan_for_day_portions(token)
76
+ scan_for token, RepeaterDayPortion,
77
+ {
78
+ /^ams?$/ => :am,
79
+ /^pms?$/ => :pm,
80
+ /^mornings?$/ => :morning,
81
+ /^afternoons?$/ => :afternoon,
82
+ /^evenings?$/ => :evening,
83
+ /^(night|nite)s?$/ => :night
84
+ }
85
+ end
86
+
87
+ # token - The Token object we want to scan.
88
+ #
89
+ # Returns a new Repeater object.
90
+ def self.scan_for_times(token)
91
+ scan_for token, RepeaterTime, /^\d{1,2}(:?\d{2})?([\.:]?\d{2})?$/
92
+ end
93
+
94
+ # token - The Token object we want to scan.
95
+ #
96
+ # Returns a new Repeater object.
97
+ def self.scan_for_units(token)
98
+ {
99
+ /^years?$/ => :year,
100
+ /^seasons?$/ => :season,
101
+ /^months?$/ => :month,
102
+ /^fortnights?$/ => :fortnight,
103
+ /^weeks?$/ => :week,
104
+ /^weekends?$/ => :weekend,
105
+ /^(week|business)days?$/ => :weekday,
106
+ /^days?$/ => :day,
107
+ /^hours?$/ => :hour,
108
+ /^minutes?$/ => :minute,
109
+ /^seconds?$/ => :second
110
+ }.each do |item, symbol|
111
+ if item =~ token.word
112
+ klass_name = 'Repeater' + symbol.to_s.capitalize
113
+ klass = Chronic.const_get(klass_name)
114
+ return klass.new(symbol)
115
+ end
116
+ end
117
+ return nil
118
+ end
119
+
120
+ def <=>(other)
121
+ width <=> other.width
122
+ end
123
+
124
+ # returns the width (in seconds or months) of this repeatable.
125
+ def width
126
+ raise("Repeater#width must be overridden in subclasses")
127
+ end
128
+
129
+ # returns the next occurance of this repeatable.
130
+ def next(pointer)
131
+ raise("Start point must be set before calling #next") unless @now
132
+ end
133
+
134
+ def this(pointer)
135
+ raise("Start point must be set before calling #this") unless @now
136
+ end
137
+
138
+ def to_s
139
+ 'repeater'
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,53 @@
1
+ module Chronic
2
+ class RepeaterDay < Repeater #:nodoc:
3
+ DAY_SECONDS = 86_400 # (24 * 60 * 60)
4
+
5
+ def initialize(type)
6
+ super
7
+ end
8
+
9
+ def next(pointer)
10
+ super
11
+
12
+ if !@current_day_start
13
+ @current_day_start = Chronic.time_class.local(@now.year, @now.month, @now.day)
14
+ end
15
+
16
+ direction = pointer == :future ? 1 : -1
17
+ @current_day_start += direction * DAY_SECONDS
18
+
19
+ Span.new(@current_day_start, @current_day_start + DAY_SECONDS)
20
+ end
21
+
22
+ def this(pointer = :future)
23
+ super
24
+
25
+ case pointer
26
+ when :future
27
+ day_begin = Chronic.construct(@now.year, @now.month, @now.day, @now.hour)
28
+ day_end = Chronic.construct(@now.year, @now.month, @now.day) + DAY_SECONDS
29
+ when :past
30
+ day_begin = Chronic.construct(@now.year, @now.month, @now.day)
31
+ day_end = Chronic.construct(@now.year, @now.month, @now.day, @now.hour)
32
+ when :none
33
+ day_begin = Chronic.construct(@now.year, @now.month, @now.day)
34
+ day_end = Chronic.construct(@now.year, @now.month, @now.day) + DAY_SECONDS
35
+ end
36
+
37
+ Span.new(day_begin, day_end)
38
+ end
39
+
40
+ def offset(span, amount, pointer)
41
+ direction = pointer == :future ? 1 : -1
42
+ span + direction * amount * DAY_SECONDS
43
+ end
44
+
45
+ def width
46
+ DAY_SECONDS
47
+ end
48
+
49
+ def to_s
50
+ super << '-day'
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,52 @@
1
+ module Chronic
2
+ class RepeaterDayName < Repeater #:nodoc:
3
+ DAY_SECONDS = 86400 # (24 * 60 * 60)
4
+
5
+ def initialize(type)
6
+ super
7
+ end
8
+
9
+ def next(pointer)
10
+ super
11
+
12
+ direction = pointer == :future ? 1 : -1
13
+
14
+ if !@current_date
15
+ @current_date = Date.new(@now.year, @now.month, @now.day)
16
+ @current_date += direction
17
+
18
+ day_num = symbol_to_number(@type)
19
+
20
+ while @current_date.wday != day_num
21
+ @current_date += direction
22
+ end
23
+ else
24
+ @current_date += direction * 7
25
+ end
26
+ next_date = @current_date.succ
27
+ Span.new(Chronic.construct(@current_date.year, @current_date.month, @current_date.day), Chronic.construct(next_date.year, next_date.month, next_date.day))
28
+ end
29
+
30
+ def this(pointer = :future)
31
+ super
32
+
33
+ pointer = :future if pointer == :none
34
+ self.next(pointer)
35
+ end
36
+
37
+ def width
38
+ DAY_SECONDS
39
+ end
40
+
41
+ def to_s
42
+ super << '-dayname-' << @type.to_s
43
+ end
44
+
45
+ private
46
+
47
+ def symbol_to_number(sym)
48
+ lookup = {:sunday => 0, :monday => 1, :tuesday => 2, :wednesday => 3, :thursday => 4, :friday => 5, :saturday => 6}
49
+ lookup[sym] || raise("Invalid symbol specified")
50
+ end
51
+ end
52
+ end