gitlab-chronic 0.10.3

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