Class: X12

Inherits:
Object show all
Includes:
Enumerable
Defined in:
lib/x12-lite.rb

Overview

[ X12 ]=====================================================================

Constant Summary collapse

VERSION =
"0.3.3"
LEN =

ISA field widths

[3, 2, 10, 2, 10, 2, 15, 2, 15, 6, 4, 1, 5, 9, 1, 1]
BCS =

Basic Character Set (also adds ‘#’ from the Extended Character Set)

<<~'end'.gsub(/\s+/, '').concat(' ').split('')
  A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
  0 1 2 3 4 5 6 7 8 9
  ! " # & ' ( ) * + , - . / : ; = ?
end

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(obj = nil, *etc) ⇒ X12

delimiter pos chr ———- — —  field 4 (*)  composite 105 (:)  repetition 83 (^)  segment 106 (~)



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/x12-lite.rb', line 69

def initialize(obj=nil, *etc)
  if obj.is_a?(String) && !etc.empty?
    obj = etc.dup.unshift(obj)
  elsif obj
    obj = obj.dup # does this need to be a deep clone?
  end
  case obj
    when nil
    when String then @str = obj unless obj.empty?
    when Array
    when Hash
    when IO     then @str = obj = obj.read
    when X12    then @str = obj.to_s
    else raise "unable to handle #{arg.class} objects"
  end
  @str ||= isa_widths!("ISA*00**00**ZZ**ZZ****^*00501**0*P*:~")
  @str =~ /\AISA(.).{78}(.).{21}(.)(.)/ or raise "malformed X12"
  @fld, @com, @rep, @seg = $~.captures.values_at(0, 2, 1, 3)
  @rep = "^" if @rep == "U"
  @sep = [@fld, @com, @rep, @seg]
  @bad = regex_chars!(BCS + @sep) # invalid in txn bytes #!# BARELY USED NOW???
  @chr = regex_chars!(BCS - @sep) # invalid in user data #!# NOT USED RIGHT NOW
  case obj
    when String, nil then to_a; @str = nil
    when Array       then set(obj.shift, obj.shift) until obj.empty?
    when Hash        then obj.each {|k, v| set(k, v) unless v.nil?}
  end
  to_s unless @str
end

Class Method Details

.[](*args) ⇒ Object



104
105
106
# File 'lib/x12-lite.rb', line 104

def self.[](*args)
  new(*args)
end

.load(file) ⇒ Object



99
100
101
102
# File 'lib/x12-lite.rb', line 99

def self.load(file)
  str = File.open(file, "r:bom|utf-8", &:read) rescue "unreadable file"
  new(str)
end

Instance Method Details

#data(*args) ⇒ Object Also known as: get, set, [], []=



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/x12-lite.rb', line 221

def data(*args)
  len = args.size; return update(*args) if len > 2
  pos = args[0] or return @str
  val = args[1]

  # Syntax: seg(num)-fld(rep).com
  pos =~ /^(..[^-.(]?)?(?:\((\d*|[+!?*]?)\))?[-.]?(\d+)?(?:\((\d*|[+!?*]?)\))?[-.]?(\d+)?$/
  seg = $1 or return ""; want = /^#{seg}[^#{Regexp.escape(@seg)}\r\n]*/i
  num = $2 && $2.to_i; new_num = $2 == "+"; ask_num = $2 == "?"; all_num = $2 == "*"
  rep = $4 && $4.to_i; new_rep = $4 == "+"; ask_rep = $4 == "?"; all_rep = $4 == "*"
  fld = $3 && $3.to_i; len > 1 && fld == 0 and raise "zero index on field"
  com = $5 && $5.to_i; len > 1 && com == 0 and raise "zero index on component"

  # NOTE: When doing a get, a missing num or rep means get the first
  # NOTE: When doing a set, a missing num or rep means set the last
  # NOTE: ask_num and ask_rep are mutually exclusive, how should we handle?
  # NOTE: all_num and all_rep are mutually exclusive, how should we handle?
  # NOTE: ask_* is only for get
  # NOTE: all_* is only for get and set [is this correct?]

  if len == 1 # get
    to_s unless @str
    return @str.scan(want).size if ask_num && !ask_rep
    return @str.scan(want).inject([]) do |ary, out|
      out = loop do
        out = out.split(@fld)[fld    ] or break if fld
        break out.split(@rep).size if ask_rep
        out = out.split(@rep)[rep - 1] or break if rep || (com && (rep ||= 1))
        out = out.split(@com)[com - 1] or break if com
        break out
      end
      ary << out if out
      ary
    end if all_num
    out = @str.scan(want)[num - 1] or return "" if num ||= 1
    out = out.split(@fld)[fld    ] or return "" if fld
    return out.split(@rep).size if ask_rep
    out = out.split(@rep)[rep - 1] or return "" if rep || (com && (rep ||= 1))
    out = out.split(@com)[com - 1] or return "" if com
    out
  else # set
    to_a unless @ary
    @str &&= nil
    our = @ary.select {|now| now[0] =~ want}
    unless all_num
      num ||= 0 # default to last
      row = our[num - 1] or pad = num - our.size
      pad = 1 if (num == 0 && our.size == 0 || new_num)
      pad and pad.times { @ary.push(row = [seg.upcase]) }
      val = our.size + pad if new_num && val == :num # auto-number
      our = [row]
    end

    # prepare the source and decide how to update
    val ||= ""
    how = case
    when        !rep && !com # replace fields
      val = val.join(@fld) if Array === val
      val = val.to_s.split(@fld, -1)
      :fld
    when fld &&  rep && !com # replace repeats
      val = val.join(@rep) if Array === val
      val.include?(@fld) and raise "invalid separator for repeats"
      val = val.to_s.split(@rep, -1)
      :rep
    when fld &&          com # replace components
      val = val.join(@com) if Array === val
      val.include?(@fld) and raise "invalid separator for repeats"
      val.include?(@rep) and raise "invalid separator for repeats"
      val = val.to_s.split(@com, -1)
      :com
    end or raise "invalid fld/rep/com: #{[fld, rep, com].inspect}"
    val = [""] if val.empty?

    #!# TODO: val.dup to prevent sharing issues???

    # replace the target
    our.each do |row|
      case how
      when :fld
        if fld
          pad = fld - row.size
          pad.times { row.push("") } if pad > 0
          row[fld, val.size] = val
        else
          row[1..-1] = val
        end
      when :rep
        if (was = row[fld] ||= "").empty?
          was << @rep * (rep - 1) if rep > 1
          was << val.join(@rep)
        else
          ufr = was.split(@rep, -1) # unpacked repeats
          pad = rep - ufr.size
          pad = 1 if new_rep || rep == 0 && ufr.empty?
          pad.times { ufr.push("") } if pad > 0
          ufr[rep - 1, val.size] = val
          was.replace(ufr.join(@rep)) # repacked repeats
        end
      when :com
        rep ||= 0 # default to last

        if (one = row[fld] ||= "").empty?
          one << @rep * (rep - 1) if rep > 1
          one << @com * (com - 1) if com > 1
          one << val.join(@com)
        else
          ufr = one.split(@rep, -1) # unpacked repeats
          pad = rep - ufr.size
          pad = 1 if new_rep || rep == 0 && ufr.empty?
          pad.times { ufr.push("") } if pad > 0

          if (two = ufr[rep - 1] ||= "").empty?
            two << @com * (com - 1) if com > 1
            two << val.join(@com)
          else
            ucr = two.split(@com, -1) # unpacked components
            pad = com - ucr.size
            pad.times { ucr.push("") } if pad > 0
            ucr[com - 1, val.size] = val
            two.replace(ucr.join(@com)) # repacked components
          end
          one.replace(ufr.join(@rep)) # repacked repeats
        end
      end
    end

    # enforce ISA field widths
    isa_widths(row) if seg =~ /isa/i

    nil
  end
end

#each(seg = nil) ⇒ Object



371
372
373
374
375
376
# File 'lib/x12-lite.rb', line 371

def each(seg=nil)
  to_a.each do |row|
    next if seg && !(seg === row.first)
    yield row
  end
end

#each!Object

means this each may change @ary, so clear @str in case



379
380
381
382
383
# File 'lib/x12-lite.rb', line 379

def each!(...)
  out = each(...)
  @str &&= nil
  out
end

#grep(seg) ⇒ Object



385
386
387
388
389
390
# File 'lib/x12-lite.rb', line 385

def grep(seg)
  reduce([]) do |ary, row|
    ary.push(block_given? ? yield(row) : row) if seg === row.first
    ary
  end
end

#isa_widths(row) ⇒ Object



209
210
211
212
213
214
# File 'lib/x12-lite.rb', line 209

def isa_widths(row)
  row.each_with_index do |was, i|
    len = LEN[i]
    was.replace(was.ljust(len)[...len]) if was && len && was.size != len
  end
end

#isa_widths!(str) ⇒ Object



216
217
218
219
# File 'lib/x12-lite.rb', line 216

def isa_widths!(str)
  sep = str[3] or return str
  isa_widths(str.split(sep)).join(sep)
end

#normalize(obj) ⇒ Object



194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/x12-lite.rb', line 194

def normalize(obj)
  if Array === obj
    obj.each_with_index do |elt, i|
      str = (String === elt) ? elt : (obj[i] = elt.to_s)
      str.upcase!
      str.gsub!(@bad, ' ')
    end
  else
    str = (String === obj) ? obj : obj.to_s
    str.upcase!
    str.gsub!(@bad, ' ')
    str
  end
end

#rawObject



146
147
148
# File 'lib/x12-lite.rb', line 146

def raw
  to_s.delete("\n").upcase #!# NOTE: Fix these... should all sets be checked?
end

#regex_chars(ary, invert = false) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/x12-lite.rb', line 108

def regex_chars(ary, invert=false)
  chrs = ary.sort.uniq # ordered list of given characters
            .chunk_while {|prev, curr| curr.ord == prev.ord + 1 } # find runs
            .map do |chunk| # build character ranges
              (chunk.length > 1 ? [chunk.first, chunk.last] : [chunk.first])
              .map {|chr| "^[]-\\".include?(chr) ? Regexp.escape(chr) : chr }
              .join("-")
            end
            .join # join ranges together
            .prepend("[#{'^' if invert}") # invert or not
            .concat("]") # reject these character ranges
  /#{chrs}/ # return as a regex
end

#regex_chars!(ary, invert = true) ⇒ Object



122
123
124
# File 'lib/x12-lite.rb', line 122

def regex_chars!(ary, invert=true)
  regex_chars(ary, invert)
end

#show(*opts) ⇒ Object



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/x12-lite.rb', line 155

def show(*opts)
  full = opts.include?(:full) # show body at top
  deep = opts.include?(:deep) # dive into repeats
  down = opts.include?(:down) # show segments in lowercase
  list = opts.include?(:list) # give back a list or print it
  hide = opts.include?(:hide) # hide output
  only = opts.include?(:only) # only show first of each segment type
  left = opts.grep(Integer).first || 15 # left justify size

  out = full ? [to_s] : []

  unless hide
    out << "" if full
    nums = Hash.new(0)
    segs = to_a
    segs.each_with_index do |flds, i|
      seg = down ? flds.first.downcase : flds.first.upcase
      num = (nums[seg] += 1)
      flds.each_with_index do |fld, j|
        next if !fld || fld.empty? || j == 0
        if deep
          reps = fld.split(@rep)
          if reps.size > 1
            reps.each_with_index do |set, k|
              tag = "#{seg}%s-#{j}(#{k + 1})" % [num > 1 && !only ? "(#{num})" : ""]
              out << (tag.ljust(left) + set)
            end
            next
          end
        end
        tag = "#{seg}%s-#{j}" % [num > 1 && !only ? "(#{num})" : ""]
        out << (tag.ljust(left) + ansi(fld, "fff", "369"))
      end
    end
  end

  list ? out : (puts out)
end

#show!Object



150
151
152
153
# File 'lib/x12-lite.rb', line 150

def show!
  to_a.each {|r| puts ansi(r.inspect, "fff", "369") }
  self
end

#to_aObject



126
127
128
# File 'lib/x12-lite.rb', line 126

def to_a
  @ary ||= @str.strip.split(/[#{Regexp.escape(@seg)}\r\n]+/).map {|str| str.split(@fld, -1)}
end

#to_a!Object



130
131
132
133
134
# File 'lib/x12-lite.rb', line 130

def to_a!
  to_a
  @str = nil
  @ary
end

#to_sObject



136
137
138
# File 'lib/x12-lite.rb', line 136

def to_s
  @str ||= @ary.inject("") {|str, seg| str << seg.join(@fld) << "#{@seg}\n"}.chomp
end

#to_s!Object



140
141
142
143
144
# File 'lib/x12-lite.rb', line 140

def to_s!
  to_s
  @ary = nil
  @str
end

#update(*etc) ⇒ Object



360
361
362
363
364
365
366
367
368
369
# File 'lib/x12-lite.rb', line 360

def update(*etc)
  etc = etc.first if etc.size == 1
  case etc
  when nil
  when Array then etc.each_slice(2) {|pos, val| data(pos, val) if val }
  when Hash  then etc.each          {|pos, val| data(pos, val) if val }
  else raise "unable to update X12 objects with #{etc.class} types"
  end
  self
end