edi4r 0.9.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/AuthorCopyright +10 -0
  2. data/COPYING +56 -0
  3. data/ChangeLog +106 -0
  4. data/README +66 -0
  5. data/TO-DO +35 -0
  6. data/Tutorial +609 -0
  7. data/VERSION +1 -0
  8. data/bin/edi2xml.rb +103 -0
  9. data/bin/editool.rb +151 -0
  10. data/bin/xml2edi.rb +50 -0
  11. data/data/edifact/iso9735/SDCD.10000.csv +10 -0
  12. data/data/edifact/iso9735/SDCD.20000.csv +10 -0
  13. data/data/edifact/iso9735/SDCD.30000.csv +11 -0
  14. data/data/edifact/iso9735/SDCD.40000.csv +31 -0
  15. data/data/edifact/iso9735/SDCD.40100.csv +31 -0
  16. data/data/edifact/iso9735/SDED.10000.csv +37 -0
  17. data/data/edifact/iso9735/SDED.20000.csv +37 -0
  18. data/data/edifact/iso9735/SDED.30000.csv +43 -0
  19. data/data/edifact/iso9735/SDED.40000.csv +129 -0
  20. data/data/edifact/iso9735/SDED.40100.csv +130 -0
  21. data/data/edifact/iso9735/SDMD.10000.csv +0 -0
  22. data/data/edifact/iso9735/SDMD.20000.csv +0 -0
  23. data/data/edifact/iso9735/SDMD.30000.csv +6 -0
  24. data/data/edifact/iso9735/SDMD.40000.csv +17 -0
  25. data/data/edifact/iso9735/SDMD.40100.csv +17 -0
  26. data/data/edifact/iso9735/SDSD.10000.csv +8 -0
  27. data/data/edifact/iso9735/SDSD.20000.csv +8 -0
  28. data/data/edifact/iso9735/SDSD.30000.csv +12 -0
  29. data/data/edifact/iso9735/SDSD.40000.csv +34 -0
  30. data/data/edifact/iso9735/SDSD.40100.csv +34 -0
  31. data/data/edifact/untdid/EDCD.d01b.csv +200 -0
  32. data/data/edifact/untdid/EDCD.d96a.csv +161 -0
  33. data/data/edifact/untdid/EDED.d01b.csv +641 -0
  34. data/data/edifact/untdid/EDED.d96a.csv +462 -0
  35. data/data/edifact/untdid/EDMD.d01b.csv +3419 -0
  36. data/data/edifact/untdid/EDMD.d96a.csv +2144 -0
  37. data/data/edifact/untdid/EDSD.d01b.csv +158 -0
  38. data/data/edifact/untdid/EDSD.d96a.csv +127 -0
  39. data/data/edifact/untdid/IDCD.d01b.csv +95 -0
  40. data/data/edifact/untdid/IDMD.d01b.csv +238 -0
  41. data/data/edifact/untdid/IDSD.d01b.csv +75 -0
  42. data/lib/edi4r.rb +928 -0
  43. data/lib/edi4r/diagrams.rb +567 -0
  44. data/lib/edi4r/edi4r-1.2.dtd +20 -0
  45. data/lib/edi4r/edifact-rexml.rb +221 -0
  46. data/lib/edi4r/edifact.rb +1627 -0
  47. data/lib/edi4r/rexml.rb +256 -0
  48. data/lib/edi4r/standards.rb +495 -0
  49. data/test/eancom2webedi.rb +380 -0
  50. data/test/groups.edi +1 -0
  51. data/test/in1.edi +1 -0
  52. data/test/in1.inh +3 -0
  53. data/test/in2.edi +1 -0
  54. data/test/in2.xml +350 -0
  55. data/test/test_basics.rb +209 -0
  56. data/test/test_edi_split.rb +53 -0
  57. data/test/test_loopback.rb +21 -0
  58. data/test/test_minidemo.rb +84 -0
  59. data/test/test_rexml.rb +98 -0
  60. data/test/test_tut_examples.rb +131 -0
  61. data/test/webedi2eancom.rb +408 -0
  62. metadata +110 -0
@@ -0,0 +1,20 @@
1
+ <!ELEMENT Interchange (Header?, (Message|MsgGroup)*, Trailer?)>
2
+ <!ELEMENT MsgGroup (Header, Message+, Trailer?)>
3
+ <!ELEMENT Message (Header, (Segment|SegmentGroup)+, Trailer?)>
4
+ <!ELEMENT Header (Segment, Parameter*)>
5
+ <!ELEMENT Trailer (Segment, Parameter*)>
6
+ <!ELEMENT SegmentGroup (Segment|SegmentGroup)+>
7
+ <!ELEMENT Segment (CDE | DE)+>
8
+ <!ELEMENT CDE (DE+)>
9
+ <!ELEMENT DE (#PCDATA)>
10
+ <!ELEMENT Parameter (#PCDATA)>
11
+
12
+ <!ATTLIST Parameter name NMTOKEN #REQUIRED>
13
+ <!ATTLIST DE name NMTOKEN #REQUIRED
14
+ instance NMTOKEN "1">
15
+ <!ATTLIST CDE name NMTOKEN #REQUIRED
16
+ instance NMTOKEN "1">
17
+ <!ATTLIST Segment name NMTOKEN #REQUIRED>
18
+ <!ATTLIST SegmentGroup name NMTOKEN #REQUIRED>
19
+ <!ATTLIST Interchange standard_key (E|I) #REQUIRED
20
+ version CDATA #REQUIRED>
@@ -0,0 +1,221 @@
1
+ # UN/EDIFACT add-ons to EDI module,
2
+ # Methods for XML support for the UN/EDIFACT module
3
+ #
4
+ # :include: ../../AuthorCopyright
5
+ #
6
+ # $Id: edifact-rexml.rb,v 1.1 2006/08/01 11:14:18 werntges Exp $
7
+ #--
8
+ # $Log: edifact-rexml.rb,v $
9
+ # Revision 1.1 2006/08/01 11:14:18 werntges
10
+ # Initial revision
11
+ #
12
+ #
13
+ # Derived from "edifact.rb" (precursor) by HWW
14
+ #
15
+ # To-do list:
16
+ # SV4 - Support & testing
17
+ #++
18
+ #
19
+ # This is the XML add-on for UN/EDIFACT module of edi4r (hence '::E')
20
+ #
21
+ # It leaves all real work to the base classes. Only the UNA information
22
+ # is treated in a special way (as a "Parameter" element of the header)
23
+ # and dealt with here.
24
+
25
+ module EDI::E
26
+
27
+ class Interchange
28
+ #
29
+ # Returns a REXML document that represents the interchange
30
+ #
31
+ # xdoc:: REXML document that contains the XML representation of
32
+ # a UN/EDIFACT interchange
33
+ #
34
+ def Interchange.parse_xml( xdoc )
35
+ _root = xdoc.root
36
+ _header = _root.elements["Header"]
37
+ _trailer = _root.elements["Trailer"]
38
+ _una = _header.elements["Parameter[@name='UNA']"]
39
+ _una = _una.text if _una
40
+ raise "Empty UNA" if _una and _una.empty? # remove later!
41
+ # S001: Works for both batch and interactive EDI:
42
+ _s001 = _header.elements["Segment/CDE[@name='S001']"]
43
+ _version = _s001.elements["DE[@name='0002']"].text.to_i
44
+ _charset = _s001.elements["DE[@name='0001']"].text
45
+ params = { :charset => _charset, :version => _version }
46
+ if _una
47
+ params[:una_string] = _una
48
+ params[:show_una] = true
49
+ end
50
+ ic = Interchange.new( params )
51
+ if _root.elements["Message"].nil? # correct ??
52
+ _root.elements.each('MsgGroup') do |xel|
53
+ ic.add( MsgGroup.parse_xml( ic, xel ), false )
54
+ end
55
+ else
56
+ _root.elements.each('Message') do |xel|
57
+ ic.add( Message.parse_xml( ic, xel ), false )
58
+ end
59
+ end
60
+
61
+ ic.header = Segment.parse_xml( ic, _header.elements["Segment"] )
62
+ ic.trailer = Segment.parse_xml( ic, _trailer.elements["Segment"] )
63
+ ic.validate
64
+ ic
65
+ end
66
+
67
+ #
68
+ # Read +maxlen+ bytes from $stdin (default) or from given stream
69
+ # (UN/EDIFACT data expected), and peek into first segment (UNB/UIB).
70
+ #
71
+ # Returns an empty Interchange object with a properly header filled.
72
+ #
73
+ # Intended use:
74
+ # Efficient routing by reading just UNB data: sender/recipient/ref/test
75
+ #
76
+ def Interchange.peek_xml(xdoc) # Handle to REXML document
77
+ _root = xdoc.root
78
+ _header = _root.elements["Header"]
79
+ _trailer = _root.elements["Trailer"]
80
+ _una = _header.elements["Parameter[@name='UNA']"]
81
+ _una = _una.text if _una
82
+ raise "Empty UNA" if _una and _una.empty? # remove later!
83
+ # S001: Works for both batch and interactive EDI:
84
+ _s001 = _header.elements["Segment/CDE[@name='S001']"]
85
+ _version = _s001.elements["DE[@name='0002']"].text.to_i
86
+ _charset = _s001.elements["DE[@name='0001']"].text
87
+ params = { :charset => _charset, :version => _version }
88
+ if _una
89
+ params[:una_string] = _una
90
+ params[:show_una] = true
91
+ end
92
+ ic = Interchange.new( params )
93
+
94
+ ic.header = Segment.parse_xml( ic, _header.elements["Segment"] )
95
+ ic.trailer = Segment.parse_xml( ic, _trailer.elements["Segment"] )
96
+
97
+ ic
98
+ end
99
+
100
+
101
+ #
102
+ # Returns a REXML document that represents the interchange
103
+ #
104
+ def to_xml( xdoc = REXML::Document.new )
105
+ rc = super
106
+ # Add parameter(s) to header in rc[1]
107
+ unless @una.nil? #@una.empty?
108
+ xel = REXML::Element.new('Parameter')
109
+ rc[1] << xel
110
+ xel.attributes["name"] = 'UNA'
111
+ xel.text = @una.to_s
112
+ end
113
+ # rc
114
+ xdoc
115
+ end
116
+
117
+
118
+ #
119
+ # Returns a REXML document that represents the interchange
120
+ # according to DIN 16557-4
121
+ #
122
+ def to_din16557_4( xdoc = REXML::Document.new )
123
+ externalID = "SYSTEM \"edifact.dtd\""
124
+ doc_element_name = 'EDIFACTINTERCHANGE'
125
+ xdoc << REXML::XMLDecl.new
126
+ xdoc << REXML::DocType.new( doc_element_name, externalID )
127
+
128
+ doc_el = REXML::Element.new( doc_element_name )
129
+ xel = REXML::Element.new( 'UNA' )
130
+ xel.attributes["UNA1"] = una.ce_sep.chr
131
+ xel.attributes["UNA2"] = una.de_sep.chr
132
+ xel.attributes["UNA3"] = una.decimal_sign.chr
133
+ xel.attributes["UNA4"] = una.esc_char.chr
134
+ xel.attributes["UNA5"] = una.rep_sep.chr
135
+ xel.attributes["UNA6"] = una.seg_term.chr
136
+ xdoc.elements << doc_el
137
+ doc_el.elements << xel
138
+
139
+ super( xdoc.root )
140
+ xdoc
141
+ end
142
+
143
+ end
144
+
145
+ class Segment
146
+ def to_din16557_4( xdoc )
147
+ xel = REXML::Element.new( self.name )
148
+ names.uniq.each do |nm|
149
+ # Array of all items with this name
150
+ a = self[nm]; max = a.size
151
+ raise "DIN16557-4 does not support more than 9 repetitions" if max > 9
152
+ raise "Lookup error (should never occur)" if max == 0
153
+ if max == 1
154
+ obj = a.first
155
+ obj.to_din16557_4( xel ) unless obj.empty?
156
+ else
157
+ a.each_with_index do |obj, i|
158
+ obj.to_din16557_4( xel, i+1 ) unless obj.empty?
159
+ end
160
+ end
161
+ end
162
+ xdoc.elements << xel
163
+ end
164
+ end
165
+
166
+
167
+ class CDE
168
+ def to_din16557_4( xel, rep=nil )
169
+ prefix = name
170
+ prefix += rep.to_s if rep
171
+ names.uniq.each do |nm|
172
+ # Array of all items with this name
173
+ a = self[nm]; max = a.size
174
+ raise "DIN16557-4 does not support more than 9 repetitions" if max > 9
175
+ raise "Lookup error (should never occur)" if max == 0
176
+ if max == 1
177
+ obj = a.first
178
+ obj.to_din16557_4( xel, nil, prefix ) unless obj.empty?
179
+ else
180
+ a.each_with_index do |obj, i|
181
+ obj.to_din16557_4( xel, i+1, prefix ) unless obj.empty?
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+
189
+ class DE
190
+ def to_din16557_4( xel, rep=nil, prefix='' )
191
+ nm = prefix + 'D' + name
192
+ nm += rep.to_s if rep
193
+ xel.attributes[nm] = to_s( true )
194
+ end
195
+ end
196
+
197
+ =begin
198
+ de_instance_counter = Hash.new(0)
199
+ xseg_or_cde.elements.each('DE') do |xde|
200
+ de_name = xde.attributes['name']
201
+ i = (xde.attributes['instance'] || 1).to_i - 1
202
+ seg_or_cde[de_name][i].parse( xde.text, true )
203
+ end
204
+ =end
205
+ end # module EDI::E
206
+
207
+
208
+
209
+ module EDI
210
+ class Collection_HT
211
+ #
212
+ # NOTE: Makes sense only in the UN/EDIFACT context,
213
+ # so we list this method here.
214
+ #
215
+ def to_din16557_4( xparent )
216
+ header.to_din16557_4( xparent )
217
+ each {|obj| obj.to_din16557_4( xparent )}
218
+ trailer.to_din16557_4( xparent )
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,1627 @@
1
+ # UN/EDIFACT add-ons to EDI module,
2
+ # API to parse and create UN/EDIFACT data
3
+ #
4
+ # :include: ../../AuthorCopyright
5
+ #
6
+ # $Id: edifact.rb,v 1.10 2006/08/01 11:14:07 werntges Exp $
7
+ #--
8
+ # $Log: edifact.rb,v $
9
+ # Revision 1.10 2006/08/01 11:14:07 werntges
10
+ # Release 0.9.4.1 -- see ChangeLog
11
+ #
12
+ # Revision 1.9 2006/05/26 16:56:41 werntges
13
+ # V 0.9.3 snapshot. Many improvements (see ChangeLog), RDoc, more I-EDI support
14
+ #
15
+ # Revision 1.8 2006/05/01 22:23:55 werntges
16
+ # Preparing for 0.9.2: See ChangeLog for new features
17
+ #
18
+ # Revision 1.7 2006/04/28 14:31:50 werntges
19
+ # 0.9.1 snapshot
20
+ #
21
+ # Revision 1.6 2006/03/28 22:23:40 werntges
22
+ # changed to using symbols as parameter keys, e.g. :charset
23
+ # implemented as new module EDI::E, abandoning Interchange_E and alike
24
+ # bug fixes re. UNA (@una, setters)
25
+ #
26
+ # Revision 1.5 2006/03/22 16:52:42 werntges
27
+ # snapshot after edi4r-0.8.2.gem
28
+ #
29
+ # Revision 1.4 2004/02/19 17:31:52 heinz
30
+ # HWW: Snapshot after REMADV mapping
31
+ #
32
+ # Revision 1.3 2004/02/14 12:10:19 heinz
33
+ # HWW: Minor improvements
34
+ #
35
+ # Revision 1.2 2004/02/11 23:31:59 heinz
36
+ # HWW: First release after finishing basic tests
37
+ #
38
+ # Revision 1.1 2004/02/10 00:25:13 heinz
39
+ # Initial revision
40
+ #
41
+ #
42
+ # Derived from "edi.rb" V 1.11 on 2004-02-09 by HWW
43
+ #
44
+ # To-do list:
45
+ # validate - add functionality
46
+ # charset - check for valid chars (add UNOD-UNOZ)
47
+ # UNT count - compensate for empty segments which won't show!
48
+ # MsgGroup - improve support
49
+ # NDB - enable support of subsets
50
+ # NDB - support codelists
51
+ # SV4 - Support for repetitions
52
+ # SV4 - Support for new service segments
53
+ # SV4 - Support for I-EDI releases
54
+ #++
55
+ #
56
+ # This is the UN/EDIFACT module of edi4r (hence '::E')
57
+ #
58
+ # It implements EDIFACT versions of classes Interchange, MsgGroup, Message,
59
+ # Segment, CDE, and DE in sub-module 'E' of module 'EDI'.
60
+
61
+ module EDI::E
62
+
63
+ #
64
+ # Use pattern for allowed chars of UNOC charset if none given explicitly
65
+ #
66
+ Illegal_Charset_Patterns = Hash.new(/[^-A-Za-z0-9 .,()\/=!%"&*;<>'+:?\xa0-\xff]+/)
67
+ Illegal_Charset_Patterns['UNOA'] = /[^-A-Z0-9 .,()\/=!%"&*;<>'+:?]+/
68
+ Illegal_Charset_Patterns['UNOB'] = /[^-A-Za-z0-9 .,()\/=!%"&*;<>'+:?]+/
69
+ # more to come...
70
+
71
+ #########################################################################
72
+ #
73
+ # Utility: Separator method for UN/EDIFACT segments/CDEs
74
+ #
75
+ # The given string typically comprises an EDIFACT segment or a CDE.
76
+ # We want to split it into its elements and return those in an array.
77
+ # The tricky part is the proper handling of character escaping!
78
+ #
79
+ # Examples:
80
+ # CDE = "1234:ABC:567" --> ['1234','ABC','567']
81
+ # CDE = "1234::567" --> ['1234','','567']
82
+ # CDE = ":::SOMETEXT" --> ['','','','SOMETEXT']
83
+ # Seg = "TAG+1++2:3:4+A?+B=C" --> ['TAG','1','','2:3:4','A+B=C']
84
+ #
85
+ # NOTE: This function might be a good candidate for implementation in "C"
86
+ #
87
+ # Also see: ../../test/test_edi_split.rb
88
+ #
89
+ # str:: String to split
90
+ # s:: Separator char (an Integer)
91
+ # e:: Escape / release char (an Integer)
92
+ # max:: Max. desired number of result items, default = all
93
+ #
94
+ # Returns:
95
+ # Array of split results (strings without their terminating separator)
96
+
97
+ def edi_split( str, s, e, max=0 )
98
+ results, item, start = [], '', 0
99
+ while start < str.length do
100
+ # match_at = index of next separator, or -1 if none found
101
+ match_at = ((start...str.length).find{|i| str[i] == s}) || str.length
102
+ item += str[start...match_at]
103
+ # Count escapes in front of separator. No real separator if odd!
104
+ escapes = count_escapes( item, e )
105
+ if escapes & 1 == 1 # odd
106
+ raise EDISyntaxError, "Pending escape char in #{str}" if match_at == str.length
107
+ (escapes/2+1).times {item.chop!} # chop off duplicate escapes
108
+ item << s # add separator as regular character
109
+ else # even
110
+ (escapes/2).times {item.chop!} # chop off duplicate escapes
111
+ results << item
112
+ item = ''
113
+ end
114
+ start = match_at + 1
115
+ end
116
+ #
117
+ # Do not return trailing empty items
118
+ #
119
+ results << item unless item.empty?
120
+ return results if results.empty?
121
+ while results.last.empty?; results.pop; end
122
+ results
123
+ end
124
+
125
+ class EDISyntaxError < ArgumentError
126
+ end
127
+
128
+ def count_escapes( str, e ) # :nodoc:
129
+ n = 0
130
+ (str.length-1).downto(0) do |i|
131
+ if str[i]==e
132
+ n += 1
133
+ else
134
+ return n
135
+ end
136
+ end
137
+ n
138
+ end
139
+
140
+ module_function :edi_split, :count_escapes
141
+
142
+
143
+ #########################################################################
144
+ #
145
+ # Here we extend class Time by some methods that help us maximize
146
+ # its use in the UN/EDIFACT context.
147
+ #
148
+ # Basic idea:
149
+ # * Use the EDIFACT qualifiers of DE 2379 in DTM directly
150
+ # to parse dates and to create them upon output.
151
+ # * Use augmented Time objects as values of DE 2380 instead of strings
152
+ #
153
+ # Currently supported formats: 101, 102, 201, 203, 204
154
+
155
+ class ::Time
156
+ attr_accessor :format
157
+
158
+ def Time.edifact(str, fmt=102)
159
+ msg = "Time.edifact: #{str} does not match format #{fmt}"
160
+ case fmt.to_s
161
+ when '101'
162
+ rc = str =~ /(\d\d)(\d\d)(\d\d)(.+)?/
163
+ raise msg unless rc and rc==0; warn msg if $4
164
+ year = $1.to_i
165
+ year += (year < 69) ? 2000 : 1900 # See ParseDate
166
+ dtm = Time.local(year, $2, $3)
167
+
168
+ when '102'
169
+ rc = str =~ /(\d\d\d\d)(\d\d)(\d\d)(.+)?/
170
+ raise msg unless rc and rc==0; warn msg if $4
171
+ dtm = Time.local($1, $2, $3)
172
+
173
+ when '201'
174
+ rc = str =~ /(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(.+)?/
175
+ raise msg unless rc and rc==0; warn msg if $6
176
+ year = $1.to_i
177
+ year += (year < 69) ? 2000 : 1900 # See ParseDate
178
+ dtm = Time.local(year, $2, $3, $4, $5)
179
+
180
+ when '203'
181
+ rc = str =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(.+)?/
182
+ raise msg unless rc and rc==0; warn msg if $6
183
+ dtm = Time.local($1, $2, $3, $4, $5)
184
+
185
+ when '204'
186
+ rc = str =~ /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(.+)?/
187
+ raise msg unless rc and rc==0; warn msg if $7
188
+ dtm = Time.local($1, $2, $3, $4, $5, $6)
189
+
190
+ else
191
+ raise "Time.edifact: Format #{fmt} not supported - sorry"
192
+ end
193
+ dtm.format = fmt.to_s
194
+ dtm
195
+ end
196
+
197
+ alias to_s_orig to_s
198
+
199
+ def to_s
200
+ return to_s_orig unless @format
201
+ case @format.to_s
202
+ when '101'
203
+ "%02d%02d%02d" % [year % 100, mon, day]
204
+ when '102'
205
+ "%04d%02d%02d" % [year, mon, day]
206
+ when '201'
207
+ "%02d%02d%02d%02d%02d" % [year % 100, mon, day, hour, min]
208
+ when '203'
209
+ "%04d%02d%02d%02d%02d" % [year, mon, day, hour, min]
210
+ when '204'
211
+ "%04d%02d%02d%02d%02d%2d" % [year, mon, day, hour, min, sec]
212
+ else # Should never occur
213
+ raise "Time.edifact: Format #{format
214
+ } not supported - sorry"
215
+ end
216
+ end
217
+ end
218
+
219
+ #########################################################################
220
+ #
221
+ # Class UNA is a model of UN/EDIFACT's UNA pseudo-segment.
222
+ # It provides getters and setters that let you manipulate the six special
223
+ # characters of UN/EDIFACT. Note that the chars are passed as integers,
224
+ # i.e. ASCII codes.
225
+ #
226
+ class UNA < EDI::Object
227
+
228
+ attr_reader :pattern_esc, :pattern_unesc # :nodoc:
229
+ attr_reader :ce_sep, :de_sep, :rep_sep, :esc_char, :seg_term, :decimal_sign
230
+
231
+ #
232
+ # Sets the decimal sign. UN/EDIFACT allows only ?, and ?.
233
+ #
234
+ def decimal_sign= (chr)
235
+ chr = chr[0] if chr.is_a? String
236
+ raise "Illegal decimal sign: #{chr}" unless chr==?. || chr==?,
237
+ @decimal_sign = chr
238
+ set_chars
239
+ end
240
+
241
+ #
242
+ # Sets the composite data element separator. Default is ?:
243
+ #
244
+ def ce_sep= (chr)
245
+ chr = chr[0] if chr.is_a? String
246
+ @ce_sep = chr
247
+ set_chars # Update derived Regexp objects!
248
+ end
249
+
250
+ #
251
+ # Sets the data element separator. Default is ?+
252
+ #
253
+ def de_sep= (chr)
254
+ chr = chr[0] if chr.is_a? String
255
+ @de_sep = chr
256
+ set_chars # Update derived Regexp objects!
257
+ end
258
+
259
+ #
260
+ # Sets the repetition separator. Default is ?* .
261
+ # Only applicable to Syntax Version 4 !
262
+ #
263
+ def rep_sep= (chr)
264
+ raise NoMethodError, "Syntax version 4 required" unless root.version==4
265
+ chr = chr[0] if chr.is_a? String
266
+ @rep_sep = chr
267
+ set_chars # Update derived Regexp objects!
268
+ end
269
+
270
+ #
271
+ # Sets the segment terminator. Default is ?'
272
+ #
273
+ def seg_term= (chr)
274
+ chr = chr[0] if chr.is_a? String
275
+ @seg_term = chr
276
+ set_chars # Update derived Regexp objects!
277
+ end
278
+
279
+ #
280
+ # Sets the escape character. Default is ??
281
+ #
282
+ def esc_char= (chr)
283
+ chr = chr[0] if chr.is_a? String
284
+ @esc_char = chr
285
+ set_chars # Update derived Regexp objects!
286
+ end
287
+
288
+ #
289
+ # Generates the UNA object
290
+ # * Requires that "version" and "charset" of parent/root (Interchange)
291
+ # be already defined.
292
+ # * Sets the UN/EDIFACT defaults if source string 'UNA......' not given
293
+ #
294
+ def initialize( root, source=nil )
295
+ super( root, root, 'UNA')
296
+
297
+ raise "UNA.new requires 'version' in the interchange" unless root.version
298
+ raise "UNA.new requires 'charset' in the interchange" unless root.charset
299
+
300
+ if source =~ /^UNA(......)$/ # Take what's given
301
+ @chars = $1
302
+
303
+ elsif (source == nil or source.empty?) # Use EDIFACT default rules
304
+ if root.version==2 and root.charset=='UNOB'
305
+ @chars = "\x11\x12.? \x14"
306
+ elsif root.version==4
307
+ @chars = ":+.?*'"
308
+ else
309
+ @chars = ":+.? '"
310
+ end
311
+
312
+ else
313
+ raise "This is not a valid UNA source string: #{source}"
314
+ end
315
+
316
+ @ce_sep, @de_sep, @decimal_sign,
317
+ @esc_char, @rep_sep, @seg_term = @chars.split('').map{|c| c[0]}
318
+ set_patterns
319
+ end
320
+
321
+ def to_s
322
+ 'UNA'+@chars
323
+ end
324
+
325
+ private
326
+
327
+ def set_chars
328
+ @chars=[@ce_sep, @de_sep, @decimal_sign, @esc_char, @rep_sep, @seg_term ]
329
+ @chars=@chars.map{|c| c.chr}.join('')
330
+ # Prevent duplicates
331
+ raise "Must not assign special char more than once!" if @chars=~/(.).*\1/
332
+ set_patterns
333
+ end
334
+
335
+ #
336
+ # Adjust match patterns anew when one of the UNA separators / special
337
+ # characters is changed.
338
+ #
339
+ def set_patterns
340
+ special_chars = [ @ce_sep, @de_sep, @esc_char, @seg_term ]
341
+ special_chars.push @rep_sep if root.version == 4
342
+ special_chars = special_chars.map{|c| c.chr}
343
+ @pattern_esc = Regexp.new( [ '([', special_chars, '])' ].flatten.join)
344
+ @pattern_unesc = Regexp.new( [
345
+ '([^', @esc_char, ']?)', '[', @esc_char,
346
+ ']([', special_chars,'])'
347
+ ].flatten.join )
348
+ root.show_una = true
349
+ end
350
+ end
351
+
352
+ #########################################################################
353
+ #
354
+ # Interchange: Class of the top-level objects of UN/EDIFACT data
355
+ #
356
+ class Interchange < EDI::Interchange
357
+
358
+ attr_accessor :show_una
359
+ attr_reader :e_linebreak, :e_indent # :nodoc:
360
+ attr_reader :charset, :una
361
+ attr_reader :messages_created, :groups_created
362
+
363
+
364
+ @@interchange_defaults = {
365
+ :i_edi => false, :charset => 'UNOB', :version => 3,
366
+ :show_una => true, :una_string => nil,
367
+ :sender => nil, :recipient => nil,
368
+ :interchange_control_reference => '1', :application_reference => nil,
369
+ :interchange_agreement_id => nil,
370
+ :acknowledgment_request => nil, :test_indicator => nil,
371
+ :output_mode => :verbatim
372
+ }
373
+ @@interchange_default_keys = @@interchange_defaults.keys
374
+
375
+ # Create an empty UN/EDIFACT interchange
376
+ #
377
+ # == Supported parameters (passed hash-style):
378
+ #
379
+ # === Essentials, should not be changed later
380
+ # :charset :: Sets S001.0001, default = 'UNOB'
381
+ # :version :: Sets S001.0002, default = 3
382
+ # :i_edi :: Interactive EDI mode, a boolean (UIB instead of UNB ...), default = false
383
+ #
384
+ # === Optional parameters affecting to_s, with corresponding setters
385
+ # :show_una :: Adds UNA sement to output, default = true
386
+ # :output_mode :: See setter output_mode=(), default = :verbatim
387
+ # :una_string :: See class UNA for setters, default = nil
388
+ #
389
+ # === Optional UNB presets for your convenience, may be changed later
390
+ # :sender :: Presets DE S002/0004, default = nil
391
+ # :recipient :: Presets DE S003/0010, default = nil
392
+ # :interchange_control_reference :: Presets DE 0020, default = '1'
393
+ # :application_reference :: Presets DE 0026, default = nil
394
+ # :interchange_agreement_id :: Presets DE 0032, default = nil
395
+ # :acknowledgment_request :: Presets DE 0031, default = nil
396
+ # :test_indicator :: Presets DE 0035, default = nil
397
+ #
398
+ # === Notes
399
+ # * Date and time in S004 are set to the current values automatically.
400
+ # * Add or change any data element later. except those in S001.
401
+ #
402
+ # === Examples:
403
+ # - ic = EDI::E::Interchange.new # Empty interchange, default settings
404
+ # - ic = EDI::E::Interchange.new(:charset=>'UNOC',:output_mode=>:linebreak)
405
+
406
+ def initialize( user_par={} )
407
+ super( user_par ) # just in case...
408
+ if (illegal_keys = user_par.keys - @@interchange_default_keys) != []
409
+ msg = "Illegal parameter(s) found: #{illegal_keys.join(', ')}\n"
410
+ msg += "Valid param keys (symbols): #{@@interchange_default_keys.join(', ')}"
411
+ raise ArgumentError, msg
412
+ end
413
+ par = @@interchange_defaults.merge( user_par )
414
+
415
+ @messages_created = @groups_created = 0
416
+
417
+ @syntax = 'E' # par[:syntax] # E = UN/EDIFACT
418
+ @e_iedi = par[:i_edi]
419
+ @charset = par[:charset]
420
+ @version = par[:version]
421
+ @una = UNA.new(self, par[:una_string])
422
+ self.output_mode = par[:output_mode]
423
+ self.show_una = par[:show_una]
424
+
425
+ check_consistencies
426
+ init_ndb( @version )
427
+
428
+ if @e_iedi # Interactive EDI
429
+
430
+ raise "I-EDI not supported yet"
431
+
432
+ # Fill in what we already know about I-EDI:
433
+
434
+ @header = new_segment('UIB')
435
+ @trailer = new_segment('UIZ')
436
+ @header.cS001.d0001 = par[:charset]
437
+ @header.cS001.d0002 = par[:version]
438
+
439
+ @header.cS002.d0004 = par[:sender] unless par[:sender].nil?
440
+ @header.cS003.d0010 = par[:recipient] unless par[:recip].nil?
441
+ @header.cS302.d0300 = par[:interchange_control_reference]
442
+ # FIXME: More to do in S302...
443
+
444
+ x= :test_indicator; @header.d0035 = par[x] unless par[x].nil?
445
+
446
+ t = Time.now
447
+ @header.cS300.d0338 = t.strftime(par[:version]==4 ? '%Y%m%d':'%y%m%d')
448
+ @header.cS300.d0314 = t.strftime("%H%M")
449
+
450
+ @trailer.d0036 = 0
451
+ ch, ct = @header.cS302, @trailer.cS302
452
+ ct.d0300, ct.d0303, ct.d0051, ct.d0304 = ch.d0300, ch.d0303, ch.d0051, ch.d0304
453
+ else # Batch EDI
454
+
455
+ @header = new_segment('UNB')
456
+ @trailer = new_segment('UNZ')
457
+ @header.cS001.d0001 = par[:charset]
458
+ @header.cS001.d0002 = par[:version]
459
+ @header.cS002.d0004 = par[:sender] unless par[:sender].nil?
460
+ @header.cS003.d0010 = par[:recipient] unless par[:recip].nil?
461
+ @header.d0020 = par[:interchange_control_reference]
462
+
463
+ x= :application_reference; @header.d0026 = par[x] unless par[x].nil?
464
+ x= :acknowledgment_request; @header.d0031 = par[x] unless par[x].nil?
465
+ x= :interchange_agreement_id; @header.d0032 = par[x] unless par[x].nil?
466
+ x= :test_indicator; @header.d0035 = par[x] unless par[x].nil?
467
+
468
+ t = Time.now
469
+ @header.cS004.d0017 = t.strftime(par[:version]==4 ? '%Y%m%d':'%y%m%d')
470
+ @header.cS004.d0019 = t.strftime("%H%M")
471
+
472
+ @trailer.d0036 = 0
473
+ end
474
+ end
475
+
476
+
477
+ #
478
+ # Reads EDIFACT data from given stream (default: $stdin),
479
+ # parses it and returns an Interchange object
480
+ #
481
+ def Interchange.parse( hnd=$stdin, auto_validate=true )
482
+ ic = nil
483
+ buf = hnd.read
484
+ return ic if buf.empty?
485
+
486
+ ic, segment_list = Interchange.parse_buffer( buf )
487
+ # Remember to update ndb to SV4-1 now if d0076 of UNB/S001 tells so
488
+
489
+ # Deal with 'trash' after UNZ
490
+
491
+ if ic.is_iedi?
492
+ init_seg = Regexp.new('^UIB'); tag_init = 'UIB'
493
+ exit_seg = Regexp.new('^UIZ'); tag_exit = 'UIZ'
494
+ else
495
+ init_seg = Regexp.new('^UNB'); tag_init = 'UNB'
496
+ exit_seg = Regexp.new('^UNZ'); tag_exit = 'UNZ'
497
+ end
498
+
499
+ last_seg = nil
500
+ loop do
501
+ last_seg = segment_list.pop
502
+ case last_seg
503
+ when /^[A-Z]{3}/ # Segment tag?
504
+ unless last_seg =~ exit_seg
505
+ raise "Parse error: #{tag_exit} is not last segment! Found: #{last_seg}"
506
+ end
507
+ break
508
+ when /\n/, /\r\n/, ''
509
+ # ignore linebreaks at end of file, do not warn.
510
+ else
511
+ warn "WARNING: Data found after #{tag_exit} segment - ignored!"
512
+ warn "Found: \'#{last_seg}\'"
513
+ end
514
+ end
515
+ trailer = Segment.parse(ic, last_seg, tag_exit)
516
+
517
+ # Assure that there is only one UNB/UNZ or UIB/UIZ
518
+
519
+ err_flag = false
520
+ segment_list.each do |seg|
521
+ if seg =~ init_seg
522
+ warn "ERROR: Another interchange header found in file!"
523
+ err_flag = true
524
+ end
525
+ if seg =~ exit_seg
526
+ warn "ERROR: Another interchange trailer found in file!"
527
+ err_flag = true
528
+ end
529
+ end
530
+ raise "FATAL ERROR - exiting" if err_flag
531
+
532
+ # OK, ready to deal with content now:
533
+
534
+ case segment_list[0]
535
+ when /^UNH/
536
+ init_seg = Regexp.new('^UNH')
537
+ exit_seg = Regexp.new('^UNT')
538
+ group_mode = false
539
+ when /^UNG/
540
+ init_seg = Regexp.new('^UNG')
541
+ exit_seg = Regexp.new('^UNE')
542
+ group_mode = true
543
+ when /^UIH/ # There is no 'UIG'!
544
+ init_seg = Regexp.new('^UIH')
545
+ exit_seg = Regexp.new('^UIT')
546
+ group_mode = false
547
+ else
548
+ raise "Expected: UNH, UNG, or UIH. Found: #{segment_list[0]}"
549
+ end
550
+
551
+ while segbuf = segment_list.shift
552
+ case segbuf
553
+
554
+ when init_seg
555
+ sub_list = Array.new
556
+ sub_list.push segbuf
557
+
558
+ when exit_seg
559
+ sub_list.push segbuf
560
+ if group_mode
561
+ ic.add( MsgGroup.parse(ic, sub_list), auto_validate )
562
+ else
563
+ ic.add( Message.parse(ic, sub_list), auto_validate )
564
+ end
565
+
566
+ else
567
+ sub_list.push segbuf
568
+ end
569
+
570
+ end # while
571
+
572
+ # Finally add the trailer from the originally read data,
573
+ # thereby overwriting the temporary interchange trailer.
574
+ # Note that the temporary trailer got modified by add()ing
575
+ # to the interchange.
576
+ ic.trailer = trailer
577
+ ic
578
+ end
579
+
580
+ #
581
+ # Read +maxlen+ bytes from $stdin (default) or from given stream
582
+ # (UN/EDIFACT data expected), and peek into first segment (UNB/UIB).
583
+ #
584
+ # Returns an empty Interchange object with a properly header filled.
585
+ #
586
+ # Intended use:
587
+ # Efficient routing by reading just UNB data: sender/recipient/ref/test
588
+ #
589
+ def Interchange.peek(hnd=$stdin, maxlen=128) # Handle to input stream
590
+ buf = hnd.read( maxlen )
591
+ return nil if buf.empty?
592
+ ic, dummy = Interchange.parse_buffer( buf, 1 )
593
+
594
+ # Create a dummy trailer
595
+ tag = ic.is_iedi? ? 'UIZ' : 'UNZ'
596
+ trailer_string = tag.dup << ic.una.de_sep << '0' << ic.una.de_sep << '0'
597
+ ic.trailer= Segment.parse(ic, trailer_string, tag)
598
+
599
+ ic
600
+ end
601
+
602
+ #
603
+ # INTERNAL USE ONLY:
604
+ # Turn buffer into array of segments (array size <= s_max),
605
+ # read UNB/UIB, create an Interchange object with a header,
606
+ # return this interchange and the array of segments
607
+ #
608
+ def Interchange.parse_buffer( buf, s_max=0 ) # :nodoc:
609
+ case buf
610
+ # UN/EDIFACT case
611
+ when /^(UNA......)?\r?\n?U([IN])B.(UNO[A-Z]).([1-4])/
612
+ par = @@interchange_defaults.dup
613
+ par[:una_string], par[:charset], par[:version], par[:i_edi] =
614
+ $1, $3, $4.to_i, $2=='I'
615
+ ic = Interchange.new( par )
616
+ buf.sub!(/^UNA....../,'') # remove pseudo segment
617
+
618
+ else
619
+ raise "Is this really UN/EDIFACT? File starts with: #{buf[0,23]}"
620
+ end
621
+
622
+ segments = EDI::E.edi_split(buf, ic.una.seg_term, ic.una.esc_char, s_max)
623
+ # Remove <cr><lf> (some sources are not EDIFACT compliant)
624
+ segments.each {|s| s.sub!(/\s*(.*)/, '\1')}
625
+ ic.header = Segment.parse(ic, segments.shift, ic.is_iedi? ? 'UIB':'UNB')
626
+
627
+ [ic, segments]
628
+ end
629
+
630
+ #
631
+ # Returns +true+ if this is an I-EDI interchange (Interactive EDI)
632
+ #
633
+ def is_iedi?
634
+ @e_iedi
635
+ end
636
+
637
+ # This method modifies the behaviour of method to_s():
638
+ # UN/EDIFACT interchanges and their components are turned into strings
639
+ # either "verbatim" (default) or in some more readable way.
640
+ # This method corresponds to a parameter with same name at creation time.
641
+ #
642
+ # Valid values:
643
+ #
644
+ # :linebreak :: One-segment-per-line representation
645
+ # :indented :: Like :linebreak but with additional indentation
646
+ # (2 blanks per hierarchy level).
647
+ # :verbatim :: No linebreak (default), ISO compliant
648
+ #
649
+ def output_mode=( value )
650
+ super( value )
651
+ @e_linebreak = @e_indent = ''
652
+ case value
653
+ when :verbatim
654
+ # NOP (default)
655
+ when :linebreak
656
+ @e_linebreak = "\n"
657
+ when :indented
658
+ @e_linebreak = "\n"
659
+ @e_indent = ' '
660
+ else
661
+ raise "Unknown output mode '#{value}'. Supported modes: :linebreak, :indented, :verbatim (default)"
662
+ end
663
+ end
664
+
665
+
666
+ # Add either a MsgGroup or Message object to the interchange.
667
+ # Note: Don't mix both types!
668
+ #
669
+ # UNZ/UIZ counter DE 0036 is automatically incremented.
670
+
671
+ def add( obj, auto_validate=true )
672
+ super
673
+ @trailer.d0036 += 1 #if @trailer # @trailer doesn't exist yet when parsing
674
+ # FIXME: Warn/fail if UNH/UIH/UNG id is not unique (at validation?)
675
+ end
676
+
677
+
678
+ # Derive an empty message group from this interchange context.
679
+ # Parameters may be passed hash-like. See MsgGroup.new for details
680
+ #
681
+ def new_msggroup(params={}) # to be completed ...
682
+ @groups_created += 1
683
+ MsgGroup.new(self, params)
684
+ end
685
+
686
+ # Derive an empty message from this interchange context.
687
+ # Parameters may be passed hash-like. See Message.new for details
688
+ #
689
+ def new_message(params={})
690
+ @messages_created += 1
691
+ Message.new(self, params)
692
+ end
693
+
694
+ # Derive an empty segment from this interchange context
695
+ # For internal use only (header / trailer segment generation)
696
+ #
697
+ def new_segment(tag) # :nodoc:
698
+ Segment.new(self, tag)
699
+ end
700
+
701
+
702
+ # Parse a message group (when group mode detected)
703
+ # Internal use only.
704
+
705
+ def parse_msggroup(list) # :nodoc:
706
+ MsgGroup.parse(self, list)
707
+ end
708
+
709
+ # Parse a message (when message mode detected)
710
+ # Internal use only.
711
+
712
+ def parse_message(list) # :nodoc:
713
+ Message.parse(self, list)
714
+ end
715
+
716
+ # Parse a segment (header or trailer expected)
717
+ # Internal use only.
718
+
719
+ def parse_segment(buf, tag) # :nodoc:
720
+ Segment.parse(self, buf, tag)
721
+ end
722
+
723
+
724
+ # Returns the string representation of the interchange.
725
+ #
726
+ # Type conversion and escaping are provided.
727
+ # The UNA object is shown when +show_una+ is set to +true+ .
728
+ # See +output_mode+ for modifiers.
729
+
730
+ def to_s
731
+ s = show_una ? una.to_s + @e_linebreak : ''
732
+ postfix = '' << una.seg_term << @e_linebreak
733
+ s << super( postfix )
734
+ end
735
+
736
+
737
+ # Yields a readable, properly indented list of all contained objects,
738
+ # including the empty ones. This may be a very long string!
739
+
740
+ def inspect( indent='', symlist=[] )
741
+ symlist << :una
742
+ super
743
+ end
744
+
745
+
746
+ # Returns the number of warnings found, writes warnings to STDERR
747
+
748
+ def validate( err_count=0 )
749
+ if (h=self.size) != (t=@trailer.d0036)
750
+ warn "Counter UNZ/UIZ, DE0036 does not match content: #{t} vs. #{h}"
751
+ err_count += 1
752
+ end
753
+ if (h=@header.cS001.d0001) != @charset
754
+ warn "Charset UNZ/UIZ, S001/0001 mismatch: #{h} vs. #@charset"
755
+ err_count += 1
756
+ end
757
+ if (h=@header.cS001.d0002) != @version
758
+ warn "Syntax version UNZ/UIZ, S001/0002 mismatch: #{h} vs. #@version"
759
+ err_count += 1
760
+ end
761
+ check_consistencies
762
+
763
+ if is_iedi?
764
+ if (t=@trailer.cS302.d0300) != (h=@header.cS302.d0300)
765
+ warn "UIB/UIZ mismatch in initiator ref (S302/0300): #{h} vs. #{t}"
766
+ err_count += 1
767
+ end
768
+ # FIXME: Add more I-EDI checks
769
+ else
770
+ if (t=@trailer.d0020) != (h=@header.d0020)
771
+ warn "UNB/UNZ mismatch in refno (DE0020): #{h} vs. #{t}"
772
+ err_count += 1
773
+ end
774
+ end
775
+
776
+ # FIXME: Check if messages/groups are uniquely numbered
777
+
778
+ super
779
+ end
780
+
781
+ private
782
+
783
+ #
784
+ # Private method: Loads EDIFACT norm database
785
+ #
786
+ def init_ndb(d0002, d0076 = nil)
787
+ @basedata = EDI::Dir::Directory.create(root.syntax,
788
+ :d0002 => @version,
789
+ :d0076 => d0076,
790
+ :is_iedi => is_iedi?)
791
+ end
792
+
793
+ #
794
+ # Private method: Check if basic UNB elements are set properly
795
+ #
796
+ def check_consistencies
797
+ # FIXME - @syntax should be completely avoided, use sub-module name
798
+ if not ['E'].include?(@syntax) # More anticipated here
799
+ raise "#{@syntax} - syntax not supported!"
800
+ end
801
+ case @version
802
+ when 1
803
+ if @charset != 'UNOA'
804
+ raise "Syntax version 1 permits only charset UNOA!"
805
+ end
806
+ when 2
807
+ if not @charset =~ /UNO[AB]/
808
+ raise "Syntax version 2 permits only charsets UNOA, UNOB!"
809
+ end
810
+ when 3
811
+ if not @charset =~ /UNO[A-F]/
812
+ raise "Syntax version 3 permits only charsets UNOA...UNOF!"
813
+ end
814
+ when 4
815
+ # A,B: ISO 646 subsets, C-K: ISO-8859-x, X: ISO 2022, Y: ISO 10646-1
816
+ if not @charset =~ /UNO[A-KXY]/
817
+ raise "Syntax version 4 permits only charsets UNOA...UNOZ!"
818
+ end
819
+ else
820
+ raise "#{@version} - no such syntax version!"
821
+ end
822
+ if @e_iedi and @version != 4
823
+ raise "Inconsistent parameters - I-EDI requires syntax version 4!"
824
+ end
825
+ @illegal_charset_pattern = Illegal_Charset_Patterns['@version']
826
+ # Add more rules ...
827
+ end
828
+
829
+ end
830
+
831
+ #########################################################################
832
+ #
833
+ # Class EDI::E::MsgGroup
834
+ #
835
+ # This class implements a group of business documents of the same type
836
+ # Its header unites features from UNB as well as from UNH.
837
+ #
838
+ class MsgGroup < EDI::MsgGroup
839
+
840
+ attr_reader :messages_created
841
+
842
+ @@msggroup_defaults = {
843
+ :msg_type => 'ORDERS', :version => 'D', :release => '96A',
844
+ :resp_agency => 'UN', :assigned_code => nil # e.g. 'EAN008'
845
+ }
846
+ @@msggroup_default_keys = @@msggroup_defaults.keys
847
+
848
+ # Creates an empty UN/EDIFACT message group
849
+ # Don't use directly - use +new_msggroup+ of class Interchange instead!
850
+ #
851
+ # == First parameter
852
+ #
853
+ # This is always the parent object (an interchange object).
854
+ # Use method +new_msggroup+ in the corresponding object instead
855
+ # of creating message groups unattended - the parent reference
856
+ # will be accounted for automatically.
857
+ #
858
+ # == Second parameter
859
+ #
860
+ # List of supported hash keys:
861
+ #
862
+ # === UNG presets for your convenience, may be changed later
863
+ #
864
+ # :msg_type :: Sets DE 0038, default = 'INVOIC'
865
+ # :resp_agency :: Sets DE 0051, default = 'UN'
866
+ # :version :: Sets S008.0052, default = 'D'
867
+ # :release :: Sets S008.0054, default = '96A'
868
+ #
869
+ # === Optional parameters, required depending upon use case
870
+ #
871
+ # :assigned_code :: Sets S008.0057 (subset), default = nil
872
+ # :sender :: Presets DE S006/0040, default = nil
873
+ # :recipient :: Presets DE S007/0044, default = nil
874
+ # :group_reference :: Presets DE 0048, auto-incremented
875
+ #
876
+ # == Notes
877
+ #
878
+ # * The functional group reference number in UNG and UNE (0048) is set
879
+ # automatically to a number that is unique for this message group and
880
+ # the running process (auto-increment).
881
+ # * The counter in UNG (0060) is set automatically to the number
882
+ # of included messages.
883
+ # * The trailer segment (UNE) is generated automatically.
884
+ # * Whenever possible, <b>avoid writing to the counters of
885
+ # the message header or trailer segments</b>!
886
+
887
+ def initialize( p, user_par={} )
888
+ super( p, user_par )
889
+ @messages_created = 0
890
+
891
+ if user_par.is_a? Hash
892
+ preset_group( user_par )
893
+ @header = new_segment('UNG')
894
+ @trailer = new_segment('UNE')
895
+ @trailer.d0060 = 0
896
+
897
+ @header.d0038 = @name
898
+ @header.d0051 = @resp_agency
899
+ cde = @header.cS008
900
+ cde.d0052 = @version
901
+ cde.d0054 = @release
902
+ cde.d0057 = @subset
903
+
904
+ @header.cS006.d0040 = user_par[:sender] || root.header.cS002.d0004
905
+ @header.cS007.d0044 = user_par[:recipient] || root.header.cS003.d0010
906
+ @header.d0048 = user_par[:group_reference] || p.groups_created
907
+ # @trailer.d0048 = @header.d0048
908
+
909
+ t = Time.now
910
+ @header.cS004.d0017 = t.strftime(p.version==4 ? '%Y%m%d':'%y%m%d')
911
+ @header.cS004.d0019 = t.strftime("%H%M")
912
+
913
+ elsif user_par.is_a? Segment
914
+
915
+ @header = user_par
916
+ raise "UNG expected, #{@header.name} found!" if @header.name != 'UNG'
917
+ @header.parent = self
918
+ @header.root = self.root
919
+
920
+ # Assign a temporary UNE segment
921
+ de_sep = root.una.de_sep
922
+ @trailer = Segment.parse(root, 'UNE' << de_sep << '0' << de_sep << '0')
923
+
924
+ s008 = @header.cS008
925
+ @name = @header.d0038
926
+ @version = s008.d0052
927
+ @release = s008.d0054
928
+ @resp_agency = @header.d0051
929
+ @subset = s008.d0057
930
+ else
931
+ raise "First parameter: Illegal type!"
932
+ end
933
+
934
+ end
935
+
936
+
937
+ # Internal use only!
938
+
939
+ def preset_group(user_par) # :nodoc:
940
+ if (illegal_keys = user_par.keys - @@msggroup_default_keys) != []
941
+ msg = "Illegal parameter(s) found: #{illegal_keys.join(', ')}\n"
942
+ msg += "Valid param keys (symbols): #{@@msggroup_default_keys.join(', ')}"
943
+ raise ArgumentError, msg
944
+ end
945
+ par = @@msggroup_defaults.merge( user_par )
946
+
947
+ @name = par[:msg_type]
948
+ @version = par[:version]
949
+ @release = par[:release]
950
+ @resp_agency = par[:resp_agency]
951
+ @subset = par[:assigned_code]
952
+ # FIXME: Eliminate use of @version, @release, @resp_agency, @subset
953
+ # They get outdated whenever their UNG counterparts are changed
954
+ # Try to keep @name updated, or pass it a generic name
955
+ end
956
+
957
+
958
+ def MsgGroup.parse (p, segment_list) # List of segments
959
+ grp = p.new_msggroup(:msg_type => 'DUMMY')
960
+
961
+ # We now expect a sequence of segments that comprises one group,
962
+ # starting with UNG and ending with UNE, and with messages in between.
963
+ # We process the UNG/UNE envelope separately, then work on the content.
964
+
965
+ header = grp.parse_segment(segment_list.shift, 'UNG')
966
+ trailer = grp.parse_segment(segment_list.pop, 'UNE')
967
+
968
+ init_seg = Regexp.new('^UNH')
969
+ exit_seg = Regexp.new('^UNT')
970
+
971
+ while segbuf = segment_list.shift
972
+ case segbuf
973
+
974
+ when init_seg
975
+ sub_list = Array.new
976
+ sub_list.push segbuf
977
+
978
+ when exit_seg
979
+ sub_list.push segbuf
980
+ grp.add grp.parse_message(sub_list)
981
+
982
+ else
983
+ sub_list.push segbuf
984
+ end
985
+ end
986
+
987
+ grp.header = header
988
+ grp.trailer = trailer
989
+ grp
990
+ end
991
+
992
+
993
+ def new_message(params={})
994
+ @messages_created += 1
995
+ Message.new(self, params)
996
+ end
997
+
998
+ def new_segment(tag) # :nodoc:
999
+ Segment.new(self, tag)
1000
+ end
1001
+
1002
+
1003
+ def parse_message(list) # :nodoc:
1004
+ Message.parse(self, list)
1005
+ end
1006
+
1007
+ def parse_segment(buf, tag) # :nodoc:
1008
+ Segment.parse(self, buf, tag)
1009
+ end
1010
+
1011
+
1012
+ def add( msg )
1013
+ super
1014
+ @trailer.d0060 = @trailer.d0060.to_i if @trailer.d0060.is_a? String
1015
+ @trailer.d0060 += 1
1016
+ end
1017
+
1018
+
1019
+ def to_s
1020
+ postfix = '' << root.una.seg_term << root.e_linebreak
1021
+ super( postfix )
1022
+ end
1023
+
1024
+
1025
+ def validate( err_count=0 )
1026
+
1027
+ # Consistency checks
1028
+
1029
+ if (a=@trailer.d0060) != (b=self.size)
1030
+ warn "UNE: DE 0060 (#{a}) does not match number of messages (#{b})"
1031
+ err_count += 1
1032
+ end
1033
+ a, b = @trailer.d0048, @header.d0048
1034
+ if a != b
1035
+ warn "UNE: DE 0048 (#{a}) does not match reference in UNG (#{b})"
1036
+ err_count += 1
1037
+ end
1038
+
1039
+ # FIXME: Check if messages are uniquely numbered
1040
+
1041
+ super
1042
+ end
1043
+
1044
+ end
1045
+
1046
+
1047
+ #########################################################################
1048
+ #
1049
+ # Class EDI::E::Message
1050
+ #
1051
+ # This class implements a single business document according to UN/EDIFACT
1052
+
1053
+ class Message < EDI::Message
1054
+ # private_class_method :new
1055
+
1056
+ @@message_defaults = {
1057
+ :msg_type => 'ORDERS', :version => 'D', :release => '96A',
1058
+ :resp_agency => 'UN', :assigned_code => nil # e.g. 'EAN008'
1059
+ }
1060
+ @@message_default_keys = @@message_defaults.keys
1061
+
1062
+ # Creates an empty UN/EDIFACT message
1063
+ # Don't use directly - use +new_message+ of class Interchange or MsgGroup instead!
1064
+ #
1065
+ # == First parameter
1066
+ #
1067
+ # This is always the parent object, either a message group
1068
+ # or an interchange object.
1069
+ # Use method +new_message+ in the corresponding object instead
1070
+ # of creating messages unattended, and the parent reference
1071
+ # will be accounted for automatically.
1072
+ #
1073
+ # == Second parameter, case "Hash"
1074
+ #
1075
+ # List of supported hash keys:
1076
+ #
1077
+ # === Essentials, should not be changed later
1078
+ #
1079
+ # :msg_type :: Sets S009.0065, default = 'ORDERS'
1080
+ # :version :: Sets S009.0052, default = 'D'
1081
+ # :release :: Sets S009.0054, default = '96A'
1082
+ # :resp_agency :: Sets S009.0051, default = 'UN'
1083
+ #
1084
+ # === Optional parameters, required depending upon use case
1085
+ #
1086
+ # :assigned_code :: Sets S009.0057 (subset), default = nil
1087
+ #
1088
+ # == Second parameter, case "Segment"
1089
+ #
1090
+ # This mode is only used internally when parsing data.
1091
+ #
1092
+ # == Notes
1093
+ #
1094
+ # * The counter in UNH (0062) is set automatically to a
1095
+ # number that is unique for the running process.
1096
+ # * The trailer segment (usually UNT) is generated automatically.
1097
+ # * Whenever possible, <b>avoid write access to the
1098
+ # message header or trailer segments</b>!
1099
+
1100
+ def initialize( p, user_par={} )
1101
+ super( p, user_par )
1102
+
1103
+ # First param is either a hash or segment UNH
1104
+ # - If Hash: Build UNH from given parameters
1105
+ # - If Segment: Extract some crucial parameters
1106
+ if user_par.is_a? Hash
1107
+ preset_msg( user_par )
1108
+ par = {
1109
+ :d0065 => @name, :d0052=> @version, :d0054=> @release,
1110
+ :d0051 => @resp_agency, :d0057 => @subset, :is_iedi => root.is_iedi?
1111
+ }
1112
+ @maindata = EDI::Dir::Directory.create(root.syntax, par )
1113
+
1114
+ if root.is_iedi?
1115
+ @header = new_segment('UIH')
1116
+ @trailer = new_segment('UIT')
1117
+ cde = @header.cS306
1118
+ # cde.d0113 = @sub_id
1119
+ @header.d0340 = p.messages_created
1120
+ else
1121
+ @header = new_segment('UNH')
1122
+ @trailer = new_segment('UNT')
1123
+ cde = @header.cS009
1124
+ @header.d0062 = p.messages_created
1125
+ end
1126
+ cde.d0065 = @name
1127
+ cde.d0052 = @version
1128
+ cde.d0054 = @release
1129
+ cde.d0051 = @resp_agency
1130
+ cde.d0057 = @subset
1131
+
1132
+ elsif user_par.is_a? Segment
1133
+ @header = user_par
1134
+ raise "UNH expected, #{@header.name} found!" if @header.name != 'UNH'
1135
+ # I-EDI support to be added!
1136
+ @header.parent = self
1137
+ @header.root = self.root
1138
+ @trailer = Segment.new(root, 'UNT') # temporary
1139
+ s009 = @header.cS009
1140
+ @name = s009.d0065
1141
+ @version = s009.d0052
1142
+ @release = s009.d0054
1143
+ @resp_agency = s009.d0051
1144
+ @subset = s009.d0057
1145
+ par = {
1146
+ :d0065 => @name, :d0052=> @version, :d0054=> @release,
1147
+ :d0051 => @resp_agency, :d0057 => @subset, :is_iedi => root.is_iedi?
1148
+ }
1149
+ @maindata = EDI::Dir::Directory.create(root.syntax, par )
1150
+ else
1151
+ raise "First parameter: Illegal type!"
1152
+ end
1153
+
1154
+ @trailer.d0074 = 2 if @trailer # Just UNH and UNT so far
1155
+ end
1156
+
1157
+ #
1158
+ # Derive a new segment with the given name from this message context.
1159
+ # The call will fail if the message name is unknown to this message's
1160
+ # UN/TDID (not in EDMD/IDMD).
1161
+ #
1162
+ # == Example:
1163
+ # seg = msg.new_segment( 'BGM' )
1164
+ # seg.d1004 = '220'
1165
+ # # etc.
1166
+ # msg.add seg
1167
+ #
1168
+ def new_segment( tag )
1169
+ Segment.new(self, tag)
1170
+ end
1171
+
1172
+ # Internal use only!
1173
+
1174
+ def parse_segment(buf, tag) # :nodoc:
1175
+ Segment.parse(self, buf, tag)
1176
+ end
1177
+
1178
+ # Internal use only!
1179
+
1180
+ def preset_msg(user_par) # :nodoc:
1181
+ if (illegal_keys = user_par.keys - @@message_default_keys) != []
1182
+ msg = "Illegal parameter(s) found: #{illegal_keys.join(', ')}\n"
1183
+ msg += "Valid param keys (symbols): #{@@message_default_keys.join(', ')}"
1184
+ raise ArgumentError, msg
1185
+ end
1186
+
1187
+ # Use UNG as source for defaults if present
1188
+ ung = parent.header
1189
+ if parent.is_a?(MsgGroup) && ung.d0038
1190
+ s008 = ung.cS008
1191
+ par = {
1192
+ :msg_type=> ung.d0038, :version=> s008.d0052, :release=> s008.d0054,
1193
+ :resp_agency => ung.d0051, :assigned_code => s008.d0057
1194
+ }.merge( user_par )
1195
+ else
1196
+ par = @@message_defaults.merge( user_par )
1197
+ end
1198
+
1199
+ @name = par[:msg_type]
1200
+ @version = par[:version]
1201
+ @release = par[:release]
1202
+ @resp_agency = par[:resp_agency]
1203
+ @subset = par[:assigned_code]
1204
+ # FIXME: Eliminate use of @version, @release, @resp_agency, @subset
1205
+ # They get outdated whenever their UNH counterparts are changed
1206
+ # Try to keep @name updated, or pass it a generic name
1207
+ end
1208
+
1209
+
1210
+ # Returns a new Message object that contains the data of the
1211
+ # strings passed in the +segment_list+ array. Uses the context
1212
+ # of the given +parent+ object and configures message as a child.
1213
+
1214
+ def Message.parse (parent, segment_list)
1215
+
1216
+ if parent.root.is_iedi?
1217
+ h, t, re_t = 'UIH', 'UIT', /^UIT/
1218
+ else
1219
+ h, t, re_t = 'UNH', 'UNT', /^UNT/
1220
+ end
1221
+
1222
+ # Segments comprise a single message
1223
+ # Temporarily assign a parent, or else service segment lookup fails
1224
+ header = parent.parse_segment(segment_list.shift, h)
1225
+ msg = parent.new_message(header)
1226
+ trailer = msg.parse_segment( segment_list.pop, t )
1227
+
1228
+ segment_list.each do |segbuf|
1229
+ seg = Segment.parse( msg, segbuf )
1230
+ if segbuf =~ re_t # FIXME: Should that case ever occur?
1231
+ msg.trailer = seg
1232
+ else
1233
+ msg.add(seg)
1234
+ end
1235
+ end
1236
+ msg.trailer = trailer
1237
+ msg
1238
+ end
1239
+
1240
+
1241
+ #
1242
+ # Add a previously derived segment to the end of this message (append)
1243
+ # Make sure that all mandatory elements have been supplied.
1244
+ #
1245
+ # == Notes
1246
+ #
1247
+ # * Strictly add segments in the sequence described by this message's
1248
+ # branching diagram!
1249
+ #
1250
+ # * Adding a segment will automatically increase the corresponding
1251
+ # counter in the message trailer.
1252
+ #
1253
+ # == Example:
1254
+ # seg = msg.new_segment( 'BGM' )
1255
+ # seg.d1004 = '220'
1256
+ # # etc.
1257
+ # msg.add seg
1258
+ #
1259
+ def add( seg )
1260
+ super
1261
+ @trailer.d0074 = @trailer.d0074.to_i if @trailer.d0074.is_a? String
1262
+ @trailer.d0074 += 1 # What if new segment is/remains empty??
1263
+ end
1264
+
1265
+
1266
+ def validate( err_count=0 )
1267
+ # Check sequence of segments against library,
1268
+ # thereby adding location information to each segment
1269
+
1270
+ par = {
1271
+ :d0065 => @name, :d0052=> @version, :d0054=> @release,
1272
+ :d0051 => @resp_agency, :d0057 => @subset,
1273
+ :d0002 => root.version, :is_iedi => root.is_iedi?,
1274
+ :d0076 => nil # SV 4-1 support still missing here
1275
+ }
1276
+ diag = EDI::Diagram::Diagram.create( root.syntax, par )
1277
+ ni = EDI::Diagram::NodeInstance.new(diag)
1278
+
1279
+ ni.seek!( @header )
1280
+ @header.update_with( ni )
1281
+ each do |seg|
1282
+ if ni.seek!(seg)
1283
+ seg.update_with( ni )
1284
+ else
1285
+ # FIXME: Do we really have to fail here, or would a "warn" suffice?
1286
+ raise "seek! failed for #{seg.name} when starting at #{ni.name}"
1287
+ end
1288
+ end
1289
+ ni.seek!( @trailer )
1290
+ @trailer.update_with( ni )
1291
+
1292
+
1293
+ # Consistency checks
1294
+
1295
+ if (a=@trailer.d0074) != (b=self.size+2)
1296
+ warn "DE 0074 (#{a}) does not match number of segments (#{b})"
1297
+ err_count += 1
1298
+ end
1299
+
1300
+ if root.is_iedi?
1301
+ a, b = @trailer.d0340, @header.d0340
1302
+ else
1303
+ a, b = @trailer.d0062, @header.d0062
1304
+ end
1305
+ if a != b
1306
+ warn "Trailer reference (#{a}) does not match header reference (#{b})"
1307
+ err_count += 1
1308
+ end
1309
+
1310
+ if parent.is_a? MsgGroup
1311
+ ung = parent.header; s008 = ung.cS008; s009 = header.cS009
1312
+ a, b = s009.d0065, ung.d0038
1313
+ if a != b
1314
+ warn "Message type (#{a}) does not match that of group (#{b})"
1315
+ err_count += 1
1316
+ end
1317
+ a, b = s009.d0052, s008.d0052
1318
+ if a != b
1319
+ warn "Message version (#{a}) does not match that of group (#{b})"
1320
+ err_count += 1
1321
+ end
1322
+ a, b = s009.d0054, s008.d0054
1323
+ if a != b
1324
+ warn "Message release (#{a}) does not match that of group (#{b})"
1325
+ err_count += 1
1326
+ end
1327
+ a, b = s009.d0051, ung.d0051
1328
+ if a != b
1329
+ warn "Message responsible agency (#{a}) does not match that of group (#{b})"
1330
+ err_count += 1
1331
+ end
1332
+ a, b = s009.d0057, s008.d0057
1333
+ if a != b
1334
+ warn "Message association assigned code (#{a}) does not match that of group (#{b})"
1335
+ err_count += 1
1336
+ end
1337
+
1338
+ end
1339
+
1340
+ # Now check each segment
1341
+ super( err_count )
1342
+ end
1343
+
1344
+
1345
+ def to_s
1346
+ postfix = '' << root.una.seg_term << root.e_linebreak
1347
+ super( postfix )
1348
+ end
1349
+
1350
+ end
1351
+
1352
+
1353
+ #########################################################################
1354
+ #
1355
+ # Class EDI::E::Segment
1356
+ #
1357
+ # This class implements UN/EDIFACT segments like BGM, NAD etc.,
1358
+ # including the service segments UNB, UNH ...
1359
+ #
1360
+
1361
+ class Segment < EDI::Segment
1362
+
1363
+ # A new segment must have a parent (usually, a message). This is the
1364
+ # first parameter. The second is a string with the desired segment tag.
1365
+ #
1366
+ # Don't create segments without their context - use Message#new_segment()
1367
+ # instead.
1368
+
1369
+ def initialize(p, tag)
1370
+ super( p, tag )
1371
+
1372
+ each_BCDS(tag) do |entry|
1373
+ id = entry.name
1374
+ status = entry.status
1375
+
1376
+ # FIXME: Code redundancy in type detection - remove later!
1377
+ case id
1378
+ when /[CES]\d{3}/ # Composite
1379
+ add new_CDE(id, status)
1380
+ when /\d{4}/ # Simple DE
1381
+ add new_DE(id, status, fmt_of_DE(id))
1382
+ else # Should never occur
1383
+ raise "Not a legal DE or CDE id: #{id}"
1384
+ end
1385
+ end
1386
+ end
1387
+
1388
+
1389
+ def new_CDE(id, status)
1390
+ CDE.new(self, id, status)
1391
+ end
1392
+
1393
+
1394
+ def new_DE(id, status, fmt)
1395
+ DE.new(self, id, status, fmt)
1396
+ end
1397
+
1398
+
1399
+ # Reserved for internal use
1400
+
1401
+ def Segment.parse (p, buf, tag_expected=nil)
1402
+ # Buffer contains a single segment
1403
+ obj_list = EDI::E::edi_split( buf, p.root.una.de_sep, p.root.una.esc_char )
1404
+ tag = obj_list.shift # First entry must be the segment tag
1405
+
1406
+ raise "Illegal tag: #{tag}" unless tag =~ /[A-Z]{3}/
1407
+ if tag_expected and tag_expected != tag
1408
+ raise "Wrong segment name! Expected: #{tag_expected}, found: #{tag}"
1409
+ end
1410
+
1411
+ seg = p.new_segment(tag)
1412
+ seg.each {|obj| obj.parse( obj_list.shift ) }
1413
+ seg
1414
+ # Error handling needed here if obj_list is not exhausted now!
1415
+ end
1416
+
1417
+
1418
+ def to_s
1419
+ s = ''
1420
+ return s if empty?
1421
+
1422
+ rt = self.root
1423
+
1424
+ indent = rt.e_indent * (self.level || 0)
1425
+ s << indent << name << rt.una.de_sep
1426
+ skip_count = 0
1427
+ each {|obj|
1428
+ if obj.empty?
1429
+ skip_count += 1
1430
+ else
1431
+ if skip_count > 0
1432
+ s << rt.una.de_sep.chr * skip_count
1433
+ skip_count = 0
1434
+ end
1435
+ s << obj.to_s
1436
+ skip_count += 1
1437
+ end
1438
+ }
1439
+ s
1440
+ end
1441
+
1442
+
1443
+ # Some exceptional setters, required for data consistency
1444
+
1445
+ # Don't change DE 0001! d0001=() raises an exception when called.
1446
+ def d0001=( value ); fail "Charset not modifiable!"; end
1447
+
1448
+ # Don't change DE 0002! d0002=() raises an exception when called.
1449
+ def d0002=( value ); fail "EDIFACT Syntax version not modifiable!"; end
1450
+
1451
+ # Setter for DE 0020 in UNB & UNZ (interchange control reference)
1452
+ def d0020=( value )
1453
+ return super unless self.name=~/UN[BZ]/
1454
+ parent.header['0020'].first.value = value
1455
+ parent.trailer['0020'].first.value = value
1456
+ end
1457
+
1458
+ # Setter for DE 0048 in UNE & UNG (group reference)
1459
+ def d0048=( value )
1460
+ return super unless self.name=~/UN[GE]/
1461
+ parent.header['0048'].first.value = value
1462
+ parent.trailer['0048'].first.value = value
1463
+ end
1464
+
1465
+ # Setter for DE 0062 in UNH & UNT (message reference number)
1466
+ def d0062=( value ) # UNH
1467
+ return super unless self.name=~/UN[HT]/
1468
+ parent.header['0062'].first.value = value
1469
+ parent.trailer['0062'].first.value = value
1470
+ end
1471
+
1472
+ # Setter for DE 0340 in UIH & UIT (message reference number)
1473
+ def d0340=( value ) # UIH
1474
+ return super unless self.name=~/UI[HT]/
1475
+ parent.header['0340'].first.value = value
1476
+ parent.trailer['0340'].first.value = value
1477
+ end
1478
+
1479
+ end
1480
+
1481
+
1482
+ #########################################################################
1483
+ #
1484
+ # Class EDI::E::CDE
1485
+ #
1486
+ # This class implements UN/EDIFACT composite data elements C507 etc.,
1487
+ # including the service CDEs S001, S009 ...
1488
+ #
1489
+ # For internal use only.
1490
+
1491
+ class CDE < EDI::CDE
1492
+
1493
+ def initialize(p, name, status)
1494
+ super(p, name, status)
1495
+
1496
+ each_BCDS(name) do |entry|
1497
+ id = entry.name
1498
+ status = entry.status
1499
+ # FIXME: Code redundancy in type detection - remove later!
1500
+ if id =~ /\d{4}/
1501
+ add new_DE(id, status, fmt_of_DE(id))
1502
+ else # Should never occur
1503
+ raise "Not a legal DE: #{id}"
1504
+ end
1505
+ end
1506
+ end
1507
+
1508
+ def new_DE(id, status, fmt)
1509
+ DE.new(self, id, status, fmt)
1510
+ end
1511
+
1512
+
1513
+ def parse (buf) # Buffer contains content of a single CDE
1514
+ return nil unless buf
1515
+ obj_list = EDI::E::edi_split( buf, root.una.ce_sep, root.una.esc_char )
1516
+ each {|obj| obj.parse( obj_list.shift ) }
1517
+ # FIXME: Error handling needed here if obj_list is not exhausted now!
1518
+ end
1519
+
1520
+
1521
+ def to_s
1522
+ rt = self.root
1523
+ s = ''; skip_count = 0
1524
+ ce_sep = rt.una.ce_sep.chr
1525
+ each {|de|
1526
+ if de.empty?
1527
+ skip_count += 1
1528
+ else
1529
+ if skip_count > 0
1530
+ s << ce_sep * skip_count
1531
+ skip_count = 0
1532
+ end
1533
+ s << de.to_s
1534
+ skip_count += 1
1535
+ end
1536
+ }
1537
+ s
1538
+ end
1539
+
1540
+ end
1541
+
1542
+
1543
+ #########################################################################
1544
+ #
1545
+ # Class EDI::E::DE
1546
+ #
1547
+ # This class implements UN/EDIFACT data elements 1004, 2005 etc.,
1548
+ # including the service DEs 0001, 0004, ...
1549
+ #
1550
+ # For internal use only.
1551
+
1552
+ class DE < EDI::DE
1553
+
1554
+ def initialize( p, name, status, fmt )
1555
+ super( p, name, status, fmt )
1556
+ raise "Illegal DE name: #{name}" unless name =~ /\d{4}/
1557
+ # check if supported format syntax
1558
+ # check if supported status value
1559
+ end
1560
+
1561
+
1562
+ # Generate the DE content from the given string representation.
1563
+ # +buf+ contains a single DE string, possibly escaped
1564
+
1565
+ def parse( buf, already_escaped=false )
1566
+ return nil unless buf
1567
+ return @value = nil if buf.empty?
1568
+ @value = already_escaped ? buf : unescape(buf)
1569
+ if format[0] == ?n
1570
+ # Normalize decimal sign
1571
+ @value.sub!(/,/, '.')
1572
+ # Select appropriate Numeric, FIXME: Also match exponents!
1573
+ self.value = @value=~/\d+\.\d+/ ? @value.to_f : @value.to_i
1574
+ end
1575
+ @value
1576
+ end
1577
+
1578
+
1579
+ def to_s( no_escape=false )
1580
+ return '' if empty?
1581
+ s = if @value.is_a? Numeric
1582
+ # Adjust decimal sign
1583
+ super().sub(/[.,]/, root.una.decimal_sign.chr)
1584
+ else
1585
+ super().to_s
1586
+ end
1587
+ no_escape ? s : escape(s)
1588
+ end
1589
+
1590
+
1591
+ # The proper method to assign values to a DE.
1592
+ # The passed value must respond to +to_i+ .
1593
+
1594
+ def value=( val )
1595
+ # Suppress trailing decimal part if Integer value
1596
+ ival = val.to_i
1597
+ val = ival if val.is_a? Float and val == ival
1598
+ super
1599
+ end
1600
+
1601
+
1602
+ private
1603
+
1604
+ def escape (str)
1605
+ rt = self.root
1606
+ raise "Must have a root to do this" if rt == nil
1607
+
1608
+ esc = rt.una.esc_char.chr
1609
+ esc << ?\\ if esc == '\\' # Special case if backslash!
1610
+
1611
+ if rt.charset == 'UNOA'
1612
+ # Implicit conversion to uppercase - convenient,
1613
+ # but could be argued against!
1614
+ str.upcase.gsub(rt.una.pattern_esc, esc+'\1')
1615
+ else
1616
+ str.gsub(rt.una.pattern_esc, esc+'\1')
1617
+ end
1618
+ end
1619
+
1620
+ def unescape (str)
1621
+ rt = self.root
1622
+ raise "Must have a root to do this" if rt == nil
1623
+ str.gsub(rt.una.pattern_unesc, '\1\2')
1624
+ end
1625
+ end
1626
+
1627
+ end # module EDI