edi4r 0.9.4.1 → 0.9.6.2

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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/AuthorCopyright +3 -3
  3. data/{ChangeLog → Changelog} +60 -0
  4. data/README +15 -10
  5. data/Tutorial +2 -3
  6. data/VERSION +1 -1
  7. data/bin/edi2xml.rb +12 -16
  8. data/bin/editool.rb +9 -5
  9. data/bin/sedas2eancom02.rb +1385 -0
  10. data/bin/xml2edi.rb +7 -12
  11. data/data/edifact/iso9735/SDCD.20000.csv +1 -0
  12. data/data/edifact/iso9735/SDCD.3for2.csv +1 -0
  13. data/data/edifact/iso9735/SDED.20000.csv +6 -0
  14. data/data/edifact/iso9735/SDED.30000.csv +43 -43
  15. data/data/edifact/iso9735/SDED.3for2.csv +6 -0
  16. data/data/edifact/iso9735/SDED.40000.csv +129 -129
  17. data/data/edifact/iso9735/SDED.40100.csv +130 -130
  18. data/data/edifact/iso9735/SDMD.20000.csv +6 -0
  19. data/data/edifact/iso9735/SDMD.30000.csv +6 -6
  20. data/data/edifact/iso9735/SDMD.3for2.csv +6 -0
  21. data/data/edifact/iso9735/SDMD.40000.csv +17 -17
  22. data/data/edifact/iso9735/SDMD.40100.csv +17 -17
  23. data/data/edifact/iso9735/SDSD.20000.csv +5 -0
  24. data/data/edifact/iso9735/SDSD.3for2.csv +5 -0
  25. data/data/edifact/untdid/EDMD.d01b.csv +1 -1
  26. data/data/sedas/EDCD..csv +0 -0
  27. data/data/sedas/EDED..csv +859 -0
  28. data/data/sedas/EDMD..csv +16 -0
  29. data/data/sedas/EDSD..csv +44 -0
  30. data/lib/edi4r.rb +147 -67
  31. data/lib/edi4r/ansi_x12-rexml.rb +91 -0
  32. data/lib/edi4r/ansi_x12.rb +1684 -0
  33. data/lib/edi4r/diagrams.rb +75 -14
  34. data/lib/edi4r/edifact-rexml.rb +4 -3
  35. data/lib/edi4r/edifact.rb +505 -202
  36. data/lib/edi4r/rexml.rb +13 -7
  37. data/lib/edi4r/sedas.rb +854 -0
  38. data/lib/edi4r/standards.rb +150 -33
  39. data/test/damaged_file.edi +1 -0
  40. data/test/eancom2webedi.rb +1 -0
  41. data/test/groups.edi +1 -1
  42. data/test/test_basics.rb +16 -9
  43. data/test/test_edi_split.rb +30 -0
  44. data/test/test_loopback.rb +7 -2
  45. data/test/test_rexml.rb +34 -2
  46. data/test/test_service_messages.rb +190 -0
  47. data/test/test_streaming.rb +167 -0
  48. data/test/test_tut_examples.rb +3 -1
  49. data/test/webedi2eancom.rb +1 -0
  50. metadata +121 -77
@@ -1,11 +1,15 @@
1
+ # -*- encoding: iso-8859-1 -*-
1
2
  # Add-on to EDI module "EDI4R"
2
3
  # Classes for XML support, here: Basic classes
3
4
  #
4
5
  # :include: ../../AuthorCopyright
5
6
  #
6
- # $Id: rexml.rb,v 1.1 2006/08/01 11:14:29 werntges Exp $
7
+ # $Id$
7
8
  #--
8
9
  # $Log: rexml.rb,v $
10
+ # Revision 1.2 2007/03/14 23:59:25 werntges
11
+ # Bug fix (Joko's report) in DE#to_xml: Attribute "instance" generated now
12
+ #
9
13
  # Revision 1.1 2006/08/01 11:14:29 werntges
10
14
  # Initial revision
11
15
  #
@@ -23,6 +27,7 @@
23
27
 
24
28
  require 'rexml/document'
25
29
  # require 'diagrams-xml'
30
+ require 'edi4r/ansi_x12-rexml' if EDI.constants.include? 'A'
26
31
  require 'edi4r/edifact-rexml' if EDI.constants.include? 'E'
27
32
  require 'edi4r/idoc-rexml' if EDI.constants.include? 'I'
28
33
 
@@ -38,7 +43,7 @@ module EDI
38
43
  def to_xml( xel_parent, instance=1 )
39
44
  xel = REXML::Element.new( normalized_class_name )
40
45
  xel.attributes["name"] = @name
41
- xel.attributes["instance"] = instance if instance > 1
46
+ xel.attributes["instance"] = instance.to_s if instance > 1
42
47
  xel_parent.elements << xel
43
48
  instance_counter = Hash.new(0)
44
49
  each do |obj|
@@ -102,7 +107,7 @@ module EDI
102
107
  end
103
108
  hnd = REXML::Document.new( hnd ) if hnd.is_a? IO or hnd.is_a? String
104
109
 
105
- key = hnd.root.attributes['standard_key']
110
+ key = hnd.root.attributes['standard_key'].strip
106
111
  raise "Unsupported standard key: #{key}" if key == 'generic'
107
112
  EDI::const_get(key)::const_get('Interchange').parse_xml( hnd )
108
113
  # class_sym = (key+'Interchange').intern
@@ -112,9 +117,9 @@ module EDI
112
117
 
113
118
  def to_xml( xel_parent )
114
119
  externalID = "PUBLIC\n" + " "*9
115
- externalID += "'-//FH Wiesbaden FB DCSM//DTD XML Representation of EDI data V1.2//EN'\n"
120
+ externalID += "'-//Hochschule RheinMain FB DCSM//DTD XML Representation of EDI data V1.2//EN'\n"
116
121
  externalID += " "*9
117
- externalID += "'http://edi01.informatik.fh-wiesbaden.de/edi4r/edi4r-1.2.dtd'"
122
+ externalID += "'http://edi01.cs.hs-rm.de/edi4r/edi4r-1.2.dtd'"
118
123
  xel_parent << REXML::XMLDecl.new
119
124
  xel_parent << REXML::DocType.new( normalized_class_name, externalID )
120
125
 
@@ -124,7 +129,7 @@ module EDI
124
129
  pos = self.class.to_s =~ /EDI::((.*?)::)?Interchange/
125
130
  raise "This is not an Interchange object: #{rc}!" unless pos==0
126
131
  xel.attributes["standard_key"] = ($2 and not $2.empty?) ? $2 : "generic"
127
- xel.attributes["version"] = @version
132
+ xel.attributes["version"] = @version.to_s
128
133
  xel.attributes.delete "name"
129
134
  rc
130
135
  end
@@ -244,9 +249,10 @@ module EDI
244
249
 
245
250
  class DE
246
251
 
247
- def to_xml( xel_parent, instance=nil )
252
+ def to_xml( xel_parent, instance=1 )
248
253
  xel = REXML::Element.new( 'DE' )
249
254
  xel.attributes["name"] = @name
255
+ xel.attributes["instance"] = instance.to_s if instance > 1
250
256
  xel_parent.elements << xel
251
257
  xel.text = self.to_s( true ) # don't escape!
252
258
  xel
@@ -0,0 +1,854 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- encoding: iso-8859-1 -*-
3
+ #
4
+ # :main:README
5
+ # :title:sedas
6
+ #
7
+ # SEDAS module: API to parse (and create) SEDAS data
8
+ #
9
+ # == Synopsis
10
+ #
11
+ # It module adds basic SEDAS capabilities, as were needed for a limited
12
+ # SEDAS-to-EANCOM migration project.
13
+ # Note that SEDAS is a "dying" standard, so it is not fully supported here.
14
+ #
15
+ # == Usage
16
+ #
17
+ # require 'sedas'
18
+ #
19
+ # == Description
20
+ #
21
+ # The edi4r base gem comes with a limited set of SEDAS data structures
22
+ # focused on invoice data, as well as abstract classes for the
23
+ # handling of general EDI data structures.
24
+ # This add-on builds upon the same abstract classes and provides support
25
+ # for SEDAS data.
26
+ #
27
+ # Only one release (1993) is considered. Record types 00,01,98,99,
28
+ # 12-17/22-27, 29, 50, and 51 are supported, as well as follow-up types
29
+ # 01 (for 12/22, 15/25, 29), 02 (for 13/23, 16/26, 29), and 08
30
+ # (for 12-17/22-27).
31
+ #
32
+ # :include:../../AuthorCopyright
33
+ #--
34
+ # $Id: sedas.rb,v 1.2 2007/04/10 22:21:05 werntges Exp $
35
+ #
36
+ # $Log: sedas.rb,v $
37
+ # Revision 1.2 2007/04/10 22:21:05 werntges
38
+ # Final refinements, preparing pre-release
39
+ #
40
+ # Revision 1.1 2007/03/30 14:43:09 werntges
41
+ # Initial revision
42
+ #
43
+ #
44
+ #
45
+ # To-do list:
46
+ # * Support for generation of SEDAS data (low prio)
47
+ # * Removal of some remaining code from IDOC precursor source
48
+ # * Formal support of character sets
49
+ #
50
+
51
+
52
+ # SEDAS add-ons to EDI module
53
+ # API to parse and create SEDAS
54
+ #
55
+
56
+ module EDI
57
+
58
+ # Notes:
59
+ #
60
+ # SEDAS data are organized as record sets. A physical file or
61
+ # transmission unit is always framed by record types 00 and 99.
62
+ # Within such a physical unit, one or more logical units may occur,
63
+ # framed by records 01 and 98.
64
+ #
65
+ # SEDAS data are self-contained already at the message level.
66
+ # An "Interchange" of the SEDAS type is just a concatenation
67
+ # of messages; a single message per Interchange is quite acceptable.
68
+ # However, one such message will usually map to several messages
69
+ # of the UN/EDIFACT type. As an example, a sequence of record types 29
70
+ # may map to one or several INVOIC messages (BGM+393), depending on
71
+ # the number of group changes (document number changes).
72
+ #
73
+ # Also note that SEDAS record types 50, 51 are considered master data.
74
+ # They are referred to by earlier records to avoid redundant data.
75
+ # Here we treat this master data block as a single "message" - but not
76
+ # with mapping in mind.
77
+ #
78
+ # Here we use the Interchange level only to maintain the information
79
+ # about the physical/logical unit data.
80
+ #
81
+
82
+ module S
83
+
84
+ class Interchange < EDI::Interchange
85
+
86
+ attr_accessor :charset, :output_mode
87
+ attr_reader :basedata
88
+
89
+ def init_ndb
90
+ @basedata = EDI::Dir::Directory.create( root.syntax )
91
+ end
92
+
93
+ # Currently, this is almost a dummy method
94
+ # It might grow into something useful.
95
+ # Keep it to stay consistent with other module add-ons.
96
+ #
97
+ def check_consistencies
98
+ if 'S' != @syntax
99
+ raise "#{@syntax} - syntax must be 'S' (SEDAS)!"
100
+ end
101
+
102
+ case @charset
103
+ when 'ISO-8859-15'
104
+ # ok
105
+ # when '...'
106
+ else
107
+ raise "#{@charset} - character set not supported!"
108
+ end
109
+ # Add more rules ...
110
+ end
111
+
112
+
113
+ def initialize( user_par={} )
114
+ super( user_par ) # just in case...
115
+ par = {:syntax => 'S',
116
+ :output_mode => nil,
117
+ :charset => 'ISO-8859-15', # not used yet...
118
+ }.update( user_par )
119
+
120
+ @syntax = par[:syntax] # UN/EDIFACT
121
+ @output_mode = par[:output_mode]
122
+ @charset = par[:charset]
123
+
124
+ # Temporary - adjust to current SEDAS needs:
125
+ @illegal_charset_pattern = /[^-A-Za-z0-9 .,()\/=!%"&*;<>'+:?\xa0-\xff]+/
126
+
127
+ check_consistencies
128
+ init_ndb
129
+ end
130
+
131
+
132
+ #
133
+ # Reads SEDAS data from given stream (default: $stdin),
134
+ # parses it and returns an Interchange object
135
+ #
136
+ def Interchange.parse( hnd=$stdin, auto_validate=true )
137
+ builder = StreamingBuilder.new( auto_validate )
138
+ builder.go( hnd )
139
+ builder.interchange
140
+ end
141
+
142
+ #
143
+ # Read +maxlen+ bytes from $stdin (default) or from given stream
144
+ # (SEDAS data expected), and peek into first segment (00).
145
+ #
146
+ # Returns an empty Interchange object with a properly header filled.
147
+ #
148
+ # Intended use:
149
+ # Efficient routing by reading just 00 data: sender/recipient/ref/test
150
+ #
151
+ def Interchange.peek(hnd=$stdin, params={}) # Handle to input stream
152
+ builder = StreamingBuilder.new( false )
153
+ if params[:deep_peek]
154
+ def builder.on_segment( s, tag )
155
+ end
156
+ else
157
+ def builder.on_01( s )
158
+ throw :done
159
+ end
160
+ def builder.on_msg_start( s, tag )
161
+ throw :done # FIXME: UNZ??
162
+ end
163
+ end
164
+ builder.go( hnd )
165
+ builder.interchange
166
+ end
167
+
168
+
169
+ def Interchange.parse_xml(xdoc)
170
+ ic = Interchange.new # ({:sap_type => xdoc.root.attributes['version']})
171
+ xdoc.root.elements.each('Message') do |xel|
172
+ ic.add( Message.parse_xml( ic, xel ) )
173
+ end
174
+ ic
175
+ end
176
+
177
+
178
+ def new_msggroup(params)
179
+ MsgGroup.new(self, params)
180
+ end
181
+
182
+ def new_message(params)
183
+ Message.new(self, params)
184
+ end
185
+
186
+ def new_segment(tag)
187
+ Segment.new(self, tag)
188
+ end
189
+
190
+
191
+ def parse_message(list)
192
+ Message.parse(self, list)
193
+ end
194
+
195
+ def parse_segment(buf)
196
+ Segment.parse(self, buf)
197
+ end
198
+
199
+ end
200
+
201
+
202
+
203
+ class MsgGroup < EDI::MsgGroup
204
+
205
+ def new_message(params)
206
+ Message.new(self, params)
207
+ end
208
+
209
+ def new_segment(tag)
210
+ Segment.new(self, tag)
211
+ end
212
+
213
+ def parse_segment(buf)
214
+ Segment.parse(self, buf)
215
+ end
216
+
217
+ end
218
+
219
+
220
+ class Message < EDI::Message
221
+ attr_accessor :maindata
222
+ # private_class_method :new
223
+
224
+ # @@msgCounter = 1
225
+
226
+ def preset_msg(user_par={})
227
+ # lower-case names for internal keys,
228
+ # upper-case names for EDI_DC field names
229
+ par = {:sedas_type => '12',
230
+ }.update( user_par )
231
+ @pars = par
232
+ end
233
+
234
+
235
+ def initialize( p, user_par )
236
+ super( p, user_par )
237
+ # @parent, @root = p, p.root
238
+
239
+ # First param is either a hash or segment EDI_DC
240
+ # If Hash: Build EDI_DC from given parameters
241
+ # If Segment: Extract some crucial parameters
242
+
243
+ @maindata = Dir::Directory.create( root.syntax )
244
+ if user_par.is_a? Hash
245
+ preset_msg( user_par)
246
+ @name = @pars[:sedas_type]
247
+
248
+ @header = nil # new_segment(p.dc_sig.strip) # typically, "EDI_DC40"
249
+ # @header.dIDOCTYP = @pars[:IDOCTYP]
250
+ # etc.
251
+ =begin
252
+ elsif user_par.is_a? EDI::S::Segment
253
+ @header = user_par
254
+ if @header.name !~ /^[12]2/
255
+ raise "12/22 expected, '#{@header.name}' found!"
256
+ end
257
+ @header.parent = self
258
+ @header.root = self.root
259
+ @pars = Hash.new
260
+ @pars[:sedas_type]= header.name[0,2] # e.g. '12', '15'
261
+ # @pars[:IDOCTYP] = @header.dIDOCTYP
262
+ # @maindata = Dir::Directory.create( root.syntax,
263
+ # :SEDASTYPE=> @pars[:sedas_type]
264
+ # )
265
+ =end
266
+ else
267
+ raise "Parameter 'user_par': Illegal type!"
268
+ end
269
+
270
+ @trailer = nil
271
+ # @@msgCounter += 1
272
+ end
273
+
274
+
275
+ def new_segment(tag)
276
+ Segment.new(self, tag)
277
+ end
278
+
279
+ def parse_segment(buf)
280
+ Segment.parse(self, buf)
281
+ end
282
+
283
+
284
+ def Message.parse (p, segment_list)
285
+ # Segments comprise a single message
286
+
287
+ # Temporarily assign p as segment parent,
288
+ # or else service segment lookup fails:
289
+ raise "NOT SUPPORTED anymore!"
290
+ header = p.parse_segment(segment_list.shift, p.dc_sig.strip)
291
+
292
+ msg = p.new_message(header) # Now use header rec as template
293
+
294
+ segment_list.each {|segbuf| msg.add Segment.parse( msg, segbuf ) }
295
+
296
+ msg.trailer = nil
297
+ msg
298
+ end
299
+
300
+
301
+ def validate
302
+ # Check sequence of segments against library,
303
+ # thereby adding location information to each segment
304
+ diag = EDI::Diagram::Diagram.create(root.syntax,
305
+ :SEDASTYPE=> @pars[:sedas_type]
306
+ )
307
+ ni = EDI::Diagram::NodeInstance.new(diag)
308
+ # puts "Initial node instance is: #{ni.name}"
309
+ if @header
310
+ ni.seek!( @header )
311
+ @header.update_with( ni )
312
+ end
313
+ each {|seg|
314
+ # if (key = seg.name) !~ /.*\d\d\d/
315
+ # key = Regexp.new(key+'(\d\d\d)?')
316
+ # end
317
+
318
+ begin
319
+ if ni.seek!(seg.name) # key) # (seg)
320
+ seg.update_with( ni )
321
+ else
322
+ raise "seek! failed for #{seg.name} when starting at #{ni.name}"
323
+ end
324
+ =begin
325
+ rescue EDI::EDILookupError
326
+ warn key
327
+ if key =~ /(.*)\d\d\d/
328
+ key = $1
329
+ retry
330
+ else
331
+ raise
332
+ end
333
+ =end
334
+ end
335
+ }
336
+ # ni.seek!( @trailer )
337
+ # @trailer.update_with( ni )
338
+
339
+ # Now check each segment
340
+ super
341
+ end
342
+
343
+ end
344
+
345
+
346
+
347
+ class Segment < EDI::Segment
348
+
349
+ def initialize(p, tag)
350
+ super( p, tag )
351
+
352
+ segment_list = root.basedata.segment(tag)
353
+ raise "Segment \'#{tag}\' not found" if segment_list.nil?
354
+
355
+ # each_BCDS_Entry('s'+tag) do |entry| # This does not work here...
356
+ segment_list.each do |entry|
357
+ id = entry.name
358
+ status = entry.status
359
+ # puts "Seeking DE >>#{tag+':'+id}<<"
360
+ # Regular lookup
361
+ fmt = fmt_of_DE(tag+':'+id)
362
+ add new_DE(id, status, fmt)
363
+ end
364
+ end
365
+
366
+
367
+ def new_DE(id, status, fmt)
368
+ DE.new(self, id, status, fmt)
369
+ end
370
+
371
+
372
+ # Buffer contains a single segment (line)
373
+ def Segment.parse (p, buf, tag_expected=nil)
374
+ case tag = buf[0,2]
375
+ when '00', '01', '03', '51', '96', '98','99'
376
+ # take just the rec_id as tag
377
+ else
378
+ # append "Folgesatz" id
379
+ tag +=buf[146,2]
380
+ end
381
+ seg = p.new_segment(tag)
382
+
383
+ if tag_expected and tag_expected != tag
384
+ raise "Wrong segment name! Expected: #{tag_expected}, found: #{tag}"
385
+ end
386
+
387
+ seg.each {|obj| obj.parse(buf) }
388
+ seg
389
+ end
390
+
391
+
392
+ def to_s
393
+ line = ''
394
+ crlf = "\x0d\x0a"
395
+ return line if empty?
396
+ if root.output_mode == :linebreak
397
+ each do |obj|
398
+ next if obj.empty?
399
+ line << name.ljust(12)+obj.name.ljust(12)+'['+obj.to_s+']' unless obj.empty?
400
+ end
401
+ else
402
+ last_nonempty_de = nil
403
+ each {|obj| last_nonempty_de = obj unless obj.empty? }
404
+ each {|obj| line += obj.to_s; break if obj.object_id == last_nonempty_de.object_id }
405
+ end
406
+ line << crlf
407
+ line
408
+ end
409
+
410
+ private
411
+
412
+ # SEDAS field names direcly qualify as Ruby method names,
413
+ # and there are neither composites nor arrays, so we can
414
+ # simplify access to fields here.
415
+ #
416
+
417
+ def method_missing(sym, *par)
418
+ if sym.id2name =~ /^(\w+)(=)?/
419
+ rc = lookup($1)
420
+ if rc.is_a? Array
421
+ if rc.size==1
422
+ rc = rc.first
423
+ elsif rc.size==0
424
+ return super
425
+ end
426
+ end
427
+ if $2
428
+ # Setter
429
+ raise TypeError, "Can't assign to a #{rc.class} object '#$1'" unless rc.is_a? DE
430
+ rc.value = par[0]
431
+ else
432
+ # Getter
433
+ return rc.value if rc.is_a? DE
434
+ err_msg = "No DE with name '#$1' found, instead there is a '#{rc.class}'!"
435
+ raise TypeError, err_msg
436
+ end
437
+ else
438
+ super
439
+ end
440
+ end
441
+
442
+ end
443
+
444
+
445
+ # There are no CDEs in IDocs:
446
+ # class CDE_E < CDE
447
+ # end
448
+
449
+
450
+ class DE < EDI::DE
451
+
452
+ # ae, oe, ue, sz, Ae, Oe, Ue, Paragraph, `, ´
453
+ @@umlaute_iso636_de = '{-}~[-]@`´'
454
+ @@umlaute_cp850 = "\x84\x94\x81\xE1\x8E\x99\x9A\xF5''"
455
+ @@umlaute_iso8859_1 = "\xE4\xF6\xFC\xDF\xC4\xD6\xDC\xA7''"
456
+
457
+ def initialize( p, name, status, fmt )
458
+ super( p, name, status, fmt )
459
+ fmt =~ /(a|an|n|d|t)(\.\.)?(\d+):(\d+)/
460
+ raise "Illegal format string in field #{name}: #{fmt}" if $3.nil? or $4.nil?
461
+ @length, @offset = $3.to_i, $4.to_i-1
462
+ # puts "#{name}: len=#{@length.to_s}, off=#{@offset.to_s}"
463
+ # check if supported format syntax
464
+ # check if supported status value
465
+ end
466
+
467
+
468
+ def parse( buf ) # Buffer contains segment line; extract sub-string!
469
+ # msg = "DE #{@name}: Buffer missing or too short"
470
+ if buf.nil? or buf.length < @offset# +@length
471
+ @value = nil
472
+ return
473
+ end
474
+
475
+ # Sure that "strip" is always ok, and that we can ignore whitespace??
476
+ # case @name
477
+ # when 'SEGNUM'
478
+ @value = buf[@offset...@offset+@length]
479
+ if self.format[0]==?n # Numerical?
480
+ if @value =~ /^ *$/ # Optional numerical field
481
+ @value = nil
482
+ return
483
+ end
484
+ code = @value[-1]
485
+ @value[-1] = ?0
486
+ @value = @value.to_i
487
+
488
+ if RUBY_VERSION >= '1.9'
489
+ case code
490
+ when ?A..?I
491
+ @value += (code.ord-?@.ord)
492
+ when ?J..?R
493
+ @value += (code.ord-?I.ord)
494
+ @value = -@value
495
+ when ?}, "\x81", "\xfc" # ü (0xFC) to be confirmed
496
+ @value = -@value
497
+ when ?{, ?0, ' ' # Blank, ä to be confirmed
498
+ # noop
499
+ when ?1..?9
500
+ @value += (code.ord-?0.ord)
501
+ else
502
+ raise "#{self.name}: #{code} is not a valid last char of a numerical field"
503
+ end
504
+ else # older Ruby version
505
+ case code
506
+ when ?A..?I
507
+ @value += (code-?@)
508
+ when ?J..?R
509
+ @value += (code-?I)
510
+ @value = -@value
511
+ when ?}, 0x81, 0xfc # ü (0xFC) to be confirmed
512
+ @value = -@value
513
+ when ?{, ?0, 0x20 # Blank, ä to be confirmed
514
+ # noop
515
+ when ?1..?9
516
+ @value += (code-?0)
517
+ else
518
+ raise "#{self.name}: #{code.chr} (#{code}) is not a valid last char of a numerical field"
519
+ end
520
+ end # Ruby version
521
+ elsif @value.is_a? String
522
+ @value.tr!(@@umlaute_cp850, @@umlaute_iso8859_1)
523
+ @value.tr!(@@umlaute_iso636_de, @@umlaute_iso8859_1)
524
+ end
525
+ # else
526
+ # @value = buf[@offset...@offset+@length].strip
527
+ # end
528
+ # @value = nil if @value =~ /^\s*$/
529
+ end
530
+
531
+
532
+ def validate( err_count=0 )
533
+ location = "#{parent.name} - #{@name}"
534
+ @format =~ /((a|an|n|d|t)(\.\.)?(\d+)):\d+/
535
+ fmt = [$2, $3, $4].join
536
+ case $1
537
+ when 'd8'
538
+ if !empty? && value !~ /^\d{8}$/
539
+ warn "#{location}: Format \'#@format\' violated: #@value"
540
+ err_count+=1
541
+ end
542
+ when 't6'
543
+ if !empty? && value !~ /^\d{6}$/
544
+ warn "#{location}: Format \'#@format\' violated: #@value"
545
+ err_count+=1
546
+ end
547
+ when /^[dt].*/
548
+ warn "validate in DE #@name: Format \'#@format\' not validated yet!"
549
+ else
550
+ return super( err_count, fmt )
551
+ end
552
+ err_count
553
+ end
554
+
555
+ # TODO: reversal of charset mapping, debugging (round-circle ok?)
556
+ #
557
+ def to_s
558
+ # return '' if empty?
559
+ if self.format[0]==?n # Numerical?
560
+ if @value && @value < 0
561
+ @value = -@value
562
+ value_str = @value.to_s
563
+ value_str[-1] = case value_str[-1]
564
+ when '0' then '}'
565
+ when '1' then 'J'
566
+ when '2' then 'K'
567
+ when '3' then 'L'
568
+ when '4' then 'M'
569
+ when '5' then 'N'
570
+ when '6' then 'O'
571
+ when '7' then 'P'
572
+ when '8' then 'Q'
573
+ when '9' then 'R'
574
+ else raise "Illegal last digit in numeric value '#{value_str}'"
575
+ end
576
+ value_str.rjust(@length,'0')[0,@length] # left-padded with '0'
577
+ else
578
+ @value.to_s.rjust(@length,'0')[0,@length] # left-padded with '0'
579
+ end
580
+ else
581
+ @value.to_s.ljust(@length)[0,@length] # right-padded with ' '
582
+ end
583
+ end
584
+
585
+ end
586
+
587
+
588
+ #########################################################################
589
+ #
590
+ # = Class StreamingParser
591
+ #
592
+ # For a documentation, see class EDI::E::StreamingParser;
593
+ # this class is its SEDAS equivalent.
594
+
595
+ class StreamingParser
596
+
597
+ def initialize
598
+ @path = 'input stream'
599
+ end
600
+
601
+ # Convenience method. Returns the path of the File object
602
+ # passed to method +go+ or just string 'input stream'
603
+ def path
604
+ @path
605
+ end
606
+
607
+ # Called at start of reading - overwrite for your init purposes.
608
+ # Note: Must *not* throw <tt>:done</tt> !
609
+ #
610
+ def on_interchange_start
611
+ end
612
+
613
+ # Called at EOF - overwrite for your cleanup purposes.
614
+ # Note: Must *not* throw <tt>:done</tt> !
615
+ #
616
+ def on_interchange_end
617
+ end
618
+
619
+ # Called when UNB or UIB encountered
620
+ #
621
+ def on_00( s )
622
+ end
623
+
624
+ # Called when UNZ or UIZ encountered
625
+ #
626
+ def on_99( s )
627
+ end
628
+
629
+ # Called when UNG encountered
630
+ #
631
+ def on_01( s )
632
+ end
633
+
634
+ # Called when UNE encountered
635
+ #
636
+ def on_98( s )
637
+ end
638
+
639
+ # Called when UNH or UIH encountered
640
+ #
641
+ def on_msg_start( s )
642
+ end
643
+
644
+ # Called when UNT or UIT encountered
645
+ #
646
+ def on_msg_end
647
+ end
648
+
649
+ # Called when any other segment encountered
650
+ #
651
+ def on_segment( s, tag )
652
+ end
653
+
654
+ # This callback is usually kept empty. It is called when the parser
655
+ # finds strings between segments or in front of or trailing an interchange.
656
+ #
657
+ # Strictly speaking, such strings are not permitted by the SEDAS
658
+ # syntax rules. However, some people e.g. seem to add empty lines
659
+ # at the end of a file. The default settings thus ignore such occurrences.
660
+ #
661
+ # If you need strict conformance checking, feel free to put some code
662
+ # into this callback method, otherwise just ignore it.
663
+ #
664
+ #
665
+ def on_other( s )
666
+ end
667
+
668
+ # Called upon syntax errors. Parsing should be aborted now.
669
+ #
670
+ def on_error(err, offset, fragment, c=nil)
671
+ raise err, "offset = %d, last chars = %s%s" %
672
+ [offset, fragment, c.nil? ? '<EOF>' : c.chr]
673
+ end
674
+
675
+ #
676
+ # The one-pass reader & dispatcher of segments, SAX-style.
677
+ #
678
+ # It reads sequentially through the given records and
679
+ # generates calls to the callbacks <tt>on_...</tt>
680
+ # Parameter +hnd+ may be any object supporting method +gets+.
681
+ #
682
+ def go( hnd )
683
+ rec, rec_id, line_no, rec_no, pos_no, folgesatz = nil, nil, 0, 0, 0, nil
684
+ @path = hnd.path if hnd.respond_to? :path
685
+ @msg_begun = @msg_50_begun = false
686
+ @msg_29_nr = nil
687
+
688
+ self.on_interchange_start
689
+
690
+ catch(:done) do
691
+ loop do
692
+ begin
693
+ rec = hnd.gets
694
+ line_no += 1
695
+ break if rec.nil?
696
+ rec.chomp!
697
+ unless rec =~ /^\s*$/
698
+ pos_no = rec[125,6].to_i
699
+ rec_id = rec[0,2]
700
+ raise "Wrong record order at line #{line_no}! Expected: #{pos_no}, found: #{rec_no}" if rec_no != pos_no && rec_id !~ /00|01|99/
701
+ folgesatz = rec[146,2] # not valid for 00,01,03,51,96,98,99
702
+ end
703
+ case rec
704
+ when /^\s*$/
705
+ self.on_other( rec )
706
+ when /^00/
707
+ self.on_00( rec )
708
+ when /^99/
709
+ self.on_99( rec )
710
+ when /^01/
711
+ rec_no = pos_no
712
+ # raise "SA01: pos_nr=#{pos_no}, expected 1" if pos_no != 1
713
+ self.on_01( rec )
714
+ when /^98/
715
+ @msg_50_begun = false
716
+ self.on_98( rec )
717
+
718
+ when /^[12][25]/
719
+ self.on_msg_end if @msg_begun
720
+ self.on_msg_start( rec ) if folgesatz=='00'
721
+ self.on_segment( rec, rec_id+folgesatz )
722
+
723
+ when /^[12][3467]/
724
+ self.on_segment( rec, rec_id+folgesatz )
725
+
726
+ when /^29/ # Group change triggers new message!
727
+ if rec[30,1]=='2' # Reli-Nr.
728
+ nr_of_sa29 = rec[31..37]
729
+ else
730
+ raise "Unexpected SA29 condition"
731
+ end
732
+ if @msg_29_nr != nr_of_sa29 # Group change!
733
+ self.on_msg_end if @msg_begun
734
+ self.on_msg_start( rec )
735
+ @msg_29_nr = nr_of_sa29
736
+ end
737
+ self.on_segment( rec, rec_id+folgesatz )
738
+
739
+ when /^50/ # Only first occurrence of SA50 starts a message!
740
+ if not( @msg_50_begun )
741
+ self.on_msg_end if @msg_begun
742
+ raise "First SA50 - syntax error!" if folgesatz!='00'
743
+ self.on_msg_start( rec )
744
+ @msg_50_begun = true
745
+ end
746
+ self.on_segment( rec, rec_id+folgesatz )
747
+
748
+ when /^51/
749
+ self.on_segment( rec, '5100' )
750
+
751
+ else
752
+ $stderr.puts "Unsupported record type: '#{rec_id}#{folgesatz}' - ignored"
753
+ end
754
+
755
+ rescue
756
+ warn "Error at record #{rec_no}, record id=#{rec_id}"
757
+ raise
758
+ end
759
+ rec_no += 1
760
+ end # loop
761
+
762
+ end # catch(:done)
763
+
764
+ self.on_interchange_end
765
+ # offset
766
+ end
767
+ end # StreamingParser
768
+
769
+ #########################################################################
770
+ #
771
+ # = Class StreamingBuilder
772
+ #
773
+ # The StreamingBuilder parses the input stream just like StreamingParser
774
+ # and in addition builds the complete interchange.
775
+ #
776
+ # This method is the new basis of Interchange.parse. You might want to
777
+ # study its callbacks to get some ideas on how to create a special-purpose
778
+ # parser/builder of your own.
779
+ #
780
+
781
+ class StreamingBuilder < StreamingParser
782
+ def initialize(auto_validate=true)
783
+ @ic = nil
784
+ @curr_group = @curr_msg = nil
785
+ @una = nil
786
+ @is_iedi = false
787
+ @auto_validate = auto_validate
788
+ end
789
+
790
+
791
+ def interchange
792
+ @ic
793
+ end
794
+
795
+
796
+ def on_00( s )
797
+ @ic = Interchange.new
798
+ @ic.header = @ic.parse_segment( s )
799
+ end
800
+
801
+ def on_99( s )
802
+ @ic.trailer = @ic.parse_segment( s )
803
+ end
804
+
805
+ def on_01( s )
806
+ @curr_group = @ic.new_msggroup( @ic.parse_segment( s ) )
807
+ @curr_group.header = @curr_group.parse_segment( s )
808
+ end
809
+
810
+ def on_98( s )
811
+ self.on_msg_end if @msg_begun
812
+ @curr_group.trailer = @curr_group.parse_segment( s )
813
+ @ic.add( @curr_group, @auto_validate )
814
+ end
815
+
816
+ def on_msg_start( s )
817
+ @curr_msg = @curr_group.new_message( :sedas_type => s[0,2] )
818
+ @msg_begun = true
819
+ # @curr_msg.header = Segment.parse( @curr_msg, s )
820
+ end
821
+
822
+ def on_msg_end
823
+ @curr_group.add( @curr_msg )
824
+ @msg_begun = false
825
+ @msg_29_nr = nil
826
+ end
827
+
828
+ # Overwrite this method to react on segments of interest
829
+ #
830
+ # Note: For a skeleton Builder (just UNB/UNG/UNT etc), overwrite with
831
+ # an empty method.
832
+ #
833
+ def on_segment( s, tag )
834
+ @curr_msg.add @curr_msg.parse_segment( s )
835
+ super
836
+ end
837
+
838
+
839
+ def on_other( s )
840
+ warn "Empty record/line found - ignored!"
841
+ end
842
+
843
+ def on_interchange_end
844
+ if @auto_validate
845
+ @ic.header.validate
846
+ @ic.trailer.validate
847
+ # Content is already validated through @ic.add() and @curr_group.add()
848
+ end
849
+ end
850
+
851
+ end # StreamingBuilder
852
+
853
+ end # module S
854
+ end # module EDI