timesheet 0.2.0
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/History.txt +4 -0
- data/Manifest.txt +24 -0
- data/README.rdoc +104 -0
- data/Rakefile +29 -0
- data/bin/timesheet +7 -0
- data/lib/timesheet.rb +82 -0
- data/lib/timesheet/range_extensions.rb +24 -0
- data/lib/timesheet/time_entry.rb +50 -0
- data/lib/timesheet/time_log.rb +71 -0
- data/lib/timesheet/time_report.rb +113 -0
- data/lib/timesheet/timesheet_parser.rb +242 -0
- data/lib/timesheet/trollop.rb +761 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/spec/range_extensions_spec.rb +17 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/time_entry_spec.rb +137 -0
- data/spec/time_log_spec.rb +164 -0
- data/spec/time_report_spec.rb +104 -0
- data/spec/timesheet_parser_spec.rb +235 -0
- data/spec/timesheet_spec.rb +188 -0
- data/tasks/rspec.rake +21 -0
- metadata +274 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.rdoc
|
4
|
+
Rakefile
|
5
|
+
bin/timesheet
|
6
|
+
lib/timesheet.rb
|
7
|
+
lib/timesheet/range_extensions.rb
|
8
|
+
lib/timesheet/time_entry.rb
|
9
|
+
lib/timesheet/time_log.rb
|
10
|
+
lib/timesheet/time_report.rb
|
11
|
+
lib/timesheet/timesheet_parser.rb
|
12
|
+
lib/timesheet/trollop.rb
|
13
|
+
script/console
|
14
|
+
script/destroy
|
15
|
+
script/generate
|
16
|
+
spec/range_extensions_spec.rb
|
17
|
+
spec/spec.opts
|
18
|
+
spec/spec_helper.rb
|
19
|
+
spec/time_entry_spec.rb
|
20
|
+
spec/time_log_spec.rb
|
21
|
+
spec/time_report_spec.rb
|
22
|
+
spec/timesheet_parser_spec.rb
|
23
|
+
spec/timesheet_spec.rb
|
24
|
+
tasks/rspec.rake
|
data/README.rdoc
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
= timesheet
|
2
|
+
|
3
|
+
* http://github.com/jschank/timesheet
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
<i>Timesheet</i> is simple ruby application for tracking time spent on projects.
|
8
|
+
It is a console application that uses a simple text file storage back-end (YAML::Store)
|
9
|
+
|
10
|
+
The main idea is to be able to produce reports of hours spent, such that I can use geektool to display the reports.
|
11
|
+
|
12
|
+
== FEATURES/PROBLEMS:
|
13
|
+
|
14
|
+
with <i>timesheet</i> you can:
|
15
|
+
|
16
|
+
* <b>add</b> new entries to the database
|
17
|
+
* <b>edit</b> existing entries
|
18
|
+
* <b>delete</b> existing entries
|
19
|
+
* <b>list</b> existing entries, and specify a time range
|
20
|
+
* <b>produce</b> simple reports
|
21
|
+
|
22
|
+
== SYNOPSIS:
|
23
|
+
|
24
|
+
<i>Timesheet</i> is a script for keeping track of time spent on various projects.
|
25
|
+
|
26
|
+
=== Usage:
|
27
|
+
|
28
|
+
timesheet [OPTIONS] COMMAND [ARGS]
|
29
|
+
|
30
|
+
COMMAND is any of the following:
|
31
|
+
add
|
32
|
+
edit
|
33
|
+
delete
|
34
|
+
list
|
35
|
+
report
|
36
|
+
|
37
|
+
OPTIONS are:
|
38
|
+
<tt>--debug, -d</tt>:: Show debugging information while processing
|
39
|
+
<tt>--version, -v</tt>:: Print version and exit
|
40
|
+
<tt>--help, -h</tt>:: Show this message
|
41
|
+
|
42
|
+
=== For help run:
|
43
|
+
|
44
|
+
timesheet COMMAND --help
|
45
|
+
|
46
|
+
for more information on a specific command.
|
47
|
+
|
48
|
+
=== Your Data
|
49
|
+
|
50
|
+
your <i>timesheet</i> data will be stored in a hidden directory under your user account. <i>Timesheet</i> figures out
|
51
|
+
where this is by referencing the "HOME" environment variable. The default location is therefore:
|
52
|
+
<tt>/Users/someuser/.timesheet/store.yaml</tt>
|
53
|
+
|
54
|
+
You may override this location to any specific location you want by setting the "TIMESHEET_DATA_FILE" environment variable.
|
55
|
+
This should be the full path to where you want the data stored. Including filename and extension.
|
56
|
+
You only need to set this if you are unsatisfied with the default location.
|
57
|
+
|
58
|
+
=== the .idea folder
|
59
|
+
|
60
|
+
This project was created using RubyMine 2.0, and that IDE stores its state in the .idea folder.
|
61
|
+
If you aren't using RubyMine, you can delete it.
|
62
|
+
|
63
|
+
== REQUIREMENTS:
|
64
|
+
|
65
|
+
* FIX (list of requirements)
|
66
|
+
|
67
|
+
== INSTALL:
|
68
|
+
|
69
|
+
sudo gem install timesheet
|
70
|
+
|
71
|
+
== To Do
|
72
|
+
|
73
|
+
- Add indicator in reports for start or end times which had to be trimmed
|
74
|
+
- Add indicator in reports that exclude comments, that comments are available, when they are, i.e. after a summary,
|
75
|
+
you might want to know that you should do a detail, or byday report
|
76
|
+
- Add option to produce pdf file, for this to work, I need to ensure that all the reports are using <b>ruport</b>.
|
77
|
+
- If comments get too long, we may want to exclude them from the detail report
|
78
|
+
- Add a new command to inspect a single entry. Just dump all the fields for a given record number
|
79
|
+
- Might want to track comments separately and/or make an entries comments an array rather than a single comment.
|
80
|
+
|
81
|
+
== LICENSE:
|
82
|
+
|
83
|
+
(The MIT License)
|
84
|
+
|
85
|
+
Copyright (c) 2009 John F. Schank III
|
86
|
+
|
87
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
88
|
+
a copy of this software and associated documentation files (the
|
89
|
+
'Software'), to deal in the Software without restriction, including
|
90
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
91
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
92
|
+
permit persons to whom the Software is furnished to do so, subject to
|
93
|
+
the following conditions:
|
94
|
+
|
95
|
+
The above copyright notice and this permission notice shall be
|
96
|
+
included in all copies or substantial portions of the Software.
|
97
|
+
|
98
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
99
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
100
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
101
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
102
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
103
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
104
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'hoe', '>= 2.1.0'
|
3
|
+
require 'hoe'
|
4
|
+
require 'fileutils'
|
5
|
+
require './lib/timesheet'
|
6
|
+
|
7
|
+
Hoe::plugin :newgem
|
8
|
+
Hoe::plugin :gemcutter
|
9
|
+
# Hoe.plugin :website
|
10
|
+
# Hoe.plugin :cucumberfeatures
|
11
|
+
|
12
|
+
# Generate all the Rake tasks
|
13
|
+
# Run 'rake -T' to see list of generated tasks (from gem root directory)
|
14
|
+
$hoe = Hoe.spec 'timesheet' do
|
15
|
+
self.developer 'John F. Schank III', 'jschank@mac.com'
|
16
|
+
self.rubyforge_name = self.name # TODO this is default value
|
17
|
+
self.extra_deps = [['activesupport', '>= 0'], ['archive-tar-minitar', '>= 0'], ['chronic', '>= 0'], ['color', '>= 0'], ['fastercsv', '>= 0'], ['hoe', '>= 0'], ['json_pure', '>= 0'], ['newgem', '>= 0'], ['pdf-writer', '>= 0'], ['rake', '>= 0'], ['RedCloth', '>= 0'], ['rich_units', '>= 0'], ['rspec', '>= 0'], ['rubigen', '>= 0'], ['rubyforge', '>= 0'], ['ruport', '>= 0'], ['syntax', '>= 0'], ['transaction-simple', '>= 0']]
|
18
|
+
# self.extra_deps = [['activesupport','>= 2.0.2']]
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'newgem/tasks'
|
23
|
+
Dir['tasks/**/*.rake'].each { |t| load t }
|
24
|
+
|
25
|
+
# TODO - want other tests/tasks run by default? Add them to the list
|
26
|
+
# remove_task :default
|
27
|
+
# task :default => [:spec, :features]
|
28
|
+
|
29
|
+
|
data/bin/timesheet
ADDED
data/lib/timesheet.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'fileutils'
|
6
|
+
require 'rich_units'
|
7
|
+
require 'pathname'
|
8
|
+
require 'yaml'
|
9
|
+
require 'yaml/store'
|
10
|
+
require 'timesheet/range_extensions'
|
11
|
+
require 'timesheet/time_entry'
|
12
|
+
require 'timesheet/time_log'
|
13
|
+
require 'timesheet/time_report'
|
14
|
+
require 'timesheet/trollop'
|
15
|
+
require 'timesheet/timesheet_parser'
|
16
|
+
|
17
|
+
class Timesheet
|
18
|
+
|
19
|
+
VERSION = '0.2.0'
|
20
|
+
|
21
|
+
def self.run(params)
|
22
|
+
command_hash = {}
|
23
|
+
command_hash = TimesheetParser.parse(params)
|
24
|
+
|
25
|
+
raise "Cannot determine location for data. Try timesheet --help for details." unless (ENV["TIMESHEET_DATA_FILE"] || ENV["HOME"])
|
26
|
+
|
27
|
+
data_file ||= ENV["TIMESHEET_DATA_FILE"]
|
28
|
+
data_file ||= (ENV["HOME"] + "/.timesheet/store.yaml")
|
29
|
+
|
30
|
+
FileUtils.makedirs(File.dirname(data_file))
|
31
|
+
|
32
|
+
store = YAML::Store.new(data_file)
|
33
|
+
tl = TimeLog.new(store)
|
34
|
+
ts = Timesheet.new(tl)
|
35
|
+
ts.process(command_hash)
|
36
|
+
|
37
|
+
rescue Exception => e
|
38
|
+
raise if command_hash[:debug]
|
39
|
+
puts e.message
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
def initialize(timelog)
|
44
|
+
@timelog = timelog
|
45
|
+
end
|
46
|
+
|
47
|
+
def process(command_opts)
|
48
|
+
command = command_opts[:command]
|
49
|
+
case command
|
50
|
+
when :add : process_add_command(command_opts)
|
51
|
+
when :edit : process_edit_command(command_opts)
|
52
|
+
when :delete : process_delete_command(command_opts)
|
53
|
+
when :report : process_report_command(command_opts)
|
54
|
+
else raise "Unknown command #{command}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def process_add_command(command_opts)
|
59
|
+
te = TimeEntry.new(command_opts[:project], command_opts[:start], command_opts[:end], command_opts[:comment])
|
60
|
+
@timelog.add(te)
|
61
|
+
end
|
62
|
+
|
63
|
+
def process_edit_command(command_opts)
|
64
|
+
record_number = command_opts.delete(:record_number)
|
65
|
+
@timelog.update(record_number, command_opts)
|
66
|
+
end
|
67
|
+
|
68
|
+
def process_delete_command(command_opts)
|
69
|
+
record_number = command_opts.delete(:record_number)
|
70
|
+
@timelog.delete(record_number)
|
71
|
+
end
|
72
|
+
|
73
|
+
def process_report_command(command_opts)
|
74
|
+
start_time = command_opts[:start]
|
75
|
+
end_time = command_opts[:end]
|
76
|
+
entries = @timelog.extract_entries(start_time, end_time)
|
77
|
+
time_report = TimeReport.new(entries)
|
78
|
+
time_report.report(command_opts)
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Thanks to: http://opensoul.org/2007/2/13/ranges-include-or-overlap-with-ranges
|
2
|
+
class Range
|
3
|
+
|
4
|
+
|
5
|
+
def overlap?(range)
|
6
|
+
self.include?(range.first) || range.include?(self.first)
|
7
|
+
end
|
8
|
+
|
9
|
+
alias_method :include_without_range?, :include?
|
10
|
+
|
11
|
+
def include_with_range?(value)
|
12
|
+
if value.is_a?(Range)
|
13
|
+
last = value.exclude_end? ? value.last - 1 : value.last
|
14
|
+
self.include?(value.first) && self.include?(last)
|
15
|
+
else
|
16
|
+
include_without_range?(value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
alias_method :include?, :include_with_range?
|
21
|
+
|
22
|
+
# alias_method_chain :include?, :range
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class TimeEntry
|
2
|
+
|
3
|
+
include Comparable
|
4
|
+
|
5
|
+
def initialize(project, start_time, end_time, comment = nil)
|
6
|
+
|
7
|
+
raise ArgumentError.new("Start time must come before end time") if start_time > end_time
|
8
|
+
|
9
|
+
@project = project
|
10
|
+
@start_time = start_time
|
11
|
+
@end_time = end_time
|
12
|
+
@comment = comment
|
13
|
+
@record_number = nil
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_accessor :project
|
18
|
+
attr_reader :start_time
|
19
|
+
attr_reader :end_time
|
20
|
+
attr_accessor :comment
|
21
|
+
attr_accessor :record_number
|
22
|
+
|
23
|
+
def conflict?(other_entry)
|
24
|
+
to_range.overlap? other_entry.to_range
|
25
|
+
end
|
26
|
+
|
27
|
+
def start_time=(new_start_time)
|
28
|
+
raise ArgumentError.new("Start time must come before end time") if new_start_time > @end_time
|
29
|
+
@start_time = new_start_time
|
30
|
+
end
|
31
|
+
|
32
|
+
def end_time=(new_end_time)
|
33
|
+
raise ArgumentError.new("End time must come after start time") if @start_time > new_end_time
|
34
|
+
@end_time = new_end_time
|
35
|
+
end
|
36
|
+
|
37
|
+
def duration
|
38
|
+
return 0 if @start_time == nil || @end_time == nil
|
39
|
+
RichUnits::Duration.new(@end_time - @start_time)
|
40
|
+
end
|
41
|
+
|
42
|
+
def <=>(other_time_entry)
|
43
|
+
self.start_time <=> other_time_entry.start_time
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_range
|
47
|
+
Range.new(@start_time, @end_time, true)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
class TimeLog
|
2
|
+
|
3
|
+
def initialize(store)
|
4
|
+
@store = store
|
5
|
+
@store.transaction do
|
6
|
+
@store[:entries] ||= []
|
7
|
+
@store[:entry_record_number] ||= 0
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def count
|
12
|
+
@store.transaction do
|
13
|
+
@store[:entries].count
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def add(entry)
|
18
|
+
@store.transaction do
|
19
|
+
raise ArgumentError.new("Cannot add an entry which conflicts with existing entries.") if @store[:entries].any? { |e| e.conflict? entry }
|
20
|
+
@store[:entry_record_number] += 1
|
21
|
+
entry.record_number = @store[:entry_record_number]
|
22
|
+
@store[:entries] << entry
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def find(record_number)
|
27
|
+
@store.transaction do
|
28
|
+
found_entries = @store[:entries].select{|e| e.record_number == record_number}
|
29
|
+
raise "Record number #{record_number} is not unique in the database." if found_entries.count > 1
|
30
|
+
raise ArgumentError.new("Cannot find an entry with record number #{record_number} to edit") if found_entries.count == 0
|
31
|
+
|
32
|
+
found_entries.first
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def extract_entries(start_time, end_time)
|
37
|
+
covered_entries = []
|
38
|
+
@store.transaction do
|
39
|
+
span_to_cover = Range.new(start_time, end_time, true)
|
40
|
+
covered_entries = @store[:entries].select{|e| span_to_cover.overlap? e.to_range }
|
41
|
+
end
|
42
|
+
covered_entries
|
43
|
+
end
|
44
|
+
|
45
|
+
def update(record_number, properties)
|
46
|
+
@store.transaction do
|
47
|
+
found_entries = @store[:entries].select{|e| e.record_number == record_number}
|
48
|
+
raise "Record number #{record_number} is not unique in the database." if found_entries.count > 1
|
49
|
+
raise ArgumentError.new("Cannot find an entry with record number #{record_number} to edit") if found_entries.count == 0
|
50
|
+
|
51
|
+
entry = found_entries.first
|
52
|
+
entry.project = properties[:project] if properties[:project]
|
53
|
+
entry.start_time = properties[:start] if properties[:start]
|
54
|
+
entry.end_time = properties[:end] if properties[:end]
|
55
|
+
entry.comment = properties[:comment] if properties[:comment]
|
56
|
+
raise ArgumentError.new("The new times for this entry conflict with existing entries.") if @store[:entries].any? { |e| e != entry && e.conflict?(entry) }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def delete(record_number)
|
61
|
+
@store.transaction do
|
62
|
+
found_entries = @store[:entries].select{|e| e.record_number == record_number}
|
63
|
+
raise "Record number #{record_number} is not unique in the database." if found_entries.count > 1
|
64
|
+
raise ArgumentError.new("Cannot find an entry with record number #{record_number} to delete.") if found_entries.count == 0
|
65
|
+
|
66
|
+
@store[:entries].delete(found_entries.first)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'ruport'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
class TimeReport
|
5
|
+
|
6
|
+
def initialize(entries)
|
7
|
+
@entries = entries
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_accessor :entries
|
11
|
+
|
12
|
+
def trim(start_time, end_time)
|
13
|
+
@entries.map! do |e|
|
14
|
+
e.start_time = start_time if e.to_range.include?(start_time)
|
15
|
+
e.end_time = end_time if e.to_range.include?(end_time)
|
16
|
+
e
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def report(command_options, outstream = STDOUT)
|
21
|
+
trim(command_options[:start], command_options[:end])
|
22
|
+
|
23
|
+
detail_report(outstream) if command_options[:detail]
|
24
|
+
summary_report(outstream) if command_options[:summary]
|
25
|
+
byday_report(outstream) if command_options[:byday]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def detail_report(outstream)
|
31
|
+
table = Ruport::Data::Table({ :column_names => ["Id", "Project", "Start - Stop", "Hours", "Comment"]}) do |table|
|
32
|
+
@entries.sort.each do |entry|
|
33
|
+
row = []
|
34
|
+
|
35
|
+
# format record number
|
36
|
+
row << sprintf("%5d", entry.record_number)
|
37
|
+
|
38
|
+
# format project
|
39
|
+
row << entry.project
|
40
|
+
|
41
|
+
# format start - stop
|
42
|
+
str = ""
|
43
|
+
str << entry.start_time.strftime("%m/%d/%Y at %I:%M %p")
|
44
|
+
str << " to "
|
45
|
+
str << entry.end_time.strftime("%m/%d/%Y at ") if ( (entry.start_time.year != entry.end_time.year) && (entry.start_time.yday != entry.end_time.yday))
|
46
|
+
str << entry.end_time.strftime("%I:%M %p")
|
47
|
+
row << str
|
48
|
+
|
49
|
+
# format duration
|
50
|
+
str = ""
|
51
|
+
str << entry.duration.strftime("%d Days ") if entry.duration.days > 0
|
52
|
+
str << entry.duration.strftime("%hh %mm")
|
53
|
+
row << str
|
54
|
+
|
55
|
+
# format comment
|
56
|
+
row << entry.comment
|
57
|
+
|
58
|
+
table << row
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
outstream.puts table.as(:text, {:ignore_table_width => true})
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
def summary_report(outstream)
|
67
|
+
summary = Hash.new(0)
|
68
|
+
|
69
|
+
total = @entries.inject(0) do |sum, entry|
|
70
|
+
summary[entry.project] += entry.duration.to_i
|
71
|
+
sum += entry.duration.to_i
|
72
|
+
end
|
73
|
+
|
74
|
+
width = summary.keys.max { |a, b| a.length <=> b.length }.length
|
75
|
+
summary.sort.each do |key, value|
|
76
|
+
duration = RichUnits::Duration.new(value)
|
77
|
+
duration.reset_segments(:hours, :minutes)
|
78
|
+
outstream.puts "#{key.rjust(width)} #{duration.strftime("%hh").rjust(4)}#{duration.strftime("%mm").rjust(4)}"
|
79
|
+
end
|
80
|
+
|
81
|
+
total_duration = RichUnits::Duration.new(total)
|
82
|
+
total_duration.reset_segments(:hours, :minutes)
|
83
|
+
outstream.puts "-" * (width + 4 + 4 + 1)
|
84
|
+
outstream.puts "#{"Total".rjust(width)} #{total_duration.strftime("%hh").rjust(4)}#{total_duration.strftime("%mm").rjust(4)}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def byday_report(outstream)
|
88
|
+
|
89
|
+
hash = {}
|
90
|
+
comments = {}
|
91
|
+
@entries.each do |entry|
|
92
|
+
start_date = Date.new(entry.start_time.year, entry.start_time.month, entry.start_time.day)
|
93
|
+
hash[start_date] ||= Hash.new(0)
|
94
|
+
hash[start_date][entry.project] += entry.duration.to_i
|
95
|
+
|
96
|
+
comments[start_date] ||= Hash.new { |h, k| h[k] = [] }
|
97
|
+
comments[start_date][entry.project] << entry.comment
|
98
|
+
end
|
99
|
+
|
100
|
+
hash.sort.each do |entry_date, project_hash|
|
101
|
+
outstream.puts entry_date.strftime("%D")
|
102
|
+
project_hash.sort.each do |project, duration|
|
103
|
+
project_time = RichUnits::Duration.new(duration)
|
104
|
+
project_time.reset_segments(:hours, :minutes)
|
105
|
+
outstream.puts "\t#{project} #{project_time.strftime("%hh %mm")}"
|
106
|
+
comments[entry_date][project].each do |comment|
|
107
|
+
outstream.puts "\t - #{comment}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|