Alternate implementation
[rbot-mark] / mark2.rb
1 # vim: set sw=2 et:
2 # Author: Giuseppe Bilotta <giuseppe.bilotta@gmail.com>
3 # New markov chain plugin
4
5 class Array
6   def butlast
7     first(self.size-1)
8   end
9
10   def butfirst
11     last(self.size-1)
12   end
13
14   def pick_one
15     self[rand(self.size)]
16   end
17
18   def pick_some(n)
19     return nil if self.empty?
20     i = rand(self.size)
21     if n < 0
22       count = rand(self.size-i)
23     else
24       count = rand([n, self.size-i].min)
25     end
26     self[i, count]
27   end
28 end
29
30 class ChanceHashValue
31   attr_accessor :count, :key, :parent
32   def initialize(parent=nil, key=nil)
33     @count = 0
34     @prev = nil
35     @next = nil
36     @parent = parent
37     @key = key
38   end
39
40   def prev
41     @prev = ChanceHash.new(self, nil) unless @prev
42     @prev
43   end
44
45   def next
46     @next = ChanceHash.new(nil, self) unless @next
47     @next
48   end
49
50 end
51
52 class ChanceHash
53
54   attr_reader :next_of, :prev_of
55   def initialize(prev_of=nil, next_of=nil)
56     @hash = Hash.new { |hash, key|
57       hash[key] = ChanceHashValue.new(hash, key)
58     }
59     @picker = []
60     @total = 0
61     @valid_pick = false
62     @prev_of = prev_of
63     @next_of = next_of
64   end
65
66   def size
67     @hash.size
68   end
69
70   def [](key)
71     @hash[key]
72   end
73
74   def increase(el)
75     @hash[el].count += 1
76     @valid_pick = false
77     return @hash[el]
78   end
79
80   def decrease(el)
81     @hash[el].count -= 1
82     @hash.delete(el) if @hash[el].count == 0
83     @valid_pick = false
84     return @hash[el]
85   end
86
87   def make_picker
88     @picker.clear
89     total = 0
90     @hash.each { |key, val|
91       if val.count > 0
92         total += val.count
93         @picker << [total, key]
94       end
95     }
96     @total = total
97     @valid_pick = true
98   end
99
100   def picker
101     make_picker unless @valid_pick
102     @picker.dup
103   end
104
105   def random
106     case @hash.size
107     when 0
108       return nil
109     when 1
110       return @hash.keys.first
111     else
112       make_picker unless @valid_pick
113       pick = rand(@total)
114       @picker.each { |ch, el|
115         return el if pick < ch
116       }
117     end
118   end
119 end
120
121 class MarkovChainer
122   # Word or nonword regexp:
123   # can be used to scan a string splitting it into
124   # words and nonwords.
125   WNW = /\w+|\W/u
126
127   attr_reader :max_order
128   def initialize(ord=5)
129     # mkv[i] holds the chains of order i
130     @max_order = ord
131     @mkv = ChanceHash.new
132   end
133
134   def add_forward(array, base=@mkv)
135     return if array.compact.empty?
136     sym = array.shift
137     s = sym and sym.to_sym
138     base.increase(s) if s or base != @mkv
139     return if array.empty?
140     add_forward(array, base[s].next) if s or base == @mkv
141   end
142
143   def add_backwards(array, base=@mkv)
144     return if array.compact.empty?
145     sym = array.pop
146     s = sym and sym.to_sym
147     base.increase(s) if s or base != @mkv
148     return if array.empty?
149     add_backwards(array, base[s].prev) if s or base == @mkv
150   end
151
152   def add(array)
153     raise "Too many words in new data" if array.size > @max_order + 1
154     add_forward(array.dup)
155     add_backwards(array.dup)
156   end
157
158   def simple_learn(text)
159     syms = text.scan(WNW).map { |w| w.intern } 
160     syms.unshift(nil)
161     syms.push(nil)
162
163     syms.size.times { |i|
164       ord = [@max_order, syms.size-i].min
165       v = syms[i, ord+1]
166       # puts "Learning #{v.inspect}"
167       add(v)
168     }
169     # pp @mkv
170   end
171
172   def learn(text, o={})
173     opts = {:lowercase => false}.merge o
174
175     lc = opts[:lowercase]
176
177     simple_learn(text)
178     if lc
179       simple_learn(text.downcase)
180     end
181
182     # pp @mkv if defined? pp
183   end
184
185   def raw_next(syms, o={})
186     max_order = o.fetch(:max_order, @max_order)
187     ar = syms.last([max_order, syms.size].min).dup
188     ord = ar.size
189     base = @mkv
190     until ar.empty?
191       base = base[ar.shift].next
192     end
193     base.random
194   end
195
196   def next(text, o={})
197     syms = text.scan(WNW).map { |w| w.intern }
198     raw_next(syms, o)
199   end
200
201   def raw_prev(syms, o={})
202     max_order = o.fetch(:max_order, @max_order)
203     ar = syms.first([max_order, syms.size].min).dup
204     ord = ar.size
205     base = @mkv
206     until ar.empty?
207       base = base[ar.pop].prev
208     end
209     base.random
210   end
211
212   def prev(text, o={})
213     syms = text.scan(WNW).map { |w| w.intern }
214     raw_prev(syms, o)
215   end
216
217   def complete_prev(text, o={})
218     syms = text.scan(WNW).map { |w| w.intern }
219     prev = raw_prev(syms, o)
220     while prev do
221       syms.unshift(prev)
222       prev = raw_prev(syms, o)
223     end
224     return syms.to_s
225   end
226
227   def complete_next(text, o={})
228     syms = text.scan(WNW).map { |w| w.intern }
229     nxt = raw_next(syms, o)
230     while nxt do
231       syms.push(nxt)
232       nxt = raw_next(syms, o)
233     end
234     return syms.to_s
235   end
236
237   def complete(text, o={})
238     txt = text
239     while txt.empty? do
240       txt = @mkv.random.to_s
241     end
242     syms = txt.scan(WNW).map { |w| w.intern }
243     prev = raw_prev(syms, o)
244     nxt = raw_next(syms, o)
245     while nxt or prev do
246       # Keep adding only on the side where we
247       # didn't come across a nil already
248       if prev
249         syms.unshift(prev)
250         prev = raw_prev(syms, o)
251       end
252       if nxt
253         syms.push(nxt)
254         nxt = raw_next(syms, o)
255       end
256     end
257     return syms.to_s
258   end
259
260 end
261