time-sheet 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []