be9-punch 0.1.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/README +27 -0
- data/Rakefile +24 -0
- data/VERSION.yml +4 -0
- data/bin/punch +622 -0
- metadata +106 -0
data/README
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
NAME
|
2
|
+
punch
|
3
|
+
|
4
|
+
SYNOPSIS
|
5
|
+
punch (in|out|log|clock|status|list|total|delete|dump|parse) [options]+
|
6
|
+
|
7
|
+
DESCRIPTION
|
8
|
+
i can has time tracking!
|
9
|
+
|
10
|
+
punch is a k.i.s.s. tool for tracking the hours spent on various projects.
|
11
|
+
it supports logging hours under a project name, adding notes about work
|
12
|
+
done during that period, and several very simple reporting tools that
|
13
|
+
operate over a window of time.
|
14
|
+
|
15
|
+
run 'punch help modename' for more info.
|
16
|
+
|
17
|
+
PARAMETERS
|
18
|
+
--help, -h
|
19
|
+
|
20
|
+
EXAMPLES
|
21
|
+
. punch in projectname
|
22
|
+
. punch in projectname 'an hour ago'
|
23
|
+
. punch log projectname 'rewriting your java app in ruby...'
|
24
|
+
. punch out projectname
|
25
|
+
. punch total projectname --after 2007-12-03 --before 2007-12-07
|
26
|
+
. punch total projectname --after 'yesterday morning' --before 'today at noon'
|
27
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'jeweler'
|
5
|
+
|
6
|
+
Jeweler::Tasks.new do |s|
|
7
|
+
s.name = "punch"
|
8
|
+
s.summary = 'Punch timetracker'
|
9
|
+
s.email = 'info@simplyexcited.co.uk'
|
10
|
+
s.homepage = 'http://codeforpeople.com/lib/ruby/punch/'
|
11
|
+
s.description = 'Simple commmand line based timetracker in ruby.'
|
12
|
+
s.authors = ['Ara T. Howard']
|
13
|
+
|
14
|
+
s.files = FileList["[A-Z]*", "bin/**/*"]
|
15
|
+
|
16
|
+
s.add_dependency 'main', '>= 2.6.0'
|
17
|
+
s.add_dependency 'systemu', '>= 1.2.0'
|
18
|
+
s.add_dependency 'orderedhash', '>= 0.0.3'
|
19
|
+
s.add_dependency 'attributes', '>= 5.0.0'
|
20
|
+
s.add_dependency 'chronic'
|
21
|
+
end
|
22
|
+
rescue LoadError
|
23
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
24
|
+
end
|
data/VERSION.yml
ADDED
data/bin/punch
ADDED
@@ -0,0 +1,622 @@
|
|
1
|
+
#! /usr/bin/env ruby
|
2
|
+
|
3
|
+
Main {
|
4
|
+
description <<-txt
|
5
|
+
i can has time tracking!
|
6
|
+
|
7
|
+
punch is a k.i.s.s. tool for tracking the hours spent on various projects.
|
8
|
+
it supports logging hours under a project name, adding notes about work
|
9
|
+
done during that period, and several very simple reporting tools that
|
10
|
+
operate over a window of time.
|
11
|
+
|
12
|
+
run 'punch help modename' for more info.
|
13
|
+
txt
|
14
|
+
|
15
|
+
examples <<-txt
|
16
|
+
. punch in projectname
|
17
|
+
. punch in projectname 'an hour ago'
|
18
|
+
. punch log projectname 'rewriting your java app in ruby...'
|
19
|
+
. punch out projectname
|
20
|
+
. punch total projectname --after 2007-12-03 --before 2007-12-07
|
21
|
+
. punch total projectname --after 'yesterday morning' --before 'today at noon'
|
22
|
+
txt
|
23
|
+
|
24
|
+
|
25
|
+
mode(:in){
|
26
|
+
description 'punch in on a project'
|
27
|
+
|
28
|
+
examples <<-txt
|
29
|
+
. punch in projectname
|
30
|
+
. punch in projectname 2007-01-01T09
|
31
|
+
. punch in projectname now
|
32
|
+
. punch in projectname 'today at around noon'
|
33
|
+
. punch in projectname 2007-01-01T09 --message 'new years day'
|
34
|
+
txt
|
35
|
+
|
36
|
+
mixin :argument_project, :optional_now, :option_db, :option_message, :option_noop
|
37
|
+
|
38
|
+
run{ y db.punch(:in, project, 'now' => now, 'message' => message) }
|
39
|
+
}
|
40
|
+
|
41
|
+
|
42
|
+
mode(:out){
|
43
|
+
description 'punch out of a project or, iff no project given, all open projects'
|
44
|
+
|
45
|
+
examples <<-txt
|
46
|
+
. punch out projectname
|
47
|
+
. punch out projectname 2007-01-01T09
|
48
|
+
. punch out projectname now
|
49
|
+
. punch out projectname 2007-01-01T17 --message 'new years day'
|
50
|
+
. punch out projectname 2007-01-01T05pm -m 'new years day'
|
51
|
+
. punch out projectname an hour ago
|
52
|
+
. punch out
|
53
|
+
txt
|
54
|
+
|
55
|
+
mixin :optional_project, :optional_now, :option_db, :option_message, :option_noop
|
56
|
+
|
57
|
+
run{ y db.punch(:out, project, 'now' => now, 'message' => message) }
|
58
|
+
}
|
59
|
+
|
60
|
+
|
61
|
+
mode(:log){
|
62
|
+
description 'log a message for a project you are currently logged into'
|
63
|
+
|
64
|
+
examples <<-txt
|
65
|
+
. punch log projectname 'xml - welcome to my pain cave!'
|
66
|
+
. punch log projectname should have used yaml
|
67
|
+
txt
|
68
|
+
|
69
|
+
mixin :argument_project, :option_db, :argument_message, :option_noop
|
70
|
+
|
71
|
+
run{ y db.log(project, message) }
|
72
|
+
}
|
73
|
+
|
74
|
+
|
75
|
+
mode(:clock){
|
76
|
+
description 'punch in and out retroactively'
|
77
|
+
|
78
|
+
examples <<-txt
|
79
|
+
. punch clock projectname 2007-01-01T09am 2007-01-01T05pm -m "working on new year's day"
|
80
|
+
txt
|
81
|
+
|
82
|
+
mixin :argument_project, :argument_punch_in, :argument_punch_out, :option_db, :option_message, :option_noop
|
83
|
+
|
84
|
+
run{ y db.clock(project, punch_in, punch_out, 'message' => message) }
|
85
|
+
}
|
86
|
+
|
87
|
+
mode(:status){
|
88
|
+
description 'shows the status of named project, or all projects iff none given'
|
89
|
+
|
90
|
+
examples <<-txt
|
91
|
+
. punch status
|
92
|
+
. punch status projectname
|
93
|
+
txt
|
94
|
+
|
95
|
+
mixin :optional_project, :option_db
|
96
|
+
|
97
|
+
run{ y db.status(project) }
|
98
|
+
}
|
99
|
+
|
100
|
+
mode(:list){
|
101
|
+
description 'list a single/all projects, possibly filtering by time'
|
102
|
+
|
103
|
+
examples <<-txt
|
104
|
+
. punch list projectname
|
105
|
+
. punch list projectname --after 2007-01-01 --before 2007-01-31
|
106
|
+
. punch list
|
107
|
+
. punch list -A 2007-01-01 -B 2007-01-31
|
108
|
+
txt
|
109
|
+
|
110
|
+
mixin :optional_project, :option_after, :option_before, :option_db
|
111
|
+
|
112
|
+
run{ y db.list(project, 'window' => (after .. before)) }
|
113
|
+
}
|
114
|
+
|
115
|
+
mode(:total){
|
116
|
+
description 'total the time for a single/all projects, possibly filtering by time'
|
117
|
+
|
118
|
+
examples <<-txt
|
119
|
+
. punch total projectname
|
120
|
+
. punch total projectname --after 2007-01-01 --before 2007-01-31
|
121
|
+
. punch total
|
122
|
+
. punch total -A 2007-01-01 -B 2007-01-31
|
123
|
+
txt
|
124
|
+
|
125
|
+
mixin :optional_project, :option_after, :option_before, :option_db
|
126
|
+
|
127
|
+
run{ y db.total(project, 'window' => (after .. before)) }
|
128
|
+
}
|
129
|
+
|
130
|
+
mode(:delete){
|
131
|
+
description 'delete all records for a project'
|
132
|
+
|
133
|
+
examples <<-txt
|
134
|
+
. punch delete projectname
|
135
|
+
txt
|
136
|
+
|
137
|
+
mixin :argument_project, :option_db, :option_noop
|
138
|
+
|
139
|
+
run{ y db.delete(project) }
|
140
|
+
}
|
141
|
+
|
142
|
+
mode(:dump){
|
143
|
+
description 'cat the yaml db with lock held'
|
144
|
+
|
145
|
+
examples <<-txt
|
146
|
+
. punch dump
|
147
|
+
. punch dump projectname
|
148
|
+
txt
|
149
|
+
|
150
|
+
mixin :optional_project, :option_db
|
151
|
+
|
152
|
+
run{ y db.dump(project) }
|
153
|
+
}
|
154
|
+
|
155
|
+
mode(:parse){
|
156
|
+
description 'show how a time string will be parsed'
|
157
|
+
|
158
|
+
examples <<-txt
|
159
|
+
. punch parse 'yesterday morning'
|
160
|
+
. punch parse today at noon
|
161
|
+
. punch parse yesterday at 9pm
|
162
|
+
txt
|
163
|
+
|
164
|
+
argument(:spec){
|
165
|
+
arity -2
|
166
|
+
attr
|
167
|
+
}
|
168
|
+
|
169
|
+
run{ y Time.parse(spec.join(' ')) }
|
170
|
+
}
|
171
|
+
|
172
|
+
|
173
|
+
mixin :argument_project do
|
174
|
+
argument(:project){
|
175
|
+
attr
|
176
|
+
}
|
177
|
+
end
|
178
|
+
|
179
|
+
mixin :optional_project do
|
180
|
+
argument(:project){
|
181
|
+
optional
|
182
|
+
attr
|
183
|
+
}
|
184
|
+
end
|
185
|
+
|
186
|
+
mixin :argument_punch_in do
|
187
|
+
argument(:punch_in){
|
188
|
+
cast{|value| Time.parse value}
|
189
|
+
attr
|
190
|
+
}
|
191
|
+
end
|
192
|
+
|
193
|
+
mixin :argument_punch_out do
|
194
|
+
argument(:punch_out){
|
195
|
+
cast{|value| Time.parse value}
|
196
|
+
attr
|
197
|
+
}
|
198
|
+
end
|
199
|
+
|
200
|
+
mixin :argument_message do
|
201
|
+
argument(:message){
|
202
|
+
attr
|
203
|
+
arity -2
|
204
|
+
}
|
205
|
+
end
|
206
|
+
|
207
|
+
mixin :option_message do
|
208
|
+
option(:message, :m){
|
209
|
+
argument :required
|
210
|
+
attr
|
211
|
+
}
|
212
|
+
end
|
213
|
+
|
214
|
+
mixin :option_now do
|
215
|
+
option(:now, :n){
|
216
|
+
argument :required
|
217
|
+
desc 'consider this time as the current time'
|
218
|
+
cast{|value| Time.parse value}
|
219
|
+
default Time::Now
|
220
|
+
attr
|
221
|
+
}
|
222
|
+
end
|
223
|
+
|
224
|
+
mixin :optional_now do
|
225
|
+
argument(:now){
|
226
|
+
optional
|
227
|
+
desc 'consider this time as the current time'
|
228
|
+
arity -2
|
229
|
+
default Time::Now
|
230
|
+
}
|
231
|
+
attribute(:now){ Time.parse param['now'].values.join(' ') }
|
232
|
+
end
|
233
|
+
|
234
|
+
mixin :option_db do
|
235
|
+
option(:db){
|
236
|
+
argument :required
|
237
|
+
default File.join(Home, '.punch.yml')
|
238
|
+
attr{|param| DB.new(param.value) }
|
239
|
+
}
|
240
|
+
end
|
241
|
+
|
242
|
+
mixin :option_noop do
|
243
|
+
option(:noop, :N){
|
244
|
+
cast{|value| $NOOP = value}
|
245
|
+
}
|
246
|
+
end
|
247
|
+
|
248
|
+
mixin :option_after do
|
249
|
+
option(:after, :A){
|
250
|
+
argument :required
|
251
|
+
desc 'limit data shown to entries after this iso8601 timestamp'
|
252
|
+
default Time::Beginning
|
253
|
+
cast :time
|
254
|
+
attr
|
255
|
+
}
|
256
|
+
end
|
257
|
+
|
258
|
+
mixin :option_before do
|
259
|
+
option(:before, :B){
|
260
|
+
argument :required
|
261
|
+
desc 'limit data shown to entries before this iso8601 timestamp'
|
262
|
+
default Time::End
|
263
|
+
cast :time
|
264
|
+
attr
|
265
|
+
}
|
266
|
+
end
|
267
|
+
|
268
|
+
run{ help! }
|
269
|
+
}
|
270
|
+
|
271
|
+
|
272
|
+
|
273
|
+
|
274
|
+
|
275
|
+
|
276
|
+
|
277
|
+
#
|
278
|
+
# bootstrap libs and litter some crap into ruby built-ins ;-)
|
279
|
+
#
|
280
|
+
|
281
|
+
BEGIN {
|
282
|
+
%w(yaml yaml/store time pathname tempfile fileutils).each {|lib| require lib}
|
283
|
+
%w(rubygems main orderedhash chronic attributes).each do |depends|
|
284
|
+
begin
|
285
|
+
require depends
|
286
|
+
rescue
|
287
|
+
abort "gem install #{depends}"
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
Home = File.expand_path(ENV['HOME'] || '~')
|
292
|
+
|
293
|
+
### hackity hack, don't talk back
|
294
|
+
|
295
|
+
class Object
|
296
|
+
def yaml_inline!
|
297
|
+
class << self
|
298
|
+
def to_yaml_style() :inline end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
class Time
|
304
|
+
Beginning = Time.at(0).iso8601
|
305
|
+
|
306
|
+
End = Time.at((2**31)-1).iso8601
|
307
|
+
|
308
|
+
Now = Time.now.iso8601
|
309
|
+
|
310
|
+
Null = Time.at(0).instance_eval do
|
311
|
+
def to_s() "" end
|
312
|
+
def inspect() "" end
|
313
|
+
self
|
314
|
+
end
|
315
|
+
|
316
|
+
def to_s(n=0) iso8601(n) end
|
317
|
+
alias_method 'inspect', 'to_s'
|
318
|
+
|
319
|
+
### hack to fix Time.parse bug
|
320
|
+
Parse = Time.method 'parse'
|
321
|
+
def self.parse string
|
322
|
+
if string =~ %r'^\d\d\d\d-\d\d-\d\dT\d\d:?$'
|
323
|
+
string = string.sub(%r/:$/,'') + ':00'
|
324
|
+
end
|
325
|
+
if string =~ %r/\ban\b/
|
326
|
+
string = string.sub(%r/\ban\b/, '1')
|
327
|
+
end
|
328
|
+
if string =~ %r'^\d\d\d\d-\d\d-\d\d'
|
329
|
+
Parse.call string
|
330
|
+
else
|
331
|
+
Chronic.parse string, :context => :past
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
def to_yaml( opts = {} )
|
336
|
+
YAML::quick_emit( object_id, opts ) do |out|
|
337
|
+
tz = "Z"
|
338
|
+
# from the tidy Tobias Peters <t-peters@gmx.de> Thanks!
|
339
|
+
unless self.utc?
|
340
|
+
utc_same_instant = self.dup.utc
|
341
|
+
utc_same_writing = Time.utc(year,month,day,hour,min,sec,usec)
|
342
|
+
difference_to_utc = utc_same_writing - utc_same_instant
|
343
|
+
if (difference_to_utc < 0)
|
344
|
+
difference_sign = '-'
|
345
|
+
absolute_difference = -difference_to_utc
|
346
|
+
else
|
347
|
+
difference_sign = '+'
|
348
|
+
absolute_difference = difference_to_utc
|
349
|
+
end
|
350
|
+
difference_minutes = (absolute_difference/60).round
|
351
|
+
tz = "%s%02d:%02d" % [ difference_sign, difference_minutes / 60, difference_minutes % 60]
|
352
|
+
end
|
353
|
+
standard = self.strftime( "%Y-%m-%dT%H:%M:%S" )
|
354
|
+
standard += ".%02d" % [usec] #if usec.nonzero?
|
355
|
+
standard += "%s" % [tz]
|
356
|
+
if to_yaml_properties.empty?
|
357
|
+
out.scalar( taguri, standard, :plain )
|
358
|
+
else
|
359
|
+
out.map( taguri, to_yaml_style ) do |map|
|
360
|
+
map.add( 'at', standard )
|
361
|
+
to_yaml_properties.each do |m|
|
362
|
+
map.add( m, instance_variable_get( m ) )
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
class Numeric
|
371
|
+
def hms s = self.to_i
|
372
|
+
h, s = s.divmod 3600
|
373
|
+
m, s = s.divmod 60
|
374
|
+
[h.to_i, m.to_i, s]
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
class String
|
379
|
+
def hms
|
380
|
+
h, m, s = split %r/:/, 3
|
381
|
+
[h.to_i, m.to_i, s.to_i]
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
class Hash
|
386
|
+
def getopt key, default = nil
|
387
|
+
return self[key.to_s] if has_key?(key.to_s)
|
388
|
+
return self[key.to_s.to_sym] if has_key?(key.to_s.to_sym)
|
389
|
+
return self[key] if has_key?(key)
|
390
|
+
default
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
class DB
|
395
|
+
attr 'pathname'
|
396
|
+
attr 'db'
|
397
|
+
|
398
|
+
def initialize pathname
|
399
|
+
@pathname = Pathname.new pathname.to_s
|
400
|
+
if $NOOP
|
401
|
+
t = Tempfile.new Process.pid.to_s
|
402
|
+
t.write IO.read(pathname)
|
403
|
+
t.close
|
404
|
+
@db = YAML::Store.new t.path
|
405
|
+
else
|
406
|
+
@db = YAML::Store.new @pathname.to_s
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
def punch which, project, options = {}
|
411
|
+
now = options.getopt 'now'
|
412
|
+
messages = [
|
413
|
+
"punch #{ which } @ #{ now }",
|
414
|
+
options.getopt('message'),
|
415
|
+
].flatten.compact
|
416
|
+
@db.transaction do
|
417
|
+
if project
|
418
|
+
unless (roots = @db.roots).include?(project) # check for typos
|
419
|
+
alternatives = roots.map {|existing_proj|
|
420
|
+
p1, p2 = [existing_proj, project].map {|p| p.instance_eval("chars.to_a")}
|
421
|
+
((p1|p2).size - p2.size).abs <= 1 ? existing_proj : nil
|
422
|
+
}.compact.uniq
|
423
|
+
puts "Psst: is '#{project}' what you meant to type, or should it be '#{alternatives.join(' or ')}'?\n"
|
424
|
+
end
|
425
|
+
create project
|
426
|
+
list = @db[project.to_s]
|
427
|
+
entry = list.last
|
428
|
+
case which.to_s
|
429
|
+
when /in/
|
430
|
+
if(entry and entry['out'].nil?)
|
431
|
+
abort 'you first need to punch out'
|
432
|
+
else
|
433
|
+
entry = create_entry('in' => now, 'out' => nil, 'log' => [messages].flatten.compact)
|
434
|
+
list << entry
|
435
|
+
end
|
436
|
+
when /out/
|
437
|
+
if(entry.nil? or (entry and (entry['in'].nil? or entry['out'])))
|
438
|
+
abort 'you first need to punch in'
|
439
|
+
else
|
440
|
+
entry = create_entry entry
|
441
|
+
sum = now - Time.parse(entry['in'].to_s)
|
442
|
+
entry['out'] = now
|
443
|
+
entry['log'] = [entry['log'], messages].flatten.compact
|
444
|
+
entry['total'] = ('%0.2d:%0.2d:%0.2d' % sum.hms)
|
445
|
+
list[-1] = entry
|
446
|
+
end
|
447
|
+
end
|
448
|
+
entry
|
449
|
+
else
|
450
|
+
entries = OrderedHash.new
|
451
|
+
@db.roots.each do |project|
|
452
|
+
list = @db[project]
|
453
|
+
next unless Array === list
|
454
|
+
list.each do |entry|
|
455
|
+
next unless entry
|
456
|
+
if(entry['in'] and entry['out'].nil?)
|
457
|
+
entry = create_entry entry
|
458
|
+
sum = now - Time.parse(entry['in'].to_s)
|
459
|
+
entry['out'] = now
|
460
|
+
entry['log'] = [entry['log'], messages].flatten.compact
|
461
|
+
entry['total'] = ('%0.2d:%0.2d:%0.2d' % sum.hms)
|
462
|
+
list[-1] = entry
|
463
|
+
(entries[project] ||= []) << entry
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
467
|
+
entries
|
468
|
+
end
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
def log project, message, options = {}
|
473
|
+
@db.transaction do
|
474
|
+
create project
|
475
|
+
message = [message].flatten.compact.join(' ')
|
476
|
+
list = @db[project.to_s]
|
477
|
+
entry = list.last
|
478
|
+
if(entry.nil? or (entry and entry['out']))
|
479
|
+
abort 'you first need to punch in'
|
480
|
+
end
|
481
|
+
entry = create_entry entry
|
482
|
+
entry['log'] = [entry['log'], message].flatten.compact
|
483
|
+
list[-1] = entry
|
484
|
+
entry
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
def clock project, punch_in, punch_out, options = {}
|
489
|
+
@db.transaction do
|
490
|
+
log = [
|
491
|
+
"punch in @ #{ punch_in }",
|
492
|
+
options.getopt('message'),
|
493
|
+
"punch out @ #{ punch_out }",
|
494
|
+
].flatten.compact
|
495
|
+
create project
|
496
|
+
list = @db[project.to_s]
|
497
|
+
entry = create_entry('in' => punch_in, 'out' => punch_out, 'log' => log)
|
498
|
+
list << entry
|
499
|
+
@db[project.to_s] = list.sort_by{|e| e.values_at('in', 'out')}
|
500
|
+
entry
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
def list project, options = {}
|
505
|
+
window = options['window'] || options[:window]
|
506
|
+
@db.transaction do
|
507
|
+
create project
|
508
|
+
if project
|
509
|
+
@db[project.to_s].select do |entry|
|
510
|
+
a, b = entry.values_at 'in', 'out'
|
511
|
+
if a and b
|
512
|
+
window === Time.parse(a.to_s) and window === Time.parse(b.to_s)
|
513
|
+
end
|
514
|
+
end.map{|entry| create_entry entry}
|
515
|
+
else
|
516
|
+
@db.roots
|
517
|
+
end
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
def status project, options = {}
|
522
|
+
@db.transaction do
|
523
|
+
h = Hash.new
|
524
|
+
@db.roots.each do |root|
|
525
|
+
entry = @db[root].last
|
526
|
+
a, b =
|
527
|
+
if entry
|
528
|
+
entry.values_at 'in', 'out'
|
529
|
+
else
|
530
|
+
[false, 'out']
|
531
|
+
end
|
532
|
+
#status = b ? "out @ #{ b }" : "in @ #{ a }"
|
533
|
+
#status = b ? ['out', b] : ['in', a]
|
534
|
+
#status.yaml_inline!
|
535
|
+
status = b ? Hash['out', b] : Hash['in', a]
|
536
|
+
if project
|
537
|
+
h[project] = status if project == root
|
538
|
+
else
|
539
|
+
h[root] = status
|
540
|
+
end
|
541
|
+
end
|
542
|
+
h
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
def total project, options = {}
|
547
|
+
window = options['window'] || options[:window]
|
548
|
+
@db.transaction do
|
549
|
+
if project
|
550
|
+
create project
|
551
|
+
selected = @db[project.to_s].select do |entry|
|
552
|
+
a, b = entry.values_at 'in', 'out'
|
553
|
+
window === Time.parse(a.to_s) and window === Time.parse(b.to_s)
|
554
|
+
end
|
555
|
+
filtered = { project => selected }
|
556
|
+
else
|
557
|
+
filtered = Hash.new
|
558
|
+
@db.roots.each do |project|
|
559
|
+
selected = @db[project.to_s].select do |entry|
|
560
|
+
a, b = entry.values_at 'in', 'out'
|
561
|
+
window === Time.parse(a.to_s) and window === Time.parse(b.to_s)
|
562
|
+
end
|
563
|
+
filtered.update project => selected
|
564
|
+
end
|
565
|
+
end
|
566
|
+
total = Hash.new
|
567
|
+
filtered.each do |project, selected|
|
568
|
+
sum = 0
|
569
|
+
selected.each do |entry|
|
570
|
+
a, b = entry.values_at 'in', 'out'
|
571
|
+
next unless a and b
|
572
|
+
a = Time.parse(a.to_s)
|
573
|
+
b = Time.parse(b.to_s)
|
574
|
+
sum += (b - a)
|
575
|
+
end
|
576
|
+
total.update project => ('%0.2d:%0.2d:%0.2d' % sum.hms)
|
577
|
+
end
|
578
|
+
total
|
579
|
+
end
|
580
|
+
end
|
581
|
+
|
582
|
+
def delete project
|
583
|
+
@db.transaction do
|
584
|
+
@db.delete project
|
585
|
+
project
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
def dump project
|
590
|
+
@db.transaction do
|
591
|
+
dumped = OrderedHash.new
|
592
|
+
if project
|
593
|
+
dumped[project] = Array.new
|
594
|
+
@db[project].each do |entry|
|
595
|
+
dumped[project] << create_entry(entry)
|
596
|
+
end
|
597
|
+
else
|
598
|
+
@db.roots.each do |project|
|
599
|
+
dumped[project] = Array.new
|
600
|
+
@db[project].each do |entry|
|
601
|
+
dumped[project] << create_entry(entry)
|
602
|
+
end
|
603
|
+
end
|
604
|
+
end
|
605
|
+
dumped
|
606
|
+
end
|
607
|
+
end
|
608
|
+
|
609
|
+
def create project
|
610
|
+
project = project.to_s.strip
|
611
|
+
@db[project.to_s] ||= Array.new unless project.empty?
|
612
|
+
end
|
613
|
+
|
614
|
+
def create_entry hash = {}
|
615
|
+
entry = OrderedHash.new
|
616
|
+
%w[ in out log total ].each do |key|
|
617
|
+
entry[key] = hash.getopt key
|
618
|
+
end
|
619
|
+
entry
|
620
|
+
end
|
621
|
+
end
|
622
|
+
}
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: be9-punch
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ara T. Howard
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-03-03 00:00:00 -08:00
|
13
|
+
default_executable: punch
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: main
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.6.0
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: systemu
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.2.0
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: orderedhash
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.0.3
|
44
|
+
version:
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: attributes
|
47
|
+
type: :runtime
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 5.0.0
|
54
|
+
version:
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: chronic
|
57
|
+
type: :runtime
|
58
|
+
version_requirement:
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: "0"
|
64
|
+
version:
|
65
|
+
description: Simple commmand line based timetracker in ruby.
|
66
|
+
email: info@simplyexcited.co.uk
|
67
|
+
executables:
|
68
|
+
- punch
|
69
|
+
extensions: []
|
70
|
+
|
71
|
+
extra_rdoc_files: []
|
72
|
+
|
73
|
+
files:
|
74
|
+
- VERSION.yml
|
75
|
+
- Rakefile
|
76
|
+
- README
|
77
|
+
- bin/punch
|
78
|
+
has_rdoc: true
|
79
|
+
homepage: http://codeforpeople.com/lib/ruby/punch/
|
80
|
+
post_install_message:
|
81
|
+
rdoc_options:
|
82
|
+
- --inline-source
|
83
|
+
- --charset=UTF-8
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: "0"
|
91
|
+
version:
|
92
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: "0"
|
97
|
+
version:
|
98
|
+
requirements: []
|
99
|
+
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 1.2.0
|
102
|
+
signing_key:
|
103
|
+
specification_version: 2
|
104
|
+
summary: Punch timetracker
|
105
|
+
test_files: []
|
106
|
+
|