worked 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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>