syc-task 0.0.2 → 0.0.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.
- 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
|