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.
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