chronic_2011 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 (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