formatr 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (18) hide show
  1. data/lib/formatr.rb +1462 -0
  2. data/test/1 +55 -0
  3. data/test/10 +7 -0
  4. data/test/11 +2 -0
  5. data/test/12 +3 -0
  6. data/test/13 +290 -0
  7. data/test/14 +5 -0
  8. data/test/15 +8 -0
  9. data/test/2 +129 -0
  10. data/test/3 +9 -0
  11. data/test/4 +100 -0
  12. data/test/5 +200 -0
  13. data/test/6 +6 -0
  14. data/test/7 +6 -0
  15. data/test/8 +40 -0
  16. data/test/format_test.pl +377 -0
  17. data/test/test_formatr.rb +761 -0
  18. metadata +62 -0
@@ -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