formatr 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|