x12-lite 0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (7) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE +21 -0
  4. data/README.md +11 -0
  5. data/lib/x12-lite.rb +858 -0
  6. data/x12-lite.gemspec +15 -0
  7. metadata +47 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: beb279f2d650c4542ef4e8d1c1c7776bd186f60fa68285b7b3cdba327b8dda6f
4
+ data.tar.gz: 68a9267df83e0a11f3f27aafde4d6215e710d758a15b80ff37ddf3db0674f8ee
5
+ SHA512:
6
+ metadata.gz: b781293ca7d307daa74db041c84beed21cd73db1ed9890674611c3ed6997d34f05e196a0d7f96d281ee65016b6526302f0de1e1ac59629548fe7bf16b50eddb8
7
+ data.tar.gz: 61dcd151d94c6e109e10a1053038a21ac5801b896035e951e88d6a0096ed0a29e4ee96c5b9ba94863c2182231d0c24512076bcc5c6f500d28fd3849449a12e85
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Steve Shreeve
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,11 @@
1
+ # x12
2
+ Ruby gem to parse and generate X.12 transactions
3
+
4
+ ### Example
5
+
6
+ ```
7
+ x12 = X12.new
8
+ x12["seg-2(5).2"] = "a:b"
9
+
10
+ puts x12
11
+ ```
data/lib/x12-lite.rb ADDED
@@ -0,0 +1,858 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # ==============================================================================
4
+ # x12-lite.rb: X12 library for Ruby
5
+ #
6
+ # Author: Steve Shreeve <steve.shreeve@trusthealth.com>
7
+ # Date: October 7, 2024
8
+ #
9
+ # Legal: All rights reserved.
10
+ # ==============================================================================
11
+
12
+ require "enumerator"
13
+ require "find"
14
+
15
+ class Object
16
+ def blank?
17
+ respond_to?(:empty?) or return !self
18
+ empty? or respond_to?(:strip) && strip.empty?
19
+ end unless defined? blank?
20
+ end
21
+
22
+ # ==[ ANSI colors ]=============================================================
23
+
24
+ def hex(str=nil)
25
+ ($hex ||= {})[str] ||= begin
26
+ str =~ /\A#?(?:(\h\h)(\h\h)(\h\h)|(\h)(\h)(\h))\z/ or return
27
+ r, g, b = $1 ? [$1, $2, $3] : [$4*2, $5*2, $6*2]
28
+ [r.hex, g.hex, b.hex] * ";"
29
+ end
30
+ end
31
+
32
+ def fg(rgb=nil); rgb ? "\e[38;2;#{hex(rgb)}m" : "\e[39m"; end
33
+ def bg(rgb=nil); rgb ? "\e[48;2;#{hex(rgb)}m" : "\e[49m"; end
34
+
35
+ def ansi(str, f=nil, b=nil)
36
+ [
37
+ (fg(f) if f),
38
+ (bg(b) if b),
39
+ str,
40
+ (bg if b),
41
+ (fg if f),
42
+ ].compact.join
43
+ end
44
+
45
+ # ==[ X12 ]=====================================================================
46
+
47
+ class X12
48
+ VERSION="0.2.0"
49
+
50
+ include Enumerable
51
+
52
+ # ISA field widths
53
+ LEN = [3, 2, 10, 2, 10, 2, 15, 2, 15, 6, 4, 1, 5, 9, 1, 1]
54
+
55
+ # Basic Character Set (also adds '#' from the Extended Character Set)
56
+ BCS = <<~'end'.gsub(/\s+/, '').concat(' ').split('')
57
+ 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
58
+ 0 1 2 3 4 5 6 7 8 9
59
+ ! " # & ' ( ) * + , - . / : ; = ?
60
+ end
61
+
62
+ # delimiter pos chr
63
+ # ---------- --- ---
64
+ # field 4 (*)
65
+ # composite 105 (:)
66
+ # repetition 83 (^)
67
+ # segment 106 (~)
68
+
69
+ def initialize(obj=nil, *etc)
70
+ if obj.is_a?(String) && !etc.empty?
71
+ obj = etc.dup.unshift(obj)
72
+ elsif obj
73
+ obj = obj.dup # does this need to be a deep clone?
74
+ end
75
+ case obj
76
+ when nil
77
+ when String then @str = obj unless obj.empty?
78
+ when Array
79
+ when Hash
80
+ when IO then @str = obj = obj.read
81
+ when X12 then @str = obj.to_s
82
+ else raise "unable to handle #{arg.class} objects"
83
+ end
84
+ @str ||= isa_widths!("ISA*00**00**ZZ**ZZ****^*00501**0*P*:~")
85
+ @str =~ /\AISA(.).{78}(.).{21}(.)(.)/ or raise "malformed X12"
86
+ @fld, @com, @rep, @seg = $~.captures.values_at(0, 2, 1, 3)
87
+ @rep = "^" if @rep == "U"
88
+ @sep = [@fld, @com, @rep, @seg]
89
+ @bad = regex_chars!(BCS + @sep) # invalid in txn bytes #!# BARELY USED NOW???
90
+ @chr = regex_chars!(BCS - @sep) # invalid in user data #!# NOT USED RIGHT NOW
91
+ case obj
92
+ when String, nil then to_a; @str = nil
93
+ when Array then set(obj.shift, obj.shift) until obj.empty?
94
+ when Hash then obj.each {|k, v| set(k, v) unless v.nil?}
95
+ end
96
+ to_s unless @str
97
+ end
98
+
99
+ def self.load(file)
100
+ str = File.open(file, "r:bom|utf-8", &:read) rescue "unreadable file"
101
+ new(str)
102
+ end
103
+
104
+ def self.[](*args)
105
+ new(*args)
106
+ end
107
+
108
+ def regex_chars(ary, invert=false)
109
+ chrs = ary.sort.uniq # ordered list of given characters
110
+ .chunk_while {|prev, curr| curr.ord == prev.ord + 1 } # find runs
111
+ .map do |chunk| # build character ranges
112
+ (chunk.length > 1 ? [chunk.first, chunk.last] : [chunk.first])
113
+ .map {|chr| "^[]-\\".include?(chr) ? Regexp.escape(chr) : chr }
114
+ .join("-")
115
+ end
116
+ .join # join ranges together
117
+ .prepend("[#{'^' if invert}") # invert or not
118
+ .concat("]") # reject these character ranges
119
+ /#{chrs}/ # return as a regex
120
+ end
121
+
122
+ def regex_chars!(ary, invert=true)
123
+ regex_chars(ary, invert)
124
+ end
125
+
126
+ def to_a
127
+ @ary ||= @str.strip.split(/[#{Regexp.escape(@seg)}\r\n]+/).map {|str| str.split(@fld, -1)}
128
+ end
129
+
130
+ def to_a!
131
+ to_a
132
+ @str = nil
133
+ @ary
134
+ end
135
+
136
+ def to_s
137
+ @str ||= @ary.inject("") {|str, seg| str << seg.join(@fld) << "#{@seg}\n"}.chomp
138
+ end
139
+
140
+ def to_s!
141
+ to_s
142
+ @ary = nil
143
+ @str
144
+ end
145
+
146
+ def raw
147
+ to_s.delete("\n").upcase #!# NOTE: Fix these... should all sets be checked?
148
+ end
149
+
150
+ def show!
151
+ to_a.each {|r| puts ansi(r.inspect, "fff", "369") }
152
+ self
153
+ end
154
+
155
+ def show(*opts)
156
+ full = opts.include?(:full) # show body at top
157
+ deep = opts.include?(:deep) # dive into repeats
158
+ down = opts.include?(:down) # show segments in lowercase
159
+ list = opts.include?(:list) # give back a list or print it
160
+ hide = opts.include?(:hide) # hide output
161
+ only = opts.include?(:only) # only show first of each segment type
162
+ left = opts.grep(Integer).first || 15 # left justify size
163
+
164
+ out = full ? [to_s] : []
165
+
166
+ unless hide
167
+ out << "" if full
168
+ nums = Hash.new(0)
169
+ segs = to_a
170
+ segs.each_with_index do |flds, i|
171
+ seg = down ? flds.first.downcase : flds.first.upcase
172
+ num = (nums[seg] += 1)
173
+ flds.each_with_index do |fld, j|
174
+ next if !fld || fld.empty? || j == 0
175
+ if deep
176
+ reps = fld.split(@rep)
177
+ if reps.size > 1
178
+ reps.each_with_index do |set, k|
179
+ tag = "#{seg}%s-#{j}(#{k + 1})" % [num > 1 && !only ? "(#{num})" : ""]
180
+ out << (tag.ljust(left) + set)
181
+ end
182
+ next
183
+ end
184
+ end
185
+ tag = "#{seg}%s-#{j}" % [num > 1 && !only ? "(#{num})" : ""]
186
+ out << (tag.ljust(left) + wrap(fld, "fff", "369"))
187
+ end
188
+ end
189
+ end
190
+
191
+ list ? out : (puts out)
192
+ end
193
+
194
+ def normalize(obj)
195
+ if Array === obj
196
+ obj.each_with_index do |elt, i|
197
+ str = (String === elt) ? elt : (obj[i] = elt.to_s)
198
+ str.upcase!
199
+ str.gsub!(@bad, ' ')
200
+ end
201
+ else
202
+ str = (String === obj) ? obj : obj.to_s
203
+ str.upcase!
204
+ str.gsub!(@bad, ' ')
205
+ str
206
+ end
207
+ end
208
+
209
+ def isa_widths(row)
210
+ row.each_with_index do |was, i|
211
+ len = LEN[i]
212
+ was.replace(was.ljust(len)[...len]) if was && len && was.size != len
213
+ end
214
+ end
215
+
216
+ def isa_widths!(str)
217
+ sep = str[3] or return str
218
+ isa_widths(str.split(sep)).join(sep)
219
+ end
220
+
221
+ def data(*args)
222
+ len = args.size; return update(*args) if len > 2
223
+ pos = args[0] or return @str
224
+ val = args[1]
225
+
226
+ # Syntax: seg(num)-fld(rep).com
227
+ pos =~ /^(..[^-.(]?)?(?:\((\d*|[+!?*]?)\))?[-.]?(\d+)?(?:\((\d*|[+!?*]?)\))?[-.]?(\d+)?$/
228
+ seg = $1 or return ""; want = /^#{seg}[^#{Regexp.escape(@seg)}\r\n]*/i
229
+ num = $2 && $2.to_i; new_num = $2 == "+"; ask_num = $2 == "?"; all_num = $2 == "*"
230
+ rep = $4 && $4.to_i; new_rep = $4 == "+"; ask_rep = $4 == "?"; all_rep = $4 == "*"
231
+ fld = $3 && $3.to_i; len > 1 && fld == 0 and raise "zero index on field"
232
+ com = $5 && $5.to_i; len > 1 && com == 0 and raise "zero index on component"
233
+
234
+ # NOTE: When doing a get, a missing num or rep means get the first
235
+ # NOTE: When doing a set, a missing num or rep means set the last
236
+ # NOTE: ask_num and ask_rep are mutually exclusive, how should we handle?
237
+ # NOTE: all_num and all_rep are mutually exclusive, how should we handle?
238
+ # NOTE: ask_* is only for get
239
+ # NOTE: all_* is only for get and set [is this correct?]
240
+
241
+ if len == 1 # get
242
+ to_s unless @str
243
+ return @str.scan(want).size if ask_num && !ask_rep
244
+ return @str.scan(want).inject([]) do |ary, out|
245
+ out = loop do
246
+ out = out.split(@fld)[fld ] or break if fld
247
+ break out.split(@rep).size if ask_rep
248
+ out = out.split(@rep)[rep - 1] or break if rep || (com && (rep ||= 1))
249
+ out = out.split(@com)[com - 1] or break if com
250
+ break out
251
+ end
252
+ ary << out if out
253
+ ary
254
+ end if all_num
255
+ out = @str.scan(want)[num - 1] or return "" if num ||= 1
256
+ out = out.split(@fld)[fld ] or return "" if fld
257
+ return out.split(@rep).size if ask_rep
258
+ out = out.split(@rep)[rep - 1] or return "" if rep || (com && (rep ||= 1))
259
+ out = out.split(@com)[com - 1] or return "" if com
260
+ out
261
+ else # set
262
+ to_a unless @ary
263
+ @str &&= nil
264
+ our = @ary.select {|now| now[0] =~ want}
265
+ unless all_num
266
+ num ||= 0 # default to last
267
+ row = our[num - 1] or pad = num - our.size
268
+ pad = 1 if (num == 0 && our.size == 0 || new_num)
269
+ pad and pad.times { @ary.push(row = [seg.upcase]) }
270
+ val = our.size + pad if new_num && val == :num # auto-number
271
+ our = [row]
272
+ end
273
+
274
+ # prepare the source and decide how to update
275
+ val ||= ""
276
+ how = case
277
+ when !rep && !com # replace fields
278
+ val = val.join(@fld) if Array === val
279
+ val = val.to_s.split(@fld, -1)
280
+ :fld
281
+ when fld && rep && !com # replace repeats
282
+ val = val.join(@rep) if Array === val
283
+ val.include?(@fld) and raise "invalid separator for repeats"
284
+ val = val.to_s.split(@rep, -1)
285
+ :rep
286
+ when fld && com # replace components
287
+ val = val.join(@com) if Array === val
288
+ val.include?(@fld) and raise "invalid separator for repeats"
289
+ val.include?(@rep) and raise "invalid separator for repeats"
290
+ val = val.to_s.split(@com, -1)
291
+ :com
292
+ end or raise "invalid fld/rep/com: #{[fld, rep, com].inspect}"
293
+ val = [""] if val.empty?
294
+
295
+ #!# TODO: val.dup to prevent sharing issues???
296
+
297
+ # replace the target
298
+ our.each do |row|
299
+ case how
300
+ when :fld
301
+ if fld
302
+ pad = fld - row.size
303
+ pad.times { row.push("") } if pad > 0
304
+ row[fld, val.size] = val
305
+ else
306
+ row[1..-1] = val
307
+ end
308
+ when :rep
309
+ if (was = row[fld] ||= "").empty?
310
+ was << @rep * (rep - 1) if rep > 1
311
+ was << val.join(@rep)
312
+ else
313
+ ufr = was.split(@rep, -1) # unpacked repeats
314
+ pad = rep - ufr.size
315
+ pad = 1 if new_rep || rep == 0 && ufr.empty?
316
+ pad.times { ufr.push("") } if pad > 0
317
+ ufr[rep - 1, val.size] = val
318
+ was.replace(ufr.join(@rep)) # repacked repeats
319
+ end
320
+ when :com
321
+ rep ||= 0 # default to last
322
+
323
+ if (one = row[fld] ||= "").empty?
324
+ one << @rep * (rep - 1) if rep > 1
325
+ one << @com * (com - 1) if com > 1
326
+ one << val.join(@com)
327
+ else
328
+ ufr = one.split(@rep, -1) # unpacked repeats
329
+ pad = rep - ufr.size
330
+ pad = 1 if new_rep || rep == 0 && ufr.empty?
331
+ pad.times { ufr.push("") } if pad > 0
332
+
333
+ if (two = ufr[rep - 1] ||= "").empty?
334
+ two << @com * (com - 1) if com > 1
335
+ two << val.join(@com)
336
+ else
337
+ ucr = two.split(@com, -1) # unpacked components
338
+ pad = com - ucr.size
339
+ pad.times { ucr.push("") } if pad > 0
340
+ ucr[com - 1, val.size] = val
341
+ two.replace(ucr.join(@com)) # repacked components
342
+ end
343
+ one.replace(ufr.join(@rep)) # repacked repeats
344
+ end
345
+ end
346
+ end
347
+
348
+ # enforce ISA field widths
349
+ isa_widths(row) if seg =~ /isa/i
350
+
351
+ nil
352
+ end
353
+ end
354
+
355
+ alias_method :get, :data
356
+ alias_method :set, :data
357
+ alias_method :[], :data
358
+ alias_method :[]=, :data
359
+
360
+ def update(*etc)
361
+ etc = etc.first if etc.size == 1
362
+ case etc
363
+ when nil
364
+ when Array then etc.each_slice(2) {|pos, val| data(pos, val) if val }
365
+ when Hash then etc.each {|pos, val| data(pos, val) if val }
366
+ else raise "unable to update X12 objects with #{etc.class} types"
367
+ end
368
+ self
369
+ end
370
+
371
+ # def each(seg=nil)
372
+ # to_a.each do |item|
373
+ # next if seg && !(seg === item[0])
374
+ # yield item
375
+ # end
376
+ # end
377
+ #
378
+ # # means this each may change @ary, so clear @str in case
379
+ # def each!(...)
380
+ # out = each(...)
381
+ # @str &&= nil
382
+ # out
383
+ # end
384
+ #
385
+ # def grep(who)
386
+ # inject([]) do |ary, row|
387
+ # ary.push(block_given? ? yield(row) : row) if who === row.first
388
+ # ary
389
+ # end
390
+ # end
391
+ #
392
+ # def now(fmt="%Y%m%d%H%M%S")
393
+ # Time.now.strftime(fmt)
394
+ # end
395
+ #
396
+ # def guid
397
+ # ("%9.6f" % Time.now.to_f).to_s.sub(".", "")
398
+ # end
399
+ #
400
+ # def each_pair
401
+ # nums = Hash.new(0)
402
+ # segs = to_a
403
+ # segs.each_with_index do |flds, i|
404
+ # seg = flds.first.downcase
405
+ # num = nums[seg] += 1
406
+ # msh = seg == "msh"
407
+ # adj = msh ? 1 : 0
408
+ # flds.each_with_index do |fld, j|
409
+ # next if !fld || fld.empty? || j == 0
410
+ # if !msh and (reps = fld.split(@rep)).size > 1
411
+ # reps.each_with_index do |set, k|
412
+ # tag = "#{seg}%s-#{j + adj}(#{k + 1})" % [num > 1 ? "(#{num})" : ""]
413
+ # yield(tag, set)
414
+ # end
415
+ # next
416
+ # else
417
+ # tag = "#{seg}%s-#{j + adj}" % [num > 1 ? "(#{num})" : ""]
418
+ # yield(tag, fld)
419
+ # end
420
+ # end
421
+ # end
422
+ # end
423
+ #
424
+ # def to_pairs
425
+ # ary = []
426
+ # saw = Hash.new(0)
427
+ #
428
+ # to_a.each_with_index do |row, i|
429
+ # seg = row.first.downcase
430
+ # num = saw[seg] += 1
431
+ # msh = seg.upcase == "MSH"
432
+ # adj = msh ? 1 : 0
433
+ # row.each_with_index do |val, j|
434
+ # next if val.blank? || j == 0
435
+ # tag = "#{seg}%s-#{j + adj}" % [num > 1 ? "(#{num})" : ""]
436
+ # if !msh && val.include?(@rep)
437
+ # val.split(@rep).each_with_index do |val, k|
438
+ # ary << [tag + "(#{k + 1})", val] unless val.blank?
439
+ # end
440
+ # else
441
+ # ary << [tag, val]
442
+ # end
443
+ # end
444
+ # end
445
+ #
446
+ # ary
447
+ # end
448
+ #
449
+ # def pluck(row, *ask)
450
+ # return if ask.empty?
451
+ #
452
+ # str = (String === row) ? row : row.join(@fld)
453
+ # say = []
454
+ #
455
+ # msh = str =~ /^MSH\b/i # is this an MSH segment?
456
+ #
457
+ # ask.each do |pos|
458
+ # say.push(nil) && next if pos.nil?
459
+ # pos = pos.to_s unless pos.is_a?(String)
460
+ # pos =~ /^([A-Z]..)?(?:\((\d*)\))?[-.]?(\d+)?(?:\((\d*)\))?[-.]?(\d+)?[-.]?(\d+)?$/i
461
+ # seg = $1 && $1.upcase; raise "asked for a segment of #{seg}, but given #{str}" if (seg && seg != str[0, seg.size].upcase)
462
+ # num = $2 && $2.to_i; # this will be ignored
463
+ # fld = $3 && $3.to_i
464
+ # rep = $4 && $4.to_i
465
+ # com = $5 && $5.to_i
466
+ # sub = $6 && $6.to_i
467
+ #
468
+ # fld -= 1 if msh && fld # MSH fields are offset by one
469
+ #
470
+ # out = str.dup
471
+ # out = out.split(@fld)[fld ] if out && fld
472
+ # out = out.split(@rep)[rep - 1] if out && rep || (com && (rep ||= 1)) # default to first
473
+ # out = out.split(@com)[com - 1] if out && com
474
+ # out = out.split(@sub)[sub - 1] if out && sub
475
+ # say << (out || "")
476
+ # end
477
+ #
478
+ # say.size > 1 ? say : say.first
479
+ # end
480
+ #
481
+ # def populate(hash, want)
482
+ # list = find(*want.values)
483
+ # keys = want.keys
484
+ # keys.size == list.size or raise "mismatch (#{keys.size} keys, but #{list.size} values)"
485
+ # keys.each {|item| hash[item] = list.shift }
486
+ # hash
487
+ # end
488
+ #
489
+ # def glean(want, *rest)
490
+ # row = rest.pop if Array === rest.last || Hash === rest.last
491
+ # want = rest.unshift(want) if String === want || Numeric === want
492
+ # want, row = row, want if Array === want && Hash === row
493
+ #
494
+ # case row
495
+ # when Array
496
+ # case want
497
+ # when String, Array
498
+ # vals = pluck(row, *want)
499
+ # when Hash
500
+ # keys = want.keys
501
+ # vals = pluck(row, *want.values)
502
+ # hash = keys.zip(vals).to_h
503
+ # else raise "unable to glean X12 segments with #{want.class} types"
504
+ # end
505
+ # when nil
506
+ # case want
507
+ # when String, Array
508
+ # vals = find(*want)
509
+ # when Hash
510
+ # keys = want.keys
511
+ # vals = Array(find(*want.values)) # ensure we get an array
512
+ # hash = keys.zip(vals).to_h
513
+ # else raise "unable to glean X12 segments with #{want.class} types"
514
+ # end
515
+ # else raise "unable to glean X12 objects from #{row.class} types"
516
+ # end
517
+ # end
518
+ #
519
+ # # NOTE: this could be merged with grep()
520
+ # def slice(who, *ask)
521
+ # return if ask.empty?
522
+ #
523
+ # all = ask.map do |pos|
524
+ # pos.to_s =~ /^([A-Z]..)?(?:\((\d*)\))?[-.]?(\d+)?(?:\((\d*)\))?[-.]?(\d+)?[-.]?(\d+)?$/i
525
+ # seg = $1 && $1.upcase
526
+ # num = $2 && $2.to_i; # this will be ignored
527
+ # fld = $3 && $3.to_i or raise "invalid field specifier in '#{pos}'"
528
+ # rep = $4 && $4.to_i
529
+ # com = $5 && $5.to_i
530
+ # sub = $6 && $6.to_i
531
+ # [seg, num, fld, rep, com, sub]
532
+ # end
533
+ #
534
+ # grep(who).map do |row|
535
+ # str ||= row[0]
536
+ # msh ||= str == "msh" # is this an MSH segment?
537
+ # all.inject([]) do |ary, (seg, num, fld, rep, com, sub)|
538
+ # raise "scanning #{str} segments, but asked for #{seg}" if (seg && seg != str)
539
+ # out = row[msh ? fld - 1 : fld].dup
540
+ # out = out.split(@rep)[rep - 1] if out && rep || (com && (rep ||= 1)) # default to first
541
+ # out = out.split(@com)[com - 1] if out && com
542
+ # out = out.split(@sub)[sub - 1] if out && sub
543
+ # ary << (out || "")
544
+ # end
545
+ # end
546
+ # end
547
+ #
548
+ # def find(*ask)
549
+ # return if ask.empty?
550
+ #
551
+ # str = to_s
552
+ # say = []
553
+ #
554
+ # seg = nil
555
+ #
556
+ # ask.each do |pos|
557
+ # say.push(nil) && next if pos.nil?
558
+ # pos = pos.to_s unless pos.is_a?(String)
559
+ # pos =~ /^([A-Z]..)?(?:\((\d*|\?|\*)\))?[-.]?(\d+)?(?:\((\d*|\?|\*)\))?[-.]?(\d+)?[-.]?(\d+)?$/i or raise "bad query #{pos.inspect}"
560
+ # seg = $1 if $1; want = /^#{seg}.*/i
561
+ # num = $2 && $2.to_i; ask_num = $2 == "?"; all_num = $2 == "*"
562
+ # rep = $4 && $4.to_i; ask_rep = $4 == "?"; all_rep = $4 == "*"
563
+ # fld = $3 && $3.to_i
564
+ # com = $5 && $5.to_i
565
+ # sub = $6 && $6.to_i
566
+ #
567
+ # # p [seg, num, rep, fld, com, sub]
568
+ #
569
+ # msh = seg =~ /^MSH$/i # is this an MSH segment?
570
+ # fld -= 1 if msh && fld # MSH fields are offset by one
571
+ #
572
+ # if all_num
573
+ # raise "multi query allows only one selector" if ask.size > 1
574
+ # return str.scan(want).inject([]) do |ary, out|
575
+ # out = loop do
576
+ # out = out.split(@fld)[fld ] or break if fld
577
+ # break out.split(@rep).size if ask_rep
578
+ # out = out.split(@rep)[rep - 1] or break if rep || (com && (rep ||= 1)) # default to first
579
+ # out = out.split(@com)[com - 1] or break if com
580
+ # out = out.split(@sub)[sub - 1] or break if sub
581
+ # break out
582
+ # end
583
+ # ary << out if out
584
+ # ary
585
+ # end
586
+ # end
587
+ #
588
+ # say << loop do
589
+ # out = ""
590
+ # break str.scan( want).size if ask_num && !ask_rep
591
+ # out = str.scan( want)[num - 1] or break "" if num ||= 1 # default to first
592
+ # out = out.split(@fld)[fld ] or break "" if fld
593
+ # break out.split(@rep).size if ask_rep
594
+ # out = out.split(@rep)[rep - 1] or break "" if rep || (com && (rep ||= 1)) # default to first
595
+ # out = out.split(@com)[com - 1] or break "" if com
596
+ # out = out.split(@sub)[sub - 1] or break "" if sub
597
+ # break out
598
+ # end
599
+ # end
600
+ #
601
+ # say.size > 1 ? say : say.first
602
+ # end
603
+ end
604
+
605
+ if __FILE__ == $0
606
+
607
+ require "optparse"
608
+
609
+ trap("INT" ) { abort "\n" }
610
+ trap("PIPE") { abort "\n" } rescue nil
611
+
612
+ opts = {
613
+ # "lower" => true,
614
+ # "ignore" => true,
615
+ # "message" => true,
616
+ # "spacer" => true,
617
+ }
618
+
619
+ OptionParser.new.instance_eval do
620
+ @banner = "usage: #{program_name} [options] <file> <file> ..."
621
+
622
+ on "-a", "--after <date>" , "After (date as 'YYYYMMDD' or time as 'YYYYMMDD HHMMSS')"
623
+ on "-c", "--count" , "Count messages at the end"
624
+ on "-d", "--dive" , "Dive into directories recursively"
625
+ # on "--delim <char>" , "Delimiter to use"
626
+ # on "-f", "--fields" , "Show fields"
627
+ # on "-F", "--fields-only" , "Show fields only, not repeat indicators"
628
+ on "-h", "--help" , "Show help and command usage" do Kernel.abort to_s; end
629
+ on "-i", "--ignore" , "Ignore malformed X12 files"
630
+ on "-l", "--lower" , "Show segment names in lowercase"
631
+ on "-m", "--message" , "Show message body"
632
+ on "-p", "--path" , "Show path for each message"
633
+ # on "-q", "--query <value>" , "Query a specific value"
634
+ # on "-r", "--repeats" , "Show field repeats on their own line"
635
+ on "-s", "--spacer" , "Show an empty line between messages"
636
+ # on "-t", "--tsv" , "Tab-delimit output (tsv format)"
637
+ # on "-w", "--width <width>", "Width of segment names", Integer
638
+
639
+ Kernel.abort to_s if ARGV.empty?
640
+ self
641
+ end.parse!(into: opts) rescue abort($!.message)
642
+
643
+ opts.transform_keys!(&:to_s) # stringify keys
644
+
645
+ require "time" if opts["after"]
646
+
647
+ time = Time.parse(opts["after"]) if opts["after"]
648
+ dive = opts["dive"]
649
+ # only = opts["fields-only"] and opts["fields"] = true
650
+ # quer = opts["query"].split(',').map(&:strip) if opts["query"]
651
+ # from = quer.delete("-") if quer.is_a?(Array)
652
+ # delm = opts["tsv"] ? "\t" : (opts["delim"] || "|")
653
+ # delm = {"\\n" => "\n", "\\t" => "\t"}[delm] || delm
654
+
655
+ args = []
656
+ # args << :deep if opts["repeats"]
657
+ args << :down if opts["lower"]
658
+ args << :full if opts["message"] || opts.empty?
659
+ # args << :hide if !opts["fields"]
660
+ # args << :only if only
661
+ # args << (opts["width"].to_i.between?(1, 50) ? opts["width"].to_i : 12) if opts["width"]
662
+
663
+ msgs = 0
664
+
665
+ list = []
666
+ ARGV.push(".") if dive && ARGV.empty?
667
+ ARGV.each do |path|
668
+ if File.directory?(path)
669
+ ours = []
670
+ if dive
671
+ Find.find(path) do |item|
672
+ if File.file?(item)
673
+ if time
674
+ ours << item if File.mtime(item) > time
675
+ else
676
+ ours << item
677
+ end
678
+ end
679
+ end
680
+ else
681
+ Dir[File.join(path, "*")].each do |item|
682
+ if File.file?(item)
683
+ if time
684
+ ours << item if File.mtime(item) > time
685
+ else
686
+ ours << item
687
+ end
688
+ end
689
+ end
690
+ end
691
+ list.concat(ours.sort!)
692
+ elsif File.file?(path)
693
+ if time
694
+ list << path if File.mtime(path) > time
695
+ else
696
+ list << path
697
+ end
698
+ else
699
+ warn "WARNING: unknown item in list: #{path.inspect}"
700
+ next
701
+ end
702
+ end
703
+
704
+ list.each do |file|
705
+ puts if opts["spacer"] && msgs > 0
706
+ if opts["path"]
707
+ # if quer && quer.size == 1
708
+ # print "#{file}:"
709
+ # else
710
+ puts "\n==[ #{file} ]==\n\n"
711
+ # end
712
+ end
713
+
714
+ begin
715
+ str = File.open(file, "r:bom|utf-8", &:read) rescue abort("ERROR: unable to read file: \"#{file}\"")
716
+ begin
717
+ x12 = X12.new(str)
718
+ rescue
719
+ abort "ERROR: malformed X12 file: \"#{file}\" (#{$!})" unless opts["ignore"]
720
+ next
721
+ end
722
+ # if quer
723
+ # hits = *x12.find(*quer)
724
+ # hits.unshift file if opts["path"] || from
725
+ # puts hits.join(delm)
726
+ # puts if opts["path"]
727
+ # next
728
+ # end
729
+ x12.show(*args)
730
+ msgs += 1
731
+ rescue => e
732
+ warn "WARNING: #{e.message}"
733
+ end
734
+ end
735
+
736
+ if opts["count"] && msgs > 0
737
+ puts "\nTotal messages: #{msgs}"
738
+ end
739
+ end
740
+
741
+ __END__
742
+
743
+ x12 = X12.new
744
+
745
+ # fields
746
+ x12["seg"] = nil
747
+ x12["seg"] = ""
748
+ x12["seg"] = "a"
749
+ x12["seg"] = "a*b"
750
+ x12["seg"] = "**c*d"
751
+ x12["seg"] = "**c:e^f*d"
752
+
753
+ x12["seg"] = [nil ]
754
+ x12["seg"] = ["" ]
755
+ x12["seg"] = ["","","","" ]
756
+ x12["seg"] = ["a" ]
757
+ x12["seg"] = ["a", "b" ]
758
+ x12["seg"] = ["", "", "c", "d" ]
759
+ x12["seg"] = ["", "", "c:e^f", "d"]
760
+ x12["seg"] = ["**c:e^f", "d"]
761
+
762
+ # repeats
763
+ x12["seg-2(3)"] = "^^c:e^^"
764
+ x12["seg-2(3)"] = ""
765
+ x12["seg-2(3)"] = nil
766
+ x12["seg-2(5)"] = nil
767
+ x12["seg-2(3)"] = "a"
768
+ x12["seg-2(3)"] = "a^b"
769
+ x12["seg-2(3)"] = "^^c^d"
770
+
771
+ x12["seg-2(3)"] = [nil ]
772
+ x12["seg-2(3)"] = ["" ]
773
+ x12["seg-2(3)"] = ["a" ]
774
+ x12["seg-2(3)"] = ["a", "b" ]
775
+ x12["seg-2(3)"] = ["", "", "c", "d" ]
776
+ x12["seg-2(3)"] = ["", "", "c:e^f", "d"]
777
+ x12["seg-2(3)"] = ["c:e^f", "d"]
778
+
779
+ # components
780
+ x12["seg-2(3).1"] = "c:e"
781
+ x12["seg-2(3).4"] = ""
782
+ x12["seg-2(3).1"] = nil
783
+ x12["seg-2(5).4"] = nil
784
+ x12["seg-2(3).1"] = "a"
785
+ x12["seg-2(5).2"] = "a:b"
786
+ x12["seg-2(5).4"] = "::c:d"
787
+
788
+ x12["seg-2.1"] = [nil ]
789
+ x12["seg-2.4"] = ["" ]
790
+ x12["seg-2.1"] = ["a" ]
791
+ x12["seg-2.4"] = ["a", "b" ]
792
+ x12["seg-2.1"] = ["", "", "c", "d" ]
793
+ x12["seg-2.2"] = ["", "", "c:e:f", "d"]
794
+ x12["seg-2.4"] = ["c:e:f", "d" ]
795
+
796
+ # p x12.to_a
797
+
798
+ __END__
799
+
800
+ position fld rep com
801
+ ======== === === ===
802
+
803
+ # fields
804
+ seg nil nil nil # replace fields, complete
805
+ seg-1 1 nil nil # replace fields, starting from field 1
806
+ seg-2 2 nil nil # replace fields, starting from field 2
807
+
808
+ # repeats
809
+ seg-1(1) 1 1 nil # replace repeats for field 1, starting from repeat 1
810
+ seg-1(3) 1 3 nil # replace repeats for field 1, starting from repeat 3
811
+ seg-2(1) 2 1 nil # replace repeats for field 2, starting from repeat 1
812
+ seg-2(3) 2 3 nil # replace repeats for field 2, starting from repeat 3
813
+
814
+ # components
815
+ seg-1.1 1 nil 1 # replace components for field 1, starting from component 1
816
+ seg-1.4 1 nil 4 # replace components for field 1, starting from component 4
817
+ seg-2.1 2 nil 1 # replace components for field 2, starting from component 1
818
+ seg-2.4 2 nil 4 # replace components for field 2, starting from component 4
819
+ seg-1(1).1 1 1 1 # replace components for field 1, repeat 1, starting from component 1
820
+ seg-1(1).4 1 1 4 # replace components for field 1, repeat 1, starting from component 4
821
+ seg-2(1).1 2 1 1 # replace components for field 2, repeat 1, starting from component 1
822
+ seg-2(1).4 2 1 4 # replace components for field 2, repeat 1, starting from component 4
823
+ seg-1(3).1 1 3 1 # replace components for field 1, repeat 3, starting from component 1
824
+ seg-1(3).4 1 3 4 # replace components for field 1, repeat 3, starting from component 4
825
+ seg-2(3).1 2 3 1 # replace components for field 2, repeat 3, starting from component 1
826
+ seg-2(3).4 2 3 4 # replace components for field 2, repeat 3, starting from component 4
827
+
828
+ x12 = X12.new [
829
+ "gs-2", "...",
830
+ "gs-8", "...",
831
+ "foo", "wow",
832
+ ]
833
+
834
+ x12 = X12.new <<~""
835
+ ISA*00* *00* *ZZ*HT009382-001 *ZZ*HT000004-001 *240626*0906*^*00501*000923871*0*P*:~
836
+ GS*HS*HT009382-001*HT000004-001*20240626*0906*923871*X*005010X279A1~
837
+
838
+ x12.show(:down)
839
+
840
+ __END__
841
+
842
+ class DefaultArray < Array
843
+ def initialize(default_value = nil)
844
+ @default_value = default_value
845
+ super()
846
+ end
847
+
848
+ def [](index)
849
+ self[index] = @default_value if index >= size
850
+ super
851
+ end
852
+ end
853
+
854
+ # Usage
855
+ arr = DefaultArray.new(0) # Create a new array with default value 0
856
+
857
+ puts arr[2] # => 0 (accessing non-existent element sets it to default 0)
858
+ puts arr.inspect # => [0, 0, 0] (array is now filled with default values)
data/x12-lite.gemspec ADDED
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "x12-lite"
5
+ s.version = `grep -m 1 '^\s*VERSION' lib/x12.rb | head -1 | cut -f 2 -d '"'`
6
+ s.author = "Steve Shreeve"
7
+ s.email = "steve.shreeve@gmail.com"
8
+ s.summary = "A " +
9
+ s.description = "Ruby gem to parse and generate X.12 transactions"
10
+ s.homepage = "https://github.com/shreeve/x12-lite"
11
+ s.license = "MIT"
12
+ s.platform = Gem::Platform::RUBY
13
+ s.files = `git ls-files`.split("\n") - %w[.gitignore]
14
+ s.required_ruby_version = Gem::Requirement.new(">= 3.0") if s.respond_to? :required_ruby_version=
15
+ end
metadata ADDED
@@ -0,0 +1,47 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: x12-lite
3
+ version: !ruby/object:Gem::Version
4
+ version: '0'
5
+ platform: ruby
6
+ authors:
7
+ - Steve Shreeve
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-10-07 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Ruby gem to parse and generate X.12 transactions
14
+ email: steve.shreeve@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - Gemfile
20
+ - LICENSE
21
+ - README.md
22
+ - lib/x12-lite.rb
23
+ - x12-lite.gemspec
24
+ homepage: https://github.com/shreeve/x12-lite
25
+ licenses:
26
+ - MIT
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: '3.0'
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubygems_version: 3.5.21
44
+ signing_key:
45
+ specification_version: 4
46
+ summary: A Ruby gem to parse and generate X.12 transactions
47
+ test_files: []