edi4r 0.9.4.1

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