formatr 1.10.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/formatr.rb +1462 -0
- data/test/1 +55 -0
- data/test/10 +7 -0
- data/test/11 +2 -0
- data/test/12 +3 -0
- data/test/13 +290 -0
- data/test/14 +5 -0
- data/test/15 +8 -0
- data/test/2 +129 -0
- data/test/3 +9 -0
- data/test/4 +100 -0
- data/test/5 +200 -0
- data/test/6 +6 -0
- data/test/7 +6 -0
- data/test/8 +40 -0
- data/test/format_test.pl +377 -0
- data/test/test_formatr.rb +761 -0
- metadata +62 -0
data/lib/formatr.rb
ADDED
@@ -0,0 +1,1462 @@
|
|
1
|
+
# Name:: FormatR
|
2
|
+
# Description:: Perl like formats for ruby
|
3
|
+
# Author:: Paul Rubel (paul@rubels.net)
|
4
|
+
# Release:: 1.10
|
5
|
+
# Homepage:: http://formatr.sourceforge.net
|
6
|
+
# Date:: January 2008
|
7
|
+
# License:: You can redistribute it and/or modify it under the same term as Ruby.
|
8
|
+
# Copyright (c) 2002,2003,2005,2008 Paul Rubel
|
9
|
+
#
|
10
|
+
# THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
|
11
|
+
# IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
|
12
|
+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
13
|
+
# PURPOSE.
|
14
|
+
#
|
15
|
+
# = To Test this code:
|
16
|
+
# Currently (1.10) the gem checking is broken since it tried to write out
|
17
|
+
# files to places where only root has permissions. You should be able
|
18
|
+
# to run the tests in the /test/ directory if you copy them somewhere else.
|
19
|
+
# I'm working on a fix.
|
20
|
+
#
|
21
|
+
# Try test_format.rb with no arguments. If nothing is amiss you should
|
22
|
+
# see OK (??/?? tests ?? asserts). This tests the format output
|
23
|
+
# against perl output (which is in the test directory if you don't
|
24
|
+
# have perl). If you would like to see the format output try
|
25
|
+
# test_format.rb --keep which will place the test's output in the file
|
26
|
+
# format_testfile{1-10}
|
27
|
+
#
|
28
|
+
# = Usage
|
29
|
+
# Class FormatR::Format in module FormatR provides perl like formats for ruby.
|
30
|
+
# For a summary of the methods you're likely to need please see FormatR::Format.
|
31
|
+
# Formats are used to create output with a similar format but with changing
|
32
|
+
# values.
|
33
|
+
#
|
34
|
+
# For example:
|
35
|
+
# require "format.rb"
|
36
|
+
# include FormatR
|
37
|
+
#
|
38
|
+
# top_ex = <<DOT
|
39
|
+
# Piggy Locations for @<< @#, @###
|
40
|
+
# month, day, year
|
41
|
+
#
|
42
|
+
# Number: location toe size
|
43
|
+
# -------------------------------------------
|
44
|
+
# DOT
|
45
|
+
#
|
46
|
+
# ex = <<TOD
|
47
|
+
# @) @<<<<<<<<<<<<<<<< @#.##
|
48
|
+
# num, location, toe_size
|
49
|
+
# TOD
|
50
|
+
#
|
51
|
+
# body_fmt = Format.new (top_ex, ex)
|
52
|
+
#
|
53
|
+
# body_fmt.setPageLength(10)
|
54
|
+
# num = 1
|
55
|
+
#
|
56
|
+
# month = "Sep"
|
57
|
+
# day = 18
|
58
|
+
# year = 2001
|
59
|
+
# ["Market", "Home", "Eating Roast Beef", "Having None", "On the way home"].each {|location|
|
60
|
+
# toe_size = (num * 3.5)
|
61
|
+
# body_fmt.printFormat(binding)
|
62
|
+
# num += 1
|
63
|
+
# }
|
64
|
+
#
|
65
|
+
#
|
66
|
+
# When run, the above code produces the following output:
|
67
|
+
# Piggy Locations for Sep 18, 2001
|
68
|
+
#
|
69
|
+
# Number: location toe size
|
70
|
+
# -------------------------------------------
|
71
|
+
# 1) Market 3.50
|
72
|
+
# 2) Home 7.00
|
73
|
+
# 3) Eating Roast Beef 10.50
|
74
|
+
# 4) Having None 14.00
|
75
|
+
# 5) On the way home 17.50
|
76
|
+
#
|
77
|
+
#
|
78
|
+
# More examples are found in test_format.rb
|
79
|
+
#
|
80
|
+
# = Supported Format Fields
|
81
|
+
#
|
82
|
+
# ===Standard perl formats
|
83
|
+
# These are explained at http://www.perldoc.com/perl5.6.1/pod/perlform.html and include:
|
84
|
+
# * left justified text, @<<<
|
85
|
+
# * right justified text, @>>
|
86
|
+
# * centered text @||| all of whose length is the number of characters in the
|
87
|
+
# field.
|
88
|
+
#
|
89
|
+
# * It also supports fields that start with a ^ which signifies that
|
90
|
+
# the input is a large string and after being printed the variable
|
91
|
+
# should have the printed portion removed from its value.
|
92
|
+
#
|
93
|
+
# * Numeric formats of the form @##.## which let you decide where you
|
94
|
+
# want a decimal point. It will add extra zeroes to the fractional part
|
95
|
+
# but if the whole portion is too big will write it out regardless
|
96
|
+
# of your specification (regarding the whole as more important than the
|
97
|
+
# fraction).
|
98
|
+
#
|
99
|
+
# * A line that contains a ~ will be suppressed if it will be blank
|
100
|
+
#
|
101
|
+
# * A line that contains ~~ will repeat until it is blank, be sure
|
102
|
+
# to use this feature with at least one field starting with a ^.
|
103
|
+
#
|
104
|
+
# === Scientific formats of the form @.#G##, @.#g##, @.#E##, and @.#e##
|
105
|
+
# * The use of G, g, E, and e is consistent with their use in printf.
|
106
|
+
#
|
107
|
+
# * If a G or g is specified the number of characters before the
|
108
|
+
# exponent, excluding the decimal point, will give the number of
|
109
|
+
# significant figures to be used in the output. For example:
|
110
|
+
# @.##G### with the value 1.234e-14 will print 1.23E-14 which has 3
|
111
|
+
# significant figures. This format @##.###g### with the value
|
112
|
+
# 123.4567E200 produces 1.23457e+202, with 6 significant figures.
|
113
|
+
# The capitalization of G effects whether the e is lower- or upper-case.
|
114
|
+
#
|
115
|
+
# * If a E or e is used the number of hashes between the decimal
|
116
|
+
# point and the E or e tells how many digits to print after the decimal
|
117
|
+
# point. The number of hashes after the precision argument just adds to the
|
118
|
+
# number of spaces available, I can't see how to reasonably adjust
|
119
|
+
# that given the other constraints. For example the format
|
120
|
+
# @##.#E### with the value 123.4567E200 produces 1.2E+202 since
|
121
|
+
# there is only one hash after the decimal point.
|
122
|
+
#
|
123
|
+
# * More examples of using the scientific formats can be found in test_format.rb
|
124
|
+
#
|
125
|
+
#
|
126
|
+
# = Reading in output printed by formats, FormatR::FormatReader
|
127
|
+
#
|
128
|
+
# The class FormatR::FormatReader can be used to read in text that has
|
129
|
+
# been output with a given format and attepmts to extract the values of the
|
130
|
+
# variables used as the input. It does a good job of simple formats, I'm sure
|
131
|
+
# that there are complex ones that can confuse it. Multi-line formats are supported
|
132
|
+
# but as the program can't be sure what the initial input looked like, and how it
|
133
|
+
# was broken across lines, every piece of a line is made to have at least one
|
134
|
+
# space after it.
|
135
|
+
#
|
136
|
+
# For example: if you had the following format:
|
137
|
+
#
|
138
|
+
# ~~^<<
|
139
|
+
# var
|
140
|
+
# and you fed it the string abcdef you would get the following:
|
141
|
+
# abc
|
142
|
+
# def
|
143
|
+
# But when var was assigned to it would be var = 'abc def'
|
144
|
+
#
|
145
|
+
# I don't know how to decide which is better. Perhaps an argument would help
|
146
|
+
#
|
147
|
+
#
|
148
|
+
# == The classes of variables
|
149
|
+
#
|
150
|
+
# It's not always possible to infer the class of the variable that made the
|
151
|
+
# format. By not taking in a binding to compare with many variables will end
|
152
|
+
# up as strings. Numeric formats should come out as numbers but all others will
|
153
|
+
# be strings and will need to be converted manually.
|
154
|
+
#
|
155
|
+
# == Using FormatR::FormatReader
|
156
|
+
#
|
157
|
+
# Using the FormatReader is relatively simple. You pass in a format to the
|
158
|
+
# constructor and then call readFormat and give in an array of formatted text.
|
159
|
+
# It will return a hash with the key/value pairs of the variables in the
|
160
|
+
# format. It can also be called with a block that is passed the hash.
|
161
|
+
#
|
162
|
+
# For example:
|
163
|
+
#
|
164
|
+
#
|
165
|
+
# f = []
|
166
|
+
# # make a format
|
167
|
+
# f.push( '<?xml version="1.0"?>' )
|
168
|
+
# f.push( '@@@ Blah @@@ }Blah @< @|| @#.#' )
|
169
|
+
# f.push( 'var_one,var_one,var_one,var_one,var_one,var_one,' +
|
170
|
+
# ' var_two, var_three, var_four')
|
171
|
+
# f.push( '@<<< @<<<')
|
172
|
+
# f.push( 'var_one,var_one')
|
173
|
+
# format = Format.new(f)
|
174
|
+
#
|
175
|
+
# #set values and print it out.
|
176
|
+
# var_one, var_two, var_three, var_four = 1, 2, 3, 4.3
|
177
|
+
# output_filename = "format_testfile12"
|
178
|
+
# File.open( output_filename, File::CREAT | File::WRONLY | File::TRUNC ) { |file|
|
179
|
+
# format.io = file
|
180
|
+
# format.printFormat(binding)
|
181
|
+
# }
|
182
|
+
# # read in the output
|
183
|
+
# output = []
|
184
|
+
# File.open( output_filename ){ |file|
|
185
|
+
# output = file.readlines()
|
186
|
+
# }
|
187
|
+
#
|
188
|
+
# # make a new FormatReader
|
189
|
+
# reader = FormatReader.new (format)
|
190
|
+
# # Read in the values
|
191
|
+
# res = reader.readFormat (output)
|
192
|
+
# # Check that the values are correct
|
193
|
+
# assert (res['var_one'] == var_one.to_s)
|
194
|
+
# assert (res['var_two'] == var_two.to_s)
|
195
|
+
# assert (res['var_three'] == var_three.to_s)
|
196
|
+
# assert (res['var_four'] == var_four)
|
197
|
+
#
|
198
|
+
# # or using a block for reading multiple lines:
|
199
|
+
# reader.readFormat (output) do |res|
|
200
|
+
# assert (res['var_one'] == var_one.to_s)
|
201
|
+
# assert (res['var_two'] == var_two.to_s)
|
202
|
+
# assert (res['var_three'] == var_three.to_s)
|
203
|
+
# assert (res['var_four'] == var_four)
|
204
|
+
# end
|
205
|
+
#
|
206
|
+
#
|
207
|
+
#
|
208
|
+
# = Changes:
|
209
|
+
# ==1.10.0
|
210
|
+
# * Moved from sourceforge to rubyforge and released as a gem.
|
211
|
+
# * Changed filename from format.rb to formatr.rb.
|
212
|
+
# * Checked with the most recent version of 1.9 and everything
|
213
|
+
# still works fine.
|
214
|
+
#
|
215
|
+
# ==1.09
|
216
|
+
# * Added a block form of readFormat that lets you loop through output
|
217
|
+
# instead of having to make your own loop
|
218
|
+
#
|
219
|
+
#
|
220
|
+
# ==1.08
|
221
|
+
# * Moved to Test::Unit from RubyUnit.
|
222
|
+
# * Made things work with 1.8.0pre releases. Hopefully we'll be
|
223
|
+
# ready for 1.8.0 when it finally comes out while maintaining
|
224
|
+
# 1.6.x compatability.
|
225
|
+
#
|
226
|
+
# ==1.07
|
227
|
+
# * You can now use formats without having to use eval. If you pass in
|
228
|
+
# a hash of names to values that can be used instead. There is also
|
229
|
+
# an optimization you can use by calling format.useHash(true) that
|
230
|
+
# will turn your binding into a hash while the format is being
|
231
|
+
# printed. This may speed things up. The default is still to use
|
232
|
+
# eval so that things do not break as some dynamic formats may not
|
233
|
+
# work with a hash. When a value is computed using side effects of
|
234
|
+
# some other evaluation that has taken place while printing the
|
235
|
+
# format a hash won't work. You can also use the printFormatWithHash
|
236
|
+
# method is you want to avoid evaling entirely. test_four in
|
237
|
+
# test_format.rb shows one example of how to use hashes to print formats
|
238
|
+
#
|
239
|
+
# * Page numbers are now working correctly. Before if you had a page
|
240
|
+
# number in a header or footer it was problematic. The printing of
|
241
|
+
# a page has been refactored and now works much better.
|
242
|
+
#
|
243
|
+
# * Thanks to Amos Gouaux for suggesting the setLinesLeft method!
|
244
|
+
#
|
245
|
+
#
|
246
|
+
# ==1.06
|
247
|
+
# * I thought that the ~ had to be in the front of the picture line,
|
248
|
+
# this isn't so. If you place the ~~ anywhere in the line it will
|
249
|
+
# repeat until the line is empty.
|
250
|
+
#
|
251
|
+
# * Added the FormatReader to read in formatted text and get values back
|
252
|
+
#
|
253
|
+
# ==1.05
|
254
|
+
# * Hugh Sasse sent in a patch to clean up warnings. I was sloppy with my
|
255
|
+
# spacing but hopefully have learned better. Thanks Hugh!
|
256
|
+
#
|
257
|
+
# * Fixed a bug in repeating lines using ~~ when the last line wouldn't get
|
258
|
+
# placed correctly unless it ended with a ' '
|
259
|
+
#
|
260
|
+
# * Fixed a bug where a line that started with a <,>, or | would loose
|
261
|
+
# this character if there wasn't a @ or ^ before it.
|
262
|
+
# The parsing of the non-picture parts of a picture line is greatly
|
263
|
+
# improved.
|
264
|
+
#
|
265
|
+
# ==1.04
|
266
|
+
# * Added a scientific notation formatter so you can use @#.###E##,
|
267
|
+
# @##.##e##, @#.###G##, or @##.##g##. The use of G and E is
|
268
|
+
# consistent to their use in printf. If a G or g is specified the
|
269
|
+
# number of characters before the exponent excluding the decimal
|
270
|
+
# point will give the number of significant figures to be used in the
|
271
|
+
# output. If a E or e is used the number of hashes between the decimal
|
272
|
+
# point and the E tells how many digits to print after the decimal
|
273
|
+
# point. The number of hashes after the E just adds to the
|
274
|
+
# number of spaces available, I can't see how to reasonably adjust
|
275
|
+
# that given the other constraints.
|
276
|
+
#
|
277
|
+
# ==1.03
|
278
|
+
# * If perl isn't there use cached output to test against.
|
279
|
+
#
|
280
|
+
# * better packaging, new versions won't write over the older ones when
|
281
|
+
# you unpack
|
282
|
+
#
|
283
|
+
# * Changed the Format.new call. In the past you could pass in an IO
|
284
|
+
# object as a second parameter. You now need to use the Format.io=
|
285
|
+
# method as the signature of Format.new has changed as shown
|
286
|
+
# below. None of the examples used the second parameter so hopefully
|
287
|
+
# it's safe to change
|
288
|
+
#
|
289
|
+
# * Added optional arguments to Format.new so you can set top, body, and middle
|
290
|
+
# all at once like so Format.new(top, middle, bottom) or even Format.new(top, middle).
|
291
|
+
# If you want a bottom without a top you'll either need to call setBottom or pass nil
|
292
|
+
# or an empty format for top like so Format.new (nil, middle, bottom)
|
293
|
+
#
|
294
|
+
# * Made the testing script clean up after itself unless you pass the -keep flag
|
295
|
+
#
|
296
|
+
# * Modified setTop and setBottom so you can pass in a string or an array of strings
|
297
|
+
# that can be used to specify a format instead of having to create one yourself.
|
298
|
+
# Thanks again to Hugh Sasse for not settling for a second rate interface.
|
299
|
+
#
|
300
|
+
# * Move test_format.rb over to runit.
|
301
|
+
#
|
302
|
+
# * Added functionality so that if you pass in a format string, or
|
303
|
+
# array of strings to setTop or setBottom it does the right
|
304
|
+
# thing. This way you don't need to make the extra formats just to
|
305
|
+
# pass them in.
|
306
|
+
#
|
307
|
+
#
|
308
|
+
# ==1.02
|
309
|
+
# * Allow formats to be passed in as arrays of strings as well as just long strings
|
310
|
+
#
|
311
|
+
# * Added functionality so that if the first format on a page is too
|
312
|
+
# long to fit on that page it will be printed partially with a
|
313
|
+
# bottom. Perl seems to just print the whole thing and ignore the page
|
314
|
+
# size in this case.
|
315
|
+
#
|
316
|
+
# * Fixed a bug where if your number didn't have a fractional part it
|
317
|
+
# would crash if you used a format that need a fractional portion like @##.##
|
318
|
+
#
|
319
|
+
# * On the recommendation of Hugh Sasse added
|
320
|
+
# finishPageWithoutFF(aBinding, io=@io) and
|
321
|
+
# finishPageWithFF(aBinding, io=@io) which will print out blank
|
322
|
+
# lines until the end of the page and then print the bottom, with
|
323
|
+
# and without a ^L. Only works on fixed sized bottoms.
|
324
|
+
#
|
325
|
+
# ==1.01
|
326
|
+
# * Moved to rdoc for generating documentation.
|
327
|
+
#
|
328
|
+
# ==1.00
|
329
|
+
# * Bottoms work iff you have a fixed size format and print out a
|
330
|
+
# top afterwords. This means that you will only get a bottom if you
|
331
|
+
# will print a top right after it so the last format page you print
|
332
|
+
# won't have a bottom. It's impossible to figure out if you are
|
333
|
+
# done with the format and therefore need to print the
|
334
|
+
# bottom. Perhaps in a future release we can just take fixed sized
|
335
|
+
# bottoms off the available size and get them to work that way.
|
336
|
+
# * Added support for Format.pageNumber()
|
337
|
+
# * Support ~ to be a space
|
338
|
+
# * Support ~ to suppress lines when the variables are empty
|
339
|
+
# * Support ~~ to repeat until the variables are empty
|
340
|
+
# * Support comments. If the first character in a line is a # the
|
341
|
+
# line is a comment
|
342
|
+
# * Testing now compares against perl, it's a bit easier than
|
343
|
+
# writing the tests manually.
|
344
|
+
# ==0.93
|
345
|
+
# * Added support for the ^ character to start a format
|
346
|
+
#
|
347
|
+
# == 0.92
|
348
|
+
# * Added end of page characters and introduced line counts.
|
349
|
+
#
|
350
|
+
# * Added the ability to manipulate the line count in case you write
|
351
|
+
# to the file handle yourself
|
352
|
+
#
|
353
|
+
# * Added format sizes. They just give the number of lines in the
|
354
|
+
# current format. They don't try to iterate and get some total
|
355
|
+
# count including tops and bottoms.
|
356
|
+
#
|
357
|
+
#
|
358
|
+
# = Incompatibilities/Issues
|
359
|
+
#
|
360
|
+
# * If you use bottom be sure to check that you're happy with the
|
361
|
+
# output. It doesn't currently work with variable sized bottoms. You
|
362
|
+
# can use the finishPageWith{out}FF(...) methods to print out a
|
363
|
+
# bottom if you're done printing but haven't finished a page.
|
364
|
+
#
|
365
|
+
# * Watch out for @#@??? as formats, see [ruby-talk:27782] and
|
366
|
+
# [ruby-talk:27734]. This should be fixed in a future version of
|
367
|
+
# ruby. The basic problem is that the here documents are equivalent
|
368
|
+
# to "" and not '', they will evaluate variables in them. If this is
|
369
|
+
# a problem be sure to just make a long string with '' and pass that
|
370
|
+
# in. You can also pass in a string of arrays.
|
371
|
+
#
|
372
|
+
# * Rounding seems to be broken in perl, if you try to print the following
|
373
|
+
# format with 123.355 you won't get the same answer, you'll get 123.35 and
|
374
|
+
# 123.36. FormatR rounds up and plans to unless there is a
|
375
|
+
# convincing reason not to.
|
376
|
+
# format TEST_FORMAT =
|
377
|
+
# ^#.### ^##.##
|
378
|
+
# $num, $num
|
379
|
+
# I'm betting that perl must use round to even or odd. this needs to be looked into
|
380
|
+
#
|
381
|
+
#
|
382
|
+
# =To Do/Think about:
|
383
|
+
# * Fix gem check -t formatr so that it works without write access to the test
|
384
|
+
# directory.
|
385
|
+
#
|
386
|
+
# * Have a format that chops lines that are too long to fit in the specified space
|
387
|
+
#
|
388
|
+
# * Mark so that a user can set whether to use or not FF
|
389
|
+
#
|
390
|
+
# * Watch out for vars that aren't assigned but try to be used.
|
391
|
+
#
|
392
|
+
# * blank out undefined @##.# values with ~
|
393
|
+
#
|
394
|
+
# * some install mechanism?
|
395
|
+
#
|
396
|
+
# * Is there a better name than resetPage?
|
397
|
+
#
|
398
|
+
# * Hugh Sasse: The only other thing I wanted from Perl formats, which was not there,
|
399
|
+
# was a means to set the maximum width, and create picture lines
|
400
|
+
# computationally, so I could decide I wanted this and that on the left,
|
401
|
+
# such and such on the right, and *the rest* (the middle) filled out with
|
402
|
+
# some data without having to bang away on the < key for ages, hoping
|
403
|
+
# I got the width right.
|
404
|
+
#
|
405
|
+
# I think an extra line will be useful here, between the vars and the picture line
|
406
|
+
#
|
407
|
+
# * Fix variable sized bottoms better. I'm not sure if this is
|
408
|
+
# possible. You could try computing it first but this would cause
|
409
|
+
# trouble if it depends upon the body format. I'm currently planning
|
410
|
+
# to just live with fixed sized bottoms.
|
411
|
+
#
|
412
|
+
# * The solution to this is probably to buffer the changes to the binding
|
413
|
+
# until you know they will work.
|
414
|
+
#
|
415
|
+
# =Thanks go to
|
416
|
+
# Hugh Sasse for his enlightening comments and suggestions. He has been incredibly
|
417
|
+
# helpful in making this package usable. Amos Gouaux has also been
|
418
|
+
# helpful with suggestions and code. Thanks to both of you.
|
419
|
+
|
420
|
+
module FormatR
|
421
|
+
|
422
|
+
# an exception that we can throw
|
423
|
+
class FormatException < Exception
|
424
|
+
end
|
425
|
+
|
426
|
+
# This class holds a single block of text, either something
|
427
|
+
# unchanging or a picture element of some format.
|
428
|
+
class FormatEntry
|
429
|
+
attr_accessor :val, :unchanging
|
430
|
+
|
431
|
+
def initialize (val, unchanging)
|
432
|
+
@unchanging = unchanging
|
433
|
+
@val = val
|
434
|
+
unless (unchanging)
|
435
|
+
s = val.size - 1
|
436
|
+
|
437
|
+
if (val =~ /[@^][<]{#{s},#{s}}/)
|
438
|
+
@formatter = LeftFormatter.new(val)
|
439
|
+
elsif (val =~ /[@^][>]{#{s},#{s}}/)
|
440
|
+
@formatter = RightFormatter.new(val)
|
441
|
+
elsif (val =~ /[@^][\|]{#{s},#{s}}/)
|
442
|
+
@formatter = CenterFormatter.new(val)
|
443
|
+
elsif (val =~ /[@^](#*)([\.]{0,1})(#*)([eEgG])(#+)/)
|
444
|
+
@formatter = ScientificNotationFormatter.new($1, $2, $3, $4, $5)
|
445
|
+
elsif (val =~ /[@^](#*)([\.]{0,1})(#*)/)
|
446
|
+
@formatter = NumberFormatter.new($1, $2, $3)
|
447
|
+
else
|
448
|
+
raise FormatException.new(), "Malformed format entry \"#{@val}\""
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
# is this just unchanging characters
|
454
|
+
def isUnchanging? ()
|
455
|
+
return @unchanging
|
456
|
+
end
|
457
|
+
|
458
|
+
# give back the string passed through the appropriate formatter
|
459
|
+
def formatString (string, var_name=nil, aBinding=nil)
|
460
|
+
result = @formatter.formatString(string, var_name, aBinding)
|
461
|
+
return result
|
462
|
+
end
|
463
|
+
|
464
|
+
# show our values
|
465
|
+
def to_s ()
|
466
|
+
output = "'" + @val + "' unchanging:" + @unchanging.to_s
|
467
|
+
#(output << " formatter:" + @formatter.class.to_s) if (!@unchanging)
|
468
|
+
output
|
469
|
+
end
|
470
|
+
|
471
|
+
end # end of class FormatEntry
|
472
|
+
|
473
|
+
# This is the base class for all the formats, <,>,|, and # of the
|
474
|
+
# @ or ^ persuasion. It keeps track of filled variables and the length
|
475
|
+
# string should have.
|
476
|
+
class Formatter
|
477
|
+
def initialize (val)
|
478
|
+
@len = val.size()
|
479
|
+
@filled = false
|
480
|
+
if (val =~ /\^.*/)
|
481
|
+
@filled = true
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
#if it's a filled field chop the displayed stuff off in the context given
|
486
|
+
def changeVarValue (var_value, var_name, aBinding)
|
487
|
+
result = var_value[0, @len]
|
488
|
+
max_letter = var_value[0,@len + 1].rindex(' ')
|
489
|
+
max_cr = var_value[0,@len + 1].index("\n")
|
490
|
+
max_letter = max_cr if (!max_cr.nil?)
|
491
|
+
if (var_value.length <= @len)
|
492
|
+
result = var_value
|
493
|
+
max_letter = @len
|
494
|
+
end
|
495
|
+
if (max_letter != nil)
|
496
|
+
result = var_value[0,max_letter]
|
497
|
+
end
|
498
|
+
setVarValue( var_name, var_value[result.size(),var_value.size()],
|
499
|
+
aBinding)
|
500
|
+
return result
|
501
|
+
end
|
502
|
+
|
503
|
+
# Move the call to eval into one place
|
504
|
+
# we shouldn't have to
|
505
|
+
def setVarValue (var_name, var_value, aBinding)
|
506
|
+
if var_value.nil?
|
507
|
+
var_value = ''
|
508
|
+
end
|
509
|
+
var_value.gsub!(/^\s+/,'')
|
510
|
+
if (aBinding.class != binding.class)
|
511
|
+
aBinding[var_name] = var_value
|
512
|
+
else
|
513
|
+
escaped_var_value =
|
514
|
+
var_value.gsub(/(\\*)'/) { |m| $1.length % 2 == 0 ? $1 + "\\'" : m }
|
515
|
+
to_eval = "#{var_name} = '#{escaped_var_value}'";
|
516
|
+
#puts "going to eval '#{to_eval}'"
|
517
|
+
eval(to_eval, aBinding)
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
# return a formatted string of the correct length
|
522
|
+
def formatString (var_value, var_name, aBinding)
|
523
|
+
result = var_value[0,@len]
|
524
|
+
if (! @filled)
|
525
|
+
return result
|
526
|
+
end
|
527
|
+
return changeVarValue(var_value, var_name, aBinding)
|
528
|
+
end
|
529
|
+
|
530
|
+
end #enf of class Formatter
|
531
|
+
|
532
|
+
# this format doesn't care if it's a @ or an ^, it acts the same and doesn't chop things
|
533
|
+
# used for @##.## formats
|
534
|
+
class NumberFormatter < Formatter
|
535
|
+
def initialize (wholeString, radix, fractionString)
|
536
|
+
@whole = wholeString.size + 1 # for the '@'
|
537
|
+
@fraction = fractionString.size
|
538
|
+
@radix = radix.size #should always be 1
|
539
|
+
@len = @whole + @fraction + @radix
|
540
|
+
end
|
541
|
+
|
542
|
+
# given a string that's a number spit it back with the right number of digits
|
543
|
+
# and rounded the correct amount.
|
544
|
+
def formatString (s, unused_var_name=nil, unused_aBinding=nil)
|
545
|
+
if (s.size == 1)
|
546
|
+
return formatInt(s)
|
547
|
+
end
|
548
|
+
num = s.split('.') # should this take into account internationalization?
|
549
|
+
res = num[0]
|
550
|
+
res = "" if (res.nil?) ## pgr xxx
|
551
|
+
spaceLeft = @fraction + @radix
|
552
|
+
if (res.size > @whole)
|
553
|
+
spaceLeft = @len - res.size()
|
554
|
+
end
|
555
|
+
if (spaceLeft > 0)
|
556
|
+
res += '.'
|
557
|
+
spaceLeft -= 1
|
558
|
+
end
|
559
|
+
res += getFract(num, spaceLeft) if (spaceLeft > 0)
|
560
|
+
|
561
|
+
max = @len
|
562
|
+
if (res.size > max)
|
563
|
+
res = res[0,max]
|
564
|
+
end
|
565
|
+
res.rjust(max)
|
566
|
+
end
|
567
|
+
|
568
|
+
def formatInt (s)
|
569
|
+
s.to_s.ljust(@len)
|
570
|
+
end
|
571
|
+
|
572
|
+
# what portion of the number is after the decimal point and should be printed
|
573
|
+
def getFract (num, spaceLeft)
|
574
|
+
num[1] = "" if (num[1].nil?)
|
575
|
+
@fraction.times {num[1] += '0'}
|
576
|
+
fract = num[1][0,spaceLeft + 1]
|
577
|
+
if (fract.size() >= spaceLeft + 1)
|
578
|
+
if ((fract[spaceLeft,1].to_i) >= 5 )
|
579
|
+
fract[spaceLeft - 1, 1] = ((fract[spaceLeft - 1, 1].to_i) + 1).to_s
|
580
|
+
end
|
581
|
+
end
|
582
|
+
return fract[0,spaceLeft]
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
############################################################
|
587
|
+
# make a formatter that will spit out scientific notation
|
588
|
+
############################################################
|
589
|
+
class ScientificNotationFormatter < Formatter
|
590
|
+
# Make a new formatter that will print out in scientific notation
|
591
|
+
def initialize (whole, radix, fraction, precision_g_e, exponent)
|
592
|
+
@total_size = ("@" + whole + radix + fraction + precision_g_e + exponent).size
|
593
|
+
@fraction = fraction.length
|
594
|
+
@sig_figs = ("@" + whole + fraction).length
|
595
|
+
@g_e = precision_g_e
|
596
|
+
end
|
597
|
+
|
598
|
+
def formatString (s, unused_var_name=nil, unused_aBinding=nil)
|
599
|
+
#might want to put a %0 to pad w/ 0's
|
600
|
+
precision = ((@g_e =~ /[Ee]/) ? @fraction : @sig_figs)
|
601
|
+
result = sprintf("%#{@total_size}.#{precision}#{@g_e}", s)
|
602
|
+
result
|
603
|
+
end
|
604
|
+
end
|
605
|
+
|
606
|
+
## Format things that go to the left, ala <
|
607
|
+
class LeftFormatter < Formatter
|
608
|
+
def initialize (val)
|
609
|
+
super
|
610
|
+
end
|
611
|
+
|
612
|
+
#send things left
|
613
|
+
def formatString (s, var_name, binding)
|
614
|
+
s = super
|
615
|
+
s.ljust(@len)
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
## Format things that go to the right, ala >
|
620
|
+
class RightFormatter < Formatter
|
621
|
+
def initialize (val)
|
622
|
+
super
|
623
|
+
@len = val.size()
|
624
|
+
end
|
625
|
+
|
626
|
+
#send things right
|
627
|
+
def formatString (s, var_name, binding)
|
628
|
+
s = super
|
629
|
+
s.rjust(@len)
|
630
|
+
end
|
631
|
+
end
|
632
|
+
|
633
|
+
## Format things that go to the center, ala |
|
634
|
+
class CenterFormatter < Formatter
|
635
|
+
def initialize (val)
|
636
|
+
super
|
637
|
+
@len = val.size()
|
638
|
+
end
|
639
|
+
|
640
|
+
#center things
|
641
|
+
def formatString (s, var_name, binding)
|
642
|
+
s = super
|
643
|
+
s.center(@len)
|
644
|
+
end
|
645
|
+
end
|
646
|
+
|
647
|
+
|
648
|
+
# The class that exports the functionality that a user is interested in.
|
649
|
+
class Format
|
650
|
+
public
|
651
|
+
# Set the IO that the format will be printed to, from stdout to a
|
652
|
+
# file for example.
|
653
|
+
attr_accessor :io
|
654
|
+
|
655
|
+
# Print out the specified format. You need to pass in a Binding
|
656
|
+
# object for the variables that will be used in the format. This is
|
657
|
+
# usually just a call to Kernel.binding. The next argument gives a
|
658
|
+
# file handler to print to. This is useful so that a top or
|
659
|
+
# bottom's output get written to the same place as the main format
|
660
|
+
# is going even if their formats have a different io when they're
|
661
|
+
# not attached.
|
662
|
+
def printFormat (aBinding, io = @io)
|
663
|
+
if (@use_hash)
|
664
|
+
if (aBinding.is_a?( Binding ))
|
665
|
+
printFormatFromBinding( aBinding, io )
|
666
|
+
else
|
667
|
+
printFormatWithHash( aBinding, io )
|
668
|
+
end
|
669
|
+
else
|
670
|
+
printFormatWithBinding( aBinding, io )
|
671
|
+
|
672
|
+
end
|
673
|
+
end
|
674
|
+
|
675
|
+
# print the format given that the binding is a hash of
|
676
|
+
# values. This method will not call eval at all.
|
677
|
+
def printFormatWithHash (aHash, io = @io)
|
678
|
+
useHash( true )
|
679
|
+
@binding = aHash
|
680
|
+
printBodyFormat(io)
|
681
|
+
end
|
682
|
+
|
683
|
+
#print the format given that the binding is actually a binding.
|
684
|
+
def printFormatWithBinding (aBinding, io = @io)
|
685
|
+
useHash( false )
|
686
|
+
@binding = aBinding
|
687
|
+
printBodyFormat(io)
|
688
|
+
end
|
689
|
+
|
690
|
+
# When you don't want anymore on this page just fill it with blank
|
691
|
+
# lines and print the bottom if it's there, print a ^L also. This
|
692
|
+
# is good if you want to finish off the page but print more later
|
693
|
+
# to the same file.
|
694
|
+
def finishPageWithFF (aBinding, io = @io)
|
695
|
+
finishPage(aBinding, false, io)
|
696
|
+
end
|
697
|
+
|
698
|
+
# When you don't want anymore on this page just fill it with blank
|
699
|
+
# lines and print the bottom if it's there. Don't print a ^L at
|
700
|
+
# the end. This is good if this will be the last page.
|
701
|
+
def finishPageWithoutFF (aBinding, io = @io)
|
702
|
+
finishPage(aBinding, true, io)
|
703
|
+
end
|
704
|
+
|
705
|
+
# Return how many times the top has been printed. You can use this
|
706
|
+
# to number pages. An empty top can be used if you need the page
|
707
|
+
# number but don't want to print any other header. This is a somewhat
|
708
|
+
# interesting function as the bottom is only printed when a page is
|
709
|
+
# finished or a top is needed. If this is the case we'll pretend the
|
710
|
+
# page number is one h
|
711
|
+
def pageNumber ()
|
712
|
+
if (@print_bottom)
|
713
|
+
return @page_number + 1
|
714
|
+
end
|
715
|
+
return @page_number
|
716
|
+
end
|
717
|
+
|
718
|
+
# How big is the format? May be useful if you want to try a bottom
|
719
|
+
# with a variable length format
|
720
|
+
def getSize ()
|
721
|
+
@format_length
|
722
|
+
end
|
723
|
+
|
724
|
+
# If you want something to show up before the regular text of a
|
725
|
+
# format you can specify it here. It will be printed once above
|
726
|
+
# the format it is being set within. You can pass in either a
|
727
|
+
# format or the specification of a format and it will make one for you.
|
728
|
+
def setTop (format)
|
729
|
+
top_format = format
|
730
|
+
if (!format.is_a?(Format))
|
731
|
+
top_format = Format.new(format)
|
732
|
+
end
|
733
|
+
raise FormatException.new(), "recursive format not allowed" if (top_format == self)
|
734
|
+
@top = top_format
|
735
|
+
#in case we've already set use_hash
|
736
|
+
useHash( @use_hash )
|
737
|
+
|
738
|
+
end
|
739
|
+
|
740
|
+
# Set a format to print at the end of a page. This is tricky and
|
741
|
+
# you should be careful using it. It currently has problems on
|
742
|
+
# short pages (at least). In order for a bottom to show up you
|
743
|
+
# need to finish off a page. This means that formats less than a
|
744
|
+
# page will need to be finished off with a call to one of the
|
745
|
+
# finishPageWith[out]FF methods.
|
746
|
+
def setBottom (format)
|
747
|
+
bottom_format = format
|
748
|
+
if (!format.is_a?(Format))
|
749
|
+
bottom_format = Format.new(format);
|
750
|
+
end
|
751
|
+
raise FormatException, "recursive format not allowed" if (bottom_format == self)
|
752
|
+
@bottom = bottom_format
|
753
|
+
#in case we've already set use_hash
|
754
|
+
useHash( @use_hash )
|
755
|
+
|
756
|
+
end
|
757
|
+
|
758
|
+
# Sets the number of lines on a page. If you don't want page breaks
|
759
|
+
# set this to some large number that you hope you won't offset or
|
760
|
+
# liberally use resetPage. The default is 60.
|
761
|
+
def setPageLength (len)
|
762
|
+
@page_length = len
|
763
|
+
resetPage()
|
764
|
+
end
|
765
|
+
|
766
|
+
# Sets the variable that says how many lines may be printed to the
|
767
|
+
# maximum for the page which can be set using setPageLength (anInt).
|
768
|
+
# Defaults to 60.
|
769
|
+
def resetPage ()
|
770
|
+
@lines_left = @page_length
|
771
|
+
@top.resetPage unless @top.nil?
|
772
|
+
@bottom.resetPage unless @bottom.nil?
|
773
|
+
end
|
774
|
+
|
775
|
+
# If you're writing to the file handle in another way than by
|
776
|
+
# calling printFormat you can keep the pagination working using
|
777
|
+
# this call to correctly keep track of lines.
|
778
|
+
def addToLineCount(line_change)
|
779
|
+
@lines_left += line_change
|
780
|
+
end
|
781
|
+
|
782
|
+
# If you want to tell the system how many lines are left.
|
783
|
+
def setLinesLeft(lines_left)
|
784
|
+
@lines_left = lines_left
|
785
|
+
end
|
786
|
+
|
787
|
+
|
788
|
+
# Create a new format with the given top, bottom, and middle
|
789
|
+
# formats. One argument will default to a top while two will give
|
790
|
+
# you a top and a middle. If you want a bottom and no top you'll
|
791
|
+
# need to pass an empty format in as the first argument or a
|
792
|
+
# nil. The output defaults to standard out but can be changed with
|
793
|
+
# the Format.io= method.
|
794
|
+
#
|
795
|
+
# The format is a string in the style of a perl format or an array
|
796
|
+
# of strings each of which is a line of a perl format. The passed
|
797
|
+
# in format contains multiple lines, picture lines and argument
|
798
|
+
# lines. A picture line can contain any text but if it contains an
|
799
|
+
# at field (@ followed by any number of <,>,| or a group of #'s of
|
800
|
+
# the format #*.#* or #*) it must be followed by an argument
|
801
|
+
# line. The arguments in the argument line are inserted in place
|
802
|
+
# of the at fields in the picture line. Perl documentation for
|
803
|
+
# formats can be found here:
|
804
|
+
# http://www.cpan.org/doc/manual/html/pod/perlform.html
|
805
|
+
# An example of a format is
|
806
|
+
# format = <<DOT
|
807
|
+
# Name: @<<< @<<<<<
|
808
|
+
# first_name last_name
|
809
|
+
# DOT
|
810
|
+
#
|
811
|
+
# This line specifies that when requested one line should be
|
812
|
+
# printed and that it will say "Name: #{first_name} #{last_name}\n" but
|
813
|
+
# that if either of those variables is longer than the length its
|
814
|
+
# format the result will be truncated to the length of the format.
|
815
|
+
#
|
816
|
+
# An at field specified as @<* specifies that the variable should
|
817
|
+
# be left justified within the space allocated. @>* is right
|
818
|
+
# justified, and @| is centered. #'s are used to print numbers and
|
819
|
+
# can be used to set the number of digits after the decimal
|
820
|
+
# point. However the whole number portion of an argument will
|
821
|
+
# always be printed in its entirety even if it takes space set for
|
822
|
+
# the fractional portion or even more space. If the fractional
|
823
|
+
# portion is not long enough to fill the described space it will be
|
824
|
+
# padded with 0s.
|
825
|
+
def initialize (mid_or_top, mid_format=nil, bottom_format=nil)
|
826
|
+
if (mid_or_top.nil?)
|
827
|
+
raise FormatException.new(),
|
828
|
+
" You need to pass in at least one non-nil argument"
|
829
|
+
end
|
830
|
+
@use_hash = false
|
831
|
+
@io = $stdout
|
832
|
+
@picture_lines = []
|
833
|
+
@vars = []
|
834
|
+
@top = @bottom = nil
|
835
|
+
@page_length = 60
|
836
|
+
@lines_left = @page_length
|
837
|
+
@format_length = 0
|
838
|
+
|
839
|
+
@buffered_lines = []
|
840
|
+
@print_top = true
|
841
|
+
@printed_a_body = false
|
842
|
+
@print_bottom = false
|
843
|
+
|
844
|
+
@page_number = 1
|
845
|
+
|
846
|
+
lines = ((mid_format.nil?) ? mid_or_top : mid_format)
|
847
|
+
if (lines.class == String)
|
848
|
+
lines = lines.split( /\n/ )
|
849
|
+
end
|
850
|
+
|
851
|
+
expected_vars = 0
|
852
|
+
lines.each {|line|
|
853
|
+
if (line =~ /^#.*/)
|
854
|
+
#don't do anything, it's a comment
|
855
|
+
elsif (0 != expected_vars)
|
856
|
+
expected_vars = getVarLine(line, expected_vars)
|
857
|
+
else
|
858
|
+
expected_vars = getPictureLine(line, expected_vars)
|
859
|
+
@format_length += 1
|
860
|
+
end
|
861
|
+
}
|
862
|
+
|
863
|
+
setTop(mid_or_top) if mid_format
|
864
|
+
setBottom(bottom_format) if bottom_format
|
865
|
+
end
|
866
|
+
|
867
|
+
# print out all the values we're holding for pictures
|
868
|
+
# useful for debugging
|
869
|
+
def showPictureLine ()
|
870
|
+
@picture_lines.each do |line|
|
871
|
+
puts "line:"
|
872
|
+
line.each do |element|
|
873
|
+
puts " #{element.to_s} "
|
874
|
+
end
|
875
|
+
end
|
876
|
+
end
|
877
|
+
|
878
|
+
# return an Array of picture line FormatHolder s
|
879
|
+
def getPictureLines ()
|
880
|
+
output = Array.new
|
881
|
+
@picture_lines.each_index do |i|
|
882
|
+
line = @picture_lines[i]
|
883
|
+
vars = @vars[i].dup
|
884
|
+
output_line = FormatHolder.new
|
885
|
+
output_line.repeat = line.repeat
|
886
|
+
line.each do |element|
|
887
|
+
val = element.val
|
888
|
+
var_name = nil
|
889
|
+
var_name = vars.shift() unless (element.unchanging)
|
890
|
+
var_name.strip! unless (element.unchanging)
|
891
|
+
output_line.push( [val,var_name] ) unless val == ""
|
892
|
+
end
|
893
|
+
output.push( output_line )
|
894
|
+
end
|
895
|
+
output
|
896
|
+
end
|
897
|
+
|
898
|
+
# if one format sets the @use_hash value everyone else will need
|
899
|
+
# to know too
|
900
|
+
def useHash (value)
|
901
|
+
@use_hash = value
|
902
|
+
@bottom.useHash( value ) unless (@bottom.nil?)
|
903
|
+
@top.useHash( value ) unless (@top.nil?)
|
904
|
+
end
|
905
|
+
|
906
|
+
protected
|
907
|
+
|
908
|
+
# Print out a format using the values in the given binding output
|
909
|
+
# to the given io. This method will only eval the arguments on the
|
910
|
+
# way in and out of printing the format instead of every time they
|
911
|
+
# are required. This may save time but will not work for extremely
|
912
|
+
# dynamic formats.
|
913
|
+
def printFormatFromBinding (aBinding, io = @io)
|
914
|
+
# save the bindings in a hash
|
915
|
+
collectVarValues(aBinding)
|
916
|
+
useHash( true )
|
917
|
+
@binding = @bindingVars
|
918
|
+
printFormatWithHash(@binding, io)
|
919
|
+
setVarValues(aBinding)
|
920
|
+
end
|
921
|
+
|
922
|
+
|
923
|
+
# Things you shouldn't have to deal with.
|
924
|
+
private
|
925
|
+
|
926
|
+
|
927
|
+
# place the necessary variables and values into a hash so that
|
928
|
+
# we can get them out at needed without having to eval every time.
|
929
|
+
# An interesting thing that eval gives you is JIT values. If you
|
930
|
+
# don't ask for a value that isn't defined you're all right.
|
931
|
+
def collectVarValues (aBinding)
|
932
|
+
@bindingVars = Hash.new
|
933
|
+
vars = getVarNames()
|
934
|
+
vars = vars.flatten.uniq
|
935
|
+
|
936
|
+
vars.each do |var_name|
|
937
|
+
begin
|
938
|
+
@bindingVars[var_name] = eval( var_name, aBinding )
|
939
|
+
rescue NameError
|
940
|
+
#empty, don't bind if there is nothting there
|
941
|
+
end
|
942
|
+
end
|
943
|
+
end
|
944
|
+
|
945
|
+
# at the end of printing put the values back into the environment binding
|
946
|
+
def setVarValues (aBinding)
|
947
|
+
@bindingVars.each_key do |key|
|
948
|
+
to_eval = "#{key} = "
|
949
|
+
if @bindingVars[key].class == String
|
950
|
+
to_eval += "'#{@bindingVars[key]}'"
|
951
|
+
else
|
952
|
+
to_eval += "#{@bindingVars[key]}"
|
953
|
+
end
|
954
|
+
eval(to_eval, aBinding)
|
955
|
+
end
|
956
|
+
end
|
957
|
+
|
958
|
+
#
|
959
|
+
def getVarNames
|
960
|
+
vars = @vars.flatten
|
961
|
+
vars += @top.getTopVarNames(vars) if @top
|
962
|
+
vars += @bottom.getBottomVarNames(vars) if @bottom
|
963
|
+
##if @bottom
|
964
|
+
## res = @bottom.getBottomVarNames(vars)
|
965
|
+
## vars += res
|
966
|
+
##end
|
967
|
+
vars
|
968
|
+
end
|
969
|
+
|
970
|
+
public
|
971
|
+
def getTopVarNames (vars)
|
972
|
+
vars += @vars.flatten
|
973
|
+
if (@top)
|
974
|
+
vars += @top.getTopVarNames(vars)
|
975
|
+
end
|
976
|
+
vars
|
977
|
+
end
|
978
|
+
|
979
|
+
#
|
980
|
+
def getBottomVarNames (vars)
|
981
|
+
vars += @vars.flatten
|
982
|
+
if (@bottom)
|
983
|
+
vars += @bottom.getBottomVarNames(vars)
|
984
|
+
end
|
985
|
+
vars
|
986
|
+
end
|
987
|
+
|
988
|
+
private
|
989
|
+
|
990
|
+
#how large is the bottom?
|
991
|
+
def getBottomSize ()
|
992
|
+
bottom_size = 0
|
993
|
+
(bottom_size = @bottom.getSize()) if (@bottom)
|
994
|
+
return bottom_size
|
995
|
+
end
|
996
|
+
|
997
|
+
# When you don't want anymore on this page just fill it with blank
|
998
|
+
# lines and print the bottom if it's there. If you've just finished
|
999
|
+
# a page don't bother, just do a FF if needed
|
1000
|
+
def finishPage (aBinding, suppressFF, io)
|
1001
|
+
if (!@print_top)
|
1002
|
+
if (@use_hash)
|
1003
|
+
collectVarValues(aBinding)
|
1004
|
+
@binding = @bindingVars
|
1005
|
+
end
|
1006
|
+
tryOutputFormat(io)
|
1007
|
+
bottom_size = getBottomSize();
|
1008
|
+
(@lines_left - bottom_size).times { io.puts("")}
|
1009
|
+
@print_bottom = true
|
1010
|
+
tryPrintBottom(io, suppressFF)
|
1011
|
+
end
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
# pull out the formatting
|
1015
|
+
def getPictureLine (line, expected_vars)
|
1016
|
+
num_vars = line.count('@') + line.count('^')
|
1017
|
+
if (num_vars != 0)
|
1018
|
+
expected_vars = num_vars
|
1019
|
+
else #the next line is also a picture line, so no vars this time
|
1020
|
+
@vars.push([])
|
1021
|
+
end
|
1022
|
+
nonFormats = getNonFormats(line)
|
1023
|
+
formats = getFormats(line)
|
1024
|
+
a = FormatHolder.new()
|
1025
|
+
a.repeat = (line =~ /.*~~.*/) ? true :false
|
1026
|
+
a.suppress = (line =~ /.*~.*/) ? true : false
|
1027
|
+
nonFormats.each_index {|i|
|
1028
|
+
a.push( FormatEntry.new( nonFormats[i], true )) if ( nonFormats[i] )
|
1029
|
+
a.push( FormatEntry.new( formats[i], false )) if ( formats[i] )
|
1030
|
+
}
|
1031
|
+
@picture_lines.push(a)
|
1032
|
+
return expected_vars
|
1033
|
+
end
|
1034
|
+
|
1035
|
+
# what variables should be put into the picture line above
|
1036
|
+
def getVarLine (line, expected_vars)
|
1037
|
+
vars = line.split(',')
|
1038
|
+
if (vars.size != expected_vars)
|
1039
|
+
raise FormatException.new(),"malformed format, not enough variables provided.\n" +
|
1040
|
+
"Be sure to separate using commas:" +
|
1041
|
+
"Expected #{expected_vars} but received '#{line}'"
|
1042
|
+
end
|
1043
|
+
vars.collect! {|v| v.strip}
|
1044
|
+
@vars.push(vars)
|
1045
|
+
expected_vars = 0
|
1046
|
+
return expected_vars
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
# pull out each individual format from a line and return a list of
|
1050
|
+
# them
|
1051
|
+
def getFormats (line)
|
1052
|
+
last_found = line.size()
|
1053
|
+
output = []
|
1054
|
+
var_count = line.count('@') + line.count('^')
|
1055
|
+
var_count.times {|i|
|
1056
|
+
last_found = findFormatBefore(last_found, line, output)
|
1057
|
+
}
|
1058
|
+
output
|
1059
|
+
end
|
1060
|
+
|
1061
|
+
# find a format before the position given in last_found and shove
|
1062
|
+
# it on the output
|
1063
|
+
def findFormatBefore (last_found, line, output)
|
1064
|
+
first_hat = line.rindex('^',last_found)
|
1065
|
+
first_at = line.rindex('@',last_found)
|
1066
|
+
first_hat = -1 if !first_hat
|
1067
|
+
first_at = -1 if !first_at
|
1068
|
+
first_index = (first_hat > first_at) ? first_hat : first_at
|
1069
|
+
first_char = (first_hat > first_at) ? '^' : '@'
|
1070
|
+
|
1071
|
+
line_section = line[(first_index + 1),(last_found - first_index)]
|
1072
|
+
# all the formats that we could have, blech this is ugly
|
1073
|
+
#num_re = 0
|
1074
|
+
[ /^(>+)[^>]*/, # 1
|
1075
|
+
/^(\|+)[^\|]*/, # 2
|
1076
|
+
/^(#*\.{0,1}#*[EeGg]#+).*/, # 3 for scientific notation
|
1077
|
+
/^(#+\.{0,1}#*).*/, # 5 notice that *+ for ones without a fraction
|
1078
|
+
/^(#*\.{0,1}#+).*/, # 6 +* or a whole
|
1079
|
+
/^(<*)[^<]*/ # 7
|
1080
|
+
].each {|re|
|
1081
|
+
#num_re += 1
|
1082
|
+
if (line_section =~ re)
|
1083
|
+
output.unshift(first_char + $1)
|
1084
|
+
last_found = (first_index - 1)
|
1085
|
+
return last_found
|
1086
|
+
end
|
1087
|
+
}
|
1088
|
+
end
|
1089
|
+
|
1090
|
+
# split the string into groupings that start with an @ or a ^
|
1091
|
+
def splitByAtOr (picture_line)
|
1092
|
+
return [picture_line] unless picture_line.index(/[@^]/)
|
1093
|
+
ats = []
|
1094
|
+
chars = picture_line.split('')
|
1095
|
+
index = 0
|
1096
|
+
chars.each {|c|
|
1097
|
+
if (c =~ /[@^]/)
|
1098
|
+
ats.push(index)
|
1099
|
+
end
|
1100
|
+
index += 1
|
1101
|
+
}
|
1102
|
+
ats2 = []
|
1103
|
+
if (ats[0] == 0)
|
1104
|
+
ats2.push([0,0])
|
1105
|
+
else
|
1106
|
+
ats2.push( [0, ats[0]]) unless (ats[0] == 0)
|
1107
|
+
end
|
1108
|
+
((ats.length) - 1).times { |i|
|
1109
|
+
ats2.push( [ats[i],ats[i+1] ] )
|
1110
|
+
}
|
1111
|
+
ats2.push( [ats[ats.length-1], chars.length] )
|
1112
|
+
result = []
|
1113
|
+
ats2.each {|i|
|
1114
|
+
result.push( picture_line[i[0]...i[1]])
|
1115
|
+
}
|
1116
|
+
result
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
# pull out from a picture line the components of the line that aren't formats
|
1120
|
+
def getNonFormats (picture_line)
|
1121
|
+
lines = splitByAtOr( picture_line)
|
1122
|
+
output = []
|
1123
|
+
lines = lines.each {|element|
|
1124
|
+
element.gsub!(/^[@^]#*\.{0,1}#*[EeGg]#+/, '')
|
1125
|
+
element.gsub!(/^[@^]#+\.{0,1}#*/, '')
|
1126
|
+
element.gsub!(/^[@^]#*\.{0,1}#+/, '')
|
1127
|
+
element.gsub!(/^[@^]>+/, '')
|
1128
|
+
element.gsub!(/^[@^]\|+/, '')
|
1129
|
+
element.gsub!(/^[@^]<*/, '')
|
1130
|
+
element.gsub!(/^[@^]/, '')
|
1131
|
+
element.gsub!(/~/, ' ')
|
1132
|
+
output.push(element)
|
1133
|
+
}
|
1134
|
+
return output
|
1135
|
+
end
|
1136
|
+
|
1137
|
+
## print related functions
|
1138
|
+
|
1139
|
+
# Try to save a line for outputting and perhaps a top and bottom.
|
1140
|
+
# we need the whole format to be able to print so buffer until we
|
1141
|
+
# get it
|
1142
|
+
def printLine (line, io, suppress = false)
|
1143
|
+
if (!suppress)
|
1144
|
+
line.gsub!(/\s+$/, "")
|
1145
|
+
@buffered_lines.push("#{line}\n")
|
1146
|
+
tryPrintPartialPage(io)
|
1147
|
+
tryPrintBottom(io)
|
1148
|
+
end
|
1149
|
+
end
|
1150
|
+
|
1151
|
+
# if the page is too short to hold even one format just print what
|
1152
|
+
# we can. True is we printed a partial page
|
1153
|
+
def tryPrintPartialPage (io)
|
1154
|
+
if (!@printed_a_body)
|
1155
|
+
bottom_size = 0
|
1156
|
+
#bottom_size = @bottom.getSize() if (@bottom)
|
1157
|
+
#if ((@buffered_lines.size() + bottom_size) == @lines_left)
|
1158
|
+
printBufferedLines(io)
|
1159
|
+
@print_bottom = true
|
1160
|
+
return true
|
1161
|
+
#end
|
1162
|
+
end
|
1163
|
+
return false
|
1164
|
+
end
|
1165
|
+
|
1166
|
+
# When we have a whole format try to print it, if there isn't
|
1167
|
+
# enough room we have to save it for later.
|
1168
|
+
def tryOutputFormat (io, cachedBinding = nil)
|
1169
|
+
bottom_size = getBottomSize()
|
1170
|
+
|
1171
|
+
if ((@buffered_lines.size() + bottom_size) <= @lines_left)
|
1172
|
+
printBufferedLines(io)
|
1173
|
+
else
|
1174
|
+
if (tryPrintPartialPage(io))
|
1175
|
+
return true
|
1176
|
+
else
|
1177
|
+
unless (cachedBinding.nil?)
|
1178
|
+
@binding = cachedBinding
|
1179
|
+
@buffered_lines = []
|
1180
|
+
end
|
1181
|
+
@print_bottom = true
|
1182
|
+
return false
|
1183
|
+
end
|
1184
|
+
end
|
1185
|
+
return true
|
1186
|
+
end
|
1187
|
+
|
1188
|
+
#print the buffered lines
|
1189
|
+
def printBufferedLines (io)
|
1190
|
+
io.puts(@buffered_lines)
|
1191
|
+
@lines_left -= (@buffered_lines.size())
|
1192
|
+
@buffered_lines = []
|
1193
|
+
@printed_a_body = true
|
1194
|
+
end
|
1195
|
+
|
1196
|
+
# see if a top is the right thing to print
|
1197
|
+
def tryPrintTop (io)
|
1198
|
+
if (@print_top)
|
1199
|
+
io.print "\f" unless (@page_number == 1)
|
1200
|
+
@printed_a_body = false
|
1201
|
+
@print_top = false
|
1202
|
+
if (@top)
|
1203
|
+
@top.resetPage()
|
1204
|
+
#lines = @top.printFormatWithHash(@binding, io)
|
1205
|
+
lines = @top.printFormat(@binding, io)
|
1206
|
+
@lines_left = (@page_length - lines)
|
1207
|
+
end
|
1208
|
+
end
|
1209
|
+
end
|
1210
|
+
|
1211
|
+
#we have a bottom, even if it's only ^L, try to get this working!
|
1212
|
+
def tryPrintBottom (io, suppressLF = false)
|
1213
|
+
bottom_size = getBottomSize()
|
1214
|
+
if ((0 == @buffered_lines.size) && (@lines_left == bottom_size))
|
1215
|
+
@print_bottom = true
|
1216
|
+
end
|
1217
|
+
#this bottom is a mess if we have repeating lines
|
1218
|
+
if (@print_bottom)
|
1219
|
+
@print_bottom = false
|
1220
|
+
if (@bottom)
|
1221
|
+
@bottom.printFormat(@binding, io)
|
1222
|
+
@bottom.resetPage()
|
1223
|
+
end
|
1224
|
+
@lines_left = @page_length
|
1225
|
+
@page_number += 1
|
1226
|
+
@print_top = true
|
1227
|
+
return true
|
1228
|
+
end
|
1229
|
+
return false
|
1230
|
+
end
|
1231
|
+
|
1232
|
+
# The workhorse of the format. Print the top and bottom along with
|
1233
|
+
# the body. If we can't fit even one body in print as much as we can.
|
1234
|
+
# and split it with a top and bottom. If we can fit in one just
|
1235
|
+
# buffer the bottom and start again on the next page.
|
1236
|
+
def printBodyFormat (io)
|
1237
|
+
tryPrintTop(io)
|
1238
|
+
cachedBinding = @binding.clone
|
1239
|
+
@picture_lines.each_index do |i|
|
1240
|
+
line = @picture_lines[i]
|
1241
|
+
suppress = line.suppress()
|
1242
|
+
repeat = true
|
1243
|
+
while (repeat)
|
1244
|
+
if (tryPartialFullPage(io))
|
1245
|
+
cachedBinding = @binding.clone
|
1246
|
+
end
|
1247
|
+
vars = @vars[i].dup if @vars[i]
|
1248
|
+
outputLine,suppress = composeLine(line, vars, suppress)
|
1249
|
+
if (!suppress)
|
1250
|
+
outputLine.gsub!(/\s+$/, "")
|
1251
|
+
@buffered_lines.push("#{outputLine}\n")
|
1252
|
+
end
|
1253
|
+
if ((!suppress) && line.repeat)
|
1254
|
+
suppress = line.suppress()
|
1255
|
+
else
|
1256
|
+
repeat = false
|
1257
|
+
end
|
1258
|
+
|
1259
|
+
end #while
|
1260
|
+
end # each_index
|
1261
|
+
printed = tryOutputFormat(io,cachedBinding)
|
1262
|
+
if (!printed)
|
1263
|
+
@binding = cachedBinding
|
1264
|
+
tryPrintBottom(io)
|
1265
|
+
printBodyFormat(io)
|
1266
|
+
end
|
1267
|
+
tryPrintBottom(io)
|
1268
|
+
return (@page_length - @lines_left)
|
1269
|
+
end
|
1270
|
+
|
1271
|
+
# If we have a format that is too big to fit on a page, even the
|
1272
|
+
# first time we will print it out along with a top and bottom and
|
1273
|
+
# continue on. Returns if it printed or not.
|
1274
|
+
def tryPartialFullPage (io)
|
1275
|
+
# If we can't even fit one print all we can.
|
1276
|
+
if (!@printed_a_body && (@buffered_lines.size() + getBottomSize() == @lines_left))
|
1277
|
+
tryOutputFormat(io)
|
1278
|
+
tryPrintBottom(io)
|
1279
|
+
tryPrintTop(io)
|
1280
|
+
return true
|
1281
|
+
end
|
1282
|
+
false
|
1283
|
+
end
|
1284
|
+
|
1285
|
+
|
1286
|
+
#will shift off a var if necessary returns both a line and a value of suppress
|
1287
|
+
def composeLine (line, vars, suppress)
|
1288
|
+
outputLine = ""
|
1289
|
+
line.each do |item|
|
1290
|
+
if (item.isUnchanging?())
|
1291
|
+
outputLine << "#{item.val}"
|
1292
|
+
#puts "adding >#{item.val}<" #xxx
|
1293
|
+
else #need to chop
|
1294
|
+
begin
|
1295
|
+
to_eval = vars.shift
|
1296
|
+
raise FormatException.new(),
|
1297
|
+
"Not enough variables supplied to match format" if (to_eval.nil?)
|
1298
|
+
s = getVarValue( to_eval )
|
1299
|
+
rescue NameError
|
1300
|
+
raise NameError.new(), "cannot find variable '#{to_eval}' #{$!}"
|
1301
|
+
end
|
1302
|
+
suppress = false if (("" != s) && suppress)
|
1303
|
+
res = item.formatString(s.to_s, to_eval, @binding)
|
1304
|
+
outputLine << "#{res}"
|
1305
|
+
end
|
1306
|
+
end
|
1307
|
+
return [outputLine, suppress]
|
1308
|
+
end
|
1309
|
+
|
1310
|
+
|
1311
|
+
# Move the calls to eval into one place so we can use the hash if
|
1312
|
+
# it's there and the binding if it's not
|
1313
|
+
def getVarValue (var)
|
1314
|
+
res = ""
|
1315
|
+
if (@use_hash)
|
1316
|
+
res = @binding[var]
|
1317
|
+
else
|
1318
|
+
res = eval( var, @binding )
|
1319
|
+
end
|
1320
|
+
return res
|
1321
|
+
end
|
1322
|
+
|
1323
|
+
end # class Format
|
1324
|
+
|
1325
|
+
|
1326
|
+
# a subclass of array that knows about ~ and ~~
|
1327
|
+
class FormatHolder < Array
|
1328
|
+
attr_accessor :suppress, :repeat
|
1329
|
+
end
|
1330
|
+
|
1331
|
+
|
1332
|
+
# This class takes in a format and instead of writing out the values
|
1333
|
+
# variables under the given format will read in formatted text and give
|
1334
|
+
# the values of variables as specified in the given format.
|
1335
|
+
class FormatReader
|
1336
|
+
|
1337
|
+
# Make a FormatReader given a format
|
1338
|
+
def initialize (format)
|
1339
|
+
@pictures = format.getPictureLines()
|
1340
|
+
@var_values = Hash.new
|
1341
|
+
end
|
1342
|
+
|
1343
|
+
# Given the output from a format return a hash with the values
|
1344
|
+
# of the variables given in the input mapped to the variables in
|
1345
|
+
# the format.
|
1346
|
+
def readFormat (output)
|
1347
|
+
@var_values = Hash.new
|
1348
|
+
output_line = 0
|
1349
|
+
while (output_line < output.length)
|
1350
|
+
@pictures.each_index do |i|
|
1351
|
+
repeat = true
|
1352
|
+
while (repeat)
|
1353
|
+
found_match = setLine( @pictures[i], output[output_line] )
|
1354
|
+
repeat = false #default to stopping
|
1355
|
+
if (found_match)
|
1356
|
+
output_line += 1
|
1357
|
+
end
|
1358
|
+
#we may need to repeat if it's a ~~ line
|
1359
|
+
if (@pictures[i].repeat() && found_match)
|
1360
|
+
repeat = true
|
1361
|
+
end
|
1362
|
+
end #while
|
1363
|
+
end
|
1364
|
+
if block_given?
|
1365
|
+
yield @var_values
|
1366
|
+
@var_values = Hash.new
|
1367
|
+
else
|
1368
|
+
return @var_values
|
1369
|
+
end
|
1370
|
+
end
|
1371
|
+
end
|
1372
|
+
|
1373
|
+
private
|
1374
|
+
|
1375
|
+
#given a picture line remove spaces that have probably been added
|
1376
|
+
#as padding.
|
1377
|
+
def removeSpaces (picture, data)
|
1378
|
+
result = data
|
1379
|
+
if (picture.include?('<'))
|
1380
|
+
result.gsub!( / +$/, '')
|
1381
|
+
elsif (picture.include?('>'))
|
1382
|
+
result.gsub!( /^ +/, '' )
|
1383
|
+
elsif (picture.include?('|'))
|
1384
|
+
result = result.strip
|
1385
|
+
end
|
1386
|
+
result
|
1387
|
+
end
|
1388
|
+
|
1389
|
+
# Put a value into @var_values. Guess at the type
|
1390
|
+
def saveVar (picture, name, data)
|
1391
|
+
to_save = removeSpaces( picture, data )
|
1392
|
+
#numbers
|
1393
|
+
if (picture.include?('#'))
|
1394
|
+
if (picture.include?('.'))
|
1395
|
+
@var_values[name] = to_save.to_f
|
1396
|
+
else
|
1397
|
+
@var_values[name] = to_save.to_i
|
1398
|
+
end
|
1399
|
+
# continuation lines
|
1400
|
+
elsif (picture =~ /^\^.*/)
|
1401
|
+
curval = @var_values[name]
|
1402
|
+
curval ||= ""
|
1403
|
+
# we assume you want a space between the two things split
|
1404
|
+
if (curval != "" && to_save =~ /^[^\s]*/)
|
1405
|
+
to_save = " " + to_save
|
1406
|
+
end
|
1407
|
+
@var_values[name] = curval + to_save
|
1408
|
+
else
|
1409
|
+
@var_values[name] = to_save
|
1410
|
+
end
|
1411
|
+
end
|
1412
|
+
|
1413
|
+
# Given a regexp some variable names and output fill the
|
1414
|
+
# variables in with data from the output according the the format
|
1415
|
+
# in the regexp. Will return false if no matches can
|
1416
|
+
# be found, true if matches were found.
|
1417
|
+
def findVars (regexp_string, vars, output)
|
1418
|
+
r = Regexp.new( regexp_string )
|
1419
|
+
match_data = r.match( output )
|
1420
|
+
output = Hash.new
|
1421
|
+
if (!match_data.nil?)
|
1422
|
+
matches = match_data.to_a
|
1423
|
+
matches = matches[1,matches.length]
|
1424
|
+
matches.each_with_index do |data, index|
|
1425
|
+
str, var = vars[index]
|
1426
|
+
saveVar( str, var, data )
|
1427
|
+
end
|
1428
|
+
else
|
1429
|
+
return false
|
1430
|
+
#no match data
|
1431
|
+
end
|
1432
|
+
return true
|
1433
|
+
end
|
1434
|
+
|
1435
|
+
# place the values contained in the output specified by the
|
1436
|
+
# picture line in @var_values
|
1437
|
+
def setLine (picture_line, output)
|
1438
|
+
regexp_string = ""
|
1439
|
+
vars = []
|
1440
|
+
picture_line.each_with_index {|elem, index|
|
1441
|
+
str, var = elem
|
1442
|
+
if (var.nil?)
|
1443
|
+
regexp_string << Regexp.escape(str)
|
1444
|
+
|
1445
|
+
else # capture the variable's section of the string
|
1446
|
+
vars.push( elem )
|
1447
|
+
if (index != picture_line.length - 1)
|
1448
|
+
regexp_string << '('
|
1449
|
+
regexp_string << '.' * str.length
|
1450
|
+
regexp_string << ')'
|
1451
|
+
else
|
1452
|
+
regexp_string << '(.*)'
|
1453
|
+
end
|
1454
|
+
end
|
1455
|
+
}
|
1456
|
+
|
1457
|
+
findVars( regexp_string, vars, output )
|
1458
|
+
end
|
1459
|
+
|
1460
|
+
end
|
1461
|
+
|
1462
|
+
end
|