x12-lite 0

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 (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: []