time-sheet 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c8452bc9323ceb47d374ee7c7654983c02c78e004e9f74dd35c8a51dac189432
4
+ data.tar.gz: 84f5adbec539706a2568be38fc02a8e9816c6cc1c57ef95561e551685a29ab4d
5
+ SHA512:
6
+ metadata.gz: 8a5f99c3e94ad248ed228710ea6a396821358de6246482b3e6e705818795423ef25e9f83efe2eb2a68f61caf80d10c4534a601664695227fe5cd63fc8b206942
7
+ data.tar.gz: 32b4e62a904ba3227cff54381d7c9face9b6d53f4a41cc463b16158d2cf35647ce48ed37d1f616db5916b8eff707c79fa18c016ffcb34b0747b8b9e20582621a
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.1
5
+ before_install: gem install bundler -v 1.15.4
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in mps.gemspec
6
+ gemspec
data/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # TimeSheet
2
+
3
+ This gem allows you to parse a spreadsheet with time tracking information to
4
+ generate various statistics suitable for invoices and project effort estimates.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ gem install time-sheet
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ The gem bundles an executable which includes a help command, just run
15
+ `time-sheet.rb --help`
16
+
17
+ ## Contributing
18
+
19
+ Bug reports and pull requests are welcome on GitHub at https://github.com/moritzschepp/time-sheet
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "mps"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/time-sheet.rb ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "time_sheet"
4
+ TimeSheet::Time::Cmd.new.run
data/lib/time_sheet.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'time_sheet/version'
2
+
3
+ if defined?(Bundler)
4
+ require 'pry'
5
+ end
6
+
7
+ module TimeSheet
8
+ autoload :Time, 'time_sheet/time'
9
+ autoload :TablePrinter, 'time_sheet/table_printer'
10
+
11
+ def self.root
12
+ @root ||= File.expand_path(File.dirname(__FILE__) + '/..')
13
+ end
14
+ end
@@ -0,0 +1,65 @@
1
+ class TimeSheet::TablePrinter
2
+
3
+ def initialize(data = [], options = {})
4
+ @options = options
5
+ @data = data
6
+ end
7
+
8
+ attr_reader :options
9
+
10
+ def <<(row)
11
+ @data << row
12
+ end
13
+
14
+ def flush
15
+ @widths = nil
16
+ end
17
+
18
+ def generate
19
+ flush
20
+
21
+ result = []
22
+ @data.each do |row|
23
+ output = if row == '-'
24
+ widths.map{|w| '-' * w}
25
+ else
26
+ row.each_with_index.map do |r, i|
27
+ format(r, widths[i], i == row.size - 1)
28
+ end
29
+ end
30
+ result << output.join(options[:trim] ? '|' : ' | ')
31
+ end
32
+ result.join("\n")
33
+ end
34
+
35
+ def widths
36
+ @widths ||= @data.first.each_with_index.map do |c, i|
37
+ @data.map{|row| row == '-' ? 0 : size(row[i])}.max
38
+ end
39
+ end
40
+
41
+ def format(value, width, last_column = false)
42
+ str = case value
43
+ when Integer then value.to_s.rjust(width)
44
+ when Date then value.strftime('%Y-%m-%d').rjust(width)
45
+ when Time then value.strftime('%H:%M').rjust(width)
46
+ when Float then ("%.2f" % value).rjust(width)
47
+ when nil then ' ' * width
48
+ else
49
+ last_column ? value : value.ljust(width)
50
+ end
51
+ end
52
+
53
+ def size(value)
54
+ case value
55
+ when nil then 0
56
+ when Integer then value.to_s.size
57
+ when Float then ("%.2f" % value).size
58
+ when Date then 10
59
+ when Time then 5
60
+ else
61
+ value.size
62
+ end
63
+ end
64
+
65
+ end
@@ -0,0 +1,134 @@
1
+ require 'erb'
2
+
3
+ module TimeSheet::Time
4
+ autoload :Cmd, 'time_sheet/time/cmd'
5
+ autoload :Entry, 'time_sheet/time/entry'
6
+ autoload :Exception, 'time_sheet/time/exception'
7
+ autoload :Parser, 'time_sheet/time/parser'
8
+ autoload :Util, 'time_sheet/time/util'
9
+
10
+ def self.report(options)
11
+ results = {
12
+ 'entries' => [],
13
+ 'total' => 0.0,
14
+ 'projects' => {}
15
+ }
16
+
17
+ x = nil
18
+ Parser.new(options[:location]).entries.each do |e|
19
+ unless x
20
+ x = true
21
+ end
22
+ if e.matches?(options)
23
+ results['total'] += e.duration
24
+ results['projects'][e.project] ||= {'total' => 0.0, 'activities' => {}}
25
+ results['projects'][e.project]['total'] += e.duration
26
+ results['projects'][e.project]['activities'][e.activity] ||= 0.0
27
+ results['projects'][e.project]['activities'][e.activity] += e.duration
28
+
29
+ results['entries'] << e.to_row
30
+ end
31
+ end
32
+
33
+ averages(results, options)
34
+
35
+ results
36
+ end
37
+
38
+ def self.averages(results, options)
39
+ days = 1
40
+
41
+ unless results['entries'].empty?
42
+ time = (
43
+ (options[:to] || Util.day_end) -
44
+ (options[:from] || results['entries'].first[0].to_time)
45
+ ).to_i
46
+ days = (time.to_f / 60 / 60 / 24).round
47
+ end
48
+
49
+ weeks = days / 7.0
50
+ months = days / 30.0
51
+ workdays = weeks * 5.0
52
+ worked = TimeSheet::Time::Util.hours(results['total'])
53
+
54
+ results['averages'] = {
55
+ 'days' => days,
56
+ 'weeks' => weeks,
57
+ 'months' => months,
58
+ 'workdays' => workdays,
59
+ 'worked' => worked,
60
+ 'hours_per_day' => worked / days,
61
+ 'hours_per_workday' => worked / workdays,
62
+ 'hours_per_week' => worked / weeks,
63
+ 'hours_per_month' => worked / months,
64
+ }
65
+ end
66
+
67
+ def self.invoice(options)
68
+ grouped = {}
69
+ Parser.new(options[:location]).entries.each do |e|
70
+ if e.matches?(options)
71
+ grouped[[e.date, e.description]] ||= 0
72
+ grouped[[e.date, e.description]] += e.duration.to_i
73
+ end
74
+ end
75
+ rows = []
76
+ grouped.each{|k, d| rows << [k.first, d, k.last]}
77
+ packages = [[]]
78
+ ptotal = 0
79
+
80
+ while row = rows.shift
81
+ if options[:package] > 0
82
+ new_package_size = ptotal + row[1]
83
+
84
+ if new_package_size > options[:package] * 60
85
+ filler = row.dup
86
+ filler[1] = options[:package] * 60 - ptotal
87
+ packages.last << filler
88
+ packages << []
89
+ ptotal = 0
90
+
91
+ # we re-queue the remainder of the current row so that it is picked
92
+ # up by the next iteration
93
+ row[1] -= filler[1]
94
+ rows.unshift row
95
+ next
96
+ end
97
+
98
+ if new_package_size == options[:package] * 60
99
+ packages.last << row
100
+ packages << []
101
+ ptotal = 0
102
+ next
103
+ end
104
+ end
105
+
106
+ packages.last << row
107
+ ptotal += row[1]
108
+ end
109
+
110
+ if packages.last.empty?
111
+ packages.pop
112
+ end
113
+
114
+ if options[:petty]
115
+ packages.map! do |package|
116
+ petty = 0
117
+ package.select! do |row|
118
+ if row[1] < options[:petty]
119
+ petty += row[1]
120
+ next false
121
+ end
122
+ true
123
+ end
124
+ if petty > 0
125
+ package << [nil, petty, 'misc']
126
+ end
127
+ package
128
+ end
129
+ end
130
+
131
+ packages
132
+ end
133
+
134
+ end
@@ -0,0 +1,202 @@
1
+ require 'slop'
2
+ require 'time'
3
+
4
+ class TimeSheet::Time::Cmd
5
+ def run
6
+ if d = options[:from]
7
+ if d.match(/^\d\d?-\d\d?$/)
8
+ d = "#{TimeSheet::Time::Util.now.year}-#{d}"
9
+ end
10
+
11
+ if d.match(/^\d{4}$/)
12
+ d = "#{d}-01-01"
13
+ end
14
+
15
+ options[:from] = Time.parse(d)
16
+ end
17
+
18
+ if d = options[:to]
19
+ if d.match(/^\d\d?-\d\d?$/)
20
+ d = "#{TimeSheet::Time::Util.now.year}-#{d}"
21
+ end
22
+
23
+ if d.match(/^\d{4}$/)
24
+ d = "#{d}-12-31"
25
+ end
26
+
27
+ options[:to] = Time.parse(d)
28
+ end
29
+
30
+ if options[:help]
31
+ puts options
32
+ elsif options[:version]
33
+ puts TimeSheet::VERSION
34
+ else
35
+ case command
36
+ when 'invoice'
37
+ invoice
38
+ when 'report', 'default'
39
+ report
40
+ when 'today', 't'
41
+ options[:from] = TimeSheet::Time::Util.today
42
+ options[:summary] = true
43
+ report
44
+ when 'yesterday', 'y'
45
+ options[:from] = TimeSheet::Time::Util.yesterday
46
+ options[:to] = TimeSheet::Time::Util.yesterday
47
+ options[:summary] = true
48
+ report
49
+ when 'week', 'w'
50
+ options[:from] = TimeSheet::Time::Util.week_start
51
+ options[:summary] = true
52
+ report
53
+ when 'last-week', 'lw'
54
+ options[:from] = TimeSheet::Time::Util.week_start(-1)
55
+ options[:to] = TimeSheet::Time::Util.week_end(-1)
56
+ options[:summary] = true
57
+ report
58
+ when 'month', 'm'
59
+ options[:from] = TimeSheet::Time::Util.month_start
60
+ options[:summary] = true
61
+ report
62
+ when 'last-month', 'lm'
63
+ options[:from] = TimeSheet::Time::Util.month_start(-1)
64
+ options[:to] = TimeSheet::Time::Util.month_end(-1)
65
+ options[:summary] = true
66
+ report
67
+ when 'overview'
68
+ overview
69
+ else
70
+ raise "unknown command: #{command}"
71
+ end
72
+
73
+ if options[:verbose]
74
+ puts "\noptions:"
75
+ p options.to_h
76
+ end
77
+ end
78
+ end
79
+
80
+ def options
81
+ @options ||= Slop.parse do |o|
82
+ o.banner = [
83
+ "usage: time.rb [command] [options]\n",
84
+ 'available commands:',
85
+ " report (default): list entries conforming to given criteria",
86
+ " invoice: compress similar entries and filter petty ones. Optionally package for e.g. monthly invoicing",
87
+ "\n general options:"
88
+ ].join("\n")
89
+
90
+ o.boolean '-h', '--help', 'show help'
91
+ o.boolean '--version', 'show the version'
92
+ o.array('-l', '--location', 'a location to gather data from (file or directory)',
93
+ default: ["#{ENV['HOME']}/Desktop/cloud/time"]
94
+ )
95
+ o.string '-f', '--from', 'ignore entries older than the date given'
96
+ o.string '-t', '--to', 'ignore entries more recent than the date given'
97
+ o.string '-p', '--project', 'take only entries of this project into account'
98
+ o.string '-a', '--activity', 'take only entries of this activity into account'
99
+ o.string '-d', '--description', 'consider only entries matching this description'
100
+ o.float '-r', '--rate', 'use an alternative hourly rate (default: 80.0)', default: 80.00
101
+ o.boolean '-s', '--summary', 'when reporting, add summary section'
102
+ o.boolean '--trim', 'compact the output for processing as CSV', default: false
103
+ o.boolean '-v', '--verbose', 'be more verbose'
104
+ o.separator "\n invoice options:"
105
+ o.integer '--package', 'for invoice output: build packages of this duration in hours', default: 0
106
+ o.integer '--petty', 'fold records of a certain threshold into a "misc" activity', default: 0
107
+ end
108
+ end
109
+
110
+ def command
111
+ options.arguments.shift || 'default'
112
+ end
113
+
114
+ def convert_to_time
115
+ if options[:from]
116
+ options[:from] = options[:from].to_time
117
+ end
118
+ if options[:to].is_a?(Date)
119
+ options[:to] = options[:to].to_time + 24 * 60 * 60
120
+ end
121
+ end
122
+
123
+ def invoice
124
+ convert_to_time
125
+
126
+ data = TimeSheet::Time.invoice(options)
127
+
128
+ data.each do |package|
129
+ tp = TimeSheet::TablePrinter.new package, options
130
+ puts tp.generate
131
+ puts "\n"
132
+ end
133
+
134
+ if options[:package]
135
+ package = data.last.map{|entry| entry[1]}.sum
136
+ total = options[:package] * 60
137
+ percent = (package / total.to_f * 100)
138
+
139
+ puts "last package duration: #{package}/#{total} (#{percent.round 2}%)"
140
+ end
141
+ end
142
+
143
+ def report
144
+ convert_to_time
145
+
146
+ data = TimeSheet::Time.report(options)
147
+ tp = TimeSheet::TablePrinter.new data['entries'], options
148
+ puts tp.generate
149
+
150
+
151
+ if options[:summary]
152
+ puts "\nSummary:"
153
+
154
+ tdata = [['project', 'activity', 'time [m]', 'time [h]', 'price [€]']]
155
+ tdata << [
156
+ 'all',
157
+ '',
158
+ TimeSheet::Time::Util.minutes(data['total']),
159
+ TimeSheet::Time::Util.hours(data['total']),
160
+ TimeSheet::Time::Util.price(data['total'], options[:rate])
161
+ ]
162
+
163
+ data['projects'].sort_by{|k, v| v['total']}.reverse.to_h.each do |pname, pdata|
164
+ previous = nil
165
+
166
+ tdata << '-'
167
+ tdata << [
168
+ pname,
169
+ 'all',
170
+ TimeSheet::Time::Util.minutes(pdata['total']),
171
+ TimeSheet::Time::Util.hours(pdata['total']),
172
+ TimeSheet::Time::Util.price(pdata['total'], options[:rate])
173
+ ]
174
+
175
+ pdata['activities'].sort_by{|k, v| v}.reverse.to_h.each do |aname, atotal|
176
+ tdata << [
177
+ '',
178
+ aname,
179
+ TimeSheet::Time::Util.minutes(atotal),
180
+ TimeSheet::Time::Util.hours(atotal),
181
+ TimeSheet::Time::Util.price(atotal, options[:rate])
182
+ ]
183
+ previous = pname
184
+ end
185
+ end
186
+
187
+ tdata << '-'
188
+
189
+ tp = TimeSheet::TablePrinter.new tdata, options
190
+ puts tp.generate
191
+
192
+ puts [
193
+ "days: #{data['averages']['days']}",
194
+ "worked: h/day: #{data['averages']['hours_per_day'].round(2)}",
195
+ "h/workday: #{data['averages']['hours_per_workday'].round(2)}",
196
+ "h/week: #{data['averages']['hours_per_week'].round(2)}",
197
+ "h/month(30 days): #{data['averages']['hours_per_month'].round(2)}"
198
+ ].join(', ')
199
+ end
200
+ end
201
+
202
+ end
@@ -0,0 +1,143 @@
1
+ class TimeSheet::Time::Entry
2
+ def self.now
3
+ @now ||= Time.now
4
+ end
5
+
6
+ def initialize(data)
7
+ @data = data
8
+ end
9
+
10
+ attr_accessor :prev, :next
11
+
12
+ def project
13
+ @data['project'] ||= self.prev.project
14
+ end
15
+
16
+ def activity
17
+ @data['activity'] ||= self.prev.activity
18
+ end
19
+
20
+ def description
21
+ @data['description'] ||= self.prev.description
22
+ end
23
+
24
+ def date
25
+ @date ||= @data['date'] || self.prev.date
26
+ end
27
+
28
+ def start
29
+ @start ||= Time.mktime(
30
+ date.year, date.month, date.day,
31
+ @data['start'].hour, @data['start'].min
32
+ )
33
+ end
34
+
35
+
36
+ def end
37
+ ends_at = @data['end'] || (self.next ? self.next.start : self.class.now)
38
+
39
+ @end ||= Time.mktime(
40
+ date.year, date.month, date.day,
41
+ ends_at.hour, ends_at.min
42
+ )
43
+ end
44
+
45
+ # Experiment to add timezone support. However, this would complicate every day
46
+ # handing because of daylight saving time changes.
47
+ # def start_zone
48
+ # @start_zone ||= if v = @data['start_zone']
49
+ # # allow a name prefixing the value
50
+ # v.split(/\s/).last
51
+ # elsif v = self.prev.start_zone
52
+ # v
53
+ # else
54
+ # self.class.now.getlocal.utc_offset
55
+ # # use this process' timezone
56
+ # nil
57
+ # end
58
+ # end
59
+
60
+ # def end_zone
61
+ # @end_zone ||= if v = @data['end_zone']
62
+ # # allow a name prefixing the value
63
+ # v.split(/\s/).last
64
+ # elsif self.prev && v = self.prev.end_zone
65
+ # v
66
+ # else
67
+ # # self.class.now.getlocal.utc_offset
68
+ # # use this process' timezone
69
+ # nil
70
+ # end
71
+ # end
72
+
73
+ def duration
74
+ (self.end - self.start) / 60
75
+ end
76
+
77
+ def tags
78
+ (@data['tags'] || '').split(/\s*,\s*/)
79
+ end
80
+
81
+ def working_day?
82
+ !date.saturday? && !date.sunday? && !tags.include?('holiday')
83
+ end
84
+
85
+ def matches?(filters)
86
+ from = (filters[:from] ? filters[:from] : nil)
87
+ from = from.to_time if from.is_a?(Date)
88
+ to = (filters[:to] ? filters[:to] : nil)
89
+ to = (to + 1).to_time if to.is_a?(Date)
90
+
91
+ self.class.attrib_matches_any?(description, filters[:description]) &&
92
+ self.class.attrib_matches_any?(project, filters[:project]) &&
93
+ self.class.attrib_matches_any?(activity, filters[:activity]) &&
94
+ (!from || from <= self.start) &&
95
+ (!to || to >= self.end)
96
+ end
97
+
98
+ def valid?
99
+ (duration > 0) &&
100
+ ((self.start < self.end) || !self.next)
101
+ end
102
+
103
+ def to_row
104
+ [date, start, self.end, duration.to_i, project, activity, description]
105
+ end
106
+
107
+ def to_s
108
+ values = [
109
+ date.strftime('%Y-%m-%d'),
110
+ start.strftime('%H:%M'),
111
+ self.end.strftime('%H:%M'),
112
+ duration.to_i.to_s.rjust(4),
113
+ project,
114
+ activity,
115
+ description
116
+ ].join(' | ')
117
+ end
118
+
119
+ def to_hash
120
+ return {
121
+ 'date' => date,
122
+ 'start' => start,
123
+ 'end' => self.end,
124
+ 'duration' => duration,
125
+ 'project' => project,
126
+ 'activity' => activity,
127
+ 'description' => description
128
+ }
129
+ end
130
+
131
+ def <=>(other)
132
+ (self.date <=> other.date) || self.start <=> other.start
133
+ end
134
+
135
+ def self.attrib_matches_any?(value, patterns)
136
+ return true if !patterns
137
+
138
+ patterns.split(/\s*,\s*/).any? do |pattern|
139
+ value.match(pattern)
140
+ end
141
+ end
142
+
143
+ end
@@ -0,0 +1,3 @@
1
+ class TimeSheet::Time::Exception < Exception
2
+
3
+ end
@@ -0,0 +1,70 @@
1
+ require 'spreadsheet'
2
+
3
+ class TimeSheet::Time::Parser
4
+
5
+ def initialize(dirs)
6
+ @dirs = dirs
7
+ end
8
+
9
+ def files
10
+ results = []
11
+ @dirs.each do |dir|
12
+ if File.directory?(dir)
13
+ results += Dir["#{dir}/**/*.xls"]
14
+ else
15
+ results << dir
16
+ end
17
+ end
18
+ results.sort
19
+ end
20
+
21
+ def entries
22
+ @entries ||= begin
23
+ results = []
24
+ hashes_per_file.each do |hashes|
25
+ file_results = []
26
+ hashes.each do |e|
27
+ te = TimeSheet::Time::Entry.new(e)
28
+ if file_results.last
29
+ file_results.last.next = te
30
+ te.prev = file_results.last
31
+ end
32
+ file_results << te
33
+ end
34
+ results += file_results
35
+ end
36
+ results.sort!
37
+ results.each do |r|
38
+ unless r.valid?
39
+ # byebug
40
+ raise TimeSheet::Time::Exception.new("invalid time entry: #{r.to_row.inspect}")
41
+ end
42
+ end
43
+ results
44
+ end
45
+ end
46
+
47
+ def hashes_per_file
48
+ @hashes_per_file ||= begin
49
+ files.map do |f|
50
+ results = []
51
+ Spreadsheet.open(f).worksheets.each do |sheet|
52
+ headers = sheet.rows.first.to_a
53
+ sheet.rows[1..-1].each do |row|
54
+ # TODO find a way to guard against xls sheets with 65535 (empty)
55
+ # lines, perhaps:
56
+ # break if row[1].nil?
57
+
58
+ record = {}
59
+ row.each_with_index do |value, i|
60
+ record[headers[i]] = value
61
+ end
62
+ results << record
63
+ end
64
+ end
65
+ results
66
+ end
67
+ end
68
+ end
69
+
70
+ end
@@ -0,0 +1,61 @@
1
+ module TimeSheet::Time::Util
2
+
3
+ def self.year_start(factor = 0)
4
+ Date.new(Date.today.year + factor, 1, 1)
5
+ end
6
+
7
+ def self.year_end(factor = 0)
8
+ Date.new(Date.today.year + factor, 12, 31)
9
+ end
10
+
11
+ def self.month_start(factor = 0)
12
+ tmp = Date.today.prev_month(factor * -1)
13
+ Date.new tmp.year, tmp.month, 1
14
+ end
15
+
16
+ def self.month_end(factor = 0)
17
+ tmp = (month_start(factor) + 45)
18
+ Date.new(tmp.year, tmp.month) - 1
19
+ end
20
+
21
+ def self.week_start(factor = 0)
22
+ today - (today.wday - 1) % 7 + (factor * 7)
23
+ end
24
+
25
+ def self.week_end(factor = 0)
26
+ week_start(factor) + 6
27
+ end
28
+
29
+ def self.day_start
30
+ now.to_date.to_time
31
+ end
32
+
33
+ def self.day_end
34
+ day_start + 60 * 60 * 24 - 1
35
+ end
36
+
37
+ def self.now
38
+ Time.now
39
+ end
40
+
41
+ def self.today
42
+ now.to_date
43
+ end
44
+
45
+ def self.yesterday
46
+ now.to_date - 1
47
+ end
48
+
49
+ def self.minutes(duration)
50
+ duration.to_i
51
+ end
52
+
53
+ def self.hours(duration)
54
+ (duration / 60.0).round(2)
55
+ end
56
+
57
+ def self.price(duration, rate)
58
+ (self.hours(duration) * rate).round(2)
59
+ end
60
+
61
+ end
@@ -0,0 +1,3 @@
1
+ module TimeSheet
2
+ VERSION = "0.5.2"
3
+ end
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "time_sheet/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "time-sheet"
8
+ spec.version = TimeSheet::VERSION
9
+ spec.authors = ["Moritz Schepp"]
10
+ spec.email = ["moritz.schepp@gmail.com"]
11
+ spec.license = 'GPL-3.0-only'
12
+
13
+ spec.summary = "a time tracking solution based on spreadsheets"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
16
+ f.match(%r{^(test|spec|features)/})
17
+ end
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency 'spreadsheet'
23
+ spec.add_dependency 'slop'
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.15"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+ spec.add_development_dependency 'pry'
29
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: time-sheet
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.2
5
+ platform: ruby
6
+ authors:
7
+ - Moritz Schepp
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-01-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: spreadsheet
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: slop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.15'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.15'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description:
98
+ email:
99
+ - moritz.schepp@gmail.com
100
+ executables:
101
+ - time-sheet.rb
102
+ extensions: []
103
+ extra_rdoc_files: []
104
+ files:
105
+ - ".gitignore"
106
+ - ".rspec"
107
+ - ".travis.yml"
108
+ - Gemfile
109
+ - README.md
110
+ - Rakefile
111
+ - bin/console
112
+ - bin/setup
113
+ - exe/time-sheet.rb
114
+ - lib/time_sheet.rb
115
+ - lib/time_sheet/table_printer.rb
116
+ - lib/time_sheet/time.rb
117
+ - lib/time_sheet/time/cmd.rb
118
+ - lib/time_sheet/time/entry.rb
119
+ - lib/time_sheet/time/exception.rb
120
+ - lib/time_sheet/time/parser.rb
121
+ - lib/time_sheet/time/util.rb
122
+ - lib/time_sheet/version.rb
123
+ - time_sheet.gemspec
124
+ homepage:
125
+ licenses:
126
+ - GPL-3.0-only
127
+ metadata: {}
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ">="
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ requirements: []
143
+ rubyforge_project:
144
+ rubygems_version: 2.7.6
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: a time tracking solution based on spreadsheets
148
+ test_files: []