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