time-sheet 0.15.0 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7185c488e563f07f440e280b8344671ab0c2e6236c67e28ef9bfbe3426701c90
4
- data.tar.gz: 1c8dbbd8b0f4c80c3dfcddce95a5facc298e96a9c46a048fc8dd8e7b29238092
3
+ metadata.gz: 35406dddfbbe1e74c061171bea36962f3d5632a6cedc75e846a66e53fc2fbd9e
4
+ data.tar.gz: deab85ae87457cab458141d8a4987ca592a07d0860405d2ecea1190a316e2b9e
5
5
  SHA512:
6
- metadata.gz: 617ca0d7081688afdb74b9951b809bae213d5611819bbe1de5337340d27545cc72bd0465870d8675715ae3c3fbf0dabb586792bee7c67636f683d7ce6495b7b9
7
- data.tar.gz: 165c6225e5593a0825abebfe1216a10bec2fff7387066b75af9b9a7975dcb28f8d38f939ee6b364a40bd2d79dac89e914fb2b4e53eb2ab795eb24f3dc3742485
6
+ metadata.gz: e262b17d41fbc239710af28d16d6fa00334d5cb691489e0b017f8864db76ce7b4b5ac7ba7fec53d39b821cda78522bd7fc234680ece63c184adf7b9275dcddee
7
+ data.tar.gz: 03eb5990ebcae8f95d4931a1dcaa7b5469e54f6eac61fcfe20a752c8e0c75146028b066153fed8bb8d46afeff5fa417235418db7ea82120694f224626c9ccbcc
data/README.md CHANGED
@@ -5,6 +5,11 @@ generate various statistics suitable for invoices and project effort estimates.
5
5
 
6
6
  ## Changelog
7
7
 
8
+ v1.0.0
9
+
10
+ * input files need to be .xlsx files now because we needed to replace the
11
+ spreadsheet parser, see [#1](https://github.com/moritzschepp/time-sheet/issues/1)
12
+
8
13
  v0.9.0
9
14
 
10
15
  * introduced employee data column: To use it, add a column "employee" to the
@@ -1,10 +1,10 @@
1
+ require 'yaml'
2
+
1
3
  require 'slop'
2
4
  require 'time'
3
5
 
4
6
  class TimeSheet::Time::Cmd
5
7
  def run
6
- TimeSheet.options = options
7
-
8
8
  if d = options[:from]
9
9
  if d.match(/^\d\d?-\d\d?$/)
10
10
  d = "#{TimeSheet::Time::Util.now.year}-#{d}"
@@ -85,17 +85,22 @@ class TimeSheet::Time::Cmd
85
85
  end
86
86
  end
87
87
 
88
- def default_location
89
- result = []
90
- config_file = "#{ENV['HOME']}/.time-sheet.conf"
91
- if File.exist?(config_file)
92
- File.read(config_file).split("\n").each do |line|
93
- if m = line.match(/^([a-z_]+):(.*)$/)
94
- result << m[2].strip if m[1] == 'location'
95
- end
96
- end
88
+ def config
89
+ defaults = {
90
+ 'location' => "#{ENV['HOME']}/time",
91
+ 'carry_over_activity' => 'previous'
92
+ }
93
+
94
+ file = "#{ENV['HOME']}/.time-sheet.conf"
95
+ return defaults unless File.exist?(file)
96
+
97
+ result = YAML.load_file(file)
98
+ result = defaults.merge(result)
99
+
100
+ unless result['location'].is_a?(Array)
101
+ result['location'] = [result['location']]
97
102
  end
98
- result << "#{ENV['HOME']}/time-sheet" if result.empty?
103
+
99
104
  result
100
105
  end
101
106
 
@@ -114,7 +119,7 @@ class TimeSheet::Time::Cmd
114
119
  o.boolean '-h', '--help', 'show help'
115
120
  o.boolean '--version', 'show the version'
116
121
  o.array('-l', '--location', 'a location to gather data from (file, directory or google docs share-url)',
117
- default: default_location
122
+ default: config['location']
118
123
  )
119
124
  o.string '-f', '--from', 'ignore entries older than the date given'
120
125
  o.string '-t', '--to', 'ignore entries more recent than the date given'
@@ -123,6 +128,12 @@ class TimeSheet::Time::Cmd
123
128
  o.string '--tags', 'filter by tag (comma separated, not case sensitive, prefix tag with ! to exclude)'
124
129
  o.string '-d', '--description', 'consider only entries matching this description'
125
130
  o.string '-e', '--employee', 'consider only entries for this employee'
131
+ o.string(
132
+ '--carry-over-activity',
133
+ 'mode for carrying over activity (previous|same-description)',
134
+ default: config['carry_over_activity']
135
+ )
136
+ o.string '--default-activity', 'default activity', default: config['default_activity']
126
137
  o.float '-r', '--rate', 'use an alternative hourly rate (default: 80.0)', default: 80.00
127
138
  o.boolean '-s', '--summary', 'when reporting, add summary section'
128
139
  o.boolean '--trim', 'compact the output for processing as CSV', default: false
@@ -148,6 +159,8 @@ class TimeSheet::Time::Cmd
148
159
  end
149
160
 
150
161
  def verify
162
+ TimeSheet.options = options
163
+
151
164
  convert_to_time
152
165
 
153
166
  entries = TimeSheet::Time::Parser.new(options[:location]).entries
@@ -222,7 +235,7 @@ class TimeSheet::Time::Cmd
222
235
  TimeSheet::Time::Util.hours(pdata['total']),
223
236
  TimeSheet::Time::Util.price(pdata['total'], options[:rate])
224
237
  ]
225
-
238
+
226
239
  pdata['activities'].sort_by{|k, v| v}.reverse.to_h.each do |aname, atotal|
227
240
  tdata << [
228
241
  '',
@@ -250,4 +263,4 @@ class TimeSheet::Time::Cmd
250
263
  end
251
264
  end
252
265
 
253
- end
266
+ end
@@ -14,7 +14,20 @@ class TimeSheet::Time::Entry
14
14
  end
15
15
 
16
16
  def activity
17
- @data['activity'] ||= self.prev.activity
17
+ if !@data['activity']
18
+ carry_mode = TimeSheet.options[:carry_over_activity]
19
+ use_prev = (
20
+ carry_mode == 'previous' ||
21
+ (carry_mode == 'same-description' && self.prev.description == description)
22
+ )
23
+ if use_prev
24
+ @data['activity'] = self.prev.activity
25
+ end
26
+
27
+ @data['activity'] ||= TimeSheet.options[:default_activity] || ''
28
+ end
29
+
30
+ @data['activity']
18
31
  end
19
32
 
20
33
  def description
@@ -28,7 +41,8 @@ class TimeSheet::Time::Entry
28
41
  def start
29
42
  @start ||= Time.mktime(
30
43
  date.year, date.month, date.day,
31
- @data['start'].hour, @data['start'].min
44
+ hour_for(@data['start']),
45
+ minute_for(@data['start'])
32
46
  )
33
47
  end
34
48
 
@@ -37,10 +51,31 @@ class TimeSheet::Time::Entry
37
51
 
38
52
  @end ||= Time.mktime(
39
53
  date.year, date.month, date.day,
40
- ends_at.hour, ends_at.min
54
+ hour_for(ends_at),
55
+ minute_for(ends_at)
41
56
  )
42
57
  end
43
58
 
59
+ def hour_for(timish)
60
+ case timish
61
+ when Time then timish.hour
62
+ when DateTime then timish.hour
63
+ when Integer then timish / 60 / 60
64
+ else
65
+ binding.pry
66
+ end
67
+ end
68
+
69
+ def minute_for(timish)
70
+ case timish
71
+ when Time then timish.min
72
+ when DateTime then timish.min
73
+ when Integer then timish / 60 % 60
74
+ else
75
+ binding.pry
76
+ end
77
+ end
78
+
44
79
  def employee
45
80
  @employee ||= @data['employee'] || (self.prev ? self.prev.employee : 'Me')
46
81
  end
@@ -121,7 +156,7 @@ class TimeSheet::Time::Entry
121
156
  self.exception = e
122
157
  false
123
158
  rescue StandardError => e
124
- binding.pry if Timesheet.options[:debug]
159
+ binding.pry if TimeSheet.options[:debug]
125
160
  false
126
161
  end
127
162
 
@@ -193,4 +228,4 @@ class TimeSheet::Time::Entry
193
228
  (string || '').to_s.downcase.split(/\s*,\s*/).map{|t| t.strip}
194
229
  end
195
230
 
196
- end
231
+ end
@@ -1,4 +1,4 @@
1
- require 'spreadsheet'
1
+ require 'roo'
2
2
  require 'httpclient'
3
3
  require 'csv'
4
4
 
@@ -12,7 +12,7 @@ class TimeSheet::Time::Parser
12
12
  results = []
13
13
  @dirs.each do |dir|
14
14
  if File.directory?(dir)
15
- results += Dir["#{dir}/**/*.xls"]
15
+ results += Dir["#{dir}/**/*.xlsx"]
16
16
  else
17
17
  results << dir
18
18
  end
@@ -38,7 +38,6 @@ class TimeSheet::Time::Parser
38
38
  results.sort!
39
39
  results.each do |r|
40
40
  unless r.valid?
41
- # byebug
42
41
  raise r.exception, [
43
42
  r.exception.message, ': ',
44
43
  r.data.inspect,
@@ -57,28 +56,25 @@ class TimeSheet::Time::Parser
57
56
  if f.match(/https:\/\/docs\.google\.com/)
58
57
  parse_google_doc(f)
59
58
  else
60
- parse_xls(f)
59
+ parse_xlsx(f)
61
60
  end
62
61
  end
63
62
  end
64
63
  end
65
64
 
66
- def parse_xls(filename)
65
+ def parse_xlsx(filename)
67
66
  results = []
68
67
 
69
- Spreadsheet.open(filename).worksheets.each do |sheet|
70
- headers = sheet.rows.first.to_a
71
- sheet.rows[1..-1].each.with_index do |row, i|
72
- # TODO find a way to guard against xls sheets with 65535 (empty)
73
- # lines, perhaps:
74
- # break if row[1].nil?
75
- next if row.all?{|cell| [nil, ''].include?(cell)}
68
+ xlsx = Roo::Spreadsheet.open(filename)
69
+ xlsx.each_with_pagename do |name, sheet|
70
+ headers = sheet.row(1).to_h{|e| [e, e]}
71
+ rows = sheet.parse(headers)
76
72
 
77
- record = {}
78
- row.each_with_index do |value, i|
79
- record[headers[i]] = value
80
- end
81
- results << record
73
+ rows.each do |row|
74
+ next unless row['start']
75
+ next if row.values.all?{|v| [nil, ''].include?(v)}
76
+
77
+ results << row
82
78
  end
83
79
  end
84
80
 
@@ -8,6 +8,8 @@ module TimeSheet::Time
8
8
  autoload :Util, 'time_sheet/time/util'
9
9
 
10
10
  def self.report(options)
11
+ TimeSheet.options = options
12
+
11
13
  results = {
12
14
  'entries' => [],
13
15
  'total' => 0.0,
@@ -40,7 +42,7 @@ module TimeSheet::Time
40
42
 
41
43
  unless results['entries'].empty?
42
44
  time = (
43
- (options[:to] || Util.day_end) -
45
+ (options[:to] || Util.day_end) -
44
46
  (options[:from] || results['entries'].first[1].to_time)
45
47
  ).to_i
46
48
  days = (time.to_f / 60 / 60 / 24).round
@@ -65,6 +67,8 @@ module TimeSheet::Time
65
67
  end
66
68
 
67
69
  def self.invoice(options)
70
+ TimeSheet.options = options
71
+
68
72
  grouped = {}
69
73
  Parser.new(options[:location]).entries.each do |e|
70
74
  if e.matches?(options)
@@ -102,7 +106,7 @@ module TimeSheet::Time
102
106
  next
103
107
  end
104
108
  end
105
-
109
+
106
110
  packages.last << row
107
111
  ptotal += row[1]
108
112
  end
@@ -131,4 +135,4 @@ module TimeSheet::Time
131
135
  packages
132
136
  end
133
137
 
134
- end
138
+ end
@@ -1,3 +1,3 @@
1
1
  module TimeSheet
2
- VERSION = "0.15.0"
2
+ VERSION = "1.1.0"
3
3
  end
data/time_sheet.gemspec CHANGED
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.require_paths = ["lib"]
27
27
  spec.required_ruby_version = '>= 3.1.0'
28
28
 
29
- spec.add_dependency 'spreadsheet'
29
+ spec.add_dependency 'roo'
30
30
  spec.add_dependency 'slop'
31
31
  spec.add_dependency 'httpclient'
32
32
 
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: time-sheet
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Moritz Schepp
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-30 00:00:00.000000000 Z
11
+ date: 2025-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: spreadsheet
14
+ name: roo
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -141,7 +141,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
141
141
  - !ruby/object:Gem::Version
142
142
  version: '0'
143
143
  requirements: []
144
- rubygems_version: 3.5.16
144
+ rubygems_version: 3.5.22
145
145
  signing_key:
146
146
  specification_version: 4
147
147
  summary: a time tracking solution based on spreadsheets