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