worked 0.1.0 → 0.2.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.
@@ -4,24 +4,14 @@ require 'worked/inputgrammar'
4
4
 
5
5
  class Treetop::Runtime::SyntaxNode
6
6
 
7
+ attr_reader :from, :to, :total
8
+
7
9
  # Remove all clutter ('hour', ':', etc) from the
8
10
  # time and turn into an integer
9
11
  def to_i
10
12
  text_value.gsub(/\D/, '').to_i
11
13
  end
12
14
 
13
- def from
14
- time_to_seconds_from_day(DateTime.now - total)
15
- end
16
-
17
- def to
18
- time_to_seconds_from_day(DateTime.now)
19
- end
20
-
21
- def time_to_seconds_from_day t
22
- t.hour.hours + t.min.minutes
23
- end
24
-
25
15
  # Default value so we don't have to care whether
26
16
  # there really is a minute component
27
17
  def minutes
@@ -29,22 +19,44 @@ class Treetop::Runtime::SyntaxNode
29
19
  end
30
20
  end
31
21
 
22
+ class InputGrammarParser
23
+ def initialize
24
+ @root = nil
25
+ end
26
+ end
27
+
28
+ class CannotParse < Exception ; end
29
+
32
30
  class InputParser
33
31
 
34
- def self.parse source
32
+ def self.parse(source, now = DateTime.now)
35
33
 
36
- root = InputGrammarParser.new.parse(source)
34
+ from, to, total, activity = parse_input source
37
35
 
38
- from, to = root.time.from, root.time.to
36
+ from ||= time_in_seconds_from_day(now - total)
37
+ to ||= time_in_seconds_from_day(now)
39
38
 
40
39
  if from > to
41
40
  from -= 24.hours
42
41
  end
43
42
 
44
- now = DateTime.now
45
43
  midnight = DateTime.new(now.year, now.month, now.day)
46
44
 
47
- [midnight + from.seconds, midnight + to.seconds, root.activity.text_value]
45
+ [midnight + from.seconds, midnight + to.seconds, activity]
46
+ end
47
+
48
+ private
49
+
50
+ def self.parse_input source
51
+ root = InputGrammarParser.new.parse(source)
52
+
53
+ raise CannotParse unless root
54
+
55
+ [root.time.from, root.time.to, root.time.total, root.activity.text_value]
56
+ end
57
+
58
+ def self.time_in_seconds_from_day t
59
+ t.hour.hours + t.min.minutes
48
60
  end
49
61
  end
50
62
 
@@ -0,0 +1,74 @@
1
+ require 'worked/inputparser'
2
+
3
+ class Date
4
+ def week_of_year
5
+ strftime("%U").to_i + 1
6
+ end
7
+ end
8
+
9
+ module Worked
10
+
11
+ class Entry < Struct.new(:hours, :date)
12
+ def week_of_year
13
+ date.week_of_year
14
+ end
15
+
16
+ def <=> other
17
+ self.date <=> other.date
18
+ end
19
+ end
20
+
21
+ class Reader
22
+
23
+ def self.by_week data
24
+ sort create_entries merge_by_week read_all_entries data
25
+ end
26
+
27
+ def self.read data
28
+ sort create_entries merge_by_day read_all_entries data
29
+ end
30
+
31
+ private
32
+
33
+ def self.extract_times line
34
+ from, to, rest = line.split
35
+
36
+ [DateTime.parse(from), DateTime.parse(to)]
37
+ end
38
+
39
+ def self.read_all_entries data
40
+ data.to_a.collect do |line|
41
+ from, to = extract_times line
42
+ Entry.new((to - from) * 24, from.to_date)
43
+ end
44
+ end
45
+
46
+ def self.merge_by_week entries
47
+ merge_entries(entries) {|e| e.week_of_year}
48
+ end
49
+
50
+ def self.merge_by_day entries
51
+ merge_entries(entries) {|e| e.date}
52
+ end
53
+
54
+ def self.merge_entries entries
55
+ merged = Hash.new(0)
56
+
57
+ entries.each do |entry|
58
+ merged[yield(entry)] += entry.hours
59
+ end
60
+
61
+ merged
62
+ end
63
+
64
+ def self.create_entries entries
65
+ entries.collect do |date, hours|
66
+ Entry.new(hours, date)
67
+ end
68
+ end
69
+
70
+ def self.sort entries
71
+ entries.sort
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,4 @@
1
+ desc 'Generate Treetop Parser'
2
+ task :parser_generate do
3
+ sh %{ tt lib/worked/inputgrammar.treetop }
4
+ end
@@ -3,34 +3,34 @@ require File.dirname(__FILE__) + '/test_helper.rb'
3
3
  class TestGrammar < Test::Unit::TestCase
4
4
 
5
5
  def test_hours
6
- assert_equal 5, hours("5 hours")
7
- assert_equal 5, hours("5hours")
8
- assert_equal 5, hours("5 h")
9
- assert_equal 5, hours("5h")
10
- assert_equal 5, hours("5")
6
+ assert_equal 5.hours, hours("5 hours")
7
+ assert_equal 5.hours, hours("5hours")
8
+ assert_equal 5.hours, hours("5 h")
9
+ assert_equal 5.hours, hours("5h")
10
+ assert_equal 5.hours, hours("5")
11
11
  end
12
12
 
13
13
  def test_minutes
14
- assert_equal 10, minutes("5 h 10 minutes")
15
- assert_equal 10, minutes("5 h 10 m")
16
- assert_equal 10, minutes("5 h 10m")
17
- assert_equal 10, minutes("5h10m")
18
- assert_equal 10, minutes("5:10")
14
+ assert_equal 10.minutes, minutes("5 h 10 minutes")
15
+ assert_equal 10.minutes, minutes("5 h 10 m")
16
+ assert_equal 10.minutes, minutes("5 h 10m")
17
+ assert_equal 10.minutes, minutes("5h10m")
18
+ assert_equal 10.minutes, minutes("5:10")
19
19
  end
20
20
 
21
21
  def test_hour_ranges
22
- assert_equal 4.hours, range("13 to 17")
23
- assert_equal 4.hours, range("1 to 5")
24
- assert_equal 4.hours, range("1pm to 5pm")
25
- assert_equal 4.hours, range("1 pm to 5 pm")
26
- assert_equal 16.hours, range("1 am to 5 pm")
27
- assert_equal 2.hours, range("11 to 1pm")
28
- assert_equal 2.hours, range("11pm to 1am")
22
+ assert_equal 4.hours, hours("13 to 17")
23
+ assert_equal 4.hours, hours("1 to 5")
24
+ assert_equal 4.hours, hours("1pm to 5pm")
25
+ assert_equal 4.hours, hours("1 pm to 5 pm")
26
+ assert_equal 16.hours, hours("1 am to 5 pm")
27
+ assert_equal 2.hours, hours("11 to 1pm")
28
+ assert_equal 2.hours, hours("11pm to 1am")
29
29
  end
30
30
 
31
31
  def test_hour_minute_ranges
32
- assert_equal 4.hours + 30.minutes, range("1 to 5:30")
33
- assert_equal 4.hours + 10.minutes, range("13:30 to 17:40")
32
+ assert_equal 4.hours + 30.minutes, hours("1 to 5:30")
33
+ assert_equal 4.hours + 10.minutes, hours("13:30 to 17:40")
34
34
  end
35
35
 
36
36
  def test_with_activity
@@ -39,30 +39,24 @@ class TestGrammar < Test::Unit::TestCase
39
39
  end
40
40
 
41
41
  def test_from_time
42
- assert_in_delta 2.hours, range("from #{DateTime.now.hour - 2}:#{DateTime.now.min}"), 10 #seconds of delta
42
+ assert_in_delta 2.hours, hours("from #{DateTime.now.hour - 2}:#{DateTime.now.min}"), 10.seconds #of delta
43
43
  end
44
44
 
45
45
  def activity source
46
46
  parse(source).last
47
47
  end
48
48
 
49
- def range source
50
- res = parse("#{source} on X")
51
- ((res[1] - res[0]) * 24).hours
52
- end
53
-
54
49
  def hours source
55
50
  res = parse("#{source} on X")
56
- res[1].hour - res[0].hour
51
+ ((res[1] - res[0]) * 24).hours
57
52
  end
58
53
 
59
54
  def minutes source
60
- res = parse("#{source} on X")
61
- res[1].min - res[0].min
55
+ hours(source) % 1.hour
62
56
  end
63
57
 
64
58
  def parse source
65
- InputParser.parse(source)
59
+ InputParser.parse(source, DateTime.now )
66
60
  end
67
61
 
68
62
  end
@@ -0,0 +1,101 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+ require 'worked/reader'
3
+ require 'mathn'
4
+
5
+ class TestReader < Test::Unit::TestCase
6
+
7
+ def test_single_day
8
+ data = <<-EOS
9
+ 2009-04-21T21:26:00+00:00 2009-04-21T22:26:00+00:00 fixed bugs
10
+ EOS
11
+
12
+ recorded = Worked::Reader.read(data)
13
+
14
+ assert_equal 17, recorded[0].week_of_year
15
+ assert_equal 1, recorded[0].hours
16
+ end
17
+
18
+ def test_single_day_two_records
19
+ data = <<-EOS
20
+ 2009-04-21T21:26:00+00:00 2009-04-21T22:26:00+00:00 fixed bugs
21
+ 2009-04-21T23:26:00+00:00 2009-04-21T23:46:00+00:00 fixed bugs
22
+ EOS
23
+
24
+ recorded = Worked::Reader.read(data)
25
+
26
+ assert_equal 4/3, recorded[0].hours
27
+ end
28
+
29
+ def test_multiple_days
30
+ data = <<-EOS
31
+ 2009-04-21T21:00:00+00:00 2009-04-21T22:30:00+00:00 fixed bugs
32
+ 2009-04-24T17:00:00+00:00 2009-04-24T23:30:00+00:00 fixed bugs
33
+ EOS
34
+
35
+ recorded = Worked::Reader.read(data)
36
+
37
+ assert_equal 3/2, recorded[0].hours
38
+ assert_equal 13/2, recorded[1].hours
39
+ end
40
+
41
+ def test_multiple_weeks
42
+ data = <<-EOS
43
+ 2009-04-21T21:00:00+00:00 2009-04-21T22:30:00+00:00 fixed bugs
44
+ 2009-05-24T17:00:00+00:00 2009-05-24T23:30:00+00:00 fixed bugs
45
+ EOS
46
+
47
+ recorded = Worked::Reader.read(data)
48
+
49
+ assert_equal 17, recorded[0].week_of_year
50
+ assert_equal 22, recorded[1].week_of_year
51
+ end
52
+
53
+ def test_januar_first
54
+ data = <<-EOS
55
+ 2009-01-01T00:00:00+00:00 2009-01-1T22:00:00+00:00 fixed bugs
56
+ EOS
57
+
58
+ recorded = Worked::Reader.read(data)
59
+
60
+ assert_equal 1, recorded[0].week_of_year
61
+ assert_equal 22, recorded[0].hours
62
+ end
63
+
64
+ def test_worked_over_midnight
65
+ data = <<-EOS
66
+ 2009-01-01T23:00:00+00:00 2009-01-02T02:00:00+00:00 fixed bugs
67
+ EOS
68
+
69
+ recorded = Worked::Reader.read(data)
70
+
71
+ assert_equal 3, recorded[0].hours
72
+ end
73
+
74
+ def test_multiple_weeks_2
75
+ data = <<-EOS
76
+ 2009-04-21T21:00:00+00:00 2009-04-21T22:30:00+00:00 fixed bugs
77
+ 2009-06-21T21:00:00+00:00 2009-06-21T22:30:00+00:00 fixed bugs
78
+ 2009-05-24T17:00:00+00:00 2009-05-24T23:30:00+00:00 fixed bugs
79
+ EOS
80
+
81
+ recorded = Worked::Reader.read(data)
82
+
83
+ assert_equal 17, recorded[0].week_of_year
84
+ assert_equal 22, recorded[1].week_of_year
85
+ assert_equal 26, recorded[2].week_of_year
86
+ end
87
+
88
+ def test_merge_by_week
89
+ data = <<-EOS
90
+ 2009-04-21T21:00:00+00:00 2009-04-21T22:00:00+00:00 fixed bugs
91
+ 2009-04-22T21:00:00+00:00 2009-04-22T22:00:00+00:00 fixed bugs
92
+ 2009-05-23T21:00:00+00:00 2009-05-23T22:00:00+00:00 fixed bugs
93
+ 2009-05-23T21:00:00+00:00 2009-05-23T22:00:00+00:00 fixed bugs
94
+ EOS
95
+
96
+ recorded = Worked::Reader.by_week(data)
97
+
98
+ assert_equal 2, recorded[0].hours
99
+ assert_equal 2, recorded[1].hours
100
+ end
101
+ end
@@ -1,5 +1,6 @@
1
1
  require File.join(File.dirname(__FILE__), "test_helper.rb")
2
2
  require 'tempfile'
3
+ require 'fileutils'
3
4
  require 'enumerator'
4
5
  require 'active_support'
5
6
  require 'worked/cli'
@@ -14,12 +15,16 @@ class TestWorkedCli < Test::Unit::TestCase
14
15
  reset_stdout
15
16
  end
16
17
 
18
+ def teardown
19
+ @file.unlink
20
+ end
21
+
17
22
  def reset_stdout
18
23
  @stdout_io = StringIO.new
19
24
  end
20
25
 
21
26
  def run_with argument
22
- Worked::CLI.execute(@stdout_io, argument, @file)
27
+ Worked::CLI.execute(@stdout_io, argument.split, @file)
23
28
  @file.flush
24
29
  end
25
30
 
@@ -29,6 +34,20 @@ class TestWorkedCli < Test::Unit::TestCase
29
34
  end
30
35
  end
31
36
 
37
+ def stdio
38
+ @stdout_io.string
39
+ end
40
+
41
+ def test_graph
42
+ graph_file = Tempfile.new("graph").path + ".png"
43
+
44
+ run_with " -g #{graph_file}"
45
+
46
+ assert_equal "Graph saved to #{graph_file}.\n", stdio
47
+
48
+ FileUtils::remove graph_file
49
+ end
50
+
32
51
  def test_with_new_file
33
52
  @file.open
34
53
  run_with "5 hours on coding"
@@ -42,7 +61,7 @@ class TestWorkedCli < Test::Unit::TestCase
42
61
  assert "coding", first[1]
43
62
  assert "documentation and refactoring", second[1]
44
63
 
45
- assert_equal "", @stdout_io.read
64
+ assert_equal "", stdio
46
65
 
47
66
  reset_stdout
48
67
 
@@ -54,6 +73,6 @@ class TestWorkedCli < Test::Unit::TestCase
54
73
  assert 1.hours, third[0]
55
74
  assert "refactoring", third[1]
56
75
 
57
- assert_equal "", @stdout_io.read
76
+ assert_equal "", stdio
58
77
  end
59
78
  end
@@ -0,0 +1,120 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
2
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
4
+ <head>
5
+ <link rel="stylesheet" href="stylesheets/screen.css" type="text/css" media="screen" />
6
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
7
+ <title>
8
+ Worked
9
+ </title>
10
+ <script src="javascripts/rounded_corners_lite.inc.js" type="text/javascript"></script>
11
+ <style>
12
+
13
+ </style>
14
+ <script type="text/javascript">
15
+ window.onload = function() {
16
+ settings = {
17
+ tl: { radius: 10 },
18
+ tr: { radius: 10 },
19
+ bl: { radius: 10 },
20
+ br: { radius: 10 },
21
+ antiAlias: true,
22
+ autoPad: true,
23
+ validTags: ["div"]
24
+ }
25
+ var versionBox = new curvyCorners(settings, document.getElementById("version"));
26
+ versionBox.applyCornersToAll();
27
+ }
28
+ </script>
29
+ </head>
30
+ <body>
31
+ <div id="main">
32
+
33
+ <h1>Worked</h1>
34
+ <div id="version" class="clickable" onclick='document.location = "http://rubyforge.org/projects/worked"; return false'>
35
+ <p>Get Version</p>
36
+ <a href="http://rubyforge.org/projects/worked" class="numbers">0.2.0</a>
37
+ </div>
38
+ <ul>
39
+ <li>http://worked.rubyforge.org/</li>
40
+ </ul>
41
+ <p>The easiest time recording application imagineable.</p>
42
+ <h2><span class="caps">DESCRIPTION</span>:</h2>
43
+ <p>Worked is really the simplest time recording (or tracking, whatever term you<br />
44
+ prefer) application that does all you need and nothing more (only `date &gt;&gt; file` is easier).</p>
45
+ <p>It simply takes a time or a time range and an activity and appends it to a<br />
46
+ hidden file in the current directory. The use case is, that you have &#8216;.worked&#8217;<br />
47
+ files in the root directories of your projects, and before you finish your<br />
48
+ current work period, you probably commit something and you also record your time.</p>
49
+ <p>Right now, the times are just recorded, no report generation yet.</p>
50
+ <h2><span class="caps">FEATURES</span>:</h2>
51
+ <ul>
52
+ <li>Appends a time range and an activity to a file.</li>
53
+ <li>Create a graph from the collected data, showing hours/week.</li>
54
+ </ul>
55
+ <h2><span class="caps">SYNOPSIS</span>:</h2>
56
+ <p>The syntax is quite flexible, and if you&#8217;d like to have it extended, just drop<br />
57
+ me an email. The input consists of: &lt;time&gt; on &lt;activity&gt;. So for example, you<br />
58
+ can write &#8216;5 hours on documentation&#8217;. The following formats for the &lt;time&gt; part<br />
59
+ are recognized:</p>
60
+ <ul>
61
+ <li>5 hours</li>
62
+ <li>5h</li>
63
+ <li>or simply &#8216;5&#8217;</li>
64
+ </ul>
65
+ <p>Minutes can be added or standing alone as well:</p>
66
+ <ul>
67
+ <li>5 h 10 minutes</li>
68
+ <li>5 hours 10 m</li>
69
+ <li>30 minutes</li>
70
+ <li>or simply &#8216;5:10&#8217;</li>
71
+ </ul>
72
+ <p>Stored is not the absolute time, but the point &lt;time&gt; hours ago and the current<br />
73
+ time.</p>
74
+ <p>Of course you can also specify both start- and end time of an activity with the<br />
75
+ following syntax:</p>
76
+ <ul>
77
+ <li>13 to 17</li>
78
+ <li>1 pm to 5 pm</li>
79
+ <li>13:30 to 17:40</li>
80
+ </ul>
81
+ <p>If you just want to record an activity from a certain point upto now, you can<br />
82
+ write:</p>
83
+ <ul>
84
+ <li>from 5pm</li>
85
+ <li>from 11:30</li>
86
+ </ul>
87
+ <p>Specifying dates is not supported right now, but you can of course edit the file<br />
88
+ directly if necessary.</p>
89
+ <p>Since version 0.2, worked also has the ability to generate a graph from the<br />
90
+ collected data, using the -g argument. [<span class="caps">TODO</span>: Add image here.]</p>
91
+ <h2><span class="caps">STORY</span>:</h2>
92
+ <p>The week before my new semester started, I promised to myself to do better (or,<br />
93
+ to do it at all) time recording to get a better feeling for how much I actually<br />
94
+ worked. I used to write my time into a wiki, but that was still too tedious to<br />
95
+ do so I often neglected it and had to record it from memory some days later. So I<br />
96
+ needed a tool and a few hours later, &#8216;worked&#8217; was born.</p>
97
+ <h2><span class="caps">REQUIREMENTS</span>:</h2>
98
+ <ul>
99
+ <li>Uses the really great Treetop for the input parser.</li>
100
+ </ul>
101
+ <h2><span class="caps">INSTALL</span>:</h2>
102
+ <ul>
103
+ <li>sudo gem install worked</li>
104
+ </ul>
105
+ <h2><span class="caps">TODO</span>:</h2>
106
+ <ul>
107
+ <li>Simple report generation.</li>
108
+ <li>Investigate if it can be used together with git (e.g. with hooks), so I can, for example, use the last line in a commit message to make my &#8216;worked&#8217; statement.</li>
109
+ </ul>
110
+ <p class="coda">
111
+ <a href="me@misto.ch">Mirko Stocker</a>, 12th June 2009<br>
112
+ Theme extended from <a href="http://rb2js.rubyforge.org/">Paul Battley</a>,
113
+ by Daniel Cadenas via <a href="http://depgraph.rubyforge.org/">DepGraph</a>
114
+ </p>
115
+ </div>
116
+
117
+ <!-- insert site tracking codes here, like Google Urchin -->
118
+
119
+ </body>
120
+ </html>