--------------020800050408020303020208
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
Content-Transfer-Encoding: 7bit
Hi all,
Here's my solution for the quiz No 74. It generates text with a first
order Markov Chain. Because the matrix of a Markov chain for native
language tests is sparse, the chain is stored in a hash of hashes for
better memory usage.
I attached a tar archive for all the files and the main classes for easy
reading.
There are some open issues i think, but i did not had the time to implement
them
- for usage of higher order markov chains one should add a list of
previous elements in the method MarkovChain#add_elems(). But this breaks
other parts of the code and these parts have to be adapted to this.
- better performance of the MarkovChain#rand() method
- and of course there are a lot of open issues with respect to natural
language processing, the code does not produce grammatical valid
sentences, no quotes, no comma, etc.
I programmed it to learn Ruby and to improve my coding skills, because i
have less than 2,5 weeks of experience with Ruby. I bought the
Programming Ruby" book by Dave Thomas et. al. and started to read it
two weeks and two days ago and i used it a lot the last 48 hours. I
think there are some parts of the code where there are better solutions
or could be better expressed in a more "rubyish" way.
I used Java and Perl a lot in the last years and i will use Ruby now
instead of Perl. I can remember my experiences with hashes of hashes in
Perl. This was awkward and often a pain. And on friday as i started
coding for this quiz i suddenly realized that i had none of this issues
in Ruby. Wow ! Because i know the functional language Haskell i am used
to iterators, map's, lambda's etc. and i tried to use iterators and
block in the program, but i think i have to get more experience here in
Ruby.
If you have any comments, i will be glad to receive and answer them.
Extract the archive
$ tar xzf joern_dinkla_quiz_74.tar.gz
Call the program and print the command line syntax.
$ cd net.dinkla.ruby.quiz.74/lib
$ ruby main-markov-chain.rb --help
Best regards,
Joern Dinkla
--
Joern Dinkla, http://www.dinkla.net
--------------020800050408020303020208
Content-Type: application/x-gzip;
name="joern_dinkla_quiz_74.tar.gz"
Content-Transfer-Encoding: base64
Content-Disposition: inline;
filename="joern_dinkla_quiz_74.tar.gz"
H4sICLz3OEQCA2pvZXJuX2RpbmtsYV9xdWl6Xzc0LnRhcgDtPGt32ziu+br6FazTndhbR7Fs
J96TNu2kbdrJ3Gkzm6bbD4mPD2Uxtjay5BWlOJmk97dfACT18Ku5Mz093R2xrSKRIAiAIACC
TEOR2J4fXgXcjlP31v536v9m97o7p0eHr98d2clNsvGHSwvKXre70TLf+me31eoYGKfVdfac
nrPb3d1wurvdFmOstfENSioTHgOJYeRG3u1quC+1/4eWs7EvmefHYphE8S0bRmHC/VCyCb57
gl1GMUvGgp2CcrB/gHKw9xHrdZlVZ+Mkme7v7MxmM1Id0pxhNNlp2JZ1nLCRCEXMEyFZIm4S
NvOTMePs0o9lwqLYEzF7x+Or6Jq9GsOINnsphjyVgkab8CT2b1h0CT0sDTZEMKIn5Il/LVjA
w1HKR9BDyEQy4ENOeSxFk1AocKwExoTH4ANwjbkcI1r8CZQhNlckCRAzERMUQCoBIzBwNhax
YBz+yWgiWDQViEym0MkH/LBimsxNE/jwfI+FUQIoPRo48QE+iZg/mQZiIsLEgtqJ9ZdtGo3w
EwX+CIbQkpgUWJQsCmHUcZQGHuMe/GOBLxPrLwy7TWNx7UepZEIhl8gYiUwk48jTMiWRbkLn
AYLJegPEC8QC3RLxuLHgVzBOghSAzAALoCap4ZzzkDiBuVBtY35NHLnQ5PFpAtKEL0SECG1k
TQtxKmJgcsLDoTAYiwTFgLne0KRiNxwJ4IZRGquZ1zIHlkGi0FKUO2lQLOQUdBUJADVIYx4w
i1HJ1GEaR0MhpR+OmjlPXgQYcJqg1UuBvFHMJ6Bm/hAwXPMAJlHjkSBVAQzIJsCzf6dRol9B
tye8yUQyRAVHRIQDdYvoCQSPQ7VQSIKkAnEEslOLCQhi8soPAkDnamX3YbKDW9RLAGs3O2wm
xBXNhrgBYfpIiGIc8drsmLlROhonxFjtV00CYsb2GrRGV8wCCl4jwrNxNOGgKonNeGATVWjt
9ASCEijSZ5EelsiGL4/fwscoogrLB6VVTKppwaEDDqu4+3c2xplDsmhJFGaQVs2ialkzgsjh
tOLIKEgTP0Llj1EdQPXdrBFEAdMuzSKGdQqIamhzfDmusRm/pQkhKn/m15zI/lXEgSZYrxCi
+RYmSTHqg1yDAHupOQujGUDKBKViAc2IADkb8hBEBebBxYV6W5gYrZLaluRWBcZTncm4zmAG
LD67mvHY0woP+gWkTcnsHWIN2LXYB6kzjubFzJHWGWWCARNaWGi2ZOp5AtUGZjDwf8PZHHM0
RWiCQrQeJHJfZhYr1OrzCXh8lFlaQHWFXKNwLtNwiBMAqyFbSD9xeSVARD7jEyVd1OkErXoU
gxJP+HQLfgR84np8C7jEpaFlC/ZbwdNApg81ukE0vDKToleRMaZKjXy9IKD7SCRqwgvrgZTH
8AQzf8luo1RZKR7e0jpFy9g0UwyaNAq41vmh8K+VieOhnAnybhPAcnSTxHyotJvHwzFAWdZj
BlPBbn67ZP+KRBwOVKQ0wIkY9Lo2NNqj3yzrFYdRCtwQ+mnsh4nWfDAcUBP4aNhvwcPe2Ih7
CLO1IvwKfBcAsAqk7IfbykFsk4OwY5dtb49FMLUs6yX4PuBqBMolm5b1M5LJXhPGJntGVP+o
B4CxnjfZzz9E6SR4mkFZ1kZV/rxljQLuLGieBNX7ivF/p9t15uL/Vqfb2XB6e22niv+/Sdl8
tJPKeAdMMg92XD/cIZuzPbOsWIAugKHdAhMcTZMgCkdbeSVG1bfbOsaP4kJLUWEK1TOIdOWW
ZW2yX9Ewgr9MlXUEgxhNJcaRm5YnLrPvegMCsmkKkLXldhC1kZ0rS9jHlzBFHz2ILgc0GHt/
vx2y99Q08z3w1J/ut2fsE1Vci9iNpLjfvu7T/uTSD4SsWSL0kEQV/aYj8LGjIHIpRIx97gIM
RS3kDNEvk5FHJ224IQ8AwYr1y8nbwZvjX44GuJMevP/4jh0wBxYCNXw6OX39Ia/EIUtOAuQN
vphZj/95dPry5MMRgIV+YD2GHi+PTgcnbxQG3fvxp+PXZz/BR+/vlgVdJby+pUn7BSYN7P6s
DqI8rylR1Zqstj2GZw6yv//+ZHB4+vbju6P3Z6zf1NBz8qSOWEkv4RyG06N/fDw+PXq9BA8J
nzrNHt5JTxB1u15HrdVQTNuCD8cQ57N7+GrCNI3uAdWQSxInvELoGTIjBAr2S6rGIMTwk3qr
UQCdlwCBLZmFY9gyjERch0GL3Q0Pqls+l0mcigKYko8CMlM5h1FgIE7KSYoKACCAf1qWf8mo
wg5EOErGsLk6OGAt6PDh7PXR6amN66deO4rjKN5n73zaFaFoUoyQaoi5JAOSgNMwywCXHPyA
GJ920bcsW++WHAENH7DyrakjRWsA/KWA1WFCIkUvrBIMHRX1+UzhJ86SHNm4WcXPOj6IAsZo
XfBgmAY8EWZ5udz1Az/xhUTu67lcc3vxSvdBZks9mG3bNSWdk49n9mWQSpAZsQskTIY2xIfQ
dwDEQzWMbpIYylIhuysH1XKAMdcMQlI8QH7ptT6vSzRoHj3CngsNC+2pYSvkWX7oiRuGU0y7
GXyRUw4xMb7NxiA6pkCeKWozvQB6NvNtTLYdgt0CQ9U26QScC41P4SFuCdU5VTSZ02cH/8t2
zllz/+mjF3Z/hzWeQjcEVF22FWpQRK3NanXF3IelWFOJFdibcDVwANqoKFIbArUY1LgGD5H0
xNGISNyKJASwbWruU5uRD1VZKi1AclJcPWGOWkyWAX0CJpSmZgHrttOH+Nr6U8d/pU3H18//
7rW65fhvr7frtDacvd12lf/9RvEfA4NzWErF5klgjlmy6RgclQ8GW3gjHf4E3AV74an0R8nA
QhDHYIXrfnn2lUtKLKkESZMynCazzF1K/hRtewFEBWNguQKV9C2PRmOFkSeyLJMaeBjFmCiM
VBoO66/EbQajqMB4UxFbSksi0WCYwGhQ6HBSDxtNolVO0KVhKlaa5AqLdBZYEUqmTENAKM3B
1EuBuYQsDaETWif1YMRCcJXWEGyNLOZIweehpT4OgUFK7sgSydAG/zBM9jMIHb38mAnygN19
VlWZ2PIqmU4GaibzOs+Pk1vzrawjUfGHpJuTSijKNNrYozzYIaa6Q9IydhlHE+oGKoiZXnxz
iyghWqhzkP0c6+e8z+7vkRG20HDuqrbW0hbyA2UBGWStJfU5OMkOq7KwjoInzZHU2XvK6JqM
/ZT78Qycoa2EfPL6JFMoVDR1JAAD2OwNfBYPCoroFk4CCFuWOwUJUcJUzxDOitJVGkopJB4D
rDoFyAWtzxDoqeTN9X6EgmaspogOlhDGdO69TqRnc6ScOdfuW3V3VWea/kxgJm5DM6PDx0yD
h+kkXWoDClpRit7mF0Cue146nW8nBgY4McQDA7rpqMjwojDXOfGSzfkcD1qRC1z8Tg5gHEIN
PTCws1tlYpW2FTW4wJKkPmVt/Rvo0iKOeZ7dJh6EpMLwrEbH5xOYPmpiO4TfTGQRGy4igi4I
xSwP2wMtSYRhq9itLLq32ZkhJ6sMVkCmQzzNwXVxaUwCH4IJoty41u5ykE8HMcJTKMmUIIy4
4Xgip04LSzJH869JWzPPUMFt0PoXYBTKclxaqQJvsx1TuJM0DgsLR+IYy/s45T4lKDSd562+
QSI06GZmRDglEXhsvA/m+MlVL/g1sj1BNIKPssfSONWkH7D/ESDQwFaO0qgHfPgeTdZBmUAJ
sz1M2B3YgiZNzT092XON73OZuRwRcJUxVrQMOEtJNJB6imQaYEh/3i9+P3vGagU/WpvzN3O6
DjJcWOEZms07aP68zw5ffthnm3f5QoPqvo2E1BufYZtzevQLtWe8F9trpXWgsNv/ivywXrsI
aw3DHD6+1/h/Lsf3u7cAK/O/TqdXiv9b7V6r1dtwep1Wlf/9ZvH/qtQtbQyy2VcxMLxQ3P3G
1/65dDmDjEx+GUFSCIAZIeia5XcalN/IEitcp5bm+qraesPOguRysgnNAk+SeICn2BC/7E+G
xokUYucVAfNkqAKYBjOWfTIkK278GgEUrIlObc1ZXAUGj2WxDAV/lNCgXLR2VAWcc9EsgWmC
ALGdB16qZUlgieJcgnguTi5k1VieP6mdgtzodBmRbN7hj8867VJKbjHNwAH7hD9tumJTzNRp
CS50Kw6Ep7Wbd4RIO7nPCq1dK8Eeh1LEyQJZGmYuoVaKM1ZLrBhaCB1bFFQu1PzVazbmmps1
uuCBcbRK7HOTqSLpGbkq7QwbeQgMAjI6XQ8BkV1TjSp3l0fPK6IdRVEWrJT246BhEPvo4CCk
5aN25HRRQN9C4ISyFC0h1UVlKNJHHKkEmbjxYcePThxXAe7V6vnEQ7UGOCfvrz/0LDb6OPU0
TrZ8jDS0e6aHY6fTJMIoA72vb3yuHgDHJeQ5UciLj/GeglF7cq/JEn4l8tiQcgRGZJoy3X2B
rJw02Ludw/T0s+qH8KmBjb4ZXBAuYO980Drz2V/Z3BkPxIDshx8WF8hc6hhC1s07P1sZLINa
ovg5KdlPJKisYG9I95QZMrvEokLM6yZTV/UO9KZORSusljfZI5m6j+o7F+zcbj568bR/wXaa
bOvCYVs50PcS2lTlD8Z/Sg//QOL3C/Ff21m4/9va24X4b7fd263iv28U/x0ycukqwMP1S/fF
5u7YSpUDNXtoMIqBjzdpY3AqeN8T877w90i178MrY7UzujIKBe/sRnRHEoorwOlj6tUkRWn4
QqqKxzG/JYhzQIFO2aVj4CjGJyDCH0mhAW25iRIpSFkIDkmTjVU8pW1nMTije8YYahSN49Jw
J+QTE/Iws0vnQTAwMZJ2em8QF95dzTupfSeFNcb90Yi0McX9uk6f4et97prmAq86NufuI0Rz
ja5Olh1dThI4u5nM6n1KXOaOpeSw5m4rLDitvJvxW4xRcEdOCwkrOa0VbqvkQ+dcWEZ2Mdhd
MV043orpKggJzzynAR79QQSP1Srkwb7GlcUXIfqwLZMAMkkaFA4BzmdyIMoSU715AQ3G+8iw
r8AtEF4gxCSte8tcWDlXcmG0+nnz6aMX+3a/gWOy3HHS4SzeFU5DulyyiHUB13lt62L7ycFF
gizUG33AmDlroHEa+PquLt4oodVlZJvH9ISPQAEf6z/ZaRSyN7N7NjPcPwdt+Jx3rXz8f7n/
xyzpTjIcfI0z4JXnv93uXjn/43TajrPh9LqtXuX/v43/z7M/NOVp6CdbazJC4HCE+vUQphyu
ihok7pUXTzPn/bDJpDbZvkmawmt2VgHvlHLX9480vjPAXjwhfUY1+/sfgdT9fXx/xaUweWIJ
Bjw73pkMnZWJHGhsr23EhELdabbzpExnHXxnCTxVdZrdApA+YehkVd11SLuLSLsLSA1QZ6Fm
PczccVm2caR0O4h1sHDGDI5nQC0Ss2t1lG+TtbK/xSRVEQ9mZkCgK5G0m0zhgaezHklnJZJO
kyk87S8i6a5E0qXu6m9ruVCMDmscoKEiTgawYHhQv/vcJKWzDdAyGOfg+V374LnzWQG3Hwbc
ZB147eb9Og/q14RewEt3KYpuCUXxsAUZNSt0LaMGaDVMey3MAlPrgDVTLbu9i3y17N6uYs1u
LWOuiGpxHsnSrOWNIFbQgWf9hr+HwXVWwxmCM4BFajMbuZbiDGopNezgOXMM0Q8AJZE6hvov
d+gWO3TLHZYtxsJxYkhXmedyhXdmEIjG60rbsAcm7hs6GP0icO2mtg5Ykd82ioo9nIfCdx4M
3y3Cdxbg/1YiP1IsnLebYOP7Nl1TrCt91sN9mXkN234oad1VpC2i1lLqalCzdSvmxstWdTJs
4nUKt8ngxVuiPNCI2XtzUmturC6BdAkyu7OxBnJIkJkGrgP1CFTdUyiC6TVIivvnjf+/xhnw
qvi/t5f9+o85/21hShCfe1X8/z3E/wu/5rPk11fKkXr5mPb/E6zLkbPqdwpMOwWx5zVOuT98
DPHBszdbffYfGM7OmVwaYTJcFgtlR2flEz/qkbU5rRYNr2P1/GYcJfayrJ4adZENu2asvWg0
ll0ty4MBdTZePLEhSlQ9kGGSQEnkRetN2ZfW/9c4A1i1/p3dXm8h/9/b23D22k6nWv/fw/o3
v7WXr2/Ks35hWZOSqlTsyisE9drZT0cYK6SxixndPGvMLy/jKGTl8ivskNliqdHvnsx59HNE
jStKYaezg0t8atT4ivhqfXUODgHqQ6i0jxPmy6abJo8ePXRcMkjHdGLhS3W/ANd8SjWP1OP3
U/E25i7zkxdfkxp4IFqCofoXDyJwe/vQ3s6L2xw+9fbF2eX29tEK6g4zityMFLKDT/Hh4WMf
H4AEfxw9iIzDi/DlRQh/4ovw1bqBX+Lj1YNw0rXKuvpPUeiCZWMFYmwmBxQS9eYTnqVhvqvA
cr39l4Ov8Ovfq89/O7tz8Z/T7rbbG05vt12d/34r+2893rfTUI79y0SdW9K1D3qDTRGeXdYH
6mxw0MAVS0s28N1aw1rvO0z0UK5a8evhi1uNrep/pqhKVapSlapUpSpVqUpVqlKVqlSlKlWp
SlWqUpWqVKUqValKVapSlapUpSpVqUpVqlKVqlSlKlWpSlXmyv8BQBrqLQB4AAA=
--------------020800050408020303020208
Content-Type: text/plain;
name="markov-chain.rb"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="markov-chain.rb"
#
# A Markov Chain contains a graph which edges are labeled with probabilities.
# The graph is stored as two hashes, one for the absolute probabilites, one for
# the relative probabilities. The nodes of the graph correspond to the keys of the hashes.
#
# The rand() method is worst case O(n), for small lists this is ok, but for
# large lists binary search will be better O(lg n)
#
class MarkovChain
# Initializes the hashes.
#
def initialize()
@absolute = {}
@relative = {}
@sum_edges = {}
@dirty = {}
end
# The nodes of the graph correspond to the keys of the hashes.
#
def nodes
@absolute.keys
end
# Add an edge from node a to node b.
#
def add(a, b)
@absolute[a] ||= {}
@absolute[a][b] ||= 0
@absolute[a][b] += 1
@sum_edges[a] ||= 0
@sum_edges[a] += 1
@dirty[a] = true
end
# Adds a list of elements pairwise.
# TODO this is for order = 1. For higher orders a list of previous elements
# could be added into the hash, but this will break other parts of the code
def add_elems(elems)
a = nil
elems.each() do |b|
add(a, b) if ( a )
a = b
end
end
# Calculates all the relative cumulative probabilities.
#
def recalc_all()
@relative = @absolute.dup()
@relative.each_pair do | a, hash |
recalc(a) if @dirty[a]
end
end
# Calculates the relative cumulative probabilities.
#
def recalc(a)
cum = 0.0
@relative[a] = @absolute[a].dup()
sum = @sum_edges[a] * 1.0
@relative[a].each_pair do | b, value|
cum = cum + ( value / sum )
@relative[a][b] = cum
end
@dirty.delete(a)
@relative[a]
end
# Generates a random successor of node a according to the probabilities learned
# from the example tests.
#
def rand(a)
recalc(a) if @dirty[a]
if a.nil? || @relative[a].nil? || @relative[a].length == 0
return nil
elsif @relative[a].length == 1
return @relative[a].keys[0]
else
# this is a linear search now with worst case O(n), TODO log(n) binary search
value = Kernel.rand()
candidates = @relative[a].select { |b, prob| prob > value }
return candidates[0][0]
end
end
def to_s
result = []
result << "MarkovChain"
@absolute.each_pair do | key, hash |
result << "#{key}: ABS: #{@absolute[key].to_s()} - REL: #{@relative[key].to_s()}"
end
result.join("\n")
end
end
--------------020800050408020303020208
Content-Type: text/plain;
name="story-generator.rb"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="story-generator.rb"
#
require 'markov-chain'
# A generator for stories. Fill the Markov chain with the methods add()
# or add_file() and generate a story with the method story().
#
class StoryGenerator
attr_reader :mc
# Initializes.
#
def initialize(mc = nil)
if mc.nil?
@mc = MarkovChain.new()
else
@mc = mc
end
end
# Adds the words to the MarkovChain
#
def add(words)
@mc.add_elems(words)
end
# Adds a file to the MarkovChain.
#
def add_file(file)
puts "Reading file #{file}" if ( $VERBOSE )
words = Words.parse_file(file)
if ( $VERBOSE )
puts "Read in #{words.length} words."
puts "Inserting file #{file}"
STDOUT.flush()
end
@mc.add_elems(words)
end
# Genereates a story with n words (".", "," etc. counting as a word)
#
def story(n)
elems = generate(n, ".")
format(elems)
end
# Generates a story from the Markov Chain mc of length n and which starts with a
# successor of word.
#
def generate(n, word)
lexicon = @mc.nodes()
word = lexicon[rand(lexicon.length)] if word.nil?
elems = []
1.upto(n) do |i|
word = @mc.rand(word)
# if no word is word, take a random one from the lexicon
if word.nil?
elems += ["."]
word = lexicon[rand(lexicon.length)]
end
elems << word
if ( i % LOG_WORDS_NUM == 0 && $VERBOSE )
puts "Generated #{i} words."
STDOUT.flush()
end
end
elems
end
# Formats the elements.
#
def format(elems)
text = elems.join(" ")
text.gsub!(/\ [.,!?;]\ /, '\1 ')
text
end
end
--------------020800050408020303020208
Content-Type: text/plain;
name="words.rb"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="words.rb"
# A parser for texts in native languages, for example english or german.
#
# Example:
# "To be or not to be."
#
# will be parsed into the array
#
# ["To", "be", "or", "not", "to", "be", "."]
#
class Words
attr_reader :words
# Returns the words of a file.
#
def Words.parse_file(filename)
i = 1
all_words = []
File.open(filename) do | file |
file.each_line() do |line|
ws = Words.parse(line)
next if ws.nil?
all_words += ws
i += 1
if ( i % LOG_FILE_READ_NUM == 0 && $VERBOSE )
puts " Read #{i} lines."
STDOUT.flush()
end
end
end
all_words
end
# Returns the words of a line.
#
def Words.parse(line)
# replace newline
line.gsub!(/\r\n/, '')
return nil if line.length == 0
# seperate all special characters by blanks
line.gsub!(/([,;!?:.])/, ' \1 ')
# remove unused special characters
line.gsub!(/["'\-+=\t\r\n()]/, " ")
# split the line into words
words = line.split(/[ ]+/).select { |w| w.length > 0 }
words
end
end
--------------020800050408020303020208
Content-Type: text/plain;
name="main-markov-chains.rb"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline;
filename="main-markov-chains.rb"
#!/usr/local/bin/ruby -w
require 'getoptlong'
require 'story-generator'
require 'markov-chain'
require 'words'
# Prints out the synopsis.
#
def synopsis()
puts "ruby main-markov-chains.rb [--help] [--number_of_words N|-n N] [--width W|-w W] [--verbose|-v] textfiles"
end
# these ugly global variables are used for printing out the progress
LOG_FILE_READ_NUM = 1000
LOG_WORDS_NUM = 100
# command line option
$VERBOSE = nil
$NUMBER_OF_WORDS = 100
$WIDTH = 78
opts = GetoptLong.new(
["--help", "-h", GetoptLong::NO_ARGUMENT ],
["--number_of_words", "--num", "-n", GetoptLong::REQUIRED_ARGUMENT ],
["--width", "-w", GetoptLong::REQUIRED_ARGUMENT ],
["--verbose", "-v", GetoptLong::NO_ARGUMENT ]
)
opts.each do |opt, arg|
case opt
when "--help"
synopsis()
exit(0)
when "--number_of_words"
$NUMBER_OF_WORDS = Integer(arg)
when "--verbose"
$VERBOSE = true
when "--width"
$WIDTH = Integer(arg)
end
end
files = ARGV
if files.length() == 0
STDERR.puts("Error: Missing argument")
synopsis()
exit(1)
end
# main
# our story generator
sg = StoryGenerator.new()
# feed all the files into it
files.each do |file|
sg.add_file(file)
end
# calculate the probabilities
if ( $VERBOSE )
puts "Calculating probabilities ..."
STDOUT.flush()
end
sg.mc.recalc_all()
# generate the story
if ( $VERBOSE )
puts "Generating..."
STDOUT.flush()
end
story = sg.story($NUMBER_OF_WORDS)
# and print it out formatted
index = 0
last = 0
space = 0
while index < story.length()
# remember the last non word element
space = index if ( story[index, 1] =~ /[ ,:;!?.]/ );
if (index - last == $WIDTH )
raise "There is a word larger then the width" if ( last == space+1 )
puts story[last..space]
index = space
last = space + 1
end
index += 1
end
puts story[last..-1]
--------------020800050408020303020208--