tasklogger 0.0.2
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/Rakefile +80 -0
- data/bin/tasklogger +161 -0
- data/lib/task.rb +53 -0
- data/lib/task_logger.rb +68 -0
- metadata +76 -0
data/Rakefile
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
require "rake/testtask"
|
2
|
+
|
3
|
+
task :default => :test
|
4
|
+
|
5
|
+
Rake::TestTask.new do |t|
|
6
|
+
t.libs << "test"
|
7
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
8
|
+
t.verbose = true
|
9
|
+
end
|
10
|
+
|
11
|
+
require "rubygems"
|
12
|
+
require "rake/gempackagetask"
|
13
|
+
require "rake/rdoctask"
|
14
|
+
|
15
|
+
# This builds the actual gem. For details of what all these options
|
16
|
+
# mean, and other ones you can add, check the documentation here:
|
17
|
+
#
|
18
|
+
# http://rubygems.org/read/chapter/20
|
19
|
+
#
|
20
|
+
spec = Gem::Specification.new do |s|
|
21
|
+
|
22
|
+
# Change these as appropriate
|
23
|
+
s.name = "tasklogger"
|
24
|
+
s.version = "0.0.2"
|
25
|
+
s.summary = "A simple task/time logging utility"
|
26
|
+
s.author = "Chris Roos"
|
27
|
+
s.email = "chris@seagul.co.uk"
|
28
|
+
s.homepage = "http://chrisroos.co.uk"
|
29
|
+
|
30
|
+
s.has_rdoc = true
|
31
|
+
# You should probably have a README of some kind. Change the filename
|
32
|
+
# as appropriate
|
33
|
+
# s.extra_rdoc_files = %w(README)
|
34
|
+
# s.rdoc_options = %w(--main README)
|
35
|
+
|
36
|
+
# Add any extra files to include in the gem (like your README)
|
37
|
+
s.files = %w(Rakefile) + Dir.glob("{bin,test,lib/**/*}")
|
38
|
+
s.executables = FileList["bin/**"].map { |f| File.basename(f) }
|
39
|
+
s.require_paths = ["lib"]
|
40
|
+
|
41
|
+
# If you want to depend on other gems, add them here, along with any
|
42
|
+
# relevant versions
|
43
|
+
# s.add_dependency("some_other_gem", "~> 0.1.0")
|
44
|
+
s.add_dependency 'fastercsv'
|
45
|
+
|
46
|
+
# If your tests use any gems, include them here
|
47
|
+
# s.add_development_dependency("mocha") # for example
|
48
|
+
s.add_development_dependency 'mocha'
|
49
|
+
end
|
50
|
+
|
51
|
+
# This task actually builds the gem. We also regenerate a static
|
52
|
+
# .gemspec file, which is useful if something (i.e. GitHub) will
|
53
|
+
# be automatically building a gem for this project. If you're not
|
54
|
+
# using GitHub, edit as appropriate.
|
55
|
+
#
|
56
|
+
# To publish your gem online, install the 'gemcutter' gem; Read more
|
57
|
+
# about that here: http://gemcutter.org/pages/gem_docs
|
58
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
59
|
+
pkg.gem_spec = spec
|
60
|
+
end
|
61
|
+
|
62
|
+
desc "Build the gemspec file #{spec.name}.gemspec"
|
63
|
+
task :gemspec do
|
64
|
+
file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
|
65
|
+
File.open(file, "w") {|f| f << spec.to_ruby }
|
66
|
+
end
|
67
|
+
|
68
|
+
task :package => :gemspec
|
69
|
+
|
70
|
+
# Generate documentation
|
71
|
+
Rake::RDocTask.new do |rd|
|
72
|
+
|
73
|
+
rd.rdoc_files.include("lib/**/*.rb")
|
74
|
+
rd.rdoc_dir = "rdoc"
|
75
|
+
end
|
76
|
+
|
77
|
+
desc 'Clear out RDoc and generated packages'
|
78
|
+
task :clean => [:clobber_rdoc, :clobber_package] do
|
79
|
+
rm "#{spec.name}.gemspec"
|
80
|
+
end
|
data/bin/tasklogger
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
4
|
+
require 'rubygems'
|
5
|
+
require 'fastercsv'
|
6
|
+
require 'task_logger'
|
7
|
+
|
8
|
+
unless description = ARGV[0]
|
9
|
+
puts "Please enter a description for this task."
|
10
|
+
exit 1
|
11
|
+
end
|
12
|
+
|
13
|
+
default_timelog = File.join(ENV['HOME'], 'timelog.csv')
|
14
|
+
timelog = ENV['TIMELOG_DATA'] ? ENV['TIMELOG_DATA'] : default_timelog
|
15
|
+
|
16
|
+
# puts "Logging task data to #{timelog}"
|
17
|
+
|
18
|
+
time_format = "%Y-%m-%d %H:%M"
|
19
|
+
|
20
|
+
if description == 'open'
|
21
|
+
`mate #{timelog}`
|
22
|
+
exit
|
23
|
+
end
|
24
|
+
|
25
|
+
if description == 'stop'
|
26
|
+
entries = FasterCSV.read(timelog)
|
27
|
+
last_entry = entries.last
|
28
|
+
if last_entry[1]
|
29
|
+
puts "The last entry already has an end time and I'm not going to overwrite it."
|
30
|
+
exit 1
|
31
|
+
end
|
32
|
+
last_entry[1] = Time.now.strftime(time_format)
|
33
|
+
File.open(timelog, 'w') do |file|
|
34
|
+
entries.each do |entry|
|
35
|
+
file.puts entry.to_csv
|
36
|
+
end
|
37
|
+
end
|
38
|
+
`mate #{timelog}`
|
39
|
+
exit
|
40
|
+
end
|
41
|
+
|
42
|
+
def hms(duration)
|
43
|
+
hours = duration.to_i / 3600
|
44
|
+
seconds = duration.to_i % 3600
|
45
|
+
minutes = seconds / 60
|
46
|
+
seconds = seconds % 60
|
47
|
+
[hours, minutes, seconds].join(":")
|
48
|
+
end
|
49
|
+
|
50
|
+
def decimal_hours(seconds)
|
51
|
+
minutes = seconds / 60.0
|
52
|
+
minutes / 60.0
|
53
|
+
end
|
54
|
+
|
55
|
+
if description == 'report'
|
56
|
+
require 'time'
|
57
|
+
|
58
|
+
if project_filter = ARGV[1]
|
59
|
+
csv_data = File.read(timelog)
|
60
|
+
total_duration = 0
|
61
|
+
|
62
|
+
puts ''
|
63
|
+
header = 'Start'.ljust(23) + 'End'.ljust(23) + 'Seconds'.ljust(8) + 'H:M:S'.ljust(8) + "Hours".ljust(8) + 'Project'.ljust(12) + 'Description'
|
64
|
+
puts header
|
65
|
+
puts '=' * header.length
|
66
|
+
|
67
|
+
previous_day, day_duration = nil, 0
|
68
|
+
|
69
|
+
rows = FasterCSV.parse(csv_data)
|
70
|
+
matching_entries = rows.select do |row|
|
71
|
+
start_time, end_time, project_name, description = row
|
72
|
+
project_name == project_filter
|
73
|
+
end
|
74
|
+
|
75
|
+
matching_entries.each do |row|
|
76
|
+
start_time, end_time, project_name, description = row
|
77
|
+
|
78
|
+
duration = 0
|
79
|
+
if end_time
|
80
|
+
start_time = Time.parse(start_time)
|
81
|
+
end_time = Time.parse(end_time)
|
82
|
+
duration = (end_time - start_time)
|
83
|
+
end
|
84
|
+
|
85
|
+
if (row != matching_entries.first and previous_day != start_time.send(:to_date))
|
86
|
+
# We're on a new day
|
87
|
+
puts ' ' + '-'*7 + ' ' + '-'*7 + ' ' + '-'*7
|
88
|
+
print ' '
|
89
|
+
print day_duration.to_s.ljust(8)
|
90
|
+
print hms(day_duration).ljust(8)
|
91
|
+
print format("%0.2f", decimal_hours(day_duration)).ljust(8)
|
92
|
+
puts ''
|
93
|
+
puts ''
|
94
|
+
day_duration = 0
|
95
|
+
end
|
96
|
+
previous_day = start_time.send(:to_date)
|
97
|
+
day_duration += duration
|
98
|
+
|
99
|
+
total_duration += duration
|
100
|
+
row.insert(2, duration)
|
101
|
+
row.insert(3, hms(duration))
|
102
|
+
row.insert(4, decimal_hours(duration))
|
103
|
+
|
104
|
+
print Time.parse(row[0]).strftime("%a %d %b %Y %H:%M").ljust(23)
|
105
|
+
print Time.parse(row[1]).strftime("%a %d %b %Y %H:%M").ljust(23)
|
106
|
+
print row[2].to_s.ljust(8)
|
107
|
+
print row[3].to_s.ljust(8)
|
108
|
+
print format("%0.2f", row[4]).ljust(8)
|
109
|
+
print row[5].to_s.ljust(12)
|
110
|
+
puts row[6]
|
111
|
+
|
112
|
+
if row == matching_entries.last
|
113
|
+
# We're on the last day
|
114
|
+
print ' '
|
115
|
+
print day_duration.to_s.ljust(8)
|
116
|
+
print hms(day_duration).ljust(8)
|
117
|
+
print format("%0.2f", decimal_hours(day_duration)).ljust(8)
|
118
|
+
puts ''
|
119
|
+
puts ''
|
120
|
+
day_duration = 0
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
puts ''
|
125
|
+
puts 'Total duration (H:M:S): ' + hms(total_duration)
|
126
|
+
puts "Total duration (decimal hours): #{decimal_hours(total_duration)}"
|
127
|
+
puts ''
|
128
|
+
|
129
|
+
else
|
130
|
+
d = Hash.new(0)
|
131
|
+
|
132
|
+
csv_data = File.read(timelog)
|
133
|
+
FasterCSV.parse(csv_data).each do |row|
|
134
|
+
start_time, end_time, project_name, description = row
|
135
|
+
if end_time
|
136
|
+
start_time = Time.parse(start_time)
|
137
|
+
end_time = Time.parse(end_time)
|
138
|
+
duration = (end_time - start_time)
|
139
|
+
d[project_name] += duration
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
d.each_pair do |project_name, duration|
|
144
|
+
p [project_name, hms(duration)]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
exit
|
148
|
+
end
|
149
|
+
|
150
|
+
if description == 'resume'
|
151
|
+
TaskLogger.new(timelog).resume
|
152
|
+
exit
|
153
|
+
end
|
154
|
+
|
155
|
+
if description == 'list'
|
156
|
+
TaskLogger.new(timelog).list
|
157
|
+
exit
|
158
|
+
end
|
159
|
+
|
160
|
+
# Otherwise we're starting a new task
|
161
|
+
TaskLogger.new(timelog).start description
|
data/lib/task.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
class Task
|
4
|
+
|
5
|
+
TIME_FORMAT = "%Y-%m-%d %H:%M"
|
6
|
+
DEFAULT_PROJECT = '<project-name>'
|
7
|
+
|
8
|
+
attr_reader :started_at, :finished_at
|
9
|
+
attr_accessor :project, :description
|
10
|
+
|
11
|
+
def self.from_array(task_data)
|
12
|
+
description = task_data[3]
|
13
|
+
started_at = Time.parse(task_data[0]) if task_data[0]
|
14
|
+
finished_at = Time.parse(task_data[1]) if task_data[1]
|
15
|
+
project = task_data[2]
|
16
|
+
new(description, started_at, finished_at, project)
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(description, started_at = nil, finished_at = nil, project = nil)
|
20
|
+
@started_at = started_at || Time.now
|
21
|
+
@finished_at = finished_at
|
22
|
+
@project = project || DEFAULT_PROJECT
|
23
|
+
@description = description
|
24
|
+
end
|
25
|
+
|
26
|
+
def restart!
|
27
|
+
@started_at = Time.now
|
28
|
+
@finished_at = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def finish!
|
32
|
+
@finished_at = Time.now unless finished?
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_a
|
36
|
+
[formatted_started_at, formatted_finished_at, @project, @description]
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def finished?
|
42
|
+
@finished_at
|
43
|
+
end
|
44
|
+
|
45
|
+
def formatted_started_at
|
46
|
+
@started_at.strftime(TIME_FORMAT) if @started_at
|
47
|
+
end
|
48
|
+
|
49
|
+
def formatted_finished_at
|
50
|
+
@finished_at.strftime(TIME_FORMAT) if @finished_at
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
data/lib/task_logger.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'task'
|
2
|
+
|
3
|
+
class TaskLogger
|
4
|
+
|
5
|
+
TIME_FORMAT = "%Y-%m-%d %H:%M"
|
6
|
+
|
7
|
+
def initialize(tasks_path)
|
8
|
+
@tasks_path = tasks_path
|
9
|
+
end
|
10
|
+
|
11
|
+
def start(task_description)
|
12
|
+
task = Task.new(task_description)
|
13
|
+
if last_task_data = tasks.pop
|
14
|
+
last_task = Task.from_array(last_task_data)
|
15
|
+
last_task.finish!
|
16
|
+
tasks << last_task.to_a
|
17
|
+
end
|
18
|
+
tasks << task.to_a
|
19
|
+
write_tasks
|
20
|
+
end
|
21
|
+
|
22
|
+
def resume
|
23
|
+
exit_if_missing_data
|
24
|
+
|
25
|
+
last_task_data = tasks.pop
|
26
|
+
last_task = Task.from_array(last_task_data)
|
27
|
+
last_task.finish!
|
28
|
+
|
29
|
+
penultimate_task_data = tasks.last
|
30
|
+
penultimate_task = Task.from_array(penultimate_task_data)
|
31
|
+
penultimate_task.restart!
|
32
|
+
|
33
|
+
tasks << last_task.to_a
|
34
|
+
tasks << penultimate_task.to_a
|
35
|
+
|
36
|
+
write_tasks
|
37
|
+
end
|
38
|
+
|
39
|
+
def list
|
40
|
+
exit_if_missing_data
|
41
|
+
|
42
|
+
tasks[-5, 5].each do |task|
|
43
|
+
puts task.to_csv
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def tasks
|
50
|
+
@tasks ||= File.exists?(@tasks_path) ? FasterCSV.read(@tasks_path) : []
|
51
|
+
end
|
52
|
+
|
53
|
+
def exit_if_missing_data
|
54
|
+
if tasks.empty?
|
55
|
+
puts "Error. The timelog data cannot be found."
|
56
|
+
exit 1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def write_tasks
|
61
|
+
File.open(@tasks_path, 'w') do |file|
|
62
|
+
tasks.each do |task|
|
63
|
+
file.puts task.to_csv
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tasklogger
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Chris Roos
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-04-26 00:00:00 +01:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: fastercsv
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: mocha
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description:
|
36
|
+
email: chris@seagul.co.uk
|
37
|
+
executables:
|
38
|
+
- tasklogger
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files: []
|
42
|
+
|
43
|
+
files:
|
44
|
+
- Rakefile
|
45
|
+
- lib/task.rb
|
46
|
+
- lib/task_logger.rb
|
47
|
+
has_rdoc: true
|
48
|
+
homepage: http://chrisroos.co.uk
|
49
|
+
licenses: []
|
50
|
+
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: "0"
|
61
|
+
version:
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
version:
|
68
|
+
requirements: []
|
69
|
+
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.3.5
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: A simple task/time logging utility
|
75
|
+
test_files: []
|
76
|
+
|