time_tree 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.swp
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm --create use 1.9.3@time_log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in time_tree.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Andy White
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # Timetree
2
+
3
+ Timetree is a command line utility that prints a tree-like breakdown of time spent on activities based on a simple, human readable time logging language stored in plain text files.
4
+
5
+ ## Installation
6
+
7
+ $ gem install time_tree
8
+
9
+ ## Time Log Format
10
+
11
+ Each day's log should start with a date in YYYY/MM/DD format. Any text following is ignored as comments. Following this are lines starting with a time in 24 hour format - HHMI, followed by an activity which must contain no spaces, any text following is ignored and considered as comments. Activities may have nested subcategories to any level separated by forward slashes - these will be printed as a hierarchy by timetree. Use a dash (-) for the activity to be ignored and not reported on - this means a day's log must always end with a time followed by a dash to denote when the previous line's activity finished.
12
+
13
+ Blank lines are ignored and lines starting with # are ignored as comments.
14
+
15
+ Here's an example:
16
+
17
+ 2013/04/21 A splendid day!
18
+
19
+ 0930 admin some comments
20
+ 0945 development/project1 some more comments
21
+ 1005 -
22
+ 1030 developemnt/project2
23
+ 1115 -
24
+
25
+ # Did some work in the evening!
26
+ 2230 bugfixing/project3 # Use a hash after a time line if you want but not needed
27
+ 2315 -
28
+
29
+
30
+ A day's log can't span across more than one file, but a file may contain multiple day's logs. This gives flexibility, all time may be stored in one big file or there may be multiple files, one for each day or week for example. Multiple files may be stored in a nested folder hierarchy - time tree will recursively search for files, ignoring those starting with . (dot).
31
+
32
+ ## Usage
33
+
34
+ $ timetree [options] [path]
35
+
36
+ ### Options
37
+
38
+ Reports on the given date or period. Weeks are deemed to start on Monday.
39
+
40
+ * *--all*, *-a* - include all dates recorded
41
+ * *--today*, *-t* - this is the assumed default
42
+ * *--yesterday*, *-y*
43
+ * *--week [weeks-previous]*, *-w* - the current week so far. To report on previous weeks use the optional *weeks-previous* argument, 0 (the assumed default) would mean the current week, 1 would mean last week, 2 would mean the week previous to that and so on.
44
+ * *--month [months-previous]*, *-m* - the current calendar month so far. The optional *months-revious* argument works in a similar way to *--week weeks-previous*
45
+ * *--date YYYY/MM/DD*, *-d*
46
+ * *--between YYYY/MM/DD:YYYY/MM/DD*, *-b* - reports on the given inclusive range of dates
47
+ * *--filter search-string[,search-string]*, *-f* - only reports on activities matching *search-string*
48
+
49
+ If given no path, Timetree will look in the users's home directory for a file or directory called *Time*, *time* or *.time*.
50
+
51
+ ## Contributing
52
+
53
+ 1. Fork it
54
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
55
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
56
+ 4. Push to the branch (`git push origin my-new-feature`)
57
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/timetree ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.expand_path(File.join('..', '..', 'lib'), __FILE__)
3
+ require 'time_tree'
4
+ require 'optparse'
5
+
6
+ module TimeTree
7
+ options = {:today => true}
8
+
9
+ option_parser = OptionParser.new do |opts|
10
+ opts.banner = "Usage: #{File.basename($0)} [options] [data_path]"
11
+
12
+ opts.on('-a', '--all') do
13
+ options[:all] = true
14
+ options[:today] = false
15
+ end
16
+
17
+ opts.on('-t', '--today') do
18
+ options[:all] = false
19
+ options[:today] = true
20
+ end
21
+
22
+ opts.on('-y', '--yesterday') do
23
+ options[:yesterday] = true
24
+ options[:all] = false
25
+ options[:today] = false
26
+ end
27
+
28
+ opts.on('-w [WEEKS_PREVIOUS]', '--week', 'The current week is the default') do |weeks_previous|
29
+ options[:week] = weeks_previous.to_i || 0
30
+ options[:all] = false
31
+ options[:today] = false
32
+ end
33
+
34
+ opts.on('-m [MONTHS_PREVIOUS]', '--month', 'The current month is the default') do |months_previous|
35
+ options[:month] = months_previous.to_i || 0
36
+ options[:all] = false
37
+ options[:today] = false
38
+ end
39
+
40
+ opts.on('-d DATE', '--date', 'Date must be of format YYYY/MM/DD') do |date|
41
+ if date =~ /\d\d\d\d\/\d\d\/\d\d/
42
+ options[:date] = date
43
+ options[:all] = false
44
+ options[:today] = false
45
+ else
46
+ puts "Invalid date format. Must be YYYY/MM/DD"
47
+ exit 1
48
+ end
49
+ end
50
+
51
+ opts.on('-r RANGE', '--range', 'Range must be of format YYYY/MM/DD:YYYY/MM/DD') do |range|
52
+ if range =~ /\d\d\d\d\/\d\d\/\d\d:\d\d\d\d\/\d\d\/\d\d/
53
+ options[:range] = range
54
+ options[:all] = false
55
+ options[:today] = false
56
+ else
57
+ puts "Invalid date range. Must be YYYY/MM/DD:YYYY/MM/DD"
58
+ exit 1
59
+ end
60
+ end
61
+
62
+ opts.on('-f SEARCH', '--filter') do |search|
63
+ options[:filter] = search.split(',')
64
+ end
65
+ end
66
+
67
+ option_parser.parse!
68
+ tree = ActivityTree.new
69
+ parser = FileParser.new(tree, options)
70
+
71
+ if ARGV[0]
72
+ parser.process_file(ARGV[0])
73
+ else
74
+ home = ENV['HOME']
75
+ path = parser.find_path([File.join(home, 'Time'), File.join(home, 'time'),
76
+ File.join(home, '.time')])
77
+ parser.process_file(path) if path
78
+ end
79
+
80
+ if parser.valid?
81
+ tree.process
82
+ tree.print
83
+ exit 0
84
+ else
85
+ STDERR.puts "ERRORS:"
86
+ STDERR.puts parser.errors.join("\n")
87
+ exit 1
88
+ end
89
+ end
@@ -0,0 +1 @@
1
+ foo
data/fixtures/real.txt ADDED
@@ -0,0 +1,17 @@
1
+ 1975/06/01
2
+ 0755 i/bug - failing article filtering - on train
3
+ 0825 -
4
+ 0910 i/bug
5
+ 0925 i/sysadm - fixing dead teamcity with Al
6
+ 0955 i/bug
7
+ 1025 i/adm emails
8
+ 1040 i/adm weekly update meeting
9
+ 1110 i/sysadm - learning how to do end of sprint release
10
+ 1135 i/adm tech scrum
11
+ 1200 i/bug/1760 optimise edit schedules page
12
+ 1255 - lunch
13
+ 1355 i/adm listening in on user meeting
14
+ 1405 i/sysadm release to live & sanity test
15
+ 1505 i/bug/1760
16
+ 1750 i/adm log time
17
+ 1800 -
@@ -0,0 +1,3 @@
1
+ 1975/06/01
2
+ 1200 jun1stuff
3
+ 1201 -
@@ -0,0 +1,3 @@
1
+ 1975/06/02
2
+ 1100 jun2stuff
3
+ 1102 -
@@ -0,0 +1,3 @@
1
+ 1975/06/03
2
+ 1000 jun3stuff
3
+ 1003 -
data/fixtures/time.txt ADDED
@@ -0,0 +1,3 @@
1
+ 1975/06/02
2
+ 1205 jun2stuff a comment
3
+ 1235 -
@@ -0,0 +1,36 @@
1
+ module TimeTree
2
+ class ActivityTree
3
+ attr_reader :activities, :output
4
+
5
+ def initialize
6
+ @activities = {}
7
+ @output = []
8
+ end
9
+
10
+ def load(activities, minutes, level = 0, target = @activities)
11
+ activities.unshift 'All' if level == 0
12
+ activity = activities.shift
13
+ target[activity] = {:minutes => 0, :children => {}} unless target[activity]
14
+ target[activity][:minutes] += minutes
15
+ load(activities, minutes, level+1, target[activity][:children]) if activities.any?
16
+ end
17
+
18
+ def process(level = 0, target = activities)
19
+ target.each do |activity, values|
20
+ output << "%-25s %4d min (%s)" % ["#{(1..level*2).to_a.map{' '}.join}#{activity}",
21
+ values[:minutes],
22
+ to_hrs_mins(values[:minutes])]
23
+ process(level+1, values[:children]) if values[:children].any?
24
+ end
25
+ end
26
+
27
+ def to_hrs_mins(mins)
28
+ hours = (mins/60.0).floor
29
+ "%d:%02d" % [hours, mins - (hours*60)]
30
+ end
31
+
32
+ def print
33
+ puts output.join("\n")
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,37 @@
1
+ require 'date'
2
+
3
+ module TimeTree
4
+ module DateCalculator
5
+ def prev_weekday(date, weekday, count = 0)
6
+ delta = (7 + date.wday - daynum(weekday)) % 7
7
+ date - (delta + (count * 7))
8
+ end
9
+
10
+ def next_weekday(date, weekday, count = 0)
11
+ delta = (7 - date.wday + daynum(weekday)) % 7
12
+ delta = 7 if delta == 0
13
+ date + (delta + (count * 7))
14
+ end
15
+
16
+ def date_between?(date, from, to)
17
+ date >= from && to >= date
18
+ end
19
+
20
+ def in_prev_week?(date, count = 0, ref_date = Date.today)
21
+ monday = prev_weekday(ref_date, :mon, count)
22
+ sunday = monday + 6
23
+ date_between?(date, monday, sunday)
24
+ end
25
+
26
+ def in_prev_month?(date, count = 0, ref_date = Date.today)
27
+ ref_month = ref_date >> (-1 * count)
28
+ date.year == ref_month.year && date.month == ref_month.month
29
+ end
30
+
31
+ private
32
+
33
+ def daynum(weekday)
34
+ {:mon => 1, :tue => 2, :wed => 3, :thu => 4, :fri => 5, :sat => 6, :sun => 7}[weekday]
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,143 @@
1
+ require 'date'
2
+ require 'time_tree/date_calculator'
3
+
4
+ module TimeTree
5
+ class FileParser
6
+ include DateCalculator
7
+ attr_reader :errors
8
+
9
+ def initialize(tree, options)
10
+ @errors = []
11
+ @activity_tree = tree
12
+ @options = options
13
+ end
14
+
15
+ def find_path(paths)
16
+ paths.each do |path|
17
+ return path if File.exist?(path)
18
+ end
19
+
20
+ @errors << "File not found in: #{paths.join(', ')}"
21
+ false
22
+ end
23
+
24
+ def set_file(path)
25
+ @path = path
26
+ @line_number = 0
27
+ end
28
+
29
+ def set_date(date)
30
+ @date = date
31
+ @prev_mins = nil
32
+ @prev_activities = nil
33
+ end
34
+
35
+ def process_file(path)
36
+ if File.exist?(path)
37
+ if File.directory?(path)
38
+ process_folder(path)
39
+ else
40
+ set_file(path)
41
+ File.read(path).each_line {|line| parse_line(line) }
42
+ true
43
+ end
44
+ else
45
+ @errors << "File not found: #{path}"
46
+ false
47
+ end
48
+ end
49
+
50
+ def process_folder(path)
51
+ Dir.new(path).each do |file|
52
+ process_file(File.join(path, file)) unless file =~ /^\./
53
+ end
54
+ end
55
+
56
+ def parse_line(line)
57
+ @line_number += 1
58
+
59
+ case line.chomp.sub(/#.*/, '').strip
60
+ when ''
61
+ # ignore blank lines and comments
62
+ true
63
+
64
+ when /^(\d\d\d\d\/\d\d\/\d\d) *.*$/
65
+ set_date($1)
66
+ true
67
+
68
+ when /^(\d\d\d\d) +([^ ]+) *.*$/
69
+ if minutes = mins($1)
70
+ unless @prev_mins.nil?
71
+ if minutes > @prev_mins
72
+ if @prev_activities != '-' && selected?(@date, @prev_activities)
73
+ process_line(minutes, @prev_activities)
74
+ end
75
+ else
76
+ add_error(line, 'time does not advance')
77
+ return false
78
+ end
79
+ end
80
+
81
+ @prev_mins = minutes
82
+ @prev_activities = $2
83
+ true
84
+ else
85
+ false
86
+ end
87
+
88
+ else
89
+ add_error(line, 'line not understood')
90
+ false
91
+ end
92
+ end
93
+
94
+ def add_error(line, message)
95
+ @errors << "%s:%d: %s - %s" % [File.basename(@path), @line_number, line.chomp, message]
96
+ end
97
+
98
+ def valid?
99
+ errors.empty?
100
+ end
101
+
102
+ def selected?(date = @date, activities = '', options = @options)
103
+ date_options = [:today, :yesterday, :week, :month, :date, :range]
104
+ parsed_date = Date.parse(date)
105
+
106
+ (
107
+ options.detect { |key, val| date_options.include?(key) }.nil? ||
108
+ options[:all] ||
109
+ options[:today] && parsed_date == Date.today ||
110
+ options[:yesterday] && parsed_date == Date.today-1 ||
111
+ options[:week] && in_prev_week?(parsed_date, options[:week]) ||
112
+ options[:month] && in_prev_month?(parsed_date, options[:month]) ||
113
+ options[:date] && parsed_date == Date.parse(options[:date]) ||
114
+ options[:range] && parsed_date >= Date.parse(options[:range].split(':').first) &&
115
+ parsed_date <= Date.parse(options[:range].split(':').last)
116
+ ) &&
117
+ (
118
+ options[:filter] && options[:filter].detect { |f| activities =~ Regexp.new(f) } ||
119
+ options[:filter].nil?
120
+ )
121
+ end
122
+
123
+ private
124
+
125
+ def process_line(minutes, activities)
126
+ duration = minutes - @prev_mins
127
+ @activity_tree.load(activities.split('/'), duration)
128
+ end
129
+
130
+ def mins(str)
131
+ return 1440 if str == '0000' && @prev_mins
132
+ hours = str[0..1].to_i
133
+ mins = str[2..3].to_i
134
+
135
+ if hours <= 23 && mins <= 59
136
+ (hours * 60) + mins
137
+ else
138
+ add_error(str, 'invalid time')
139
+ false
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,3 @@
1
+ module TimeTree
2
+ VERSION = "2.1.0"
3
+ end
data/lib/time_tree.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'time_tree/version'
2
+ require 'time_tree/file_parser'
3
+ require 'time_tree/activity_tree'
@@ -0,0 +1,45 @@
1
+ require 'time_tree/activity_tree'
2
+
3
+ module TimeTree
4
+ describe ActivityTree do
5
+ let(:tree) { ActivityTree.new }
6
+
7
+ before do
8
+ tree.load(%w{foo bar baz}, 10)
9
+ tree.load(%w{foo bar bam}, 5)
10
+ tree.load(%w{blah}, 12)
11
+ end
12
+
13
+ it "should have 2 root activities" do
14
+ tree.activities['All'][:children].size.should == 2
15
+ end
16
+
17
+ it "foo should have 15 mins" do
18
+ tree.activities['All'][:children]['foo'][:minutes].should == 15
19
+ end
20
+
21
+ it "bar should have 15 mins" do
22
+ tree.activities['All'][:children]['foo'][:children]['bar'][:minutes].should == 15
23
+ end
24
+
25
+ it "baz should have 10 mins" do
26
+ tree.activities['All'][:children]['foo'][:children]['bar'][:children]['baz'][:minutes].should == 10
27
+ end
28
+
29
+ it "bam should have 5 mins" do
30
+ tree.activities['All'][:children]['foo'][:children]['bar'][:children]['bam'][:minutes].should == 5
31
+ end
32
+
33
+ it "blah should have 12 mins" do
34
+ tree.activities['All'][:children]['blah'][:minutes].should == 12
35
+ end
36
+
37
+ it "blah should have no children" do
38
+ tree.activities['All'][:children]['blah'][:children].size.should == 0
39
+ end
40
+
41
+ it "should print" do
42
+ tree.print
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,110 @@
1
+ require 'time_tree/date_calculator'
2
+
3
+ module TimeTree
4
+ describe DateCalculator do
5
+ include DateCalculator
6
+
7
+ describe "prev_weekday" do
8
+ it "monday" do
9
+ prev_weekday(Date.new(2013, 5, 5), :mon, 0).should == Date.new(2013, 4, 29)
10
+ end
11
+
12
+ it "tuesday" do
13
+ prev_weekday(Date.new(2013, 5, 6), :tue, 0).should == Date.new(2013, 4, 30)
14
+ end
15
+
16
+ it "sunday" do
17
+ prev_weekday(Date.new(2013, 5, 7), :sun, 0).should == Date.new(2013, 5, 5)
18
+ end
19
+
20
+ it "sunday prev week" do
21
+ prev_weekday(Date.new(2013, 5, 8), :sun, 1).should == Date.new(2013, 4, 28)
22
+ end
23
+
24
+ it "tuesday prev prev week" do
25
+ prev_weekday(Date.new(2013, 5, 9), :tue, 2).should == Date.new(2013, 4, 23)
26
+ end
27
+ end
28
+
29
+ describe "next_weekday" do
30
+ it "monday" do
31
+ next_weekday(Date.new(2013, 5, 6), :mon, 0).should == Date.new(2013, 5, 13)
32
+ end
33
+
34
+ it "tuesday" do
35
+ next_weekday(Date.new(2013, 5, 6), :tue, 0).should == Date.new(2013, 5, 7)
36
+ end
37
+
38
+ it "sunday" do
39
+ next_weekday(Date.new(2013, 5, 7), :sun, 0).should == Date.new(2013, 5, 12)
40
+ end
41
+
42
+ it "sunday next week" do
43
+ next_weekday(Date.new(2013, 5, 8), :sun, 1).should == Date.new(2013, 5, 19)
44
+ end
45
+
46
+ it "tuesday next next week" do
47
+ next_weekday(Date.new(2013, 5, 9), :tue, 2).should == Date.new(2013, 5, 28)
48
+ end
49
+ end
50
+
51
+ describe "date_between" do
52
+ let(:jun1) { Date.new(2000, 6, 1) }
53
+ let(:jun2) { Date.new(2000, 6, 2) }
54
+ let(:jun3) { Date.new(2000, 6, 3) }
55
+
56
+ it "between" do
57
+ date_between?(jun2, jun1, jun3).should be_true
58
+ end
59
+
60
+ it "outside" do
61
+ date_between?(jun1, jun2, jun3).should be_false
62
+ end
63
+
64
+ it "at start" do
65
+ date_between?(jun1, jun1, jun3).should be_true
66
+ end
67
+
68
+ it "at end" do
69
+ date_between?(jun3, jun1, jun3).should be_true
70
+ end
71
+ end
72
+
73
+ describe "in_prev_week?" do
74
+ let(:may7) { Date.new(2013, 5, 7) }
75
+ let(:may1) { Date.new(2013, 5, 1) }
76
+
77
+ it "within - zero count" do
78
+ in_prev_week?(may7, 0, may7).should be_true
79
+ end
80
+
81
+ it "within - 1 count" do
82
+ in_prev_week?(may1, 1, may7).should be_true
83
+ end
84
+
85
+ it "outside" do
86
+ in_prev_week?(may7, 1, may7).should be_false
87
+ end
88
+ end
89
+
90
+ describe"in_prev_month?" do
91
+ let(:may31) { Date.new(2013, 5, 31) }
92
+ let(:may7) { Date.new(2013, 5, 7) }
93
+ let(:may1) { Date.new(2013, 5, 1) }
94
+ let(:apr1) { Date.new(2013, 4, 1) }
95
+
96
+ it "within - zero count" do
97
+ in_prev_month?(may1, 0, may7).should be_true
98
+ in_prev_month?(may31, 0, may7).should be_true
99
+ end
100
+
101
+ it "within - 1 count" do
102
+ in_prev_month?(apr1, 1, may7).should be_true
103
+ end
104
+
105
+ it "outside" do
106
+ in_prev_month?(apr1, 0, may7).should be_false
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,276 @@
1
+ require 'time_tree/file_parser'
2
+ require 'helper'
3
+
4
+ module TimeTree
5
+ describe FileParser do
6
+ let(:tree) { mock('ActivityTree', :load => nil) }
7
+ let(:parser) { FileParser.new(tree, {}) }
8
+ let(:home) { ENV['HOME'] }
9
+
10
+ describe "#find_path" do
11
+ it "finds a path" do
12
+ parser.find_path(['/not/on/your/nelly', "#{home}/.bash_profile"]).should =~ /bash_profile/
13
+ end
14
+
15
+ it "finds no path" do
16
+ parser.find_path(['/not/on/your/nelly', "#/not/there"]).should be_false
17
+ end
18
+ end
19
+
20
+ describe "#process_file" do
21
+ context "is a directory" do
22
+ before do
23
+ FileParser.any_instance.stub(:process_folder => true)
24
+ end
25
+
26
+ it "returns true" do
27
+ parser.process_file(fixtures).should be_true
28
+ end
29
+
30
+ it "calls #process_folder" do
31
+ parser.should_receive(:process_folder).with(fixtures).once
32
+ parser.process_file(fixtures)
33
+ end
34
+ end
35
+
36
+ context "is a file" do
37
+ before do
38
+ FileParser.any_instance.stub(:parse_line => true)
39
+ end
40
+
41
+ it "returns true" do
42
+ parser.process_file(fixtures('time.txt')).should be_true
43
+ end
44
+
45
+ it "calls process_line for each line of the file" do
46
+ parser.should_receive(:parse_line).exactly(3).times
47
+ parser.process_file(fixtures('time.txt'))
48
+ end
49
+ end
50
+
51
+ context "file or directory not found" do
52
+ before do
53
+ @result = parser.process_file('does/not/exist')
54
+ end
55
+
56
+ it "returns false" do
57
+ @result.should be_false
58
+ end
59
+
60
+ it "flags an error and sets object invalid" do
61
+ parser.errors.size.should == 1
62
+ parser.valid?.should be_false
63
+ end
64
+ end
65
+ end
66
+
67
+ describe "#process_folder" do
68
+ before do
69
+ FileParser.any_instance.stub(:process_file => true)
70
+ end
71
+
72
+ it "calls #process_file for each contained file" do
73
+ parser.should_receive(:process_file).with(fixtures('time/jun1.txt')).once
74
+ parser.should_receive(:process_file).with(fixtures('time/jun2.txt')).once
75
+ parser.should_receive(:process_file).with(fixtures('time/jun3.txt')).once
76
+ parser.process_folder(fixtures('time'))
77
+ end
78
+
79
+ it "ignores files starting with dot" do
80
+ File.open(fixtures('.a_dot_file'), 'w') {|f| f.write('foo')}
81
+ parser.should_not_receive(:process_file).with(fixtures('.a_dot_file'))
82
+ parser.process_folder(fixtures)
83
+ end
84
+ end
85
+
86
+ describe "#parse_line" do
87
+ before do
88
+ parser.set_file('my_file.txt')
89
+ end
90
+
91
+ context "date lines" do
92
+ before do
93
+ FileParser.any_instance.stub(:set_date => true)
94
+ end
95
+
96
+ context "well formed date" do
97
+ let(:good_date) { '2013/04/23 some comments' }
98
+
99
+ it "returns true" do
100
+ parser.parse_line(good_date).should be_true
101
+ end
102
+
103
+ it "calls set_date" do
104
+ parser.should_receive(:set_date).once
105
+ parser.parse_line(good_date)
106
+ end
107
+ end
108
+
109
+ context "malformed date" do
110
+ let(:bad_date) { '201b/foo/bar' }
111
+
112
+ it "returns false" do
113
+ parser.parse_line(bad_date).should be_false
114
+ end
115
+
116
+ it "does not call set_date" do
117
+ parser.should_not_receive(:set_date)
118
+ parser.parse_line(bad_date)
119
+ end
120
+
121
+ it "logs an error" do
122
+ parser.parse_line(bad_date)
123
+ parser.errors.size.should == 1
124
+ end
125
+ end
126
+ end
127
+
128
+ context "time lines" do
129
+ before do
130
+ parser.set_date('2013/01/02')
131
+ end
132
+
133
+ context "normal activities" do
134
+ it "returns true and remains valid" do
135
+ parser.parse_line("1634 adm/foo/bar dhffhkdhsdhjdf").should be_true
136
+ parser.parse_line("1635 adm/foo/bar dhffh kdh sdhjdf ").should be_true
137
+ parser.parse_line("1636 adm/foo/bar dhffh kdh sdhjdf ").should be_true
138
+ parser.parse_line("1637 adm").should be_true
139
+ parser.parse_line("1638 -").should be_true
140
+ parser.parse_line("1639 - sdf sdfs sfsad").should be_true
141
+ parser.valid?.should be_true
142
+ end
143
+
144
+ it "calls ActivityTree#load after first line" do
145
+ tree.should_receive(:load).with(%w{adm foo bar}, 1).once
146
+ parser.parse_line("1634 adm/foo/bar dhffhkdhsdhjdf")
147
+ parser.parse_line("1635 -")
148
+ end
149
+
150
+ it "flags invalid times" do
151
+ parser.parse_line("2435 - df sdfsdg").should be_false
152
+ parser.parse_line("2x3d - df sdfsdg").should be_false
153
+ parser.valid?.should be_false
154
+ end
155
+
156
+ it "flags non-advancing times" do
157
+ parser.parse_line("1253 - df sdfsdg").should be_true
158
+ parser.parse_line("1253 - df sdfsdg").should be_false
159
+ parser.valid?.should be_false
160
+ end
161
+ end
162
+ end
163
+
164
+ context "dash for activity" do
165
+ it "returns true and remains valid" do
166
+ parser.parse_line("1634 -").should be_true
167
+ parser.valid?.should be_true
168
+ end
169
+
170
+ it "does not call ActivityTree#load after first line" do
171
+ tree.should_not_receive(:load).with(%w{-}, 1)
172
+ parser.parse_line("1634 -")
173
+ parser.parse_line("1635 foo")
174
+ end
175
+ end
176
+
177
+ context "comment lines" do
178
+ it "should return true" do
179
+ parser.parse_line("# a comment").should be_true
180
+ end
181
+
182
+ it "should not log errors" do
183
+ parser.parse_line("# a comment")
184
+ parser.errors.size.should == 0
185
+ end
186
+ end
187
+ end
188
+
189
+ describe "#selected?" do
190
+ it "should be true if no date selectors" do
191
+ parser.selected?('2013/01/01', {:bish => true, :bosh => :tigers}).should be_true
192
+ end
193
+
194
+ context "today" do
195
+ let(:options) { {:today => true} }
196
+
197
+ it "should be true if date is today" do
198
+ parser.selected?(Time.now.strftime("%Y/%m/%d"), options).should be_true
199
+ end
200
+
201
+ it "should be false if date is not today" do
202
+ parser.selected?('1962/01/03', '', options).should be_false
203
+ end
204
+ end
205
+
206
+ context "yesterday" do
207
+ let(:options) { {:yesterday => true} }
208
+
209
+ it "should be true if date is yesterday" do
210
+ parser.selected?((Time.now-(24*60*60)).strftime("%Y/%m/%d"), options).should be_true
211
+ end
212
+
213
+ it "should be false if date is not yesterday" do
214
+ parser.selected?('1962/01/03', '', options).should be_false
215
+ end
216
+ end
217
+
218
+ context "week" do
219
+ end
220
+
221
+ context "month" do
222
+ end
223
+
224
+ context "specific date" do
225
+ let(:options) { {:date => '2012/01/01'} }
226
+
227
+ it "should be true if date matches" do
228
+ parser.selected?('2012/01/01', options).should be_true
229
+ end
230
+
231
+ it "should be false if date does not match" do
232
+ parser.selected?('1962/01/03', '', options).should be_false
233
+ end
234
+ end
235
+
236
+ context "date range" do
237
+ let(:options) { {:range => '2012/01/01:2012/01/02'} }
238
+
239
+ it "should be true if date within" do
240
+ parser.selected?('2012/01/01', options).should be_true
241
+ parser.selected?('2012/01/02', options).should be_true
242
+ end
243
+
244
+ it "should be false if date outside" do
245
+ parser.selected?('1962/01/03', '', options).should be_false
246
+ end
247
+ end
248
+
249
+ context "filter" do
250
+ it "finds a hit" do
251
+ parser.selected?('1962/01/03', 'foo/bar', {:filter => ['foo', 'boo']}).should be_true
252
+ end
253
+
254
+ it "finds no hit" do
255
+ parser.selected?('1962/01/03', 'foo/bar', {:filter => ['blork', 'flange']}).should be_false
256
+ end
257
+ end
258
+ end
259
+
260
+ describe "midnight edge case" do
261
+ context "no previous mins" do
262
+ it "returns 0 mins" do
263
+ parser.send(:mins, '0000').should == 0
264
+ end
265
+ end
266
+
267
+ context "previous mins" do
268
+ it "returns 1440 mins (1 day)" do
269
+ parser.set_file('afile')
270
+ parser.parse_line('2359 foo')
271
+ parser.send(:mins, '0000').should == 1440
272
+ end
273
+ end
274
+ end
275
+ end
276
+ end
data/spec/helper.rb ADDED
@@ -0,0 +1,4 @@
1
+ def fixtures(path = '')
2
+ File.expand_path(File.join('..', '..', 'fixtures', path), __FILE__)
3
+ end
4
+
@@ -0,0 +1,90 @@
1
+ require 'time_tree/activity_tree'
2
+ require 'time_tree/file_parser'
3
+ require 'helper'
4
+
5
+ module TimeTree
6
+ describe 'Integration' do
7
+ let(:tree) { ActivityTree.new }
8
+
9
+ it "nominal" do
10
+ parser = FileParser.new(tree, {})
11
+ parser.process_file(fixtures('time'))
12
+ tree.process
13
+ tree.output[0].should =~ /All +6 min/
14
+ tree.output[1].should =~ /jun1stuff +1 min/
15
+ tree.output[2].should =~ /jun2stuff +2 min/
16
+ tree.output[3].should =~ /jun3stuff +3 min/
17
+ tree.output.size.should == 4
18
+ end
19
+
20
+ it "specific date" do
21
+ parser = FileParser.new(tree, {:date => '1975/06/02'})
22
+ parser.process_file(fixtures('time'))
23
+ tree.process
24
+ tree.output[0].should =~ /All +2 min/
25
+ tree.output[1].should =~ /jun2stuff +2 min/
26
+ tree.output.size.should == 2
27
+ end
28
+
29
+ it "date range" do
30
+ parser = FileParser.new(tree, {:range => '1975/06/02:1975/06/03'})
31
+ parser.process_file(fixtures('time'))
32
+ tree.process
33
+ tree.output[0].should =~ /All +5 min/
34
+ tree.output[1].should =~ /jun2stuff +2 min/
35
+ tree.output[2].should =~ /jun3stuff +3 min/
36
+ tree.output.size.should == 3
37
+ end
38
+
39
+ it "today" do
40
+ parser = FileParser.new(tree, {:today => true})
41
+ parser.process_file(fixtures('time'))
42
+ tree.process
43
+ tree.output.size.should == 0
44
+ end
45
+
46
+ it "yesterday" do
47
+ parser = FileParser.new(tree, {:yesterday => true})
48
+ parser.process_file(fixtures('time'))
49
+ tree.process
50
+ tree.output.size.should == 0
51
+ end
52
+
53
+ it "this week" do
54
+ parser = FileParser.new(tree, {:week => 0})
55
+ parser.process_file(fixtures('time'))
56
+ tree.process
57
+ tree.output.size.should == 0
58
+ end
59
+
60
+ it "last week" do
61
+ parser = FileParser.new(tree, {:week => 1})
62
+ parser.process_file(fixtures('time'))
63
+ tree.process
64
+ tree.output.size.should == 0
65
+ end
66
+
67
+ it "this month" do
68
+ parser = FileParser.new(tree, {:month => 0})
69
+ parser.process_file(fixtures('time'))
70
+ tree.process
71
+ tree.output.size.should == 0
72
+ end
73
+
74
+ it "last month" do
75
+ parser = FileParser.new(tree, {:month => 1})
76
+ parser.process_file(fixtures('time'))
77
+ tree.process
78
+ tree.output.size.should == 0
79
+ end
80
+
81
+ it "filter" do
82
+ parser = FileParser.new(tree, {:filter => ['jun2stuff']})
83
+ parser.process_file(fixtures('time'))
84
+ tree.process
85
+ tree.output[0].should =~ /All +2 min/
86
+ tree.output[1].should =~ /jun2stuff +2 min/
87
+ tree.output.size.should == 2
88
+ end
89
+ end
90
+ end
data/time_tree.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'time_tree/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "time_tree"
8
+ gem.version = TimeTree::VERSION
9
+ gem.authors = ["Andy White"]
10
+ gem.email = ["andy@wireworldmedia.co.uk"]
11
+ gem.description = %q{Summarises a time file}
12
+ gem.summary = %q{Summarises a time file}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ gem.add_development_dependency "rspec", "~> 2.11.0"
20
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: time_tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Andy White
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-05-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.11.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 2.11.0
30
+ description: Summarises a time file
31
+ email:
32
+ - andy@wireworldmedia.co.uk
33
+ executables:
34
+ - timetree
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - .gitignore
39
+ - .rvmrc
40
+ - Gemfile
41
+ - LICENSE.txt
42
+ - README.md
43
+ - Rakefile
44
+ - bin/timetree
45
+ - fixtures/.a_dot_file
46
+ - fixtures/real.txt
47
+ - fixtures/time.txt
48
+ - fixtures/time/jun1.txt
49
+ - fixtures/time/jun2.txt
50
+ - fixtures/time/jun3.txt
51
+ - lib/time_tree.rb
52
+ - lib/time_tree/activity_tree.rb
53
+ - lib/time_tree/date_calculator.rb
54
+ - lib/time_tree/file_parser.rb
55
+ - lib/time_tree/version.rb
56
+ - spec/activity_tree_spec.rb
57
+ - spec/date_calculator_spec.rb
58
+ - spec/file_parser_spec.rb
59
+ - spec/helper.rb
60
+ - spec/integration_spec.rb
61
+ - time_tree.gemspec
62
+ homepage: ''
63
+ licenses: []
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubyforge_project:
82
+ rubygems_version: 1.8.25
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: Summarises a time file
86
+ test_files:
87
+ - spec/activity_tree_spec.rb
88
+ - spec/date_calculator_spec.rb
89
+ - spec/file_parser_spec.rb
90
+ - spec/helper.rb
91
+ - spec/integration_spec.rb