ascii-tracker 0.0.1

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