gitlab-chronic 0.10.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.gitlab-ci.yml +14 -0
  4. data/.travis.yml +10 -0
  5. data/Gemfile +3 -0
  6. data/HISTORY.md +252 -0
  7. data/LICENSE +21 -0
  8. data/README.md +188 -0
  9. data/Rakefile +68 -0
  10. data/chronic.gemspec +25 -0
  11. data/lib/chronic.rb +155 -0
  12. data/lib/chronic/date.rb +81 -0
  13. data/lib/chronic/definition.rb +128 -0
  14. data/lib/chronic/dictionary.rb +36 -0
  15. data/lib/chronic/handler.rb +97 -0
  16. data/lib/chronic/handlers.rb +672 -0
  17. data/lib/chronic/mini_date.rb +38 -0
  18. data/lib/chronic/parser.rb +222 -0
  19. data/lib/chronic/repeaters/repeater_day.rb +54 -0
  20. data/lib/chronic/repeaters/repeater_day_name.rb +53 -0
  21. data/lib/chronic/repeaters/repeater_day_portion.rb +109 -0
  22. data/lib/chronic/repeaters/repeater_fortnight.rb +72 -0
  23. data/lib/chronic/repeaters/repeater_hour.rb +59 -0
  24. data/lib/chronic/repeaters/repeater_minute.rb +59 -0
  25. data/lib/chronic/repeaters/repeater_month.rb +80 -0
  26. data/lib/chronic/repeaters/repeater_month_name.rb +95 -0
  27. data/lib/chronic/repeaters/repeater_quarter.rb +59 -0
  28. data/lib/chronic/repeaters/repeater_quarter_name.rb +40 -0
  29. data/lib/chronic/repeaters/repeater_season.rb +111 -0
  30. data/lib/chronic/repeaters/repeater_season_name.rb +43 -0
  31. data/lib/chronic/repeaters/repeater_second.rb +43 -0
  32. data/lib/chronic/repeaters/repeater_time.rb +159 -0
  33. data/lib/chronic/repeaters/repeater_week.rb +76 -0
  34. data/lib/chronic/repeaters/repeater_weekday.rb +86 -0
  35. data/lib/chronic/repeaters/repeater_weekend.rb +67 -0
  36. data/lib/chronic/repeaters/repeater_year.rb +78 -0
  37. data/lib/chronic/season.rb +26 -0
  38. data/lib/chronic/span.rb +31 -0
  39. data/lib/chronic/tag.rb +89 -0
  40. data/lib/chronic/tags/grabber.rb +29 -0
  41. data/lib/chronic/tags/ordinal.rb +52 -0
  42. data/lib/chronic/tags/pointer.rb +28 -0
  43. data/lib/chronic/tags/repeater.rb +160 -0
  44. data/lib/chronic/tags/scalar.rb +89 -0
  45. data/lib/chronic/tags/separator.rb +123 -0
  46. data/lib/chronic/tags/sign.rb +35 -0
  47. data/lib/chronic/tags/time_zone.rb +32 -0
  48. data/lib/chronic/time.rb +40 -0
  49. data/lib/chronic/token.rb +61 -0
  50. data/lib/chronic/tokenizer.rb +38 -0
  51. data/lib/chronic/version.rb +3 -0
  52. data/test/helper.rb +12 -0
  53. data/test/test_chronic.rb +203 -0
  54. data/test/test_daylight_savings.rb +122 -0
  55. data/test/test_handler.rb +128 -0
  56. data/test/test_mini_date.rb +32 -0
  57. data/test/test_parsing.rb +1537 -0
  58. data/test/test_repeater_day_name.rb +51 -0
  59. data/test/test_repeater_day_portion.rb +254 -0
  60. data/test/test_repeater_fortnight.rb +62 -0
  61. data/test/test_repeater_hour.rb +68 -0
  62. data/test/test_repeater_minute.rb +34 -0
  63. data/test/test_repeater_month.rb +50 -0
  64. data/test/test_repeater_month_name.rb +56 -0
  65. data/test/test_repeater_quarter.rb +70 -0
  66. data/test/test_repeater_quarter_name.rb +198 -0
  67. data/test/test_repeater_season.rb +40 -0
  68. data/test/test_repeater_time.rb +88 -0
  69. data/test/test_repeater_week.rb +115 -0
  70. data/test/test_repeater_weekday.rb +55 -0
  71. data/test/test_repeater_weekend.rb +74 -0
  72. data/test/test_repeater_year.rb +69 -0
  73. data/test/test_span.rb +23 -0
  74. data/test/test_token.rb +31 -0
  75. metadata +215 -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,222 @@
1
+ require 'chronic/dictionary'
2
+ require 'chronic/handlers'
3
+
4
+ module Chronic
5
+ class Parser
6
+ include Handlers
7
+
8
+ # Hash of default configuration options.
9
+ DEFAULT_OPTIONS = {
10
+ :context => :future,
11
+ :now => nil,
12
+ :hours24 => nil,
13
+ :week_start => :sunday,
14
+ :guess => true,
15
+ :ambiguous_time_range => 6,
16
+ :endian_precedence => [:middle, :little],
17
+ :ambiguous_year_future_bias => 50
18
+ }
19
+
20
+ attr_accessor :now
21
+ attr_reader :options
22
+
23
+ # options - An optional Hash of configuration options:
24
+ # :context - If your string represents a birthday, you can set
25
+ # this value to :past and if an ambiguous string is
26
+ # given, it will assume it is in the past.
27
+ # :now - Time, all computations will be based off of time
28
+ # instead of Time.now.
29
+ # :hours24 - Time will be parsed as it would be 24 hour clock.
30
+ # :week_start - By default, the parser assesses weeks start on
31
+ # sunday but you can change this value to :monday if
32
+ # needed.
33
+ # :guess - By default the parser will guess a single point in time
34
+ # for the given date or time. If you'd rather have the
35
+ # entire time span returned, set this to false
36
+ # and a Chronic::Span will be returned. Setting :guess to :end
37
+ # will return last time from Span, to :middle for middle (same as just true)
38
+ # and :begin for first time from span.
39
+ # :ambiguous_time_range - If an Integer is given, ambiguous times
40
+ # (like 5:00) will be assumed to be within the range of
41
+ # that time in the AM to that time in the PM. For
42
+ # example, if you set it to `7`, then the parser will
43
+ # look for the time between 7am and 7pm. In the case of
44
+ # 5:00, it would assume that means 5:00pm. If `:none`
45
+ # is given, no assumption will be made, and the first
46
+ # matching instance of that time will be used.
47
+ # :endian_precedence - By default, Chronic will parse "03/04/2011"
48
+ # as the fourth day of the third month. Alternatively you
49
+ # can tell Chronic to parse this as the third day of the
50
+ # fourth month by setting this to [:little, :middle].
51
+ # :ambiguous_year_future_bias - When parsing two digit years
52
+ # (ie 79) unlike Rubys Time class, Chronic will attempt
53
+ # to assume the full year using this figure. Chronic will
54
+ # look x amount of years into the future and past. If the
55
+ # two digit year is `now + x years` it's assumed to be the
56
+ # future, `now - x years` is assumed to be the past.
57
+ def initialize(options = {})
58
+ validate_options!(options)
59
+ @options = DEFAULT_OPTIONS.merge(options)
60
+ @now = options[:now] || Chronic.time_class.now
61
+ end
62
+
63
+ # Parse "text" with the given options
64
+ # Returns either a Time or Chronic::Span, depending on the value of options[:guess]
65
+ def parse(text)
66
+ tokens = tokenize(text, options)
67
+ span = tokens_to_span(tokens, options.merge(:text => text))
68
+
69
+ puts "+#{'-' * 51}\n| #{tokens}\n+#{'-' * 51}" if Chronic.debug
70
+
71
+ guess(span, options[:guess]) if span
72
+ end
73
+
74
+ # Clean up the specified text ready for parsing.
75
+ #
76
+ # Clean up the string by stripping unwanted characters, converting
77
+ # idioms to their canonical form, converting number words to numbers
78
+ # (three => 3), and converting ordinal words to numeric
79
+ # ordinals (third => 3rd)
80
+ #
81
+ # text - The String text to normalize.
82
+ #
83
+ # Examples:
84
+ #
85
+ # Chronic.pre_normalize('first day in May')
86
+ # #=> "1st day in may"
87
+ #
88
+ # Chronic.pre_normalize('tomorrow after noon')
89
+ # #=> "next day future 12:00"
90
+ #
91
+ # Chronic.pre_normalize('one hundred and thirty six days from now')
92
+ # #=> "136 days future this second"
93
+ #
94
+ # Returns a new String ready for Chronic to parse.
95
+ def pre_normalize(text)
96
+ text = text.to_s.downcase
97
+ text.gsub!(/\b(\d{1,2})\.(\d{1,2})\.(\d{4})\b/, '\3 / \2 / \1')
98
+ text.gsub!(/\b([ap])\.m\.?/, '\1m')
99
+ text.gsub!(/(\s+|:\d{2}|:\d{2}\.\d+)\-(\d{2}:?\d{2})\b/, '\1tzminus\2')
100
+ text.gsub!(/\./, ':')
101
+ text.gsub!(/([ap]):m:?/, '\1m')
102
+ text.gsub!(/'(\d{2})\b/) do
103
+ number = $1.to_i
104
+
105
+ if Chronic::Date::could_be_year?(number)
106
+ Chronic::Date::make_year(number, options[:ambiguous_year_future_bias])
107
+ else
108
+ number
109
+ end
110
+ end
111
+ text.gsub!(/['"]/, '')
112
+ text.gsub!(/,/, ' ')
113
+ text.gsub!(/^second /, '2nd ')
114
+ text.gsub!(/\bsecond (of|day|month|hour|minute|second|quarter)\b/, '2nd \1')
115
+ text.gsub!(/\bthird quarter\b/, '3rd q')
116
+ text.gsub!(/\bfourth quarter\b/, '4th q')
117
+ text.gsub!(/quarters?(\s+|$)(?!to|till|past|after|before)/, 'q\1')
118
+ text = Numerizer.numerize(text)
119
+ text.gsub!(/\b(\d)(?:st|nd|rd|th)\s+q\b/, 'q\1')
120
+ text.gsub!(/([\/\-\,\@])/) { ' ' + $1 + ' ' }
121
+ text.gsub!(/(?:^|\s)0(\d+:\d+\s*pm?\b)/, ' \1')
122
+ text.gsub!(/\btoday\b/, 'this day')
123
+ text.gsub!(/\btomm?orr?ow\b/, 'next day')
124
+ text.gsub!(/\byesterday\b/, 'last day')
125
+ text.gsub!(/\bnoon|midday\b/, '12:00pm')
126
+ text.gsub!(/\bmidnight\b/, '24:00')
127
+ text.gsub!(/\bnow\b/, 'this second')
128
+ text.gsub!('quarter', '15')
129
+ text.gsub!('half', '30')
130
+ text.gsub!(/(\d{1,2}) (to|till|prior to|before)\b/, '\1 minutes past')
131
+ text.gsub!(/(\d{1,2}) (after|past)\b/, '\1 minutes future')
132
+ text.gsub!(/\b(?:ago|before(?: now)?)\b/, 'past')
133
+ text.gsub!(/\bthis (?:last|past)\b/, 'last')
134
+ text.gsub!(/\b(?:in|during) the (morning)\b/, '\1')
135
+ text.gsub!(/\b(?:in the|during the|at) (afternoon|evening|night)\b/, '\1')
136
+ text.gsub!(/\btonight\b/, 'this night')
137
+ text.gsub!(/\b\d+:?\d*[ap]\b/,'\0m')
138
+ text.gsub!(/\b(\d{2})(\d{2})(am|pm)\b/, '\1:\2\3')
139
+ text.gsub!(/(\d)([ap]m|oclock)\b/, '\1 \2')
140
+ text.gsub!(/\b(hence|after|from)\b/, 'future')
141
+ text.gsub!(/^\s?an? /i, '1 ')
142
+ text.gsub!(/\b(\d{4}):(\d{2}):(\d{2})\b/, '\1 / \2 / \3') # DTOriginal
143
+ text.gsub!(/\b0(\d+):(\d{2}):(\d{2}) ([ap]m)\b/, '\1:\2:\3 \4')
144
+ text
145
+ end
146
+
147
+ # Guess a specific time within the given span.
148
+ #
149
+ # span - The Chronic::Span object to calcuate a guess from.
150
+ #
151
+ # Returns a new Time object.
152
+ def guess(span, mode = :middle)
153
+ return span if not mode
154
+ return span.begin + span.width / 2 if span.width > 1 and (mode == true or mode == :middle)
155
+ return span.end if mode == :end
156
+ span.begin
157
+ end
158
+
159
+ # List of Handler definitions. See Chronic.parse for a list of options this
160
+ # method accepts.
161
+ #
162
+ # options - An optional Hash of configuration options.
163
+ #
164
+ # Returns a Hash of Handler definitions.
165
+ def definitions(options = {})
166
+ SpanDictionary.new(options).definitions
167
+ end
168
+
169
+ private
170
+
171
+
172
+ def validate_options!(options)
173
+ given = options.keys.map(&:to_s).sort
174
+ allowed = DEFAULT_OPTIONS.keys.map(&:to_s).sort
175
+ non_permitted = given - allowed
176
+ raise ArgumentError, "Unsupported option(s): #{non_permitted.join(', ')}" if non_permitted.any?
177
+ end
178
+
179
+ def tokenize(text, options)
180
+ text = pre_normalize(text)
181
+ tokens = Tokenizer::tokenize(text)
182
+ [Repeater, Grabber, Pointer, Scalar, Ordinal, Separator, Sign, TimeZone].each do |tok|
183
+ tok.scan(tokens, options)
184
+ end
185
+ tokens.select { |token| token.tagged? }
186
+ end
187
+
188
+ def tokens_to_span(tokens, options)
189
+ definitions = definitions(options)
190
+
191
+ (definitions[:endian] + definitions[:date]).each do |handler|
192
+ if handler.match(tokens, definitions)
193
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
194
+ return handler.invoke(:date, good_tokens, self, options)
195
+ end
196
+ end
197
+
198
+ definitions[:anchor].each do |handler|
199
+ if handler.match(tokens, definitions)
200
+ good_tokens = tokens.select { |o| !o.get_tag Separator }
201
+ return handler.invoke(:anchor, good_tokens, self, options)
202
+ end
203
+ end
204
+
205
+ definitions[:arrow].each do |handler|
206
+ if handler.match(tokens, definitions)
207
+ good_tokens = tokens.reject { |o| o.get_tag(SeparatorAt) || o.get_tag(SeparatorSlash) || o.get_tag(SeparatorDash) || o.get_tag(SeparatorComma) || o.get_tag(SeparatorAnd) }
208
+ return handler.invoke(:arrow, good_tokens, self, options)
209
+ end
210
+ end
211
+
212
+ definitions[:narrow].each do |handler|
213
+ if handler.match(tokens, definitions)
214
+ return handler.invoke(:narrow, tokens, self, options)
215
+ end
216
+ end
217
+
218
+ puts '-none' if Chronic.debug
219
+ return nil
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,54 @@
1
+ module Chronic
2
+ class RepeaterDay < Repeater #:nodoc:
3
+ DAY_SECONDS = 86_400 # (24 * 60 * 60)
4
+
5
+ def initialize(type, width = nil, options = {})
6
+ super
7
+ @current_day_start = nil
8
+ end
9
+
10
+ def next(pointer)
11
+ super
12
+
13
+ unless @current_day_start
14
+ @current_day_start = Chronic.time_class.local(@now.year, @now.month, @now.day)
15
+ end
16
+
17
+ direction = pointer == :future ? 1 : -1
18
+ @current_day_start += direction * DAY_SECONDS
19
+
20
+ Span.new(@current_day_start, @current_day_start + DAY_SECONDS)
21
+ end
22
+
23
+ def this(pointer = :future)
24
+ super
25
+
26
+ case pointer
27
+ when :future
28
+ day_begin = Chronic.construct(@now.year, @now.month, @now.day, @now.hour)
29
+ day_end = Chronic.construct(@now.year, @now.month, @now.day) + DAY_SECONDS
30
+ when :past
31
+ day_begin = Chronic.construct(@now.year, @now.month, @now.day)
32
+ day_end = Chronic.construct(@now.year, @now.month, @now.day, @now.hour)
33
+ when :none
34
+ day_begin = Chronic.construct(@now.year, @now.month, @now.day)
35
+ day_end = Chronic.construct(@now.year, @now.month, @now.day) + DAY_SECONDS
36
+ end
37
+
38
+ Span.new(day_begin, day_end)
39
+ end
40
+
41
+ def offset(span, amount, pointer)
42
+ direction = pointer == :future ? 1 : -1
43
+ span + direction * amount * DAY_SECONDS
44
+ end
45
+
46
+ def width
47
+ DAY_SECONDS
48
+ end
49
+
50
+ def to_s
51
+ super << '-day'
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,53 @@
1
+ module Chronic
2
+ class RepeaterDayName < Repeater #:nodoc:
3
+ DAY_SECONDS = 86400 # (24 * 60 * 60)
4
+
5
+ def initialize(type, width = nil, options = {})
6
+ super
7
+ @current_date = nil
8
+ end
9
+
10
+ def next(pointer)
11
+ super
12
+
13
+ direction = pointer == :future ? 1 : -1
14
+
15
+ unless @current_date
16
+ @current_date = ::Date.new(@now.year, @now.month, @now.day)
17
+ @current_date += direction
18
+
19
+ day_num = symbol_to_number(@type)
20
+
21
+ while @current_date.wday != day_num
22
+ @current_date += direction
23
+ end
24
+ else
25
+ @current_date += direction * 7
26
+ end
27
+ next_date = @current_date.succ
28
+ Span.new(Chronic.construct(@current_date.year, @current_date.month, @current_date.day), Chronic.construct(next_date.year, next_date.month, next_date.day))
29
+ end
30
+
31
+ def this(pointer = :future)
32
+ super
33
+
34
+ pointer = :future if pointer == :none
35
+ self.next(pointer)
36
+ end
37
+
38
+ def width
39
+ DAY_SECONDS
40
+ end
41
+
42
+ def to_s
43
+ super << '-dayname-' << @type.to_s
44
+ end
45
+
46
+ private
47
+
48
+ def symbol_to_number(sym)
49
+ lookup = {:sunday => 0, :monday => 1, :tuesday => 2, :wednesday => 3, :thursday => 4, :friday => 5, :saturday => 6}
50
+ lookup[sym] || raise('Invalid symbol specified')
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,109 @@
1
+ module Chronic
2
+ class RepeaterDayPortion < Repeater #:nodoc:
3
+ PORTIONS = {
4
+ :am => 0..(12 * 60 * 60 - 1),
5
+ :pm => (12 * 60 * 60)..(24 * 60 * 60 - 1),
6
+ :morning => (6 * 60 * 60)..(12 * 60 * 60), # 6am-12am,
7
+ :afternoon => (13 * 60 * 60)..(17 * 60 * 60), # 1pm-5pm,
8
+ :evening => (17 * 60 * 60)..(20 * 60 * 60), # 5pm-8pm,
9
+ :night => (20 * 60 * 60)..(24 * 60 * 60), # 8pm-12pm
10
+ }
11
+
12
+ def initialize(type, width = nil, options = {})
13
+ super
14
+ @current_span = nil
15
+
16
+ if type.kind_of? Integer
17
+ @range = (@type * 60 * 60)..((@type + 12) * 60 * 60)
18
+ else
19
+ @range = PORTIONS[type]
20
+ @range || raise("Invalid type '#{type}' for RepeaterDayPortion")
21
+ end
22
+
23
+ @range || raise('Range should have been set by now')
24
+ end
25
+
26
+ def next(pointer)
27
+ super
28
+
29
+ unless @current_span
30
+ now_seconds = @now - Chronic.construct(@now.year, @now.month, @now.day)
31
+ if now_seconds < @range.begin
32
+ case pointer
33
+ when :future
34
+ range_start = Chronic.construct(@now.year, @now.month, @now.day) + @range.begin
35
+ when :past
36
+ range_start = Chronic.construct(@now.year, @now.month, @now.day - 1) + @range.begin
37
+ end
38
+ elsif now_seconds > @range.end
39
+ case pointer
40
+ when :future
41
+ range_start = Chronic.construct(@now.year, @now.month, @now.day + 1) + @range.begin
42
+ when :past
43
+ range_start = Chronic.construct(@now.year, @now.month, @now.day) + @range.begin
44
+ end
45
+ else
46
+ case pointer
47
+ when :future
48
+ range_start = Chronic.construct(@now.year, @now.month, @now.day + 1) + @range.begin
49
+ when :past
50
+ range_start = Chronic.construct(@now.year, @now.month, @now.day - 1) + @range.begin
51
+ end
52
+ end
53
+ offset = (@range.end - @range.begin)
54
+ range_end = construct_date_from_reference_and_offset(range_start, offset)
55
+ @current_span = Span.new(range_start, range_end)
56
+ else
57
+ days_to_shift_window =
58
+ case pointer
59
+ when :future
60
+ 1
61
+ when :past
62
+ -1
63
+ end
64
+
65
+ new_begin = Chronic.construct(@current_span.begin.year, @current_span.begin.month, @current_span.begin.day + days_to_shift_window, @current_span.begin.hour, @current_span.begin.min, @current_span.begin.sec)
66
+ new_end = Chronic.construct(@current_span.end.year, @current_span.end.month, @current_span.end.day + days_to_shift_window, @current_span.end.hour, @current_span.end.min, @current_span.end.sec)
67
+ @current_span = Span.new(new_begin, new_end)
68
+ end
69
+ end
70
+
71
+ def this(context = :future)
72
+ super
73
+
74
+ range_start = Chronic.construct(@now.year, @now.month, @now.day) + @range.begin
75
+ range_end = construct_date_from_reference_and_offset(range_start)
76
+ @current_span = Span.new(range_start, range_end)
77
+ end
78
+
79
+ def offset(span, amount, pointer)
80
+ @now = span.begin
81
+ portion_span = self.next(pointer)
82
+ direction = pointer == :future ? 1 : -1
83
+ portion_span + (direction * (amount - 1) * RepeaterDay::DAY_SECONDS)
84
+ end
85
+
86
+ def width
87
+ @range || raise('Range has not been set')
88
+ return @current_span.width if @current_span
89
+ if @type.kind_of? Integer
90
+ return (12 * 60 * 60)
91
+ else
92
+ @range.end - @range.begin
93
+ end
94
+ end
95
+
96
+ def to_s
97
+ super << '-dayportion-' << @type.to_s
98
+ end
99
+
100
+ private
101
+ def construct_date_from_reference_and_offset(reference, offset = nil)
102
+ elapsed_seconds_for_range = offset || (@range.end - @range.begin)
103
+ second_hand = ((elapsed_seconds_for_range - (12 * 60))) % 60
104
+ minute_hand = (elapsed_seconds_for_range - second_hand) / (60) % 60
105
+ hour_hand = (elapsed_seconds_for_range - minute_hand - second_hand) / (60 * 60) + reference.hour % 24
106
+ Chronic.construct(reference.year, reference.month, reference.day, hour_hand, minute_hand, second_hand)
107
+ end
108
+ end
109
+ end