edi4r 0.9.4.1 → 0.9.6.2

Sign up to get free protection for your applications and to get access to all the features.
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