chronic-davispuh 0.10.2.v0.1da32066b3f46f2506b3471e39557497e34afa27

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