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