timestream 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -2,3 +2,4 @@
2
2
  .bundle
3
3
  Gemfile.lock
4
4
  pkg/*
5
+ .*.swp
@@ -1,8 +1,65 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'rubygems'
4
+
5
+ require 'chronic'
6
+ require 'optparse'
4
7
  require 'timestream/employee_activity_report'
8
+ require 'timestream/hamster'
9
+ require 'timestream/version'
10
+
11
+ options = {}
12
+
13
+ option_parser = OptionParser.new do |option_parser|
14
+ option_parser.banner = "Usage: timestream [--date-format=DATE_FORMAT] [--week YYYY-MM-DD]"
15
+
16
+ options[:date_format] = :day_of_week
17
+ option_parser.on('-d',
18
+ '--date-format DATE_FORMAT',
19
+ "Format dates according to DATE_FORMAT. Supports DATE_FORMAT of 'day_of_week', or 'erp'.") do |date_format|
20
+ options[:date_format] = date_format.to_sym
21
+ end
22
+
23
+ option_parser.on_tail('-h', '--help', 'Show this message') do
24
+ puts option_parser
25
+ exit
26
+ end
27
+
28
+ option_parser.on_tail('--version', 'Show version') do
29
+ puts Timestream::VERSION
30
+ exit
31
+ end
32
+
33
+ option_parser.on('--week WEEK',
34
+ "Read in data from Hamster and write it out to EAR for WEEK " \
35
+ "beginning on Saturday. Use Chronic gem for parsing WEEK, " \
36
+ "so you do strings like 'last saturday'") do |week|
37
+ time = Chronic.parse(week, :context => :past)
38
+ options[:saturday] = Date.new(time.year, time.month, time.day)
39
+ end
40
+ end
41
+
42
+ option_parser.parse!
5
43
 
6
44
  employee_activity_report = Timestream::EmployeeActivityReport.new ARGV[0]
7
45
 
8
- puts employee_activity_report.to_text_table
46
+ DAYS_PER_WEEK = 7
47
+ HOURS_PER_DAY = 24
48
+ MINUTES_PER_HOUR = 60
49
+ SECONDS_PER_MINUTE = 60
50
+ HOURS_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY
51
+ MINUTES_PER_WEEK = HOURS_PER_WEEK * MINUTES_PER_HOUR
52
+ SECONDS_PER_WEEK = MINUTES_PER_WEEK * SECONDS_PER_MINUTE
53
+
54
+ if options[:saturday]
55
+ saturday_date = options[:saturday]
56
+ saturday_time = Time.local(saturday_date.year, saturday_date.month, saturday_date.day)
57
+ # next saturday - 1 second for 23:59 on Friday
58
+ friday_time = saturday_time + (SECONDS_PER_WEEK) - 1
59
+ hamster = Timestream::Hamster.new(saturday_time, friday_time)
60
+
61
+ employee_activity_report.updateFromHamster!(hamster)
62
+ end
63
+
64
+ puts employee_activity_report.to_text_table(options[:date_format])
65
+
@@ -4,25 +4,33 @@ require 'terminal-table/import'
4
4
 
5
5
  module Timestream
6
6
  class EmployeeActivityReport
7
- class Cell
8
- attr_reader :column
9
-
10
- def initialize(row, column)
11
- @column = column
12
- @row = row
13
- end
14
-
15
- attr_reader :row
16
- end
7
+ ACTIVITY_COLUMN = 2
17
8
 
18
- def cell(location)
19
- worksheet.row(location.row)[location.column]
20
- end
21
-
22
- DATE_CELL = Cell.new(2, 1)
9
+ DATE_COLUMN = 1
10
+ DATE_ROW = 2
23
11
 
24
12
  DAY_COLUMN_SPAN = 3
25
13
 
14
+ def date_column(date)
15
+ slot = date_slot(date)
16
+ slot_column(slot)
17
+ end
18
+
19
+ def date_slot(date)
20
+ day_delta = (date - saturday).to_i
21
+
22
+ case day_delta
23
+ # Saturday and Sunday map to weekend
24
+ when 0..1
25
+ 0
26
+ # All others map to their own day
27
+ when 2..6
28
+ day_delta - 1
29
+ else
30
+ raise ArgumentError, "#{date} is not in the same week as #{saturday}"
31
+ end
32
+ end
33
+
26
34
  def initialize(path)
27
35
  @path = path
28
36
  end
@@ -33,6 +41,14 @@ module Timestream
33
41
  worksheet.row(index)
34
42
  end
35
43
 
44
+ def saturday
45
+ @saturday ||= worksheet.row(DATE_ROW)[DATE_COLUMN]
46
+ end
47
+
48
+ def slot_column(slot)
49
+ 9 + (slot * DAY_COLUMN_SPAN)
50
+ end
51
+
36
52
  def spreadsheet
37
53
  @spreadsheet ||= Spreadsheet.open path
38
54
  end
@@ -79,13 +95,26 @@ module Timestream
79
95
  @timestream_by_project_number
80
96
  end
81
97
 
82
- def to_text_table
98
+ def to_text_table(date_format=:day_of_week)
83
99
  employee_activity_report = self
100
+
84
101
  table {
85
102
  self.headings = ['Project Number']
86
- self.headings.concat Timestream::SLOT_NAMES.collect { |name|
87
- name.to_s.capitalize
88
- }
103
+
104
+ case date_format
105
+ when :day_of_week
106
+ date_format = "%A"
107
+ when :erp
108
+ date_format = "%a %m/%d"
109
+ end
110
+
111
+ Timestream::SLOT_NAMES.each_index do |sunday_offset|
112
+ # XXX assign weekend activity to sunday to simplify math
113
+ sunday = employee_activity_report.saturday + 1
114
+ slot_date = sunday + sunday_offset
115
+
116
+ self.headings << slot_date.strftime(date_format)
117
+ end
89
118
 
90
119
  employee_activity_report.timestream_by_project_number.each do |project_number, timestream|
91
120
  row = [project_number]
@@ -98,6 +127,37 @@ module Timestream
98
127
  }
99
128
  end
100
129
 
130
+ def updateFromHamster(hamster)
131
+ # Categories in Hamster are mapped to Activities in EAR
132
+
133
+ hamster.timestream_by_category.each do |category, timestream|
134
+ # XXX should this been creating EAR Timestreams instead of updating rows directly?
135
+ (12 .. 36).each do |row_index|
136
+ row = self.row(row_index)
137
+ activity = row[ACTIVITY_COLUMN]
138
+
139
+ # XXX Category is assumed to be a substring of Activity since Activity is very wordy.
140
+ if activity and activity.include? category
141
+ timestream.each do |date, hours|
142
+ column = self.date_column(date)
143
+ row[column] ||= 0
144
+ row[column] += hours
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ def updateFromHamster!(hamster)
152
+ updateFromHamster(hamster)
153
+
154
+ # spreadsheet library recommends not writing back to same file, write to
155
+ # temporary file and then rename that file to original.
156
+ temporary_path = "#{path}.timestream"
157
+ spreadsheet.write(temporary_path)
158
+ File.rename(temporary_path, path)
159
+ end
160
+
101
161
  def worksheet
102
162
  @worksheet ||= spreadsheet.worksheet "EAR - Current Week"
103
163
  end
@@ -0,0 +1,101 @@
1
+ require 'rubygems'
2
+
3
+ require 'date'
4
+ require 'dbus'
5
+ require 'terminal-table/import'
6
+ require 'time'
7
+
8
+ module Timestream
9
+ class Hamster
10
+ class Fact
11
+ attr_reader :category_name
12
+ attr_reader :date
13
+ attr_reader :delta_in_seconds
14
+
15
+ def initialize(id, start_timestamp, end_timestamp, description, activity_name, activity_id, category_name, tag_list, date_timestamp, delta_in_seconds)
16
+ @id = id
17
+
18
+ # Convert start and end to Times
19
+ @start_time = Time.at(start_timestamp)
20
+ @end_time = Time.at(end_timestamp)
21
+
22
+ @description = description
23
+ @activity_name = activity_name
24
+ @activity_id = activity_id
25
+ @category_name = category_name
26
+ @tag_list = tag_list
27
+
28
+ # Convert date to a Date
29
+ date_time = Time.at(date_timestamp)
30
+ date = Date.new(date_time.year, date_time.month, date_time.day)
31
+ @date = date
32
+
33
+ @delta_in_seconds = delta_in_seconds
34
+ end
35
+ end
36
+
37
+ def facts
38
+ dbus_facts = proxy.GetFacts(@start_time.to_i, @end_time.to_i, '')
39
+ # XXX for some reason GetFacts returns facts in a one-element array
40
+ dbus_facts = dbus_facts[0]
41
+
42
+ # convert dbus facts into instances
43
+ dbus_facts.collect { |dbus_fact|
44
+ Fact.new(*dbus_fact)
45
+ }
46
+ end
47
+
48
+ def initialize(start_time, end_time)
49
+ @start_time = start_time
50
+ @end_time = end_time
51
+ end
52
+
53
+ def proxy
54
+ @proxy ||= begin
55
+ session_bus = DBus::SessionBus.instance
56
+ hamster_service = session_bus.service('org.gnome.Hamster')
57
+ hamster = hamster_service.object('/org/gnome/Hamster')
58
+ # interfaces and therefore methods won't work if not first introspected
59
+ hamster.introspect
60
+ # set interface so methods can be called directly on object
61
+ hamster.default_iface = 'org.gnome.Hamster'
62
+
63
+ hamster
64
+ end
65
+ end
66
+
67
+ class Timestream
68
+ def [](date)
69
+ @hours_by_date[date]
70
+ end
71
+
72
+ def []=(date, hours)
73
+ @hours_by_date[date] = hours
74
+ end
75
+
76
+ def initialize
77
+ @hours_by_date = Hash.new(0)
78
+ end
79
+
80
+ def each(&block)
81
+ @hours_by_date.sort.each(&block)
82
+ end
83
+ end
84
+
85
+ def timestream_by_category
86
+ if @timestream_by_category.nil?
87
+ @timestream_by_category = Hash.new { |hash, category|
88
+ hash[category] = Timestream.new
89
+ }
90
+
91
+ facts.each do |fact|
92
+ timestream = @timestream_by_category[fact.category_name]
93
+ # convert seconds to fractional hours
94
+ timestream[fact.date] += fact.delta_in_seconds.fdiv(60 * 60)
95
+ end
96
+ end
97
+
98
+ @timestream_by_category
99
+ end
100
+ end
101
+ end
@@ -1,3 +1,3 @@
1
1
  module Timestream
2
- VERSION = "0.0.3"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -14,6 +14,8 @@ Gem::Specification.new do |s|
14
14
 
15
15
  s.rubyforge_project = "timestream"
16
16
 
17
+ s.add_dependency("chronic")
18
+ s.add_dependency("ruby-dbus")
17
19
  s.add_dependency("spreadsheet")
18
20
  s.add_dependency("terminal-table")
19
21
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timestream
3
3
  version: !ruby/object:Gem::Version
4
- hash: 25
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
+ - 1
8
9
  - 0
9
- - 3
10
- version: 0.0.3
10
+ version: 0.1.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Luke Imhoff
@@ -15,11 +15,11 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-05-12 00:00:00 -05:00
18
+ date: 2011-05-16 00:00:00 -05:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
- name: spreadsheet
22
+ name: chronic
23
23
  prerelease: false
24
24
  requirement: &id001 !ruby/object:Gem::Requirement
25
25
  none: false
@@ -33,7 +33,7 @@ dependencies:
33
33
  type: :runtime
34
34
  version_requirements: *id001
35
35
  - !ruby/object:Gem::Dependency
36
- name: terminal-table
36
+ name: ruby-dbus
37
37
  prerelease: false
38
38
  requirement: &id002 !ruby/object:Gem::Requirement
39
39
  none: false
@@ -46,6 +46,34 @@ dependencies:
46
46
  version: "0"
47
47
  type: :runtime
48
48
  version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: spreadsheet
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :runtime
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: terminal-table
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ type: :runtime
76
+ version_requirements: *id004
49
77
  description: Converts Hamster report to EAR to JDE Timecard
50
78
  email:
51
79
  - luke@cray.com
@@ -62,6 +90,7 @@ files:
62
90
  - bin/timestream
63
91
  - lib/timestream.rb
64
92
  - lib/timestream/employee_activity_report.rb
93
+ - lib/timestream/hamster.rb
65
94
  - lib/timestream/version.rb
66
95
  - timestream.gemspec
67
96
  has_rdoc: true