syc-task 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/syctask/meeting.rb +23 -0
- data/lib/syctask/schedule.rb +335 -0
- data/lib/syctask/task_planner.rb +111 -0
- data/lib/syctask/task_scheduler.rb +172 -0
- data/lib/syctask/times.rb +19 -0
- data/lib/syctask/version.rb +1 -1
- data/lib/syctask.rb +2 -0
- data/lib/sycutil/console.rb +60 -0
- metadata +8 -2
@@ -0,0 +1,23 @@
|
|
1
|
+
require_relative 'times.rb'
|
2
|
+
|
3
|
+
module Syctask
|
4
|
+
|
5
|
+
class Meeting
|
6
|
+
|
7
|
+
attr_accessor :starts
|
8
|
+
attr_accessor :ends
|
9
|
+
attr_accessor :title
|
10
|
+
attr_accessor :tasks
|
11
|
+
|
12
|
+
# Sets the busy time for the schedule. The busy times have to be provided
|
13
|
+
# as hh:mm-hh:mm. Optionally a title for the busy time can be provided
|
14
|
+
def initialize(time, title="", tasks=[])
|
15
|
+
@starts = Syctask::Times.new(time[0..1])
|
16
|
+
@ends = Syctask::Times.new(time[2..3])
|
17
|
+
@title = title
|
18
|
+
@tasks = tasks
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,335 @@
|
|
1
|
+
require_relative 'times.rb'
|
2
|
+
require_relative 'meeting.rb'
|
3
|
+
|
4
|
+
module Syctask
|
5
|
+
|
6
|
+
# Schedule represents a working day with a start and end time, meeting times
|
7
|
+
# and titles and tasks. Tasks can also be associated to meetings as in an
|
8
|
+
# agenda.
|
9
|
+
# Invokation example
|
10
|
+
# work = ["8","30","18","45"]
|
11
|
+
# busy = [["9","0","10","0"],["11","30","12","15"]]
|
12
|
+
# titles = ["Ruby class room training","Discuss Ruby"]
|
13
|
+
# tasks = [task1,task2,task3,task4,task5,task6]
|
14
|
+
# schedule = Syctask::Schedule.new(work,busy,titles,tasks)
|
15
|
+
# schedule.graph.each {|output| puts output}
|
16
|
+
#
|
17
|
+
# This will create following output
|
18
|
+
# Meetings
|
19
|
+
# --------
|
20
|
+
# A - Ruby class room training
|
21
|
+
# B - Discuss Ruby
|
22
|
+
#
|
23
|
+
# A B
|
24
|
+
# xxoo/////xxx|-////oooooxoooo|---|---|---|---|
|
25
|
+
# 8 9 10 11 12 13 14 15 16 17 18 19
|
26
|
+
# 1 2 3 4 5
|
27
|
+
# 6
|
28
|
+
#
|
29
|
+
# Tasks
|
30
|
+
# -----
|
31
|
+
# 0 - 1: task1
|
32
|
+
# 1 - 2: task2
|
33
|
+
# 2 - 3: task3
|
34
|
+
# 3 - 4: task4
|
35
|
+
# 4 - 5: task5
|
36
|
+
# 5 - 6: task6
|
37
|
+
#
|
38
|
+
# Subsequent tasks are are displayed in the graph alternating with x and o.
|
39
|
+
# Meetings are indicated with / and the start is marked with A, B and so on.
|
40
|
+
# Task IDs are shown below the graph. The graph will be printed colored.
|
41
|
+
# Meetings in red, free times in green and tasks in blue. The past time is
|
42
|
+
# shown in black.
|
43
|
+
class Schedule
|
44
|
+
# Color of meetings
|
45
|
+
BUSY_COLOR = :red
|
46
|
+
# Color of free times
|
47
|
+
FREE_COLOR = :green
|
48
|
+
# Color of tasks
|
49
|
+
WORK_COLOR = :blue
|
50
|
+
# If tasks cannot be assigned to the working time this color is used
|
51
|
+
UNSCHEDULED_COLOR = :yellow
|
52
|
+
# Regex scans tasks and free times in the graph
|
53
|
+
GRAPH_PATTERN = /[\|-]+|\/+|[xo]+/
|
54
|
+
# Regex scans meetings in the graph
|
55
|
+
BUSY_PATTERN = /\/+/
|
56
|
+
# Regex scans free times in the graph
|
57
|
+
FREE_PATTERN = /[\|-]+/
|
58
|
+
# Regex scans tasks in the graph
|
59
|
+
WORK_PATTERN = /[xo]+/
|
60
|
+
|
61
|
+
# Start time of working day
|
62
|
+
attr_reader :starts
|
63
|
+
# End time of working day
|
64
|
+
attr_reader :ends
|
65
|
+
# Meetings assigned to the work time
|
66
|
+
attr_accessor :meetings
|
67
|
+
# Tasks assigned to the work time
|
68
|
+
attr_accessor :tasks
|
69
|
+
|
70
|
+
# Creates a new Schedule and initializes work time, busy times, titles and
|
71
|
+
# tasks. Work time is mandatory, busy times, titles and tasks are optional.
|
72
|
+
# Values have to be provided as
|
73
|
+
# * work time: [start_hour, start_minute, end_hour, end_minute]
|
74
|
+
# * busy time: [[start_hour, start_minute, end_hour, end_minute],[...]]
|
75
|
+
# * titles: [title,...]
|
76
|
+
# * tasks: [task,...]
|
77
|
+
def initialize(work_time, busy_time=[], titles=[], tasks=[])
|
78
|
+
@starts = Syctask::Times.new([work_time[0], work_time[1]])
|
79
|
+
@ends = Syctask::Times.new([work_time[2], work_time[3]])
|
80
|
+
@meetings = []
|
81
|
+
titles ||= []
|
82
|
+
busy_time.each.with_index do |busy,index|
|
83
|
+
title = titles[index] ? titles[index] : "Meeting #{index}"
|
84
|
+
@meetings << Syctask::Meeting.new(busy, title)
|
85
|
+
end
|
86
|
+
@tasks = tasks
|
87
|
+
end
|
88
|
+
|
89
|
+
# Sets the assignments containing tasks that are assigned to meetings.
|
90
|
+
# Returns true if succeeds
|
91
|
+
def assign(assignments)
|
92
|
+
assignments.each do |assignment|
|
93
|
+
number = assignment[0].upcase.ord - "A".ord
|
94
|
+
return false if number < 0 or number > @meetings.size
|
95
|
+
assignment[1].split(',').each do |index|
|
96
|
+
@meetings[number].tasks << @tasks[index.to_i] if @tasks[index.to_i]
|
97
|
+
end
|
98
|
+
@meetings[number].tasks.uniq!
|
99
|
+
end
|
100
|
+
true
|
101
|
+
end
|
102
|
+
|
103
|
+
# Creates a meeting list for printing. Returns the meeting list
|
104
|
+
def meeting_list
|
105
|
+
list = sprintf("%s", "Meetings\n").color(:red)
|
106
|
+
list << sprintf("%s", "--------\n").color(:red)
|
107
|
+
meeting_number = "A"
|
108
|
+
@meetings.each do |meeting|
|
109
|
+
list << sprintf("%s - %s\n", meeting_number, meeting.title).color(:red)
|
110
|
+
meeting_number.next!
|
111
|
+
meeting.tasks.each do |task|
|
112
|
+
task_color = task.done? ? :green : :blue
|
113
|
+
list << sprintf("%5s - %s\n", task.id, task.title).color(task_color)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
list
|
117
|
+
end
|
118
|
+
|
119
|
+
# Creates a meeting caption and returns it for printing
|
120
|
+
def meeting_caption
|
121
|
+
work_time, meeting_times = get_times
|
122
|
+
caption = ""
|
123
|
+
meeting_number = "A"
|
124
|
+
meeting_times.each do |times|
|
125
|
+
caption << ' ' * (times[0] - caption.size) + meeting_number
|
126
|
+
meeting_number.next!
|
127
|
+
end
|
128
|
+
sprintf("%s", caption).color(:red)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Creates the time caption for the time line
|
132
|
+
def time_caption
|
133
|
+
work_time = get_times[0]
|
134
|
+
caption = ""
|
135
|
+
work_time[0].upto(work_time[1]) do |time|
|
136
|
+
caption << time.to_s + (time < 9 ? ' ' * 3 : ' ' * 2)
|
137
|
+
end
|
138
|
+
sprintf("%s", caption)
|
139
|
+
end
|
140
|
+
|
141
|
+
# graph first creates creates the time line. Then the busy times are added.
|
142
|
+
# After that the tasks are added to the time line and the task caption and
|
143
|
+
# task list is created.
|
144
|
+
# graph returns the graph, task caption, task list and meeting list
|
145
|
+
# * time line
|
146
|
+
# * add meetings to time line
|
147
|
+
# * add tasks to time line
|
148
|
+
# * create task caption
|
149
|
+
# * create task list
|
150
|
+
# * create meeting caption
|
151
|
+
# * create meeting list
|
152
|
+
# * return time line, task caption, task list, meeting caption and meeting
|
153
|
+
# list
|
154
|
+
def graph
|
155
|
+
work_time, meeting_times = get_times
|
156
|
+
time_line = "|---" * (work_time[1]-work_time[0]) + "|"
|
157
|
+
meeting_times.each do |time|
|
158
|
+
time_line[time[0]..time[1]] = '/' * (time[1] - time[0]+1)
|
159
|
+
end
|
160
|
+
|
161
|
+
task_list, task_caption = assign_tasks_to_graph(time_line)
|
162
|
+
|
163
|
+
[meeting_list, meeting_caption,
|
164
|
+
colorize(time_line), time_caption,
|
165
|
+
task_caption, task_list]
|
166
|
+
end
|
167
|
+
|
168
|
+
private
|
169
|
+
|
170
|
+
# Colors the time line free time green, busy time red and tasks blue. The
|
171
|
+
# past time is colored black
|
172
|
+
def colorize(time_line)
|
173
|
+
time_line, future = split_time_line(time_line)
|
174
|
+
future.scan(GRAPH_PATTERN) do |part|
|
175
|
+
time_line << sprintf("%s", part).color(BUSY_COLOR) unless part.scan(BUSY_PATTERN).empty?
|
176
|
+
time_line << sprintf("%s", part).color(FREE_COLOR) unless part.scan(FREE_PATTERN).empty?
|
177
|
+
time_line << sprintf("%s", part).color(WORK_COLOR) unless part.scan(WORK_PATTERN).empty?
|
178
|
+
end if future
|
179
|
+
time_line
|
180
|
+
end
|
181
|
+
|
182
|
+
# Splits the time line at the current time. Returning the past part and the
|
183
|
+
# future part.
|
184
|
+
def split_time_line(time_line)
|
185
|
+
time = Time.now
|
186
|
+
offset = (time.hour - @starts.h) * 4 + time.min.div(15)
|
187
|
+
past = time_line.slice(0,offset)
|
188
|
+
future = time_line.slice(offset, time_line.size - offset)
|
189
|
+
[past, future]
|
190
|
+
end
|
191
|
+
|
192
|
+
# Assigns the tasks to the timeline in alternation x and o subsequent tasks.
|
193
|
+
# Returns the task list and the task caption
|
194
|
+
def assign_tasks_to_graph(time_line)
|
195
|
+
unscheduled_tasks = []
|
196
|
+
signs = ['x','o']
|
197
|
+
positions = {}
|
198
|
+
position = 0
|
199
|
+
unassigned_tasks.each.with_index do |task, index|
|
200
|
+
duration = task.duration.to_i
|
201
|
+
free_time = scan_free(time_line, duration, position)
|
202
|
+
position = free_time[0]
|
203
|
+
if position.nil?
|
204
|
+
unscheduled_tasks << task
|
205
|
+
next
|
206
|
+
end
|
207
|
+
time_line[position..(position + duration-1)] =
|
208
|
+
signs[index%2] * duration
|
209
|
+
positions[position] = task.id
|
210
|
+
end
|
211
|
+
|
212
|
+
max_id_size = 1
|
213
|
+
@tasks.each {|task| max_id_size = [task.id.to_s.size, max_id_size].max}
|
214
|
+
max_ord_size = (@tasks.size - 1).to_s.size
|
215
|
+
|
216
|
+
task_list = sprintf("%s", "Tasks\n").color(:blue)
|
217
|
+
task_list << sprintf("%s", "-----\n").color(:blue)
|
218
|
+
@tasks.each.with_index do |task, i|
|
219
|
+
if task.done?
|
220
|
+
color = :green
|
221
|
+
elsif unscheduled_tasks.find_index(task)
|
222
|
+
color = UNSCHEDULED_COLOR
|
223
|
+
else
|
224
|
+
color = WORK_COLOR
|
225
|
+
end
|
226
|
+
task_list << sprintf("%#{max_ord_size}d: %#{max_id_size}s - %s\n", i, task.id, task.title).
|
227
|
+
color(color)
|
228
|
+
end
|
229
|
+
|
230
|
+
task_caption = ""
|
231
|
+
create_caption(positions).each do |caption|
|
232
|
+
task_caption << sprintf("%s\n", caption).color(WORK_COLOR)
|
233
|
+
end
|
234
|
+
|
235
|
+
[task_list, task_caption]
|
236
|
+
|
237
|
+
end
|
238
|
+
|
239
|
+
# creates the caption of the graph with hours in 1 hour steps and task IDs
|
240
|
+
# that indicate where in the schedule a task is scheduled.
|
241
|
+
def create_caption(positions)
|
242
|
+
counter = 0
|
243
|
+
lines = [""]
|
244
|
+
positions.each do |position,id|
|
245
|
+
line_id = next_line(position,lines,counter)
|
246
|
+
legend = ' ' * [0, position - lines[line_id].size].max + id.to_s
|
247
|
+
lines[line_id] += legend
|
248
|
+
counter += 1
|
249
|
+
end
|
250
|
+
lines
|
251
|
+
end
|
252
|
+
|
253
|
+
# Creates a new line if the the task ID in the caption would override the
|
254
|
+
# task ID of a previous task. The effect is shown below
|
255
|
+
# |xx-|//o|x--|
|
256
|
+
# 8 9 10 10
|
257
|
+
# 10 101
|
258
|
+
# 11 2
|
259
|
+
# position is the position (time) within the schedule
|
260
|
+
# lines is the available ID lines (above we have 2 ID lines)
|
261
|
+
# counter is the currently displayed line. IDs are displayed alternating in
|
262
|
+
# each line, when we have 2 lines IDs will be printed in line 1,2,1,2...
|
263
|
+
def next_line(position, lines, counter)
|
264
|
+
line = lines[counter%lines.size]
|
265
|
+
return counter%lines.size if line.size == 0 or line.size < position - 1
|
266
|
+
lines.each.with_index do |line, index|
|
267
|
+
return index if line.size < position - 1
|
268
|
+
end
|
269
|
+
lines << ""
|
270
|
+
return lines.size - 1
|
271
|
+
end
|
272
|
+
|
273
|
+
# Scans the schedule for free time where a task can be added to. Count
|
274
|
+
# specifies the length of the free time and the position where to start
|
275
|
+
# scanning within the graph
|
276
|
+
def scan_free(graph, count, position)
|
277
|
+
pattern = /(?!\/)[\|-]{#{count}}(?<=-|\||\/)/
|
278
|
+
|
279
|
+
positions = []
|
280
|
+
index = position
|
281
|
+
while index and index < graph.size
|
282
|
+
index = graph.index(pattern, index)
|
283
|
+
if index
|
284
|
+
positions << index
|
285
|
+
index += 1
|
286
|
+
end
|
287
|
+
end
|
288
|
+
positions
|
289
|
+
end
|
290
|
+
|
291
|
+
# Returns the tasks that are not assigned to meetings
|
292
|
+
def unassigned_tasks
|
293
|
+
assigned = []
|
294
|
+
@meetings.each do |meeting|
|
295
|
+
assigned << meeting.tasks
|
296
|
+
end
|
297
|
+
assigned.flatten!
|
298
|
+
|
299
|
+
unassigned = []
|
300
|
+
unassigned << @tasks
|
301
|
+
unassigned.flatten.delete_if {|task| assigned.find_index(task)}
|
302
|
+
end
|
303
|
+
|
304
|
+
public
|
305
|
+
|
306
|
+
# Retrieves the work and busy times transformed to the time line scale
|
307
|
+
def get_times
|
308
|
+
work_time = [@starts.h, @ends.round_up]
|
309
|
+
meeting_times = []
|
310
|
+
@meetings.each do |meeting|
|
311
|
+
meeting_time = Array.new(2)
|
312
|
+
meeting_time[0] = hour_offset(@starts.h, meeting.starts.h) +
|
313
|
+
minute_offset(meeting.starts.m)
|
314
|
+
meeting_time[1] = hour_offset(@starts.h, meeting.ends.h) +
|
315
|
+
minute_offset(meeting.ends.m)
|
316
|
+
meeting_times << meeting_time
|
317
|
+
end if @meetings
|
318
|
+
|
319
|
+
times = [work_time, meeting_times]
|
320
|
+
end
|
321
|
+
|
322
|
+
private
|
323
|
+
|
324
|
+
# Transposes a time hour to a graph hour
|
325
|
+
def hour_offset(starts, ends)
|
326
|
+
(ends - starts) * 4
|
327
|
+
end
|
328
|
+
|
329
|
+
# Transposes a time minute to a graph minute
|
330
|
+
def minute_offset(minutes)
|
331
|
+
minutes.to_i.div(15)
|
332
|
+
end
|
333
|
+
|
334
|
+
end
|
335
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require_relative '../sycutil/console.rb'
|
3
|
+
require_relative 'task_service.rb'
|
4
|
+
|
5
|
+
module Syctask
|
6
|
+
PROMPT_STRING = '(a)dd, (c)omplete, (s)kip, (q)uit: '
|
7
|
+
|
8
|
+
class TaskPlanner
|
9
|
+
WORK_DIR = File.expand_path("~/.tasks")
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@console = Sycutil::Console.new
|
13
|
+
@service = TaskService.new
|
14
|
+
make_todo_today_file(Time.now.strftime("%Y-%m-%d"))
|
15
|
+
end
|
16
|
+
|
17
|
+
# List each task and prompt the user whether to add the task to the planned
|
18
|
+
# tasks. The user doesn't specify a duration for the task operation the
|
19
|
+
# duration will be set to 30 minutes which equals two time chunks. The
|
20
|
+
# count of planned tasks is returned
|
21
|
+
def plan_tasks(tasks, date=Time.now.strftime("%Y-%m-%d"))
|
22
|
+
already_planned = self.get_tasks(date)
|
23
|
+
count = 0
|
24
|
+
re_display = false
|
25
|
+
planned = []
|
26
|
+
tasks.each do |task|
|
27
|
+
next if already_planned.find_index {|t| t == task}
|
28
|
+
unless re_display
|
29
|
+
task.print_pretty
|
30
|
+
else
|
31
|
+
task.print_pretty(true)
|
32
|
+
re_display = false
|
33
|
+
end
|
34
|
+
choice = @console.prompt PROMPT_STRING
|
35
|
+
case choice
|
36
|
+
when 'a'
|
37
|
+
print "Duration (1 = 15 minutes, return 30 minutes): "
|
38
|
+
duration = gets.chomp
|
39
|
+
task.duration = duration.empty? ? 2 : duration
|
40
|
+
task.options[:follow_up] = date
|
41
|
+
@service.save(task.dir, task)
|
42
|
+
planned << task
|
43
|
+
count += 1
|
44
|
+
when 'c'
|
45
|
+
re_display = true
|
46
|
+
redo
|
47
|
+
when 's'
|
48
|
+
#do nothing
|
49
|
+
when 'q'
|
50
|
+
break
|
51
|
+
end
|
52
|
+
end
|
53
|
+
save_tasks(planned)
|
54
|
+
count
|
55
|
+
end
|
56
|
+
|
57
|
+
# Add the tasks to the planned tasks
|
58
|
+
def add_tasks(tasks)
|
59
|
+
save_tasks(tasks)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Remove planned tasks from the task plan based on the provided filter
|
63
|
+
# (filter options see Task#matches?). Returns the count of removed tasks
|
64
|
+
def remove_tasks(date=Time.now.strftime("%Y-%m-%d"), filter={})
|
65
|
+
planned = []
|
66
|
+
tasks = self.get_tasks(date)
|
67
|
+
tasks.each do |task|
|
68
|
+
planned << task unless task.matches?(filter)
|
69
|
+
end
|
70
|
+
save_tasks(planned, true)
|
71
|
+
tasks.size - planned.size
|
72
|
+
end
|
73
|
+
|
74
|
+
# Get planned tasks of the specified date. Retrieve only tasks that match
|
75
|
+
# the specified filter (filter options see Task#matches?)
|
76
|
+
def get_tasks(date=Time.now.strftime("%Y-%m-%d"), filter={})
|
77
|
+
make_todo_today_file(date)
|
78
|
+
tasks = []
|
79
|
+
File.open(@todo_today_file, 'r') do |file|
|
80
|
+
file.each do |line|
|
81
|
+
dir, id = line.chomp.split(",")
|
82
|
+
task = @service.read(dir, id)
|
83
|
+
tasks << task if not task.nil? and task.matches?(filter)
|
84
|
+
end
|
85
|
+
end if File.exists? @todo_today_file
|
86
|
+
tasks
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
# Creates a file where the planned tasks are saved to
|
92
|
+
def make_todo_today_file(date)
|
93
|
+
file_name = Time.now.strftime("#{date}_planned_tasks")
|
94
|
+
@todo_today_file = WORK_DIR+"/"+file_name
|
95
|
+
end
|
96
|
+
|
97
|
+
# Save the tasks to a file. If override is true the file is overriden
|
98
|
+
# otherwise the tasks are appended
|
99
|
+
def save_tasks(tasks, override=false)
|
100
|
+
mode = override ? 'w' : 'a'
|
101
|
+
FileUtils.mkdir_p WORK_DIR unless File.exists? WORK_DIR
|
102
|
+
File.open(@todo_today_file, mode) do |file|
|
103
|
+
tasks.each do |task|
|
104
|
+
file.puts("#{task.dir},#{task.id}")
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require_relative 'schedule.rb'
|
2
|
+
|
3
|
+
module Syctask
|
4
|
+
|
5
|
+
# The TaskScheduler creates a graphical representation of a working schedule
|
6
|
+
# with busy times visualized. A typical invokation would be
|
7
|
+
# work_time = "8:00-18:00"
|
8
|
+
# busy_time = "9:00-9:30,13:00-14:30"
|
9
|
+
# scheduler = Syctask::TaskScheduler.new(work_time, busy_time)
|
10
|
+
# scheduler.print_graph
|
11
|
+
# The output would be
|
12
|
+
# |---///-|---|---|---///////-|---|---|---|
|
13
|
+
# 8 9 10 11 12 13 14 15 16 17 18
|
14
|
+
# To add tasks to the schedule tasks have to provided (see Task). A task has
|
15
|
+
# a duration which indicates the time it is planned to process a task. The
|
16
|
+
# duration is an Integer 1,2,.. where 1 is 15 minutes and 2 is 30 minutes and
|
17
|
+
# so on. Assuming we have 5 tasks with a duration of 2, 5, 3, 2 and 3 15
|
18
|
+
# minute chunks. Then the invokation of
|
19
|
+
# scheduler.schedule_tasks(tasks)
|
20
|
+
# would output the schedule
|
21
|
+
# |xx-///ooooo|xxx|oo-///////xxx--|---|---|
|
22
|
+
# 8 9 10 11 12 13 14 15 16 17 18
|
23
|
+
# The tasks are added to the schedule dependent on the time chunks and the
|
24
|
+
# available free time gaps.
|
25
|
+
class TaskScheduler
|
26
|
+
# Time pattern that matches 24 hour times '12:30'
|
27
|
+
TIME_PATTERN = /(2[0-3]|[01]?[0-9]):([0-5]?[0-9])/
|
28
|
+
|
29
|
+
# Work time pattern scans time like '8:00-18:00'
|
30
|
+
WORK_TIME_PATTERN = /#{TIME_PATTERN}-#{TIME_PATTERN}/
|
31
|
+
|
32
|
+
# Busy time pattern scans times like '9:00-9:30,11:00-11:45'
|
33
|
+
BUSY_TIME_PATTERN =
|
34
|
+
/#{TIME_PATTERN}-#{TIME_PATTERN}(?=,)|#{TIME_PATTERN}-#{TIME_PATTERN}$/
|
35
|
+
|
36
|
+
# Scans assignments of tasks to meetings 'A:0,2,4;B:3,4,5'
|
37
|
+
ASSIGNMENT_PATTERN = /([a-zA-Z]):(\d+(?:,\d+|\d+;)*)/
|
38
|
+
|
39
|
+
# Working directory
|
40
|
+
WORK_DIR = File.expand_path("~/.tasks")
|
41
|
+
|
42
|
+
# Creates a new TaskScheduler.
|
43
|
+
def initialize
|
44
|
+
@work_time = []
|
45
|
+
@busy_time = []
|
46
|
+
@meetings = []
|
47
|
+
@tasks = []
|
48
|
+
end
|
49
|
+
|
50
|
+
# Set the work time. Raises an exception if begin time is after start time
|
51
|
+
# Invokation: set_work_time(["8","0","18","30"])
|
52
|
+
def set_work_time(work_time)
|
53
|
+
@work_time = process_work_time(work_time)
|
54
|
+
unless sequential?(@work_time)
|
55
|
+
raise Exception, "Begin time has to be before end time"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Set the busy times. Raises an exception if one begin time is after start
|
60
|
+
# time
|
61
|
+
# Invokation: set_busy_times([["9","30","10","45"],["12","0","13","45"]])
|
62
|
+
def set_busy_times(busy_time)
|
63
|
+
@busy_time = process_busy_time(busy_time)
|
64
|
+
@busy_time.each do |busy|
|
65
|
+
unless sequential?(busy)
|
66
|
+
raise Exception, "Begin time has to be before end time"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Sets the titles of the meetings (busy times)
|
72
|
+
# Invokation: set_meeting_titles("title1,title2,title3")
|
73
|
+
def set_meeting_titles(titles)
|
74
|
+
@meetings = titles.split(",") if titles
|
75
|
+
end
|
76
|
+
|
77
|
+
def set_tasks(tasks)
|
78
|
+
@tasks = tasks
|
79
|
+
end
|
80
|
+
|
81
|
+
# Add scheduled tasks to busy times
|
82
|
+
# Invokation: set_task_assignments([["A","1,2,3"],["B","2,5,6,7"]])
|
83
|
+
def set_task_assignments(assignments)
|
84
|
+
@assignments = assignments.scan(ASSIGNMENT_PATTERN)
|
85
|
+
raise "No valid assignment" if @assignments.empty?
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# Checks the sequence of begin and end time. Returns true if begin is before
|
91
|
+
# end time otherwise false
|
92
|
+
def sequential?(range)
|
93
|
+
return true if range[0].to_i < range[2].to_i
|
94
|
+
if range[0].to_i == range[2].to_i
|
95
|
+
return true if range[1].to_i < range[3].to_i
|
96
|
+
end
|
97
|
+
false
|
98
|
+
end
|
99
|
+
|
100
|
+
# Scans the work time and separates hours and minutes. Raises an Exception
|
101
|
+
# if work time is nil or empty
|
102
|
+
def process_work_time(work_time)
|
103
|
+
raise Exception, "Work time must not be nil" if work_time.nil?
|
104
|
+
time = work_time.scan(WORK_TIME_PATTERN).flatten
|
105
|
+
raise Exception, "Work time cannot be empty" if time.empty?
|
106
|
+
time
|
107
|
+
end
|
108
|
+
|
109
|
+
# Scans the busy times and separates hours and minutes.
|
110
|
+
def process_busy_time(busy_time)
|
111
|
+
busy_time = "" if busy_time.nil?
|
112
|
+
busy_time.scan(BUSY_TIME_PATTERN).each {|busy| busy.compact!}
|
113
|
+
end
|
114
|
+
|
115
|
+
public
|
116
|
+
|
117
|
+
# Restores the value of a previous invokation. Posible values are
|
118
|
+
# :work_time, :busy_time, :meetings and :assignments
|
119
|
+
# Returns true if a value from a previous call is available otherwise false
|
120
|
+
def restore(value)
|
121
|
+
work_time, busy_time, meetings, assignments = restore_state
|
122
|
+
@work_time = work_time if value == :work_time
|
123
|
+
@busy_time = busy_time if value == :busy_time
|
124
|
+
@meetings = meetings if value == :meetings
|
125
|
+
@assignments = assignments if value == :assignments
|
126
|
+
return false if value == :work_time and (@work_time.nil? or @work_time.empty?)
|
127
|
+
return false if value == :busy_time and (@busy_time.nil? or @busy_time.empty?)
|
128
|
+
return false if value == :meetings and (@busy_time.nil? or @meetings.empty?)
|
129
|
+
return false if value == :assignments and (@assignments.nil? or @assignments.empty?)
|
130
|
+
true
|
131
|
+
end
|
132
|
+
|
133
|
+
# Prints the meeting list, timeline and task list
|
134
|
+
def show
|
135
|
+
schedule = Syctask::Schedule.new(@work_time, @busy_time, @meetings, @tasks)
|
136
|
+
schedule.assign(@assignments) if @assignments
|
137
|
+
schedule.graph.each {|output| puts output}
|
138
|
+
save_state @work_time, @busy_time, @meetings, @assignments
|
139
|
+
true
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
# Saves the work time, busy time, meetings and assignments from the
|
145
|
+
# invokation for later retrieval
|
146
|
+
def save_state(work_time, busy_time, meetings, assignments)
|
147
|
+
state = {work_time: work_time,
|
148
|
+
busy_time: busy_time,
|
149
|
+
meetings: meetings,
|
150
|
+
assignments: assignments}
|
151
|
+
FileUtils.mkdir WORK_DIR unless File.exists? WORK_DIR
|
152
|
+
state_file = WORK_DIR+'/'+Time.now.strftime("%Y-%m-%d_time_schedule")
|
153
|
+
File.open(state_file, 'w') do |file|
|
154
|
+
YAML.dump(state, file)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Retrieves the state of the last invokation. Returns the work and busy
|
159
|
+
# time, meetings and assignments
|
160
|
+
def restore_state
|
161
|
+
state_file = WORK_DIR+'/'+Time.now.strftime("%Y-%m-%d_time_schedule")
|
162
|
+
return [[], [], [], []] unless File.exists? state_file
|
163
|
+
state = YAML.load_file(state_file)
|
164
|
+
[state[:work_time],
|
165
|
+
state[:busy_time],
|
166
|
+
state[:meetings],
|
167
|
+
state[:assignments]]
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
data/lib/syctask/version.rb
CHANGED
data/lib/syctask.rb
CHANGED
@@ -4,6 +4,8 @@ require 'syctask/task.rb'
|
|
4
4
|
require 'syctask/task_service.rb'
|
5
5
|
require 'syctask/task_scheduler.rb'
|
6
6
|
require 'syctask/task_planner.rb'
|
7
|
+
require 'syctask/schedule.rb'
|
8
|
+
require 'sycutil/console.rb'
|
7
9
|
|
8
10
|
# Add requires for other files you add to your project here, so
|
9
11
|
# you just need to require this one file in your bin file
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'io/wait'
|
2
|
+
|
3
|
+
# Module Inspector contains functions related to the Console that is helpers
|
4
|
+
# for user input
|
5
|
+
module Sycutil
|
6
|
+
|
7
|
+
# Console provides functions for user input
|
8
|
+
class Console
|
9
|
+
|
10
|
+
# Listens on Ctrl-C and exits the application
|
11
|
+
Signal.trap("INT") do
|
12
|
+
puts "-> program terminated by user"
|
13
|
+
exit
|
14
|
+
end
|
15
|
+
|
16
|
+
# Listens for key presses and returns the pressed key without pressing
|
17
|
+
# return
|
18
|
+
#
|
19
|
+
# :call-seq:
|
20
|
+
# char_if_pressed
|
21
|
+
def char_if_pressed
|
22
|
+
begin
|
23
|
+
system("stty raw -echo")
|
24
|
+
c = nil
|
25
|
+
if $stdin.ready?
|
26
|
+
c = $stdin.getc
|
27
|
+
end
|
28
|
+
c.chr if c
|
29
|
+
ensure
|
30
|
+
system "stty -raw echo"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Prompts the user for input.
|
35
|
+
#
|
36
|
+
# :call-seq:
|
37
|
+
# prompt(choice_line) -> char
|
38
|
+
#
|
39
|
+
# choice_line is the prompt string. If the prompt string contains a (x)
|
40
|
+
# sequence x is a valid choice the is relized when pressed and returned.
|
41
|
+
def prompt(choice_line)
|
42
|
+
pattern = /(?<=\()./
|
43
|
+
choices = choice_line.scan(pattern)
|
44
|
+
|
45
|
+
choice = nil
|
46
|
+
|
47
|
+
while choices.find_index(choice).nil?
|
48
|
+
print choice_line
|
49
|
+
choice = nil
|
50
|
+
choice = char_if_pressed while choice == nil
|
51
|
+
sleep 0.1
|
52
|
+
puts
|
53
|
+
end
|
54
|
+
|
55
|
+
choice
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: syc-task
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-03-
|
12
|
+
date: 2013-03-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -154,8 +154,14 @@ files:
|
|
154
154
|
- lib/syctask/version.rb
|
155
155
|
- lib/syctask/task.rb
|
156
156
|
- lib/syctask/task_service.rb
|
157
|
+
- lib/syctask/task_planner.rb
|
157
158
|
- lib/syctask/evaluator.rb
|
158
159
|
- lib/syctask.rb
|
160
|
+
- lib/syctask/task_scheduler.rb
|
161
|
+
- lib/syctask/meeting.rb
|
162
|
+
- lib/syctask/times.rb
|
163
|
+
- lib/syctask/schedule.rb
|
164
|
+
- lib/sycutil/console.rb
|
159
165
|
- README.rdoc
|
160
166
|
- syctask.rdoc
|
161
167
|
homepage: http://syc.dyndns.org/drupal/syc-task
|