ascii-tracker 0.0.1

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.
@@ -0,0 +1,168 @@
1
+ =begin
2
+ Subject: ***Re: [SOLUTION] Dice Roller (#61)*
3
+ From: *Dennis Ranke *<mail exoticorn.de>
4
+ Date: Mon, 9 Jan 2006 06:53:10 +0900
5
+ References: 174521 </cgi-bin/scat.rb/ruby/ruby-talk/174521> 174811
6
+ </cgi-bin/scat.rb/ruby/ruby-talk/174811>
7
+ In-reply-to: 174811 </cgi-bin/scat.rb/ruby/ruby-talk/174811>
8
+
9
+ Hi,
10
+
11
+ here is my second solution. Quite a bit longer, but a lot nicer.
12
+ For this I implemented a simple recursive descent parser class that
13
+ allows the tokens and the grammar to be defined in a very clean ruby
14
+ syntax. I think I'd really like to see a production quality
15
+ parser(generator) using something like this grammar format.
16
+ =end
17
+
18
+ class RDParser
19
+ attr_accessor :pos
20
+ attr_reader :rules
21
+
22
+ def initialize(&block)
23
+ @lex_tokens = []
24
+ @rules = {}
25
+ @start = nil
26
+ instance_eval(&block)
27
+ end
28
+
29
+ def parse(string)
30
+ @tokens = []
31
+ until string.empty?
32
+ raise "unable to lex '#{string}" unless @lex_tokens.any? do |tok|
33
+ #puts "match(#{string}) <- (#{tok})"
34
+ match = tok.pattern.match(string)
35
+ if match
36
+ s_tok = match.to_s
37
+ puts "(#{s_tok})" unless /^\s+$/.match(s_tok)
38
+ #puts "<<< #{s_tok} | #{tok.pattern} >>>"
39
+ @tokens << tok.block.call(s_tok) if tok.block
40
+ string = match.post_match
41
+ #puts "<<<#{s_tok}|||#{match.post_match}>>>"
42
+ true
43
+ else
44
+ false
45
+ end
46
+ end
47
+ end
48
+ @pos = 0
49
+ @max_pos = 0
50
+ @expected = []
51
+ result = @start.parse
52
+ if @pos != @tokens.size
53
+ raise "Parse error. expected: '#{@expected.join(', ')}', found
54
+ '#{@tokens[@max_pos]}'"
55
+ end
56
+ return result
57
+ end
58
+
59
+ def next_token
60
+ @pos += 1
61
+ return @tokens[@pos - 1]
62
+ end
63
+
64
+ def expect(tok)
65
+ t = next_token
66
+ if @pos - 1 > @max_pos
67
+ @max_pos = @pos - 1
68
+ @expected = []
69
+ end
70
+ return t if tok === t
71
+ @expected << tok if @max_pos == @pos - 1 && !@expected.include?(tok)
72
+ return nil
73
+ end
74
+
75
+ private
76
+
77
+ LexToken = Struct.new(:pattern, :block)
78
+
79
+ def token(pattern, &block)
80
+ @lex_tokens << LexToken.new(Regexp.new('\\A' + pattern.source), block)
81
+ end
82
+
83
+ def start(name, &block)
84
+ rule(name, &block)
85
+ @start = @rules[name]
86
+ end
87
+
88
+ def rule(name)
89
+ @current_rule = Rule.new(name, self)
90
+ @rules[name] = @current_rule
91
+ yield
92
+ @current_rule = nil
93
+ end
94
+
95
+ def match(*pattern, &block)
96
+ @current_rule.add_match(pattern, block)
97
+ end
98
+
99
+ class Rule
100
+ Match = Struct.new :pattern, :block
101
+
102
+ def initialize(name, parser)
103
+ @name = name
104
+ @parser = parser
105
+ @matches = []
106
+ @lrmatches = []
107
+ end
108
+
109
+ def add_match(pattern, block)
110
+ match = Match.new(pattern, block)
111
+ if pattern[0] == @name
112
+ pattern.shift
113
+ @lrmatches << match
114
+ else
115
+ @matches << match
116
+ end
117
+ end
118
+
119
+ def parse
120
+ match_result = try_matches(@matches)
121
+ return nil unless match_result
122
+ loop do
123
+ result = try_matches(@lrmatches, match_result)
124
+ return match_result unless result
125
+ match_result = result
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ def try_matches(matches, pre_result = nil)
132
+ match_result = nil
133
+ start = @parser.pos
134
+ matches.each do |match|
135
+ r = pre_result ? [pre_result] : []
136
+ match.pattern.each do |token|
137
+ if @parser.rules[token]
138
+ r << @parser.rules[token].parse
139
+ unless r.last
140
+ r = nil
141
+ break
142
+ end
143
+ else
144
+ nt = @parser.expect(token)
145
+ if nt
146
+ r << nt
147
+ else
148
+ r = nil
149
+ break
150
+ end
151
+ end
152
+ end
153
+ if r
154
+ if match.block
155
+ match_result = match.block.call(*r)
156
+ else
157
+ match_result = r[0]
158
+ end
159
+ break
160
+ else
161
+ @parser.pos = start
162
+ end
163
+ end
164
+ return match_result
165
+ end
166
+ end
167
+ end
168
+
@@ -0,0 +1,27 @@
1
+ module AsciiTracker
2
+ class Record
3
+
4
+ attr_reader :date, :span, :desc
5
+
6
+ def to_s; "#{date} #{HHMM.new(span)} #{desc}" end
7
+
8
+ Defaults = { :date => Date.today, :span => 0.0, :desc => nil }
9
+
10
+ # span may be any valid HHMM format
11
+ # value keys: :date, :span, and :desc
12
+ def initialize values = {} #date, span, desc = nil
13
+ values = Defaults.merge(values)
14
+ @date = values[:date]
15
+ @span = HHMM.new(values[:span]).to_f
16
+ @desc = values[:desc]
17
+ end
18
+
19
+ # 35.25 -> [1, 11, 15]
20
+ def self.hours_to_dhm(hours)
21
+ d = hours.to_i / 8
22
+ h = (hours - 8*d).to_i
23
+ m = ((60 * (hours - 8*d - h)) + 0.5).to_i
24
+ [d, h, m]
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,109 @@
1
+ module AsciiTracker
2
+ class Slot < Record
3
+
4
+ attr_reader :t_start, :t_end
5
+
6
+ def to_s; "#{date} #{t_start}-#{t_end} #{desc}" end
7
+
8
+ # supports Record values keys plus:
9
+ # :start and :end
10
+ # for interval definition
11
+ def initialize values = {}
12
+ values = Defaults.merge(values)
13
+ @t_start = HHMM.new(values[:start])
14
+ @t_end = HHMM.new(values[:end])
15
+
16
+ @duration = (@t_end - @t_start)
17
+ super values.merge(:span => @duration.to_f)
18
+
19
+ @interrupts = []
20
+ end
21
+
22
+ # returns copy only, not suited to add/delelte interrupts
23
+ def interrupts; @interrupts.clone end
24
+
25
+ # gross length is full slot length without interruptions being subtracted
26
+ def gross_length
27
+ @duration.to_f
28
+ end
29
+
30
+ # [-----] -+
31
+ # [----------------] |
32
+ # [---] | <-- overlaping
33
+ # [------] |
34
+ # [------] |
35
+ # [----------] -+
36
+ #
37
+ # self: [---------]
38
+ #
39
+ # [---] -+
40
+ # [---] | <-- not overlaping
41
+ # [----] |
42
+ # [-----] -+
43
+ #
44
+ def _24(a,b)
45
+ [a.to_f, b < a ? b.to_f + 24 : b.to_f]
46
+ end
47
+
48
+ def overlaps? slr
49
+ return false unless slr.respond_to?(:t_end)
50
+ a, b = _24(t_start, t_end)
51
+ c, d = _24(slr.t_start, slr.t_end)
52
+ #puts "..a, b, c, d: #{a}, #{b} -> #{c}, #{d}"
53
+ if d < a
54
+ #puts "..upgrade: #{a}, #{b} -> #{c}, #{d}"
55
+ c = c + 24; d = d + 24
56
+ end
57
+ #puts "..a, b, c, d: #{a}, #{b} -> #{c}, #{d} | #{c < b && a < d}"
58
+ c < b && a < d
59
+ #!slr.nil? && !(slr.t_end <= t_start || t_end <= slr.t_start)
60
+ #not(slr.t_end <= t_start || t_end <= slr.t_start)
61
+ end
62
+
63
+ # checks for a pure technical fit which does not take into account the
64
+ # already existing interruptions!
65
+ def covers? slot_or_span
66
+ slr = slot_or_span # shortcut for shorter lines below
67
+ begin
68
+ a, b = _24(t_start, t_end)
69
+ c, d = _24(slr.t_start, slr.t_end)
70
+ if d < a
71
+ #puts "..upgrade: #{a}, #{b} -> #{c}, #{d}"
72
+ c = c + 24; d = d + 24
73
+ end
74
+ a <= c && d <= b
75
+ rescue NoMethodError # not a slot?
76
+ slr.kind_of?(Record) and slr.span <= gross_length
77
+ end
78
+ end
79
+
80
+ def add_interrupt slot_or_span
81
+ slr = slot_or_span # shortcut
82
+ unless covers? slr
83
+ raise TimecardException, "interrupt not covered! #{slr}"
84
+ end
85
+
86
+ unless slr.span <= span
87
+ raise TimecardException, "'#{self}' overload(#{span}): #{slr}"
88
+ end
89
+ #raise TimecardException, "overload: #{slr}" unless slr.span <= span
90
+
91
+ # new interrupts may not overlap with existing ones!
92
+ if slr.respond_to?(:t_start)
93
+ #raise TimecardException, "overlap: #{slr}" if @interrupts.any? do |i|
94
+ # slr.overlaps? i
95
+ #end
96
+ if @interrupts.any? { |rec| slr.overlaps? rec }
97
+ raise TimecardException, "overlap: #{slr}"
98
+ end
99
+ end
100
+
101
+ @interrupts.push(slr)
102
+
103
+ # subtract interrupts from span time
104
+ #dt_lost = @interrupts.inject(0.0) { |sum, rec| sum + rec.span }
105
+ dt_lost = @interrupts.inject(0.0) { |sum, rec| sum + (rec.gross_length rescue rec.span) }
106
+ @span = gross_length - dt_lost
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,3 @@
1
+ module AsciiTracker
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,16 @@
1
+ require "asciitracker/version"
2
+
3
+ require 'date'
4
+ require 'asciitracker/hash_ext'
5
+ require 'asciitracker/hhmm'
6
+ require 'asciitracker/record'
7
+ require 'asciitracker/slot'
8
+ require 'asciitracker/model'
9
+ require 'asciitracker/controller'
10
+ require 'asciitracker/parser'
11
+ require 'asciitracker/app'
12
+
13
+ module AsciiTracker
14
+ # Your code goes here...
15
+ end
16
+
@@ -0,0 +1,130 @@
1
+ require 'spec_helper'
2
+
3
+ include AsciiTracker
4
+ describe "AsciiTracker::Controller" do
5
+ it "finds the class" do
6
+ Controller.should_not == nil
7
+ end
8
+
9
+ describe "with a controller" do
10
+ before :each do
11
+ @c = Controller.new
12
+ end
13
+ end
14
+ end
15
+
16
+ __END__
17
+
18
+ it "should have a #new_slot method" do
19
+ @c.new_day Date.today
20
+ slot = @c.new_slot(HHMM("10:00"), HHMM("12:00"), "foo bar")
21
+ @c.new_span "1.75", "some task"
22
+ @c.new_txt "some additional text"
23
+ end
24
+
25
+ class ControllerTest < Test::Unit::TestCase
26
+
27
+ include Timecard
28
+
29
+ def test_expoeasy
30
+ puts "\n--> #{self}"
31
+ c = Controller.new
32
+ Parser.parse c, <<-EOT
33
+ @expoeasy expoeasy:
34
+ @wallcms wallcms:
35
+ @private private:
36
+ 2008-07-10 18:15-04:15 expoeasy: yui data tables for visitors
37
+ 0:15 wallcms: commit reviews
38
+ 0:30 private: family & food
39
+ 20:15-22:15 private: halma
40
+ 23:45-03:45 private: @jussie's weeding
41
+ EOT
42
+ assert_equal 5, c.model.records.size
43
+ assert_equal 3, c.model.projects.size
44
+ assert_not_nil ee = c.model.records.first
45
+ assert_equal "expoeasy: yui data tables for visitors", ee.desc
46
+ assert_equal 4, ee.interrupts.size
47
+
48
+ assert_equal 10.00, ee.gross_length
49
+ assert_equal 3.25, ee.span
50
+ end
51
+
52
+ def test_day_spans
53
+ puts "\n--> #{self}"
54
+ c = Controller.new
55
+ c.new_day Date.today
56
+ c.new_span 0, "ueberstundenabbau"
57
+ assert_equal 1, c.model.records.length
58
+ rec = c.model.records.first
59
+ assert_equal 0, rec.span
60
+ assert_equal Date.today, rec.date
61
+ end
62
+
63
+ def test_slot_span_slot
64
+ puts "\n--> #{self}"
65
+ c = Controller.new
66
+ c.new_day Date.today
67
+ c.new_slot(HHMM("10:00"), HHMM("14:00"), "slot 1")
68
+ c.new_span 2.5, "span"
69
+ c.new_slot(HHMM("14:00"), HHMM("16:00"), "slot 2")
70
+ assert_equal 3, c.model.records.length
71
+ assert_equal [1.5,2.5,2], c.model.records.map { |e| e.span }
72
+ end
73
+
74
+ def test_interrupted_slot
75
+ puts "\n--> #{self}"
76
+ c = Controller.new
77
+ c.new_day Date.today
78
+ c.new_slot HHMM("10:00"), HHMM("14:00"), "office works"
79
+ c.new_span "0:15", "annoyance"
80
+ c.new_slot HHMM("10:15"), HHMM("10:30"), "another annoyance"
81
+ assert_equal 3, c.model.records.length
82
+ assert_equal 3.5, c.model.records.first.span
83
+
84
+ telco = c.new_slot(HHMM("10:30"), HHMM("11:30"), "sysadm")
85
+ assert_equal 4, c.model.records.length
86
+ assert_equal 2.5, c.model.records.first.span
87
+
88
+ c.new_span 0.25, "telco" # interrupt the sysadm task only!
89
+ assert_equal 5, c.model.records.length
90
+ assert_equal 2.5, c.model.records.first.span
91
+
92
+ assert_raise(TimecardException) do
93
+ c.new_slot HHMM("09:15"), HHMM("10:15"), "overlap"
94
+ end
95
+ assert_raise(TimecardException) do
96
+ c.new_slot HHMM("10:00"), HHMM("11:00"), "overlap"
97
+ end
98
+ assert_raise(TimecardException) do
99
+ c.new_slot HHMM("11:00"), HHMM("12:00"), "overlap"
100
+ end
101
+ assert_raise(TimecardException) do
102
+ c.new_slot HHMM("13:55"), HHMM("14:05"), "overlap"
103
+ end
104
+
105
+ assert_equal 5, c.model.by_date(Date.today).length
106
+ #records = c.model.by_date[Date.today]
107
+ #assert_equal 4, records.length
108
+ end
109
+
110
+ def test_disjunct_slots
111
+ puts "\n--> #{self}"
112
+ c = Controller.new
113
+ c.new_day Date.today
114
+ c.new_slot(HHMM("10:00"), HHMM("11:00"), "foo bar")
115
+ c.new_slot(HHMM("11:00"), HHMM("13:00"), "foo bar")
116
+ c.new_slot(HHMM("13:00"), HHMM("16:00"), "foo bar")
117
+ assert_equal 3, c.model.records.length
118
+ assert_equal [1,2,3], c.model.records.map { |e| e.span }
119
+ end
120
+
121
+ def test_new_record_api
122
+ puts "\n--> #{self}"
123
+ c = Controller.new
124
+ c.new_day Date.today
125
+ slot = c.new_slot(HHMM("10:00"), HHMM("12:00"), "foo bar")
126
+ c.new_span "1.75", "some task"
127
+ c.new_txt "some additional text"
128
+ end
129
+ end
130
+
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+
3
+ include AsciiTracker
4
+ describe "AsciiTracker::HHMM" do
5
+ it "finds the class" do
6
+ HHMM.should_not == nil
7
+ end
8
+
9
+ describe "HHMM calculus" do
10
+ it "should calc differences" do
11
+ dt = (HHMM '12:00') - (HHMM '10:00')
12
+ dt.hours.should == 2.0
13
+ dt.minutes.should == 0
14
+ end
15
+
16
+ it "should calc midnight overlaps" do
17
+ hhmm = (HHMM('00:01') - HHMM('23:59'))
18
+ hhmm.hours.should == 0
19
+ hhmm.minutes.should == 2
20
+ end
21
+
22
+ it "should calc zero length slots" do
23
+ hhmm = (HHMM('12:34') - HHMM('12:34'))
24
+ hhmm.hours.should == 0
25
+ hhmm.minutes.should == 0
26
+ end
27
+
28
+ it "should calc 24 hour slots" do
29
+ hhmm = (HHMM('24:00') - HHMM('00:00'))
30
+ hhmm.hours.should == 24
31
+ hhmm.minutes.should == 0
32
+
33
+ # 0/end - 24/start ist actually 0 minutes long!
34
+ hhmm = (HHMM('00:00') - HHMM('24:00'))
35
+ hhmm.hours.should == 0
36
+ hhmm.minutes.should == 0
37
+ end
38
+
39
+ it "should calc long overlaps" do
40
+ hhmm = (HHMM('02:00') - HHMM('14:00'))
41
+ hhmm.hours.should == 12
42
+ hhmm.minutes.should == 0
43
+ hhmm = (HHMM('02:00') - HHMM('10:00'))
44
+ hhmm.hours.should == 16
45
+ hhmm.minutes.should == 0
46
+ end
47
+ end
48
+
49
+ describe "HHMM parsing" do
50
+ it "should parse '1:30'" do
51
+ hhmm = (HHMM '1:30')
52
+ hhmm.to_a.should == [1, 30]
53
+ hhmm.to_s.should == "01:30"
54
+ hhmm.to_f.should == 1.5
55
+ end
56
+
57
+ it "should parse '1.5'" do
58
+ hhmm = (HHMM '1.5')
59
+ hhmm.to_a.should == [1, 30]
60
+ hhmm.to_s.should == "01:30"
61
+ hhmm.to_f.should == 1.5
62
+ end
63
+
64
+ it "should parse 1.5" do
65
+ hhmm = (HHMM 1.5)
66
+ hhmm.to_a.should == [1, 30]
67
+ hhmm.to_s.should == "01:30"
68
+ hhmm.to_f.should == 1.5
69
+ end
70
+
71
+ it "should parse (1, 30)" do
72
+ hhmm = (HHMM 1, 30)
73
+ hhmm.to_a.should == [1, 30]
74
+ hhmm.to_s.should == "01:30"
75
+ hhmm.to_f.should == 1.5
76
+ end
77
+
78
+ it "should parse ('1', '30')" do
79
+ hhmm = (HHMM '1', '30')
80
+ hhmm.to_a.should == [1, 30]
81
+ hhmm.to_s.should == "01:30"
82
+ hhmm.to_f.should == 1.5
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,71 @@
1
+ require 'spec_helper'
2
+
3
+ include AsciiTracker
4
+ describe "AsciiTracker::Model" do
5
+ it "finds the class" do
6
+ Model.should_not == nil
7
+ end
8
+
9
+ describe "with a model" do
10
+ before :each do
11
+ @model = Model.new
12
+ end
13
+
14
+ it "should be initially empty" do
15
+ @model.by_date(Date.today).should == []
16
+ end
17
+
18
+ it "should add slot records" do
19
+ @model.add_record(Slot.new :start=>"10:45", :end=>"12:15")
20
+ (records = @model.by_date(Date.today)).should_not == []
21
+ records.first.span.should == 1.5
22
+ end
23
+
24
+ it "should find best cascaded cover" do
25
+ @model.add_record Slot.new(:start=>"10:00", :end=>"20:00")
26
+ @model.add_record Slot.new(:start=>"11:00", :end=>"19:00")
27
+ @model.add_record Slot.new(:start=>"12:00", :end=>"17:00")
28
+ slot = Slot.new(:start=>"16:00", :end=>"17:00")
29
+ (cover = @model.find_best_cover slot).should_not == nil
30
+ cover.t_start.to_s.should == "12:00"
31
+ cover.t_end.to_s.should == "17:00"
32
+
33
+ slot = Slot.new(:start=>"11:10", :end=>"11:20")
34
+ (cover = @model.find_best_cover slot).should_not == nil
35
+ cover.t_start.to_s.should == "11:00"
36
+ cover.t_end.to_s.should == "19:00"
37
+ end
38
+
39
+ it "should find best cover" do
40
+ @model.add_record Slot.new(:start=>"10:00", :end=>"15:00")
41
+ @model.add_record Slot.new(:start=>"15:00", :end=>"20:00")
42
+ slot = Slot.new(:start=>"16:00", :end=>"17:00")
43
+ (overlaps = @model.find_overlaps slot).size.should == 1
44
+ overlap = overlaps.first
45
+ overlap.t_start.to_s.should == "15:00"
46
+ overlap.t_end.to_s.should == "20:00"
47
+ end
48
+
49
+ it "should find overlaps" do
50
+ @model.add_record Slot.new(:start=>"10:00", :end=>"20:00")
51
+ slot = Slot.new(:start=>"11:00", :end=>"13:00")
52
+ (overlaps = @model.find_overlaps slot).size.should == 1
53
+ overlap = overlaps.first
54
+ overlap.t_start.to_s.should == "10:00"
55
+ overlap.t_end.to_s.should == "20:00"
56
+ end
57
+
58
+ it "should add non overlapping slot records" do
59
+ @model.add_record(Slot.new :start=>"10:00", :end=>"11:00")
60
+ @model.add_record(Slot.new :start=>"11:00", :end=>"13:00")
61
+ (records = @model.by_date(Date.today)).should_not == []
62
+ records.first.span.should == 1
63
+ records.last.span.should == 2
64
+ end
65
+
66
+ # @rec.covers?(Slot.new(:start=>"10:45", :end=>"12:15")).should == true
67
+ # @rec.covers?(Slot.new(:start=>"10:00", :end=>"12:15")).should == false
68
+ # @rec.covers?(Slot.new(:start=>"10:45", :end=>"13:00")).should == false
69
+ end
70
+ end
71
+
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ include AsciiTracker
4
+ describe "AsciiTracker::Record" do
5
+ it "finds the class" do
6
+ Record.should_not == nil
7
+ end
8
+
9
+ it "should calc days from hours and minutes" do
10
+ Record.hours_to_dhm(0.25).should == [0, 0, 15]
11
+ Record.hours_to_dhm(1).should == [0, 1, 0]
12
+ Record.hours_to_dhm(24).should == [3, 0, 0]
13
+ Record.hours_to_dhm(35.25).should == [4, 3, 15]
14
+ end
15
+
16
+ it "should init with fixnum span" do
17
+ rec = Record.new :span => 1, :desc => "foo bar"
18
+ rec.span.should == 1.0
19
+ rec.desc.should == "foo bar"
20
+ end
21
+
22
+ it "should init with string duration span" do
23
+ rec = Record.new :span => "2.75", :desc => "bar foo"
24
+ rec.span.should == 2.75
25
+ rec.desc.should == "bar foo"
26
+ end
27
+
28
+ it "should init with without description text" do
29
+ rec = Record.new :span => "00:30"
30
+ rec.span.should == 0.5
31
+ rec.desc.should == nil
32
+ end
33
+ end