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,111 @@
1
+ module Chronic
2
+ class RepeaterSeason < Repeater #:nodoc:
3
+ SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60
4
+ SEASONS = {
5
+ :spring => Season.new(MiniDate.new(3,20), MiniDate.new(6,20)),
6
+ :summer => Season.new(MiniDate.new(6,21), MiniDate.new(9,22)),
7
+ :autumn => Season.new(MiniDate.new(9,23), MiniDate.new(12,21)),
8
+ :winter => Season.new(MiniDate.new(12,22), MiniDate.new(3,19))
9
+ }
10
+
11
+ def initialize(type, width = nil, options = {})
12
+ super
13
+ @next_season_start = nil
14
+ @next_season_end = nil
15
+ end
16
+
17
+ def next(pointer)
18
+ super
19
+
20
+ direction = pointer == :future ? 1 : -1
21
+ next_season = Season.find_next_season(find_current_season(MiniDate.from_time(@now)), direction)
22
+
23
+ find_next_season_span(direction, next_season)
24
+ end
25
+
26
+ def this(pointer = :future)
27
+ super
28
+
29
+ direction = pointer == :future ? 1 : -1
30
+
31
+ today = Chronic.construct(@now.year, @now.month, @now.day)
32
+ this_ssn = find_current_season(MiniDate.from_time(@now))
33
+ case pointer
34
+ when :past
35
+ this_ssn_start = today + direction * num_seconds_til_start(this_ssn, direction)
36
+ this_ssn_end = today
37
+ when :future
38
+ this_ssn_start = today + RepeaterDay::DAY_SECONDS
39
+ this_ssn_end = today + direction * num_seconds_til_end(this_ssn, direction)
40
+ when :none
41
+ this_ssn_start = today + direction * num_seconds_til_start(this_ssn, direction)
42
+ this_ssn_end = today + direction * num_seconds_til_end(this_ssn, direction)
43
+ end
44
+
45
+ construct_season(this_ssn_start, this_ssn_end)
46
+ end
47
+
48
+ def offset(span, amount, pointer)
49
+ Span.new(offset_by(span.begin, amount, pointer), offset_by(span.end, amount, pointer))
50
+ end
51
+
52
+ def offset_by(time, amount, pointer)
53
+ direction = pointer == :future ? 1 : -1
54
+ time + amount * direction * SEASON_SECONDS
55
+ end
56
+
57
+ def width
58
+ SEASON_SECONDS
59
+ end
60
+
61
+ def to_s
62
+ super << '-season'
63
+ end
64
+
65
+ private
66
+
67
+ def find_next_season_span(direction, next_season)
68
+ unless @next_season_start || @next_season_end
69
+ @next_season_start = Chronic.construct(@now.year, @now.month, @now.day)
70
+ @next_season_end = Chronic.construct(@now.year, @now.month, @now.day)
71
+ end
72
+
73
+ @next_season_start += direction * num_seconds_til_start(next_season, direction)
74
+ @next_season_end += direction * num_seconds_til_end(next_season, direction)
75
+
76
+ construct_season(@next_season_start, @next_season_end)
77
+ end
78
+
79
+ def find_current_season(md)
80
+ [:spring, :summer, :autumn, :winter].find do |season|
81
+ md.is_between?(SEASONS[season].start, SEASONS[season].end)
82
+ end
83
+ end
84
+
85
+ def num_seconds_til(goal, direction)
86
+ start = Chronic.construct(@now.year, @now.month, @now.day)
87
+ seconds = 0
88
+
89
+ until MiniDate.from_time(start + direction * seconds).equals?(goal)
90
+ seconds += RepeaterDay::DAY_SECONDS
91
+ end
92
+
93
+ seconds
94
+ end
95
+
96
+ def num_seconds_til_start(season_symbol, direction)
97
+ num_seconds_til(SEASONS[season_symbol].start, direction)
98
+ end
99
+
100
+ def num_seconds_til_end(season_symbol, direction)
101
+ num_seconds_til(SEASONS[season_symbol].end, direction)
102
+ end
103
+
104
+ def construct_season(start, finish)
105
+ Span.new(
106
+ Chronic.construct(start.year, start.month, start.day),
107
+ Chronic.construct(finish.year, finish.month, finish.day)
108
+ )
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,43 @@
1
+ module Chronic
2
+ class RepeaterSeasonName < RepeaterSeason #:nodoc:
3
+ SEASON_SECONDS = 7_862_400 # 91 * 24 * 60 * 60
4
+ DAY_SECONDS = 86_400 # (24 * 60 * 60)
5
+
6
+ def next(pointer)
7
+ direction = pointer == :future ? 1 : -1
8
+ find_next_season_span(direction, @type)
9
+ end
10
+
11
+ def this(pointer = :future)
12
+ direction = pointer == :future ? 1 : -1
13
+
14
+ today = Chronic.construct(@now.year, @now.month, @now.day)
15
+ goal_ssn_start = today + direction * num_seconds_til_start(@type, direction)
16
+ goal_ssn_end = today + direction * num_seconds_til_end(@type, direction)
17
+ curr_ssn = find_current_season(MiniDate.from_time(@now))
18
+ case pointer
19
+ when :past
20
+ this_ssn_start = goal_ssn_start
21
+ this_ssn_end = (curr_ssn == @type) ? today : goal_ssn_end
22
+ when :future
23
+ this_ssn_start = (curr_ssn == @type) ? today + RepeaterDay::DAY_SECONDS : goal_ssn_start
24
+ this_ssn_end = goal_ssn_end
25
+ when :none
26
+ this_ssn_start = goal_ssn_start
27
+ this_ssn_end = goal_ssn_end
28
+ end
29
+
30
+ construct_season(this_ssn_start, this_ssn_end)
31
+ end
32
+
33
+ def offset(span, amount, pointer)
34
+ Span.new(offset_by(span.begin, amount, pointer), offset_by(span.end, amount, pointer))
35
+ end
36
+
37
+ def offset_by(time, amount, pointer)
38
+ direction = pointer == :future ? 1 : -1
39
+ time + amount * direction * RepeaterYear::YEAR_SECONDS
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ module Chronic
2
+ class RepeaterSecond < Repeater #:nodoc:
3
+ SECOND_SECONDS = 1 # haha, awesome
4
+
5
+ def initialize(type, width = nil, options = {})
6
+ super
7
+ @second_start = nil
8
+ end
9
+
10
+ def next(pointer = :future)
11
+ super
12
+
13
+ direction = pointer == :future ? 1 : -1
14
+
15
+ unless @second_start
16
+ @second_start = @now + (direction * SECOND_SECONDS)
17
+ else
18
+ @second_start += SECOND_SECONDS * direction
19
+ end
20
+
21
+ Span.new(@second_start, @second_start + SECOND_SECONDS)
22
+ end
23
+
24
+ def this(pointer = :future)
25
+ super
26
+
27
+ Span.new(@now, @now + 1)
28
+ end
29
+
30
+ def offset(span, amount, pointer)
31
+ direction = pointer == :future ? 1 : -1
32
+ span + direction * amount * SECOND_SECONDS
33
+ end
34
+
35
+ def width
36
+ SECOND_SECONDS
37
+ end
38
+
39
+ def to_s
40
+ super << '-second'
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,159 @@
1
+ module Chronic
2
+ class RepeaterTime < Repeater #:nodoc:
3
+ class Tick #:nodoc:
4
+ attr_accessor :time
5
+
6
+ def initialize(time, ambiguous = false)
7
+ @time = time
8
+ @ambiguous = ambiguous
9
+ end
10
+
11
+ def ambiguous?
12
+ @ambiguous
13
+ end
14
+
15
+ def *(other)
16
+ Tick.new(@time * other, @ambiguous)
17
+ end
18
+
19
+ def to_f
20
+ @time.to_f
21
+ end
22
+
23
+ def to_s
24
+ @time.to_s + (@ambiguous ? '?' : '')
25
+ end
26
+
27
+ end
28
+
29
+ def initialize(time, width = nil, options = {})
30
+ @current_time = nil
31
+ @options = options
32
+ time_parts = time.split(':')
33
+ raise ArgumentError, "Time cannot have more than 4 groups of ':'" if time_parts.count > 4
34
+
35
+ if time_parts.first.length > 2 and time_parts.count == 1
36
+ if time_parts.first.length > 4
37
+ second_index = time_parts.first.length - 2
38
+ time_parts.insert(1, time_parts.first[second_index..time_parts.first.length])
39
+ time_parts[0] = time_parts.first[0..second_index - 1]
40
+ end
41
+ minute_index = time_parts.first.length - 2
42
+ time_parts.insert(1, time_parts.first[minute_index..time_parts.first.length])
43
+ time_parts[0] = time_parts.first[0..minute_index - 1]
44
+ end
45
+
46
+ ambiguous = false
47
+ hours = time_parts.first.to_i
48
+
49
+ if @options[:hours24].nil? or (not @options[:hours24].nil? and @options[:hours24] != true)
50
+ ambiguous = true if (time_parts.first.length == 1 and hours > 0) or (hours >= 10 and hours <= 12) or (@options[:hours24] == false and hours > 0)
51
+ hours = 0 if hours == 12 and ambiguous
52
+ end
53
+
54
+ hours *= 60 * 60
55
+ minutes = 0
56
+ seconds = 0
57
+ subseconds = 0
58
+
59
+ minutes = time_parts[1].to_i * 60 if time_parts.count > 1
60
+ seconds = time_parts[2].to_i if time_parts.count > 2
61
+ subseconds = time_parts[3].to_f / (10 ** time_parts[3].length) if time_parts.count > 3
62
+
63
+ @type = Tick.new(hours + minutes + seconds + subseconds, ambiguous)
64
+ end
65
+
66
+ # Return the next past or future Span for the time that this Repeater represents
67
+ # pointer - Symbol representing which temporal direction to fetch the next day
68
+ # must be either :past or :future
69
+ def next(pointer)
70
+ super
71
+
72
+ half_day = 60 * 60 * 12
73
+ full_day = 60 * 60 * 24
74
+
75
+ first = false
76
+
77
+ unless @current_time
78
+ first = true
79
+ midnight = Chronic.time_class.local(@now.year, @now.month, @now.day)
80
+
81
+ yesterday_midnight = midnight - full_day
82
+ tomorrow_midnight = midnight + full_day
83
+
84
+ catch :done do
85
+ if pointer == :future
86
+ if @type.ambiguous?
87
+ [midnight, midnight + half_day, tomorrow_midnight].each do |base_time|
88
+ t = adjust_daylight_savings_offset(midnight, base_time + @type.time)
89
+ (@current_time = t; throw :done) if t >= @now
90
+ end
91
+ else
92
+ [midnight, tomorrow_midnight].each do |base_time|
93
+ t = adjust_daylight_savings_offset(midnight, base_time + @type.time)
94
+ (@current_time = t; throw :done) if t >= @now
95
+ end
96
+ end
97
+ else # pointer == :past
98
+ if @type.ambiguous?
99
+ [midnight + half_day, midnight, yesterday_midnight + half_day].each do |base_time|
100
+ t = adjust_daylight_savings_offset(midnight, base_time + @type.time)
101
+ (@current_time = t; throw :done) if t <= @now
102
+ end
103
+ else
104
+ [midnight, yesterday_midnight].each do |base_time|
105
+ t = adjust_daylight_savings_offset(midnight, base_time + @type.time)
106
+ (@current_time = t; throw :done) if t <= @now
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ @current_time || raise('Current time cannot be nil at this point')
113
+ end
114
+
115
+ unless first
116
+ increment = @type.ambiguous? ? half_day : full_day
117
+ @current_time += pointer == :future ? increment : -increment
118
+ end
119
+
120
+ Span.new(@current_time, @current_time + width)
121
+ end
122
+
123
+ # Every time a time crosses a Daylight Savings interval we must adjust the
124
+ # current time by that amount. For example, if we take midnight of Daylight
125
+ # Savings and only add an hour, the offset does not change:
126
+ #
127
+ # Time.parse('2008-03-09 00:00')
128
+ # => 2008-03-09 00:00:00 -0800
129
+ # Time.parse('2008-03-09 00:00') + (60 * 60)
130
+ # => 2008-03-09 01:00:00 -0800
131
+ #
132
+ # However, if we add 2 hours, we notice the time advances to 03:00 instead of 02:00:
133
+ #
134
+ # Time.parse('2008-03-09 00:00') + (60 * 60 * 2)
135
+ # => 2008-03-09 03:00:00 -0700
136
+ #
137
+ # Since we gained an hour and we actually want 02:00, we subtract an hour.
138
+ def adjust_daylight_savings_offset(base_time, current_time)
139
+ offset_fix = base_time.gmt_offset - current_time.gmt_offset
140
+ current_time + offset_fix
141
+ end
142
+
143
+ def this(context = :future)
144
+ super
145
+
146
+ context = :future if context == :none
147
+
148
+ self.next(context)
149
+ end
150
+
151
+ def width
152
+ 1
153
+ end
154
+
155
+ def to_s
156
+ super << '-time-' << @type.to_s
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,76 @@
1
+ module Chronic
2
+ class RepeaterWeek < Repeater #:nodoc:
3
+ WEEK_SECONDS = 604800 # (7 * 24 * 60 * 60)
4
+
5
+ def initialize(type, width = nil, options = {})
6
+ super
7
+ @repeater_day_name = options[:week_start] || :sunday
8
+ @current_week_start = nil
9
+ end
10
+
11
+ def next(pointer)
12
+ super
13
+
14
+ unless @current_week_start
15
+ case pointer
16
+ when :future
17
+ first_week_day_repeater = RepeaterDayName.new(@repeater_day_name)
18
+ first_week_day_repeater.start = @now
19
+ next_span = first_week_day_repeater.next(:future)
20
+ @current_week_start = next_span.begin
21
+ when :past
22
+ first_week_day_repeater = RepeaterDayName.new(@repeater_day_name)
23
+ first_week_day_repeater.start = (@now + RepeaterDay::DAY_SECONDS)
24
+ first_week_day_repeater.next(:past)
25
+ last_span = first_week_day_repeater.next(:past)
26
+ @current_week_start = last_span.begin
27
+ end
28
+ else
29
+ direction = pointer == :future ? 1 : -1
30
+ @current_week_start += direction * WEEK_SECONDS
31
+ end
32
+
33
+ Span.new(@current_week_start, @current_week_start + WEEK_SECONDS)
34
+ end
35
+
36
+ def this(pointer = :future)
37
+ super
38
+
39
+ case pointer
40
+ when :future
41
+ this_week_start = Chronic.time_class.local(@now.year, @now.month, @now.day, @now.hour) + RepeaterHour::HOUR_SECONDS
42
+ first_week_day_repeater = RepeaterDayName.new(@repeater_day_name)
43
+ first_week_day_repeater.start = @now
44
+ this_span = first_week_day_repeater.this(:future)
45
+ this_week_end = this_span.begin
46
+ Span.new(this_week_start, this_week_end)
47
+ when :past
48
+ this_week_end = Chronic.time_class.local(@now.year, @now.month, @now.day, @now.hour)
49
+ first_week_day_repeater = RepeaterDayName.new(@repeater_day_name)
50
+ first_week_day_repeater.start = @now
51
+ last_span = first_week_day_repeater.next(:past)
52
+ this_week_start = last_span.begin
53
+ Span.new(this_week_start, this_week_end)
54
+ when :none
55
+ first_week_day_repeater = RepeaterDayName.new(@repeater_day_name)
56
+ first_week_day_repeater.start = @now
57
+ last_span = first_week_day_repeater.next(:past)
58
+ this_week_start = last_span.begin
59
+ Span.new(this_week_start, this_week_start + WEEK_SECONDS)
60
+ end
61
+ end
62
+
63
+ def offset(span, amount, pointer)
64
+ direction = pointer == :future ? 1 : -1
65
+ span + direction * amount * WEEK_SECONDS
66
+ end
67
+
68
+ def width
69
+ WEEK_SECONDS
70
+ end
71
+
72
+ def to_s
73
+ super << '-week'
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,86 @@
1
+ module Chronic
2
+ class RepeaterWeekday < Repeater #:nodoc:
3
+ DAY_SECONDS = 86400 # (24 * 60 * 60)
4
+ DAYS = {
5
+ :sunday => 0,
6
+ :monday => 1,
7
+ :tuesday => 2,
8
+ :wednesday => 3,
9
+ :thursday => 4,
10
+ :friday => 5,
11
+ :saturday => 6
12
+ }
13
+
14
+ def initialize(type, width = nil, options = {})
15
+ super
16
+ @current_weekday_start = nil
17
+ end
18
+
19
+ def next(pointer)
20
+ super
21
+
22
+ direction = pointer == :future ? 1 : -1
23
+
24
+ unless @current_weekday_start
25
+ @current_weekday_start = Chronic.construct(@now.year, @now.month, @now.day)
26
+ @current_weekday_start += direction * DAY_SECONDS
27
+
28
+ until is_weekday?(@current_weekday_start.wday)
29
+ @current_weekday_start += direction * DAY_SECONDS
30
+ end
31
+ else
32
+ loop do
33
+ @current_weekday_start += direction * DAY_SECONDS
34
+ break if is_weekday?(@current_weekday_start.wday)
35
+ end
36
+ end
37
+
38
+ Span.new(@current_weekday_start, @current_weekday_start + DAY_SECONDS)
39
+ end
40
+
41
+ def this(pointer = :future)
42
+ super
43
+
44
+ case pointer
45
+ when :past
46
+ self.next(:past)
47
+ when :future, :none
48
+ self.next(:future)
49
+ end
50
+ end
51
+
52
+ def offset(span, amount, pointer)
53
+ direction = pointer == :future ? 1 : -1
54
+
55
+ num_weekdays_passed = 0; offset = 0
56
+ until num_weekdays_passed == amount
57
+ offset += direction * DAY_SECONDS
58
+ num_weekdays_passed += 1 if is_weekday?((span.begin+offset).wday)
59
+ end
60
+
61
+ span + offset
62
+ end
63
+
64
+ def width
65
+ DAY_SECONDS
66
+ end
67
+
68
+ def to_s
69
+ super << '-weekday'
70
+ end
71
+
72
+ private
73
+
74
+ def is_weekend?(day)
75
+ day == symbol_to_number(:saturday) || day == symbol_to_number(:sunday)
76
+ end
77
+
78
+ def is_weekday?(day)
79
+ !is_weekend?(day)
80
+ end
81
+
82
+ def symbol_to_number(sym)
83
+ DAYS[sym] || raise('Invalid symbol specified')
84
+ end
85
+ end
86
+ end