chronic-davispuh 0.10.2.v0.1da32066b3f46f2506b3471e39557497e34afa27

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.travis.yml +10 -0
  4. data/Gemfile +3 -0
  5. data/HISTORY.md +243 -0
  6. data/LICENSE +21 -0
  7. data/README.md +185 -0
  8. data/Rakefile +68 -0
  9. data/chronic.gemspec +27 -0
  10. data/lib/chronic.rb +122 -0
  11. data/lib/chronic/arrow.rb +270 -0
  12. data/lib/chronic/date.rb +272 -0
  13. data/lib/chronic/definition.rb +208 -0
  14. data/lib/chronic/dictionary.rb +36 -0
  15. data/lib/chronic/handler.rb +44 -0
  16. data/lib/chronic/handlers/anchor.rb +65 -0
  17. data/lib/chronic/handlers/arrow.rb +84 -0
  18. data/lib/chronic/handlers/date.rb +270 -0
  19. data/lib/chronic/handlers/date_time.rb +72 -0
  20. data/lib/chronic/handlers/general.rb +130 -0
  21. data/lib/chronic/handlers/narrow.rb +54 -0
  22. data/lib/chronic/handlers/time.rb +167 -0
  23. data/lib/chronic/handlers/time_zone.rb +50 -0
  24. data/lib/chronic/objects/anchor_object.rb +263 -0
  25. data/lib/chronic/objects/arrow_object.rb +27 -0
  26. data/lib/chronic/objects/date_object.rb +164 -0
  27. data/lib/chronic/objects/date_time_object.rb +64 -0
  28. data/lib/chronic/objects/handler_object.rb +81 -0
  29. data/lib/chronic/objects/narrow_object.rb +85 -0
  30. data/lib/chronic/objects/time_object.rb +96 -0
  31. data/lib/chronic/objects/time_zone_object.rb +27 -0
  32. data/lib/chronic/parser.rb +154 -0
  33. data/lib/chronic/span.rb +32 -0
  34. data/lib/chronic/tag.rb +84 -0
  35. data/lib/chronic/tags/day_name.rb +34 -0
  36. data/lib/chronic/tags/day_portion.rb +33 -0
  37. data/lib/chronic/tags/day_special.rb +30 -0
  38. data/lib/chronic/tags/grabber.rb +29 -0
  39. data/lib/chronic/tags/keyword.rb +63 -0
  40. data/lib/chronic/tags/month_name.rb +39 -0
  41. data/lib/chronic/tags/ordinal.rb +52 -0
  42. data/lib/chronic/tags/pointer.rb +28 -0
  43. data/lib/chronic/tags/rational.rb +35 -0
  44. data/lib/chronic/tags/scalar.rb +101 -0
  45. data/lib/chronic/tags/season_name.rb +31 -0
  46. data/lib/chronic/tags/separator.rb +130 -0
  47. data/lib/chronic/tags/sign.rb +35 -0
  48. data/lib/chronic/tags/time_special.rb +34 -0
  49. data/lib/chronic/tags/time_zone.rb +56 -0
  50. data/lib/chronic/tags/unit.rb +174 -0
  51. data/lib/chronic/time.rb +141 -0
  52. data/lib/chronic/time_zone.rb +80 -0
  53. data/lib/chronic/token.rb +61 -0
  54. data/lib/chronic/token_group.rb +271 -0
  55. data/lib/chronic/tokenizer.rb +42 -0
  56. data/lib/chronic/version.rb +3 -0
  57. data/test/helper.rb +12 -0
  58. data/test/test_chronic.rb +190 -0
  59. data/test/test_daylight_savings.rb +98 -0
  60. data/test/test_handler.rb +113 -0
  61. data/test/test_parsing.rb +1520 -0
  62. data/test/test_span.rb +23 -0
  63. data/test/test_token.rb +31 -0
  64. metadata +218 -0
@@ -0,0 +1,64 @@
1
+ require 'chronic/handlers/date_time'
2
+
3
+ module Chronic
4
+ class DateTimeObject < HandlerObject
5
+ include DateStructure
6
+ include TimeStructure
7
+ include TimeZoneStructure
8
+
9
+ def initialize(tokens, token_index, definitions, local_date, options)
10
+ super
11
+ @normalized = false
12
+ match(tokens, @index, definitions)
13
+ end
14
+
15
+ def normalize!
16
+ return if @normalized
17
+ adjust!
18
+ @normalized = true
19
+ end
20
+
21
+ def is_valid?
22
+ normalize!
23
+ return false if @year.nil? or @month.nil? or @day.nil? or @hour.nil? or @minute.nil? or @second.nil?
24
+ ::Date.valid_date?(@year, @month, @day)
25
+ end
26
+
27
+ def get_end
28
+ year = @year
29
+ month = @month
30
+ day = @day
31
+ hour = @hour
32
+ minute = @minute
33
+ second = @second
34
+ if @precision == :subsecond
35
+ minute, second = Time::add_second(minute, second, 1.0 / @subsecond_size)
36
+ else
37
+ minute, second = Time::add_second(minute, second)
38
+ end
39
+ [year, month, day, hour, minute, second]
40
+ end
41
+
42
+ def to_span
43
+ span_start = Chronic.construct(@year, @month, @day, @hour, @minute, @second, self)
44
+ end_year, end_month, end_day, end_hour, end_minute, end_second = get_end
45
+ span_end = Chronic.construct(end_year, end_month, end_day, end_hour, end_minute, end_second, self)
46
+ Span.new(span_start, span_end, true)
47
+ end
48
+
49
+ def to_s
50
+ "year #{@year.inspect}, month #{@month.inspect}, day #{@day.inspect}, hour #{@hour.inspect}, minute #{@minute.inspect}, second #{@second.inspect}, subsecond #{@subsecond.inspect}"
51
+ end
52
+
53
+ protected
54
+
55
+ def adjust!
56
+ @second ||= 0
57
+ @second += @subsecond if @subsecond
58
+ @subsecond = 0
59
+ end
60
+
61
+ include DateTimeHandlers
62
+
63
+ end
64
+ end
@@ -0,0 +1,81 @@
1
+ require 'chronic/handlers/general'
2
+
3
+ module Chronic
4
+ class HandlerObject
5
+
6
+ attr_accessor :local_date
7
+ attr_reader :begin
8
+ attr_reader :width
9
+ attr_reader :precision
10
+ def initialize(tokens, token_index, definitions, local_date, options)
11
+ @tokens = tokens
12
+ @begin = token_index
13
+ @local_date = local_date
14
+ @options = options
15
+ @context = @options[:context]
16
+ @width = 0
17
+ @index = token_index
18
+ end
19
+
20
+ def end
21
+ @begin + @width - 1
22
+ end
23
+
24
+ def range
25
+ @begin..(@begin + @width - 1)
26
+ end
27
+
28
+ def overlap?(r2)
29
+ r1 = range
30
+ (r1.begin <= r2.end) and (r2.begin <= r1.end)
31
+ end
32
+
33
+ def is_valid?
34
+ false
35
+ end
36
+
37
+ def local_day
38
+ [@local_date.year, @local_date.month, @local_date.day]
39
+ end
40
+
41
+ def local_time
42
+ [@local_date.hour, @local_date.min, @local_date.sec + @local_date.usec.to_f / (10 ** 6)]
43
+ end
44
+
45
+ protected
46
+
47
+ def match(tokens, token_index, definitions)
48
+ definitions.each do |definition|
49
+ if Handler.match(tokens, token_index, definition.first)
50
+ self.method(definition.last).call
51
+ @width = @index - @begin
52
+ puts "\nHandler: #{self.class.name}.#{definition.last.to_s} @ #{@begin}-#{self.end} =>\n=======> #{self}" if Chronic.debug and @width > 0
53
+ return
54
+ end
55
+ end
56
+ @width = 0
57
+ end
58
+
59
+ def get_sign
60
+ (@context == :past) ? -1 : ((@context == :future) ? 1 : 0)
61
+ end
62
+
63
+ def get_modifier
64
+ (@grabber == :last) ? -1 : ((@grabber == :next) ? 1 : 0)
65
+ end
66
+
67
+ def next_tag
68
+ @index += 1
69
+ end
70
+
71
+ def handle_possible(tag)
72
+ if @tokens[@index].get_tag(tag)
73
+ next_tag
74
+ return true
75
+ end
76
+ false
77
+ end
78
+
79
+ end
80
+
81
+ end
@@ -0,0 +1,85 @@
1
+ require 'chronic/handlers/narrow'
2
+
3
+ module Chronic
4
+ class NarrowObject < HandlerObject
5
+ attr_reader :number
6
+ attr_reader :wday
7
+ attr_reader :unit
8
+ def initialize(tokens, token_index, definitions, local_date, options)
9
+ super
10
+ handle_possible(SeparatorSpace) if handle_possible(KeywordOn)
11
+ match(tokens, @index, definitions)
12
+ end
13
+
14
+ def is_valid?
15
+ true
16
+ end
17
+
18
+ def to_s
19
+ "number #{@number.inspect}, wday #{@wday.inspect}, unit #{@unit.inspect}, grabber #{@grabber.inspect}"
20
+ end
21
+
22
+ def to_span(span = nil, timezone = nil)
23
+ start = @local_date
24
+ start = span.begin if span
25
+ hour, minute, second, utc_offset = Time::split(start)
26
+ end_hour = end_minute = end_second = 0
27
+ modifier = get_modifier
28
+ sign = get_sign
29
+ if @wday
30
+ diff = Date::wday_diff(start, @wday, 0, 0)
31
+ diff += Date::WEEK_DAYS if diff < 0
32
+ diff += Date::WEEK_DAYS * (@number - 1) if @number > 1
33
+ year, month, day = Date::add_day(start.year, start.month, start.day, diff)
34
+ end_year, end_month, end_day = year, month, day
35
+ end_hour += Date::DAY_HOURS
36
+ else
37
+ case @unit
38
+ when :year
39
+ # TODO
40
+ raise "Not Implemented NarrowObject #{@unit.inspect}"
41
+ when :season
42
+ # TODO
43
+ raise "Not Implemented NarrowObject #{@unit.inspect}"
44
+ when :quarter
45
+ this_quarter = Date::get_quarter_index(start.month)
46
+ diff = Date::quarter_diff(this_quarter, @number, modifier, sign)
47
+ year, quarter = Date::add_quarter(start.year, this_quarter, diff)
48
+ year = start.year if span
49
+ month = Date::QUARTERS[quarter]
50
+ end_year, next_quarter = Date::add_quarter(year, quarter)
51
+ end_month = Date::QUARTERS[next_quarter]
52
+ day = end_day = 1
53
+ hour = minute = second = 0
54
+ when :month
55
+ day = start.day
56
+ year, month = Date::add_month(start.year, start.month, @number - 1)
57
+ end_year, end_month, end_day = year, month, day
58
+ end_year, end_month = Date::add_month(year, month, 1)
59
+ when :fortnight, :week, :weekend, :weekday
60
+ # TODO
61
+ raise "Not Implemented NarrowObject #{@unit.inspect}"
62
+ when :day
63
+ year, month, day = Date::add_day(start.year, start.month, start.day, @number - 1)
64
+ end_year, end_month, end_day = year, month, day
65
+ end_hour += Date::DAY_HOURS
66
+ when :morning, :noon, :afternoon, :evening, :night, :midnight, :hour, :minute, :second, :milisecond
67
+ # TODO
68
+ raise "Not Implemented NarrowObject #{@unit.inspect}"
69
+ else
70
+ raise "Uknown unit #{@unit.inspect}!"
71
+ end
72
+ end
73
+ span_start = Chronic.construct(year, month, day, hour, minute, second, timezone)
74
+ return nil if span and span_start >= span.end
75
+ span_end = Chronic.construct(end_year, end_month, end_day, end_hour, end_minute, end_second, timezone)
76
+ span_end = span.end if span and span_end > span.end
77
+ Span.new(span_start, span_end, true)
78
+ end
79
+
80
+ protected
81
+
82
+ include NarrowHandlers
83
+
84
+ end
85
+ end
@@ -0,0 +1,96 @@
1
+ require 'chronic/handlers/time'
2
+
3
+ module Chronic
4
+ class TimeObject < HandlerObject
5
+ include TimeStructure
6
+ attr_reader :time_special
7
+ attr_reader :day_portion
8
+ attr_reader :ambiguous
9
+ def initialize(tokens, token_index, definitions, local_date, options)
10
+ super
11
+ @ambiguous = true
12
+ @ambiguous = false if @options[:hours24] == true or @options[:ambiguous_time_range] == :none
13
+ @normalized = false
14
+ match(tokens, @index, definitions)
15
+ end
16
+
17
+ def normalize!
18
+ return if @normalized
19
+ if @hour
20
+ if @day_portion == :am # 0am to 12pm
21
+ @hour = 0 if @hour == 12
22
+ @ambiguous = false
23
+ elsif @time_special == :morning
24
+ @ambiguous = false
25
+ elsif @day_portion == :pm or # 12pm to 0am
26
+ @time_special == :afternoon or
27
+ @time_special == :evening
28
+ @hour += 12 if @hour < 12
29
+ @ambiguous = false
30
+ elsif @time_special == :night
31
+ @hour = 0 if @hour == 12
32
+ @hour += 12 if @hour < 12
33
+ @ambiguous = false
34
+ end
35
+ @hour += 12 if @ambiguous and @options[:context] != :none and @hour != 12 and @hour <= @options[:ambiguous_time_range]
36
+ elsif @time_special
37
+ if @time_special == :now
38
+ set_time
39
+ else
40
+ @hour = Time::SPECIAL[@time_special].begin
41
+ end
42
+ else
43
+ @hour = @local_date.hour
44
+ end
45
+ @ambiguous = false
46
+ @minute ||= @second ? @local_date.minute : 0
47
+ @second ||= 0
48
+ @second += @subsecond if @subsecond
49
+ @subsecond = 0
50
+ @normalized = true
51
+ end
52
+
53
+ def is_valid?
54
+ normalize!
55
+ true
56
+ end
57
+
58
+ def get_end
59
+ hour = @hour
60
+ minute = @minute
61
+ second = @second
62
+ case @precision
63
+ when :hour
64
+ hour += 1
65
+ when :minute
66
+ hour, minute = Time::add_minute(hour, minute)
67
+ when :second
68
+ minute, second = Time::add_second(minute, second)
69
+ when :subsecond
70
+ minute, second = Time::add_second(minute, second, 1.0 / @subsecond_size)
71
+ when :time_special
72
+ if @time_special != :now
73
+ hour = Time::SPECIAL[@time_special].end
74
+ minute = second = 0
75
+ end
76
+ else
77
+ # BUG! Should never happen
78
+ raise "Uknown precision #{@precision.inspect}"
79
+ end
80
+ [hour, minute, second]
81
+ end
82
+
83
+ def to_s
84
+ "hour #{@hour.inspect}, minute #{@minute.inspect}, second #{@second.inspect}, subsecond #{@subsecond.inspect}, time special #{time_special.inspect}, day portion #{@day_portion.inspect}, precision #{@precision.inspect}, ambiguous #{ambiguous.inspect}"
85
+ end
86
+
87
+ protected
88
+
89
+ def set_time
90
+ @hour, @minute, @second = local_time
91
+ end
92
+
93
+ include TimeHandlers
94
+
95
+ end
96
+ end
@@ -0,0 +1,27 @@
1
+ require 'chronic/handlers/time_zone'
2
+
3
+ module Chronic
4
+ class TimeZoneObject < HandlerObject
5
+ include TimeZoneStructure
6
+
7
+ def initialize(tokens, token_index, definitions, local_date, options)
8
+ super
9
+ match(tokens, @index, definitions)
10
+ end
11
+
12
+ def normalize!
13
+ return if @normalized
14
+ @offset = to_offset
15
+ @normalized = true
16
+ end
17
+
18
+ def is_valid?
19
+ true
20
+ end
21
+
22
+ protected
23
+
24
+ include TimeZoneHandlers
25
+
26
+ end
27
+ end
@@ -0,0 +1,154 @@
1
+ require 'chronic/dictionary'
2
+
3
+ module Chronic
4
+ class Parser
5
+
6
+ # Hash of default configuration options.
7
+ DEFAULT_OPTIONS = {
8
+ :context => :future,
9
+ :now => nil,
10
+ :hours24 => nil,
11
+ :week_start => :sunday,
12
+ :guess => true,
13
+ :ambiguous_time_range => 6,
14
+ :endian_precedence => [:middle, :little],
15
+ :ambiguous_year_future_bias => 50
16
+ }
17
+
18
+ attr_accessor :now
19
+ attr_reader :options
20
+
21
+ # options - An optional Hash of configuration options:
22
+ # :context - If your string represents a birthday, you can set
23
+ # this value to :past and if an ambiguous string is
24
+ # given, it will assume it is in the past.
25
+ # :now - Time, all computations will be based off of time
26
+ # instead of Time.now.
27
+ # :hours24 - Time will be parsed as it would be 24 hour clock.
28
+ # :week_start - By default, the parser assesses weeks start on
29
+ # sunday but you can change this value to :monday if
30
+ # needed.
31
+ # :guess - By default the parser will guess a single point in time
32
+ # for the given date or time. If you'd rather have the
33
+ # entire time span returned, set this to false
34
+ # and a Chronic::Span will be returned. Setting :guess to :end
35
+ # will return last time from Span, to :middle for middle (same as just true)
36
+ # and :begin for first time from span.
37
+ # :ambiguous_time_range - If an Integer is given, ambiguous times
38
+ # (like 5:00) will be assumed to be within the range of
39
+ # that time in the AM to that time in the PM. For
40
+ # example, if you set it to `7`, then the parser will
41
+ # look for the time between 7am and 7pm. In the case of
42
+ # 5:00, it would assume that means 5:00pm. If `:none`
43
+ # is given, no assumption will be made, and the first
44
+ # matching instance of that time will be used.
45
+ # :endian_precedence - By default, Chronic will parse "03/04/2011"
46
+ # as the fourth day of the third month. Alternatively you
47
+ # can tell Chronic to parse this as the third day of the
48
+ # fourth month by setting this to [:little, :middle].
49
+ # :ambiguous_year_future_bias - When parsing two digit years
50
+ # (ie 79) unlike Rubys Time class, Chronic will attempt
51
+ # to assume the full year using this figure. Chronic will
52
+ # look x amount of years into the future and past. If the
53
+ # two digit year is `now + x years` it's assumed to be the
54
+ # future, `now - x years` is assumed to be the past.
55
+ def initialize(options = {})
56
+ validate_options!(options)
57
+ @options = DEFAULT_OPTIONS.merge(options)
58
+ @now = options[:now] || Chronic.time_class.now
59
+ end
60
+
61
+ # Parse "text" with the given options
62
+ # Returns either a Time or Chronic::Span, depending on the value of options[:guess]
63
+ def parse(text)
64
+ text = pre_proccess(text)
65
+ text = pre_normalize(text)
66
+ puts text.inspect if Chronic.debug
67
+
68
+ tokens = Tokenizer::tokenize(' ' + text + ' ')
69
+ tag(tokens, options)
70
+
71
+ puts "+#{'-' * 51}\n| #{tokens}\n+#{'-' * 51}" if Chronic.debug
72
+
73
+ token_group = TokenGroup.new(tokens, definitions(options), @now, options)
74
+ span = token_group.to_span
75
+
76
+ guess(span, options[:guess]) if span
77
+ end
78
+
79
+ # Replace any whitespace characters to single space
80
+ def pre_proccess(text)
81
+ text.to_s.strip.gsub(/[[:space:]]+/, ' ').gsub(/\s{2,}/, ' ')
82
+ end
83
+
84
+ # Clean up the specified text ready for parsing.
85
+ #
86
+ # Clean up the string, convert number words to numbers
87
+ # (three => 3), and convert ordinal words to numeric
88
+ # ordinals (third => 3rd)
89
+ #
90
+ # text - The String text to normalize.
91
+ #
92
+ # Returns a new String ready for Chronic to parse.
93
+ def pre_normalize(text)
94
+ text.gsub!(/\b(quarters?)\b/, '<=\1=>') # workaround for Numerizer
95
+ text.gsub!(/\b\s+third\b/, ' 3rd')
96
+ text.gsub!(/\b\s+fourth\b/, ' 4th')
97
+ text = Numerizer.numerize(text)
98
+ text.gsub!(/<=(quarters?)=>/, '\1')
99
+ text
100
+ end
101
+
102
+ # Guess a specific time within the given span.
103
+ #
104
+ # span - The Chronic::Span object to calcuate a guess from.
105
+ #
106
+ # Returns a new Time object.
107
+ def guess(span, mode = :middle)
108
+ return span if not mode
109
+ return span.begin + span.width / 2 if span.width > 1 and (mode == true or mode == :middle)
110
+ return span.end if mode == :end
111
+ span.begin
112
+ end
113
+
114
+ # List of Handler definitions. See Chronic.parse for a list of options this
115
+ # method accepts.
116
+ #
117
+ # options - An optional Hash of configuration options.
118
+ #
119
+ # Returns a Hash of Handler definitions.
120
+ def definitions(options = {})
121
+ SpanDictionary.new(options).definitions
122
+ end
123
+
124
+ private
125
+
126
+
127
+ def validate_options!(options)
128
+ given = options.keys.map(&:to_s).sort
129
+ allowed = DEFAULT_OPTIONS.keys.map(&:to_s).sort
130
+ non_permitted = given - allowed
131
+ raise ArgumentError, "Unsupported option(s): #{non_permitted.join(', ')}" if non_permitted.any?
132
+ end
133
+
134
+ def tag(tokens, options)
135
+ [DayName, MonthName, SeasonName, DaySpecial, TimeSpecial, DayPortion, Grabber, Pointer, Rational, Keyword, Separator, Scalar, Ordinal, Sign, Unit, TimeZoneTag].each do |tok|
136
+ tok.scan(tokens, options)
137
+ end
138
+ previous = nil
139
+ tokens.select! do |token|
140
+ if token.tagged?
141
+ if !previous or !token.tags.first.kind_of?(Separator) or token.tags.first.class != previous.class
142
+ previous = token.tags.first
143
+ true
144
+ else
145
+ false
146
+ end
147
+ else
148
+ false
149
+ end
150
+ end
151
+ end
152
+
153
+ end
154
+ end