timetrap 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.txt ADDED
@@ -0,0 +1,23 @@
1
+ http://www.opensource.org/licenses/mit-license.php
2
+
3
+ The MIT License
4
+
5
+ Copyright (c) 2009 Sam Goldstein
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,183 @@
1
+ Timetrap
2
+ ========
3
+
4
+ Timetrap is a utility which provides an easy to use command line interface for
5
+ tracking what you spend your time on. It is a ruby port of Trevor Caira's
6
+ Timebook, a small python utility. It contains several enhancement over
7
+ Timebook notably the ability to parse natural language time strings. This
8
+ makes commands such as ``t in --at "30 minutes ago"`` possible. Timetrap is
9
+ also able to export entries to several formats (e.g. ical, csv) and is designed
10
+ to be easily extended to support additional export formats.
11
+ Timetrap maintains its state in a sqlite3 database.
12
+
13
+ To install:
14
+
15
+ $ gem sources -a http://gems.github.com (you only have to do this once)
16
+ $ sudo gem install samg-timetrap
17
+
18
+ This will place a ``t`` executable in your path.
19
+
20
+ Original Timebook available at:
21
+ http://bitbucket.org/trevor/timebook/src/
22
+
23
+
24
+ Concepts
25
+ --------
26
+
27
+ Timetrap maintains a list of *timesheets* -- distinct lists of timed *periods*.
28
+ Each period has a start and end time, with the exception of the most recent
29
+ period, which may have no end time set. This indicates that this period is
30
+ still running. Timesheets containing such periods are considered *active*. It
31
+ is possible to have multiple timesheets active simultaneously, though a single
32
+ time sheet may only have one period running at once.
33
+
34
+ Interactions with timetrap are performed through the ``t`` command on the
35
+ command line. ``t`` is followed by one of timetrap's subcommands. Often used
36
+ subcommands include ``in``, ``out``, ``switch``, ``now``, ``list`` and
37
+ ``display``. Commands may be abbreviated as long as they are unambiguous: thus
38
+ ``t switch foo`` and ``t s foo`` are identical. With the default command set,
39
+ no two commands share the first same letter, thus it is only necessary to type
40
+ the first letter of a command. Likewise, commands which display timesheets
41
+ accept abbreviated timesheet names. ``t display f`` is thus equivalent to ``t
42
+ display foo`` if ``foo`` is the only timesheet which begins with "f". Note that
43
+ this does not apply to ``t switch``, since this command also creates
44
+ timesheets. (Using the earlier example, if ``t switch f`` is entered, it would
45
+ thus be ambiguous whether a new timesheet ``f`` or switching to the existing
46
+ timesheet ``foo`` was desired).
47
+
48
+ Usage
49
+ -----
50
+
51
+ The basic usage is as follows:
52
+
53
+ $ t switch writing
54
+ $ t in document timetrap --at "10 minutes ago"
55
+ $ t out
56
+
57
+ The first command, ``t switch writing``, switches to the timesheet "writing"
58
+ (or creates it if it does not exist). ``t in document timetrap --at "10 minutes
59
+ ago"`` creates a new period in the current timesheet, and annotates it with the
60
+ description "document timetrap". The optional ``--at`` flag can be passed to start
61
+ the entry at a time other than the present. The ``--at`` flag is able to parse
62
+ natural language times (via Chronic: http://chronic.rubyforge.org/) and will
63
+ understand 'friday 13:00', 'mon 2:35', '4pm', etc. (also true of the ``edit``
64
+ command's ``--start`` and ``--end`` flags.) Note that this command would be in
65
+ error if the ``writing`` timesheet was already active. Finally, ``t out``
66
+ records the current time as the end time for the most recent period in the
67
+ ``writing`` timesheet.
68
+
69
+ To display the current timesheet, invoke the ``t display`` command::
70
+
71
+ $ t display
72
+ Timesheet: timetrap
73
+ Day Start End Duration Notes
74
+ Mon Apr 13, 2009 15:46:51 - 17:03:50 1:16:59 improved display functionality
75
+ 17:25:59 - 17:26:02 0:00:03
76
+ 18:38:07 - 18:38:52 0:00:45 working on list
77
+ 22:37:38 - 23:38:43 1:01:05 work on kill
78
+ 2:18:52
79
+ Tue Apr 14, 2009 00:41:16 - 01:40:19 0:59:03 gem packaging
80
+ 10:20:00 - 10:48:10 0:28:10 enhance edit
81
+ 1:27:13
82
+ ---------------------------------------------------------
83
+ Total 3:46:05
84
+
85
+ Each period in the timesheet is listed on a row. If the timesheet is active,
86
+ the final period in the timesheet will have no end time. After each day, the
87
+ total time tracked in the timesheet for that day is listed. Note that this is
88
+ computed by summing the durations of the periods beginning in the day. In the
89
+ last row, the total time tracked in the timesheet is shown.
90
+
91
+ Commands
92
+ --------
93
+ **archives**
94
+ Archives the selected entries (by moving them to a sheet called ``_[SHEET]``)
95
+ These entries can be seen by running ``t display _[SHEET]``.
96
+ usage: ``t archive [--start DATE] [--end DATE] [SHEET]``
97
+
98
+ **backend**
99
+ Run an interactive database session on the timetrap database. Requires the
100
+ sqlite3 command.
101
+
102
+ usage: ``t backend``
103
+
104
+ **display**
105
+ Display a given timesheet. If no timesheet is specified, show the current
106
+ timesheet. If ``all`` is passed as SHEET display all timesheets. Accepts
107
+ an optional ``--ids`` flag which will include the entries' ids in the output.
108
+ This is useful when editing an non running entry with ``edit``.
109
+
110
+ Display is designed to support a variety of export formats that can be
111
+ specified by passing the ``--format`` flag. This currently defaults to
112
+ text. iCal and csv output are also supported.
113
+
114
+ Display also allows the use of a ``--round`` or ``-r`` flag which will round
115
+ all times to 15 minute increments. See global options below.
116
+
117
+ usage: ``t display [--ids] [--round] [--start DATE] [--end DATE] [--format FMT] [SHEET | all]``
118
+
119
+ **edit**
120
+ Inserts a note associated with the an entry in the timesheet, or edits the
121
+ start or end times. Defaults to the current time although an ``--id`` flag can
122
+ be passed with the entry's id (see display.)
123
+
124
+ usage: ``t edit [--id ID] [--start TIME] [--end TIME] [NOTES]``
125
+
126
+ **format**
127
+ Deprecated
128
+ Alias for display
129
+
130
+ **in**
131
+ Start the timer for the current timesheet. Must be called before out. Notes
132
+ may be specified for this period. This is exactly equivalent to
133
+ ``t in; t edit NOTES``. Accepts an optional --at flag.
134
+
135
+ usage: ``t in [--at TIME] [NOTES]``
136
+
137
+ **kill**
138
+ Delete a timesheet or an entry. Entry's are referenced using an ``--id``
139
+ flag (see display). Sheets are referenced by name.
140
+
141
+ usage: ``t kill [--id ID] [TIMESHEET]``
142
+
143
+ **list**
144
+ List the available timesheets.
145
+
146
+ usage: ``t list``
147
+
148
+ **now**
149
+ Print the current sheet, whether it's active, and if so, how long it has been
150
+ active and what notes are associated with the current period.
151
+
152
+ usage: ``t now``
153
+
154
+ **out**
155
+ Stop the timer for the current timesheet. Must be called after in. Accepts an
156
+ optional --at flag.
157
+
158
+ usage: ``t out [--at TIME]``
159
+
160
+ **running**
161
+ Print all active sheets and any messages associated with them.
162
+
163
+ usage: ``t running``
164
+
165
+ **switch**
166
+ Switch to a new timesheet. this causes all future operation (except switch)
167
+ to operate on that timesheet. The default timesheet is called "default".
168
+
169
+ usage: ``t switch TIMESHEET``
170
+
171
+ **week**
172
+ Shortcut for display with start date set to monday of this week
173
+
174
+ usage: ``t week [--ids] [--end DATE] [--format FMT] [SHEET | all]``
175
+
176
+ Global Options
177
+ --------
178
+
179
+ **rounding**
180
+ passing a ``--round`` or ``-r`` flag to any command will round entry start
181
+ and end times to the closest 15 minute increment. This flag only affects the
182
+ display commands (e.g. display, list, week, etc.) and is non-destructive.
183
+ The actual start and end time stored by Timetrap are unaffected.
data/Rakefile ADDED
@@ -0,0 +1,60 @@
1
+ require 'spec/rake/spectask'
2
+ require 'rake/rdoctask'
3
+ require 'rake/gempackagetask'
4
+
5
+ task :default => :spec
6
+
7
+ desc "Run all specs in spec directory"
8
+ Spec::Rake::SpecTask.new(:spec) do |t|
9
+ t.spec_files = FileList['spec/**/*_spec.rb']
10
+ end
11
+
12
+ Rake::RDocTask.new do |rd|
13
+ rd.main = "README"
14
+ rd.rdoc_dir = 'doc'
15
+ rd.rdoc_files.include("README", "**/*.rb")
16
+ end
17
+
18
+ begin
19
+ require 'jeweler'
20
+ Jeweler::Tasks.new do |s|
21
+ s.name = %q{timetrap}
22
+ s.version = "0.1.2"
23
+
24
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
25
+ s.authors = ["Sam Goldstein"]
26
+ s.date = %q{2009-04-14}
27
+ s.description = %q{Command line time tracker}
28
+ s.email = %q{sgrock@gmail.com}
29
+ s.has_rdoc = true
30
+ s.homepage = "http://github.com/samg/timetrap/tree/master"
31
+ s.rdoc_options = ["--inline-source", "--charset=UTF-8"]
32
+ s.require_paths = ["lib"]
33
+ s.bindir = "bin"
34
+ s.executables = ['t']
35
+ s.summary = %q{Command line time tracker}
36
+ s.add_dependency("sequel", ">= 2.12.0")
37
+ s.add_dependency("chronic", ">= 0.2.3")
38
+ s.add_dependency("getopt-declare", ">= 1.28")
39
+ s.add_dependency("icalendar", ">= 1.1.0")
40
+
41
+ if s.respond_to? :specification_version then
42
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
43
+ s.specification_version = 2
44
+
45
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
46
+ s.add_runtime_dependency(%q<mime-types>, [">= 1.15"])
47
+ s.add_runtime_dependency(%q<diff-lcs>, [">= 1.1.2"])
48
+ else
49
+ s.add_dependency(%q<mime-types>, [">= 1.15"])
50
+ s.add_dependency(%q<diff-lcs>, [">= 1.1.2"])
51
+ end
52
+ else
53
+ s.add_dependency(%q<mime-types>, [">= 1.15"])
54
+ s.add_dependency(%q<diff-lcs>, [">= 1.1.2"])
55
+ end
56
+ end
57
+ rescue LoadError
58
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
59
+ end
60
+
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 1
3
+ :major: 1
4
+ :minor: 1
data/bin/dev_t ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ # Executable with absolute path to lib for hacking and development
3
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'timetrap')
4
+ Timetrap::CLI.invoke
data/bin/t ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ begin
3
+ require 'timetrap'
4
+ rescue LoadError
5
+ if File.symlink? __FILE__
6
+ require File.dirname(File.readlink(__FILE__)) + '/../lib/timetrap'
7
+ else
8
+ require File.dirname(__FILE__) + '/../lib/timetrap'
9
+ end
10
+ end
11
+ Timetrap::CLI.invoke
@@ -0,0 +1,227 @@
1
+ module Timetrap
2
+ module CLI
3
+ extend Helpers
4
+ attr_accessor :args
5
+ extend self
6
+
7
+ USAGE = <<-EOF
8
+
9
+ Timetrap - Simple Time Tracking
10
+
11
+ Usage: #{File.basename $0} COMMAND [OPTIONS] [ARGS...]
12
+
13
+ where COMMAND is one of:
14
+ * archive - move entries to a hidden sheet (by default named '_[SHEET]') so
15
+ they're out of the way.
16
+ usage: t archive [--start DATE] [--end DATE] [SHEET]
17
+ -s, --start <date:qs> Include entries that start on this date or later
18
+ -e, --end <date:qs> Include entries that start on this date or earlier
19
+ * backend - open an sqlite shell to the database
20
+ usage: t backend
21
+ * display - display the current timesheet or a specific. Pass `all' as
22
+ SHEET to display all sheets.
23
+ usage: t display [--ids] [--start DATE] [--end DATE] [--format FMT] [SHEET | all]
24
+ -v, --ids Print database ids (for use with edit)
25
+ -s, --start <date:qs> Include entries that start on this date or later
26
+ -e, --end <date:qs> Include entries that start on this date or earlier
27
+ -f, --format <format> The output format. Currently supports ical, csv, and
28
+ text (default).
29
+ * edit - alter an entry's note, start, or end time. Defaults to the active entry
30
+ usage: t edit [--id ID] [--start TIME] [--end TIME] [NOTES]
31
+ -i, --id <id:i> Alter entry with id <id> instead of the running entry
32
+ -s, --start <time:qs> Change the start time to <time>
33
+ -e, --end <time:qs> Change the end time to <time>
34
+ * format - deprecated: alias for display
35
+ * in - start the timer for the current timesheet
36
+ usage: t in [--at TIME] [NOTES]
37
+ -a, --at <time:qs> Use this time instead of now
38
+ * kill - delete a timesheet
39
+ usage: t kill [--id ID] [TIMESHEET]
40
+ -i, --id <id:i> Alter entry with id <id> instead of the running entry
41
+ * list - show the available timesheets
42
+ usage: t list
43
+ * now - show the status of the current timesheet
44
+ usage: t now
45
+ * out - stop the timer for the current timesheet
46
+ usage: t out [--at TIME]
47
+ -a, --at <time:qs> Use this time instead of now
48
+ * running - show all running timesheets
49
+ usage: t running
50
+ * switch - switch to a new timesheet
51
+ usage: t switch TIMESHEET
52
+ * week - shortcut for display with start date set to monday of this week
53
+ usage: t week [--ids] [--end DATE] [--format FMT] [SHEET | all]
54
+
55
+ OTHER OPTIONS
56
+ -h, --help Display this help
57
+ -r, --round Round output to 15 minute start and end times.
58
+ EOF
59
+
60
+ def parse arguments
61
+ args.parse arguments
62
+ end
63
+
64
+ def invoke
65
+ args['-h'] ? say(USAGE) : invoke_command_if_valid
66
+ end
67
+
68
+ def commands
69
+ Timetrap::CLI::USAGE.scan(/\* \w+/).map{|s| s.gsub(/\* /, '')}
70
+ end
71
+
72
+ def say *something
73
+ puts *something
74
+ end
75
+
76
+ def invoke_command_if_valid
77
+ command = args.unused.shift
78
+ set_global_options
79
+ case (valid = commands.select{|name| name =~ %r|^#{command}|}).size
80
+ when 0 then say "Invalid command: #{command}"
81
+ when 1 then send valid[0]
82
+ else
83
+ say "Ambiguous command: #{command}" if command
84
+ say(USAGE)
85
+ end
86
+ end
87
+
88
+ # currently just sets whether output should be rounded to 15 min intervals
89
+ def set_global_options
90
+ Timetrap::Entry.round = true if args['-r']
91
+ end
92
+
93
+ def archive
94
+ ee = selected_entries
95
+ out = "Archive #{ee.count} entries? "
96
+ print out
97
+ if $stdin.gets =~ /\Aye?s?\Z/i
98
+ ee.all.each do |e|
99
+ next unless e.end
100
+ e.update :sheet => "_#{e.sheet}"
101
+ end
102
+ else
103
+ say "archive aborted!"
104
+ end
105
+ end
106
+
107
+ def edit
108
+ entry = args['-i'] ? Entry[args['-i']] : Timetrap.active_entry
109
+ say "can't find entry" && return unless entry
110
+ entry.update :start => args['-s'] if args['-s'] =~ /.+/
111
+ entry.update :end => args['-e'] if args['-e'] =~ /.+/
112
+ entry.update :note => unused_args if unused_args =~ /.+/
113
+ end
114
+
115
+ def backend
116
+ exec "sqlite3 #{DB_NAME}"
117
+ end
118
+
119
+ def in
120
+ Timetrap.start unused_args, args['-a']
121
+ end
122
+
123
+ def out
124
+ Timetrap.stop args['-a']
125
+ end
126
+
127
+ def kill
128
+ if e = Entry[args['-i']]
129
+ out = "are you sure you want to delete entry #{e.id}? "
130
+ out << "(#{e.note}) " if e.note.to_s =~ /.+/
131
+ print out
132
+ if $stdin.gets =~ /\Aye?s?\Z/i
133
+ e.destroy
134
+ say "it's dead"
135
+ else
136
+ say "will not kill"
137
+ end
138
+ elsif (sheets = Entry.map{|e| e.sheet }.uniq).include?(sheet = unused_args)
139
+ victims = Entry.filter(:sheet => sheet).count
140
+ print "are you sure you want to delete #{victims} entries on sheet #{sheet.inspect}? "
141
+ if $stdin.gets =~ /\Aye?s?\Z/i
142
+ Timetrap.kill_sheet sheet
143
+ say "killed #{victims} entries"
144
+ else
145
+ say "will not kill"
146
+ end
147
+ else
148
+ victim = args['-i'] ? args['-i'].to_s.inspect : sheet.inspect
149
+ say "can't find #{victim} to kill", 'sheets:', *sheets
150
+ end
151
+ end
152
+
153
+ def display
154
+ begin
155
+ fmt_klass = if args['-f']
156
+ Timetrap::Formatters.const_get("#{args['-f'].classify}")
157
+ else
158
+ Timetrap::Formatters::Text
159
+ end
160
+ rescue
161
+ say "Invalid format specified `#{args['-f']}'"
162
+ return
163
+ end
164
+ say Timetrap.format(fmt_klass, selected_entries.order(:start).all)
165
+ end
166
+ alias_method :format, :display
167
+
168
+
169
+ def switch
170
+ sheet = unused_args
171
+ if not sheet =~ /.+/ then say "No sheet specified"; return end
172
+ say "Switching to sheet " + Timetrap.switch(sheet)
173
+ end
174
+
175
+ def list
176
+ sheets = Entry.sheets.map do |sheet|
177
+ sheet_atts = {:total => 0, :running => 0, :today => 0}
178
+ Timetrap::Entry.filter(:sheet => sheet).inject(sheet_atts) do |m, e|
179
+ e_end = e.end_or_now
180
+ m[:name] ||= sheet
181
+ m[:total] += (e_end.to_i - e.start.to_i)
182
+ m[:running] += (e_end.to_i - e.start.to_i) unless e.end
183
+ m[:today] += (e_end.to_i - e.start.to_i) if same_day?(Time.now, e.start)
184
+ m
185
+ end
186
+ end
187
+ if sheets.empty? then say "No sheets found"; return end
188
+ width = sheets.sort_by{|h|h[:name].length }.last[:name].length + 4
189
+ say " %-#{width}s%-12s%-12s%s" % ["Timesheet", "Running", "Today", "Total Time"]
190
+ sheets.each do |sheet|
191
+ star = sheet[:name] == Timetrap.current_sheet ? '*' : ' '
192
+ say "#{star}%-#{width}s%-12s%-12s%s" % [
193
+ sheet[:running],
194
+ sheet[:today],
195
+ sheet[:total]
196
+ ].map(&method(:format_seconds)).unshift(sheet[:name])
197
+ end
198
+ end
199
+
200
+ def now
201
+ if Timetrap.running?
202
+ out = "#{Timetrap.current_sheet}: #{format_duration(Timetrap.active_entry.start, Timetrap.active_entry.end_or_now)}".gsub(/ /, ' ')
203
+ out << " (#{Timetrap.active_entry.note})" if Timetrap.active_entry.note =~ /.+/
204
+ say out
205
+ else
206
+ say "#{Timetrap.current_sheet}: not running"
207
+ end
208
+ end
209
+
210
+ def running
211
+ say "Running Timesheets:"
212
+ say Timetrap::Entry.filter(:end => nil).map{|e| " #{e.sheet}: #{e.note}"}.uniq.sort
213
+ end
214
+
215
+ def week
216
+ args['-s'] = Date.today.wday == 1 ? Date.today.to_s : Date.parse(Chronic.parse(%q(last monday)).to_s).to_s
217
+ display
218
+ end
219
+
220
+ private
221
+
222
+ def unused_args
223
+ args.unused.join(' ')
224
+ end
225
+
226
+ end
227
+ end
@@ -0,0 +1,19 @@
1
+ module Timetrap
2
+ module Formatters
3
+ class Csv
4
+ attr_reader :output
5
+
6
+ def initialize entries
7
+ @output = entries.inject("start,end,note\n") do |out, e|
8
+ next(out) unless e.end
9
+ out << %|"#{e.start.strftime(time_format)}","#{e.end.strftime(time_format)}","#{e.note}"\n|
10
+ end
11
+ end
12
+
13
+ private
14
+ def time_format
15
+ "%Y-%m-%d %H:%M:%S"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ require 'icalendar'
2
+ require 'date'
3
+ module Timetrap
4
+ module Formatters
5
+ class Ical
6
+ include Icalendar
7
+ def calendar
8
+ @calendar ||= Calendar.new
9
+ end
10
+
11
+ def output
12
+ calendar.to_ical
13
+ end
14
+
15
+ def initialize entries
16
+ entries.each do |e|
17
+ next unless e.end
18
+ calendar.event do
19
+ dtstart DateTime.parse(e.start.to_s)
20
+ dtend DateTime.parse(e.end.to_s)
21
+ summary e.note
22
+ description e.note
23
+ end
24
+ end
25
+ calendar.publish
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,55 @@
1
+ module Timetrap
2
+ module Formatters
3
+ class Text
4
+ attr_accessor :output
5
+ include Timetrap::Helpers
6
+
7
+ def initialize entries
8
+ self.output = ''
9
+ sheets = entries.inject({}) do |h, e|
10
+ h[e.sheet] ||= []
11
+ h[e.sheet] << e
12
+ h
13
+ end
14
+ (sheet_names = sheets.keys.sort).each do |sheet|
15
+ self.output << "Timesheet: #{sheet}\n"
16
+ id_heading = Timetrap::CLI.args['-v'] ? 'Id' : ' '
17
+ self.output << "#{id_heading} Day Start End Duration Notes\n"
18
+ last_start = nil
19
+ from_current_day = []
20
+ sheets[sheet].each_with_index do |e, i|
21
+ from_current_day << e
22
+ e_end = e.end_or_now
23
+ self.output << "%-4s%16s%11s -%9s%10s %s\n" % [
24
+ (Timetrap::CLI.args['-v'] ? e.id : ''),
25
+ format_date_if_new(e.start, last_start),
26
+ format_time(e.start),
27
+ format_time(e.end),
28
+ format_duration(e.start, e_end),
29
+ e.note
30
+ ]
31
+
32
+ nxt = sheets[sheet].map[i+1]
33
+ if nxt == nil or !same_day?(e.start, nxt.start)
34
+ self.output << "%52s\n" % format_total(from_current_day)
35
+ from_current_day = []
36
+ else
37
+ end
38
+ last_start = e.start
39
+ end
40
+ self.output << <<-OUT
41
+ ---------------------------------------------------------
42
+ OUT
43
+ self.output << " Total%43s\n" % format_total(sheets[sheet])
44
+ self.output << "\n" unless sheet == sheet_names.last
45
+ end
46
+ if sheets.size > 1
47
+ self.output << <<-OUT
48
+ -------------------------------------------------------------
49
+ OUT
50
+ self.output << "Grand Total%41s\n" % format_total(sheets.values.flatten)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,59 @@
1
+ module Timetrap
2
+ module Helpers
3
+
4
+ def selected_entries
5
+ ee = if (sheet = sheet_name_from_string(unused_args)) == 'all'
6
+ Timetrap::Entry.filter('sheet not like ? escape "!"', '!_%')
7
+ elsif sheet =~ /.+/
8
+ Timetrap::Entry.filter('sheet = ?', sheet)
9
+ else
10
+ Timetrap::Entry.filter('sheet = ?', Timetrap.current_sheet)
11
+ end
12
+ ee = ee.filter(:start >= Date.parse(args['-s'])) if args['-s']
13
+ ee = ee.filter(:start <= Date.parse(args['-e']) + 1) if args['-e']
14
+ ee
15
+ end
16
+
17
+ def format_time time
18
+ return '' unless time.respond_to?(:strftime)
19
+ time.strftime('%H:%M:%S')
20
+ end
21
+
22
+ def format_date time
23
+ return '' unless time.respond_to?(:strftime)
24
+ time.strftime('%a %b %d, %Y')
25
+ end
26
+
27
+ def format_date_if_new time, last_time
28
+ return '' unless time.respond_to?(:strftime)
29
+ same_day?(time, last_time) ? '' : format_date(time)
30
+ end
31
+
32
+ def same_day? time, other_time
33
+ format_date(time) == format_date(other_time)
34
+ end
35
+
36
+ def format_duration stime, etime
37
+ return '' unless stime and etime
38
+ secs = etime.to_i - stime.to_i
39
+ format_seconds secs
40
+ end
41
+
42
+ def format_seconds secs
43
+ "%2s:%02d:%02d" % [secs/3600, (secs%3600)/60, secs%60]
44
+ end
45
+
46
+ def format_total entries
47
+ secs = entries.inject(0){|m, e|e_end = e.end_or_now; m += e_end.to_i - e.start.to_i if e_end && e.start;m}
48
+ "%2s:%02d:%02d" % [secs/3600, (secs%3600)/60, secs%60]
49
+ end
50
+
51
+ def sheet_name_from_string string
52
+ return "all" if string =~ /^\W*all\W*$/
53
+ return "" unless string =~ /.+/
54
+ DB[:entries].filter(:sheet.like("#{string}%")).first[:sheet]
55
+ rescue
56
+ ""
57
+ end
58
+ end
59
+ end