@rip-lang/x12 0.1.2
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.
- package/README.md +119 -0
- package/bin/rip-x12 +13 -0
- package/package.json +44 -0
- package/x12.rip +687 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/rip.png" style="width:50px" /> <br>
|
|
2
|
+
|
|
3
|
+
# Rip X12 - @rip-lang/x12
|
|
4
|
+
|
|
5
|
+
> **X12 EDI parser, editor, and query engine**
|
|
6
|
+
|
|
7
|
+
Parse, query, and edit X12 EDI transactions (270/271, 835, 837, etc.) with a path-based addressing system. Auto-detects field, repetition, component, and segment separators from the ISA header.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Install
|
|
13
|
+
bun add -g @rip-lang/x12
|
|
14
|
+
|
|
15
|
+
# Show fields
|
|
16
|
+
rip-x12 -f message.x12
|
|
17
|
+
|
|
18
|
+
# Query specific values
|
|
19
|
+
rip-x12 -q "ISA-6,GS-2" message.x12
|
|
20
|
+
|
|
21
|
+
# Show message body
|
|
22
|
+
rip-x12 -m message.x12
|
|
23
|
+
|
|
24
|
+
# Recursive directory scan
|
|
25
|
+
rip-x12 -d -f /path/to/edi/
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Library Usage
|
|
29
|
+
|
|
30
|
+
```coffee
|
|
31
|
+
import { X12 } from '@rip-lang/x12'
|
|
32
|
+
|
|
33
|
+
# Parse an X12 message
|
|
34
|
+
x12 = new X12 rawString
|
|
35
|
+
|
|
36
|
+
# Get values using path addressing
|
|
37
|
+
sender = x12.get "ISA-6" # ISA segment, field 6
|
|
38
|
+
receiver = x12.get "ISA-8" # ISA segment, field 8
|
|
39
|
+
code = x12.get "EB-1" # First EB segment, field 1
|
|
40
|
+
third = x12.get "EB(3)-4" # 3rd EB segment, field 4
|
|
41
|
+
comp = x12.get "EB(3)-4(2).1" # 3rd EB, field 4, repeat 2, component 1
|
|
42
|
+
count = x12.get "EB(?)" # Count of EB segments
|
|
43
|
+
|
|
44
|
+
# Set values
|
|
45
|
+
x12.set "ISA-6", "NEWSENDER"
|
|
46
|
+
x12.set "GS-2", "NEWID"
|
|
47
|
+
|
|
48
|
+
# Query multiple values at once
|
|
49
|
+
[sender, receiver] = x12.find "ISA-6", "ISA-8"
|
|
50
|
+
|
|
51
|
+
# Display formatted output
|
|
52
|
+
x12.show 'down', 'full' # lowercase segments, show body
|
|
53
|
+
|
|
54
|
+
# Iterate segments
|
|
55
|
+
x12.each (row) -> console.log row[0]
|
|
56
|
+
x12.each 'EB', (row) -> console.log row
|
|
57
|
+
|
|
58
|
+
# Get raw X12 string
|
|
59
|
+
output = x12.raw()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Path Addressing
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
seg(num)-fld(rep).com
|
|
66
|
+
|
|
67
|
+
seg — Segment name (2-3 chars): ISA, GS, EB, CLP, etc.
|
|
68
|
+
(num) — Segment occurrence: (1)=first, (3)=third, (?)=count, (*)=all, (+)=new
|
|
69
|
+
-fld — Field number (1-based)
|
|
70
|
+
(rep) — Repetition within field: (1)=first, (?)=count, (*)=all, (+)=new
|
|
71
|
+
.com — Component within repeat (1-based)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Examples
|
|
75
|
+
|
|
76
|
+
| Path | Meaning |
|
|
77
|
+
|------|---------|
|
|
78
|
+
| `ISA-6` | ISA segment, field 6 |
|
|
79
|
+
| `EB(3)-4` | 3rd EB segment, field 4 |
|
|
80
|
+
| `EB(*)-4` | Field 4 from ALL EB segments |
|
|
81
|
+
| `EB(?)-4` | COUNT of EB segments |
|
|
82
|
+
| `EB(3)-4(2)` | 3rd EB, field 4, 2nd repetition |
|
|
83
|
+
| `EB(3)-4(2).1` | 3rd EB, field 4, 2nd rep, 1st component |
|
|
84
|
+
|
|
85
|
+
## Separators
|
|
86
|
+
|
|
87
|
+
X12 uses four delimiter levels, auto-detected from the ISA header:
|
|
88
|
+
|
|
89
|
+
| Separator | ISA Position | Default | Purpose |
|
|
90
|
+
|-----------|-------------|---------|---------|
|
|
91
|
+
| Field | 4 | `*` | Separates fields within a segment |
|
|
92
|
+
| Repetition | 83 | `^` | Separates repeated values within a field |
|
|
93
|
+
| Component | 105 | `:` | Separates sub-components |
|
|
94
|
+
| Segment | 106 | `~` | Ends a segment |
|
|
95
|
+
|
|
96
|
+
## CLI Options
|
|
97
|
+
|
|
98
|
+
| Flag | Description |
|
|
99
|
+
|------|-------------|
|
|
100
|
+
| `-a, --after <date>` | Filter files modified after date (YYYYMMDD) |
|
|
101
|
+
| `--ansi` | ANSI color output |
|
|
102
|
+
| `-c, --count` | Count messages |
|
|
103
|
+
| `-d, --dive` | Recursive directory scan |
|
|
104
|
+
| `-f, --fields` | Show fields |
|
|
105
|
+
| `-F, --fields-only` | Fields only, no repeat indicators |
|
|
106
|
+
| `-h, --help` | Help |
|
|
107
|
+
| `-i, --ignore` | Skip malformed files |
|
|
108
|
+
| `-l, --lower` | Lowercase segment names |
|
|
109
|
+
| `-m, --message` | Show message body |
|
|
110
|
+
| `-p, --path` | Show file path |
|
|
111
|
+
| `-q, --query <val>` | Query specific values |
|
|
112
|
+
| `-s, --spacer` | Blank line between messages |
|
|
113
|
+
| `-t, --tsv` | Tab-delimited output |
|
|
114
|
+
| `-v, --version` | Show version |
|
|
115
|
+
|
|
116
|
+
## Links
|
|
117
|
+
|
|
118
|
+
- [Rip Language](https://github.com/shreeve/rip-lang)
|
|
119
|
+
- [X12 Standard](https://x12.org)
|
package/bin/rip-x12
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from 'child_process';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
|
|
7
|
+
const x12Rip = join(dirname(fileURLToPath(import.meta.url)), '..', 'x12.rip');
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
execFileSync('rip', [x12Rip, ...process.argv.slice(2)], { stdio: 'inherit' });
|
|
11
|
+
} catch (e) {
|
|
12
|
+
process.exit(e.status || 1);
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rip-lang/x12",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "X12 EDI parser, editor, and query engine — path-based addressing, separator auto-detection, zero dependencies",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "x12.rip",
|
|
7
|
+
"bin": {
|
|
8
|
+
"rip-x12": "./bin/rip-x12"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./x12.rip"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"x12",
|
|
15
|
+
"edi",
|
|
16
|
+
"healthcare",
|
|
17
|
+
"parser",
|
|
18
|
+
"270",
|
|
19
|
+
"271",
|
|
20
|
+
"835",
|
|
21
|
+
"837",
|
|
22
|
+
"bun",
|
|
23
|
+
"rip"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/shreeve/rip-lang.git",
|
|
28
|
+
"directory": "packages/x12"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/shreeve/rip-lang/tree/main/packages/x12#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/shreeve/rip-lang/issues"
|
|
33
|
+
},
|
|
34
|
+
"author": "Steve Shreeve <steve.shreeve@gmail.com>",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"rip-lang": "^3.10.0"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"x12.rip",
|
|
41
|
+
"bin/rip-x12",
|
|
42
|
+
"README.md"
|
|
43
|
+
]
|
|
44
|
+
}
|
package/x12.rip
ADDED
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# x12.rip: X12 EDI parser, editor, and query engine
|
|
3
|
+
#
|
|
4
|
+
# Author: Steve Shreeve <steve.shreeve@gmail.com>
|
|
5
|
+
# Date: Feb 9, 2026
|
|
6
|
+
#
|
|
7
|
+
# License: MIT
|
|
8
|
+
# ==============================================================================
|
|
9
|
+
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
|
|
11
|
+
import { basename, join } from 'path'
|
|
12
|
+
|
|
13
|
+
# ==[ Helpers ]=================================================================
|
|
14
|
+
|
|
15
|
+
escRe = (s) -> s.replace /[.*+?^${}()|[\]\\]/g, '\\$&'
|
|
16
|
+
|
|
17
|
+
ISA_WIDTHS = [3, 2, 10, 2, 10, 2, 15, 2, 15, 6, 4, 1, 5, 9, 1, 1]
|
|
18
|
+
|
|
19
|
+
SELECTOR = /^(..[^-.(]?)(?:\((\d*|[+!?*]?)\))?[-.]?(\d+)?(?:\((\d*|[+!?*]?)\))?[-.]?(\d+)?$/
|
|
20
|
+
|
|
21
|
+
isaWidthsStr = (str) ->
|
|
22
|
+
sep = str[3]
|
|
23
|
+
return str unless sep
|
|
24
|
+
parts = str.split sep
|
|
25
|
+
for part, i in parts
|
|
26
|
+
len = ISA_WIDTHS[i]
|
|
27
|
+
if part and len and part.length isnt len
|
|
28
|
+
parts[i] = part.padEnd(len).slice 0, len
|
|
29
|
+
parts.join sep
|
|
30
|
+
|
|
31
|
+
ansiColor = (str, fg, bgc) ->
|
|
32
|
+
m = fg.match /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/
|
|
33
|
+
return str unless m
|
|
34
|
+
r = parseInt m[1], 16
|
|
35
|
+
g = parseInt m[2], 16
|
|
36
|
+
b = parseInt m[3], 16
|
|
37
|
+
pre = "\x1b[38;2;#{r};#{g};#{b}m"
|
|
38
|
+
post = "\x1b[39m"
|
|
39
|
+
pre + str + post
|
|
40
|
+
|
|
41
|
+
# ==[ X12 Class ]===============================================================
|
|
42
|
+
|
|
43
|
+
class X12
|
|
44
|
+
constructor: (obj) ->
|
|
45
|
+
# Initialize from input type
|
|
46
|
+
if typeof obj is 'string'
|
|
47
|
+
if obj.length > 0
|
|
48
|
+
@str = obj
|
|
49
|
+
else if obj instanceof X12
|
|
50
|
+
@str = obj.toString()
|
|
51
|
+
|
|
52
|
+
# Default ISA template
|
|
53
|
+
@str or= isaWidthsStr "ISA*00**00**ZZ**ZZ****^*00501**0*P*:~"
|
|
54
|
+
|
|
55
|
+
# Detect separators from ISA header
|
|
56
|
+
m = @str.match /^ISA(.).{78}(.).{21}(.)(.)/
|
|
57
|
+
throw new Error "malformed X12" unless m
|
|
58
|
+
@fld = m[1]
|
|
59
|
+
@rep = m[2]
|
|
60
|
+
@com = m[3]
|
|
61
|
+
@seg = m[4]
|
|
62
|
+
@rep = "^" if @rep is "U"
|
|
63
|
+
|
|
64
|
+
# Parse based on input type
|
|
65
|
+
if typeof obj is 'string' or not obj?
|
|
66
|
+
@toArray()
|
|
67
|
+
@str = null
|
|
68
|
+
else if Array.isArray obj
|
|
69
|
+
arr = obj.slice 0
|
|
70
|
+
while arr.length >= 2
|
|
71
|
+
p = arr.shift()
|
|
72
|
+
v = arr.shift()
|
|
73
|
+
@set p, v
|
|
74
|
+
else if typeof obj is 'object'
|
|
75
|
+
for own key, val of obj
|
|
76
|
+
@set key, val if val?
|
|
77
|
+
|
|
78
|
+
@toString() unless @str
|
|
79
|
+
|
|
80
|
+
# ==[ Serialization ]=========================================================
|
|
81
|
+
|
|
82
|
+
toArray: ->
|
|
83
|
+
return @ary if @ary
|
|
84
|
+
re = new RegExp "[#{escRe @seg}\\r\\n]+", 'g'
|
|
85
|
+
@ary = @str.trim().split(re).map (s) => s.split @fld, -1
|
|
86
|
+
@ary
|
|
87
|
+
|
|
88
|
+
toString: ->
|
|
89
|
+
return @str if @str
|
|
90
|
+
@str = @ary.map((seg) => seg.join(@fld) + @seg).join("\n")
|
|
91
|
+
if @str.endsWith "\n"
|
|
92
|
+
@str = @str.slice 0, -1
|
|
93
|
+
@str
|
|
94
|
+
|
|
95
|
+
raw: -> @toString().replace(/\n/g, '').toUpperCase()
|
|
96
|
+
|
|
97
|
+
# ==[ ISA field widths ]======================================================
|
|
98
|
+
|
|
99
|
+
isaWidths: (row) ->
|
|
100
|
+
for was, i in row
|
|
101
|
+
len = ISA_WIDTHS[i]
|
|
102
|
+
if was and len and was.length isnt len
|
|
103
|
+
row[i] = was.padEnd(len).slice 0, len
|
|
104
|
+
row
|
|
105
|
+
|
|
106
|
+
# ==[ Display ]===============================================================
|
|
107
|
+
|
|
108
|
+
show: (...opts) ->
|
|
109
|
+
full = opts.includes 'full'
|
|
110
|
+
deep = opts.includes 'deep'
|
|
111
|
+
down = opts.includes 'down'
|
|
112
|
+
asList = opts.includes 'list'
|
|
113
|
+
hide = opts.includes 'hide'
|
|
114
|
+
only = opts.includes 'only'
|
|
115
|
+
useAnsi = opts.includes 'ansi'
|
|
116
|
+
left = 15
|
|
117
|
+
|
|
118
|
+
out = if full then [@toString()] else []
|
|
119
|
+
|
|
120
|
+
unless hide
|
|
121
|
+
out.push "" if full
|
|
122
|
+
nums = {}
|
|
123
|
+
segs = @toArray()
|
|
124
|
+
for flds in segs
|
|
125
|
+
seg = if down then flds[0].toLowerCase() else flds[0].toUpperCase()
|
|
126
|
+
nums[seg] = (nums[seg] or 0) + 1
|
|
127
|
+
num = nums[seg]
|
|
128
|
+
for fld, j in flds
|
|
129
|
+
continue if j is 0
|
|
130
|
+
continue if not fld or fld.length is 0
|
|
131
|
+
if deep
|
|
132
|
+
reps = fld.split @rep
|
|
133
|
+
if reps.length > 1
|
|
134
|
+
for item, k in reps
|
|
135
|
+
suffix = if num > 1 and not only then "(#{num})" else ""
|
|
136
|
+
tag = "#{seg}#{suffix}-#{j}(#{k + 1})"
|
|
137
|
+
out.push tag.padEnd(left) + item
|
|
138
|
+
continue
|
|
139
|
+
suffix = if num > 1 and not only then "(#{num})" else ""
|
|
140
|
+
tag = "#{seg}#{suffix}-#{j}"
|
|
141
|
+
val = if useAnsi then ansiColor(fld, "ffffff", "") else fld
|
|
142
|
+
out.push tag.padEnd(left) + val
|
|
143
|
+
|
|
144
|
+
if asList then out else console.log out.join "\n"
|
|
145
|
+
|
|
146
|
+
# ==[ Normalize ]=============================================================
|
|
147
|
+
|
|
148
|
+
normalize: (obj) ->
|
|
149
|
+
if Array.isArray obj
|
|
150
|
+
for elt, i in obj
|
|
151
|
+
str = if typeof elt is 'string' then elt else String elt
|
|
152
|
+
obj[i] = str.toUpperCase()
|
|
153
|
+
else
|
|
154
|
+
str = if typeof obj is 'string' then obj else String obj
|
|
155
|
+
str.toUpperCase()
|
|
156
|
+
|
|
157
|
+
# ==[ Data access — unified get/set ]=========================================
|
|
158
|
+
|
|
159
|
+
data: (...args) ->
|
|
160
|
+
len = args.length
|
|
161
|
+
return @update args if len > 2
|
|
162
|
+
pos = args[0]
|
|
163
|
+
return @str unless pos?
|
|
164
|
+
val = args[1]
|
|
165
|
+
|
|
166
|
+
# Parse the selector
|
|
167
|
+
m = pos.match SELECTOR
|
|
168
|
+
throw new Error "bad selector '#{pos}'" unless m
|
|
169
|
+
seg = m[1]
|
|
170
|
+
wantPat = "^#{seg}[^#{escRe @seg}\\r\\n]*"
|
|
171
|
+
want = new RegExp wantPat, 'i'
|
|
172
|
+
wantG = new RegExp wantPat, 'gim'
|
|
173
|
+
|
|
174
|
+
numStr = m[2] or ""
|
|
175
|
+
isDigit = /^\d+$/
|
|
176
|
+
num = if numStr.length > 0 and isDigit.test numStr then parseInt numStr else null
|
|
177
|
+
newNum = numStr is "+"
|
|
178
|
+
askNum = numStr is "?"
|
|
179
|
+
allNum = numStr is "*"
|
|
180
|
+
|
|
181
|
+
fldStr = m[3]
|
|
182
|
+
fld = if fldStr? then parseInt fldStr else null
|
|
183
|
+
throw new Error "zero index on field" if len > 1 and fld is 0
|
|
184
|
+
|
|
185
|
+
repStr = m[4] or ""
|
|
186
|
+
rep = if repStr.length > 0 and isDigit.test repStr then parseInt repStr else null
|
|
187
|
+
newRep = repStr is "+"
|
|
188
|
+
askRep = repStr is "?"
|
|
189
|
+
allRep = repStr is "*"
|
|
190
|
+
|
|
191
|
+
comStr = m[5]
|
|
192
|
+
com = if comStr? then parseInt comStr else null
|
|
193
|
+
throw new Error "zero index on component" if len > 1 and com is 0
|
|
194
|
+
|
|
195
|
+
if len <= 1 # GET
|
|
196
|
+
@toString() unless @str
|
|
197
|
+
matches = @str.match(wantG) or []
|
|
198
|
+
|
|
199
|
+
if askNum and not askRep
|
|
200
|
+
return matches.length
|
|
201
|
+
|
|
202
|
+
if allNum
|
|
203
|
+
result = []
|
|
204
|
+
for match in matches
|
|
205
|
+
out = match
|
|
206
|
+
if fld?
|
|
207
|
+
out = out.split(@fld)[fld]
|
|
208
|
+
continue unless out?
|
|
209
|
+
if askRep
|
|
210
|
+
result.push out.split(@rep).length
|
|
211
|
+
continue
|
|
212
|
+
if rep? or (com? and not rep?)
|
|
213
|
+
rep ?= 1
|
|
214
|
+
out = out.split(@rep)[rep - 1]
|
|
215
|
+
continue unless out?
|
|
216
|
+
if com?
|
|
217
|
+
out = out.split(@com)[com - 1]
|
|
218
|
+
continue unless out?
|
|
219
|
+
result.push out
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
num ?= 1
|
|
223
|
+
out = matches[num - 1]
|
|
224
|
+
return "" unless out?
|
|
225
|
+
|
|
226
|
+
if fld?
|
|
227
|
+
out = out.split(@fld)[fld]
|
|
228
|
+
return "" unless out?
|
|
229
|
+
|
|
230
|
+
if askRep
|
|
231
|
+
return out.split(@rep).length
|
|
232
|
+
|
|
233
|
+
if rep? or (com? and not rep?)
|
|
234
|
+
rep ?= 1
|
|
235
|
+
out = out.split(@rep)[rep - 1]
|
|
236
|
+
return "" unless out?
|
|
237
|
+
|
|
238
|
+
if com?
|
|
239
|
+
out = out.split(@com)[com - 1]
|
|
240
|
+
return "" unless out?
|
|
241
|
+
|
|
242
|
+
return out
|
|
243
|
+
|
|
244
|
+
else # SET
|
|
245
|
+
@toArray() unless @ary
|
|
246
|
+
@str = null
|
|
247
|
+
|
|
248
|
+
our = @ary.filter (now) -> want.test now[0]
|
|
249
|
+
|
|
250
|
+
unless allNum
|
|
251
|
+
num ?= 0
|
|
252
|
+
row = our[num - 1]
|
|
253
|
+
pad = if row then 0 else num - our.length
|
|
254
|
+
pad = 1 if (num is 0 and our.length is 0) or newNum
|
|
255
|
+
if pad > 0
|
|
256
|
+
for _i in [1..pad]
|
|
257
|
+
row = [seg.toUpperCase()]
|
|
258
|
+
@ary.push row
|
|
259
|
+
if newNum and val is 'num'
|
|
260
|
+
val = our.length + pad
|
|
261
|
+
our = [row]
|
|
262
|
+
|
|
263
|
+
# Prepare the value
|
|
264
|
+
val ?= ""
|
|
265
|
+
how = null
|
|
266
|
+
|
|
267
|
+
if not rep? and not com?
|
|
268
|
+
# replace fields
|
|
269
|
+
val = val.join(@fld) if Array.isArray val
|
|
270
|
+
val = String(val).split @fld, -1
|
|
271
|
+
how = 'fld'
|
|
272
|
+
else if fld? and rep? and not com?
|
|
273
|
+
# replace repeats
|
|
274
|
+
val = val.join(@rep) if Array.isArray val
|
|
275
|
+
val = String(val).split @rep, -1
|
|
276
|
+
how = 'rep'
|
|
277
|
+
else if fld? and com?
|
|
278
|
+
# replace components
|
|
279
|
+
val = val.join(@com) if Array.isArray val
|
|
280
|
+
val = String(val).split @com, -1
|
|
281
|
+
how = 'com'
|
|
282
|
+
|
|
283
|
+
throw new Error "invalid fld/rep/com: [#{fld}, #{rep}, #{com}]" unless how
|
|
284
|
+
|
|
285
|
+
val = [""] if val.length is 0
|
|
286
|
+
|
|
287
|
+
# Replace the target in each matching row
|
|
288
|
+
for row in our
|
|
289
|
+
if how is 'fld'
|
|
290
|
+
if fld?
|
|
291
|
+
pad = fld - row.length
|
|
292
|
+
if pad > 0
|
|
293
|
+
for _i in [1..pad]
|
|
294
|
+
row.push ""
|
|
295
|
+
row.splice fld, val.length, ...val
|
|
296
|
+
else
|
|
297
|
+
row.splice 1, row.length - 1, ...val
|
|
298
|
+
|
|
299
|
+
else if how is 'rep'
|
|
300
|
+
row[fld] ?= ""
|
|
301
|
+
was = row[fld]
|
|
302
|
+
if was.length is 0
|
|
303
|
+
newVal = ""
|
|
304
|
+
if rep > 1
|
|
305
|
+
newVal = @rep.repeat(rep - 1)
|
|
306
|
+
newVal += val.join @rep
|
|
307
|
+
row[fld] = newVal
|
|
308
|
+
else
|
|
309
|
+
ufr = was.split @rep, -1
|
|
310
|
+
pad = rep - ufr.length
|
|
311
|
+
if newRep or (rep is 0 and ufr.length is 0)
|
|
312
|
+
pad = 1
|
|
313
|
+
if pad > 0
|
|
314
|
+
for _i in [1..pad]
|
|
315
|
+
ufr.push ""
|
|
316
|
+
ufr.splice rep - 1, val.length, ...val
|
|
317
|
+
row[fld] = ufr.join @rep
|
|
318
|
+
|
|
319
|
+
else if how is 'com'
|
|
320
|
+
rep ?= 0
|
|
321
|
+
row[fld] ?= ""
|
|
322
|
+
one = row[fld]
|
|
323
|
+
|
|
324
|
+
if one.length is 0
|
|
325
|
+
newVal = ""
|
|
326
|
+
if rep > 1
|
|
327
|
+
newVal = @rep.repeat(rep - 1)
|
|
328
|
+
if com > 1
|
|
329
|
+
newVal += @com.repeat(com - 1)
|
|
330
|
+
newVal += val.join @com
|
|
331
|
+
row[fld] = newVal
|
|
332
|
+
else
|
|
333
|
+
ufr = one.split @rep, -1
|
|
334
|
+
pad = rep - ufr.length
|
|
335
|
+
if newRep or (rep is 0 and ufr.length is 0)
|
|
336
|
+
pad = 1
|
|
337
|
+
if pad > 0
|
|
338
|
+
for _i in [1..pad]
|
|
339
|
+
ufr.push ""
|
|
340
|
+
|
|
341
|
+
two = ufr[rep - 1] or ""
|
|
342
|
+
if two.length is 0
|
|
343
|
+
newVal = ""
|
|
344
|
+
if com > 1
|
|
345
|
+
newVal = @com.repeat(com - 1)
|
|
346
|
+
newVal += val.join @com
|
|
347
|
+
ufr[rep - 1] = newVal
|
|
348
|
+
else
|
|
349
|
+
ucr = two.split @com, -1
|
|
350
|
+
pad = com - ucr.length
|
|
351
|
+
if pad > 0
|
|
352
|
+
for _i in [1..pad]
|
|
353
|
+
ucr.push ""
|
|
354
|
+
ucr.splice com - 1, val.length, ...val
|
|
355
|
+
ufr[rep - 1] = ucr.join @com
|
|
356
|
+
|
|
357
|
+
row[fld] = ufr.join @rep
|
|
358
|
+
|
|
359
|
+
# Enforce ISA field widths
|
|
360
|
+
if seg =~ /isa/i
|
|
361
|
+
@isaWidths row
|
|
362
|
+
|
|
363
|
+
null
|
|
364
|
+
|
|
365
|
+
get: (pos) -> @data pos
|
|
366
|
+
set: (pos, val) -> @data pos, val
|
|
367
|
+
|
|
368
|
+
# ==[ Update ]================================================================
|
|
369
|
+
|
|
370
|
+
update: (etc) ->
|
|
371
|
+
if Array.isArray etc
|
|
372
|
+
i = 0
|
|
373
|
+
while i < etc.length - 1
|
|
374
|
+
@data etc[i], etc[i + 1] if etc[i + 1]?
|
|
375
|
+
i += 2
|
|
376
|
+
else if typeof etc is 'object' and etc?
|
|
377
|
+
for own pos, val of etc
|
|
378
|
+
@data pos, val if val?
|
|
379
|
+
this
|
|
380
|
+
|
|
381
|
+
# ==[ Iteration ]=============================================================
|
|
382
|
+
|
|
383
|
+
each: (seg, fn) ->
|
|
384
|
+
if typeof seg is 'function'
|
|
385
|
+
fn = seg
|
|
386
|
+
seg = null
|
|
387
|
+
for row in @toArray()
|
|
388
|
+
if seg?
|
|
389
|
+
if typeof seg is 'string'
|
|
390
|
+
continue unless row[0].toUpperCase() is seg.toUpperCase()
|
|
391
|
+
else
|
|
392
|
+
continue unless seg.test row[0]
|
|
393
|
+
fn row
|
|
394
|
+
this
|
|
395
|
+
|
|
396
|
+
grep: (seg) ->
|
|
397
|
+
result = []
|
|
398
|
+
for row in @toArray()
|
|
399
|
+
if typeof seg is 'string'
|
|
400
|
+
result.push row if row[0].toUpperCase() is seg.toUpperCase()
|
|
401
|
+
else
|
|
402
|
+
result.push row if seg.test row[0]
|
|
403
|
+
result
|
|
404
|
+
|
|
405
|
+
# ==[ Find (multi-query) ]====================================================
|
|
406
|
+
|
|
407
|
+
find: (...ask) ->
|
|
408
|
+
return undefined if ask.length is 0
|
|
409
|
+
|
|
410
|
+
str = @toString()
|
|
411
|
+
say = []
|
|
412
|
+
|
|
413
|
+
for pos in ask
|
|
414
|
+
unless pos?
|
|
415
|
+
say.push null
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
m = pos.match SELECTOR
|
|
419
|
+
throw new Error "bad selector '#{pos}'" unless m
|
|
420
|
+
seg = m[1]
|
|
421
|
+
wantPat = "^#{seg}[^#{escRe @seg}\\r\\n]*"
|
|
422
|
+
wantG = new RegExp wantPat, 'gim'
|
|
423
|
+
|
|
424
|
+
numStr = m[2] or ""
|
|
425
|
+
isDigit = /^\d+$/
|
|
426
|
+
num = if numStr.length > 0 and isDigit.test numStr then parseInt numStr else null
|
|
427
|
+
askNum = numStr is "?"
|
|
428
|
+
allNum = numStr is "*"
|
|
429
|
+
|
|
430
|
+
fldStr = m[3]
|
|
431
|
+
fld = if fldStr? then parseInt fldStr else null
|
|
432
|
+
|
|
433
|
+
repStr = m[4] or ""
|
|
434
|
+
rep = if repStr.length > 0 and isDigit.test repStr then parseInt repStr else null
|
|
435
|
+
askRep = repStr is "?"
|
|
436
|
+
|
|
437
|
+
comStr = m[5]
|
|
438
|
+
com = if comStr? then parseInt comStr else null
|
|
439
|
+
|
|
440
|
+
matches = str.match(wantG) or []
|
|
441
|
+
|
|
442
|
+
if allNum
|
|
443
|
+
throw new Error "multi query allows only one selector" if ask.length > 1
|
|
444
|
+
result = []
|
|
445
|
+
for match in matches
|
|
446
|
+
out = match
|
|
447
|
+
if fld?
|
|
448
|
+
out = out.split(@fld)[fld]
|
|
449
|
+
continue unless out?
|
|
450
|
+
if askRep
|
|
451
|
+
result.push out.split(@rep).length
|
|
452
|
+
continue
|
|
453
|
+
if rep? or (com? and not rep?)
|
|
454
|
+
rep ?= 1
|
|
455
|
+
out = out.split(@rep)[rep - 1]
|
|
456
|
+
continue unless out?
|
|
457
|
+
if com?
|
|
458
|
+
out = out.split(@com)[com - 1]
|
|
459
|
+
continue unless out?
|
|
460
|
+
result.push out
|
|
461
|
+
return result
|
|
462
|
+
|
|
463
|
+
if askNum and not askRep
|
|
464
|
+
say.push matches.length
|
|
465
|
+
continue
|
|
466
|
+
|
|
467
|
+
num ?= 1
|
|
468
|
+
out = matches[num - 1]
|
|
469
|
+
unless out?
|
|
470
|
+
say.push ""
|
|
471
|
+
continue
|
|
472
|
+
|
|
473
|
+
if fld?
|
|
474
|
+
out = out.split(@fld)[fld]
|
|
475
|
+
unless out?
|
|
476
|
+
say.push ""
|
|
477
|
+
continue
|
|
478
|
+
|
|
479
|
+
if askRep
|
|
480
|
+
say.push out.split(@rep).length
|
|
481
|
+
continue
|
|
482
|
+
|
|
483
|
+
if rep? or (com? and not rep?)
|
|
484
|
+
rep ?= 1
|
|
485
|
+
out = out.split(@rep)[rep - 1]
|
|
486
|
+
unless out?
|
|
487
|
+
say.push ""
|
|
488
|
+
continue
|
|
489
|
+
|
|
490
|
+
if com?
|
|
491
|
+
out = out.split(@com)[com - 1]
|
|
492
|
+
unless out?
|
|
493
|
+
say.push ""
|
|
494
|
+
continue
|
|
495
|
+
|
|
496
|
+
say.push out
|
|
497
|
+
|
|
498
|
+
if say.length > 1 then say else say[0]
|
|
499
|
+
|
|
500
|
+
X12.load = (file) ->
|
|
501
|
+
try
|
|
502
|
+
str = readFileSync file, 'utf-8'
|
|
503
|
+
catch
|
|
504
|
+
throw new Error "unreadable file: #{file}"
|
|
505
|
+
new X12 str
|
|
506
|
+
|
|
507
|
+
export { X12, ISA_WIDTHS, SELECTOR }
|
|
508
|
+
|
|
509
|
+
# ==[ CLI ]=====================================================================
|
|
510
|
+
|
|
511
|
+
if import.meta.main
|
|
512
|
+
|
|
513
|
+
args = process.argv.slice 2
|
|
514
|
+
|
|
515
|
+
if args.includes('-v') or args.includes('--version')
|
|
516
|
+
console.log "rip-x12 0.1.0"
|
|
517
|
+
process.exit 0
|
|
518
|
+
|
|
519
|
+
if args.includes('-h') or args.includes('--help')
|
|
520
|
+
console.log """
|
|
521
|
+
usage: rip-x12 [options] <file> <file> ...
|
|
522
|
+
|
|
523
|
+
Options:
|
|
524
|
+
-a, --after <date> After date (YYYYMMDD)
|
|
525
|
+
--ansi ANSI color output
|
|
526
|
+
-c, --count Count messages
|
|
527
|
+
-d, --dive Recursive directory scan
|
|
528
|
+
-f, --fields Show fields
|
|
529
|
+
-F, --fields-only Fields only, no repeat indicators
|
|
530
|
+
-h, --help Show help
|
|
531
|
+
-i, --ignore Skip malformed files
|
|
532
|
+
-l, --lower Lowercase segment names
|
|
533
|
+
-m, --message Show message body
|
|
534
|
+
-p, --path Show file path
|
|
535
|
+
-q, --query <val> Query specific values
|
|
536
|
+
-s, --spacer Blank line between messages
|
|
537
|
+
-t, --tsv Tab-delimited output
|
|
538
|
+
-v, --version Show version
|
|
539
|
+
"""
|
|
540
|
+
process.exit 0
|
|
541
|
+
|
|
542
|
+
# Parse options
|
|
543
|
+
opts = {}
|
|
544
|
+
paths = []
|
|
545
|
+
i = 0
|
|
546
|
+
while i < args.length
|
|
547
|
+
arg = args[i]
|
|
548
|
+
if arg is '-a' or arg is '--after'
|
|
549
|
+
i++
|
|
550
|
+
opts.after = args[i]
|
|
551
|
+
else if arg is '--ansi'
|
|
552
|
+
opts.ansi = true
|
|
553
|
+
else if arg is '-c' or arg is '--count'
|
|
554
|
+
opts.count = true
|
|
555
|
+
else if arg is '-d' or arg is '--dive'
|
|
556
|
+
opts.dive = true
|
|
557
|
+
else if arg is '-f' or arg is '--fields'
|
|
558
|
+
opts.fields = true
|
|
559
|
+
else if arg is '-F' or arg is '--fields-only'
|
|
560
|
+
opts.fields = true
|
|
561
|
+
opts.only = true
|
|
562
|
+
else if arg is '-i' or arg is '--ignore'
|
|
563
|
+
opts.ignore = true
|
|
564
|
+
else if arg is '-l' or arg is '--lower'
|
|
565
|
+
opts.lower = true
|
|
566
|
+
else if arg is '-m' or arg is '--message'
|
|
567
|
+
opts.message = true
|
|
568
|
+
else if arg is '-p' or arg is '--path'
|
|
569
|
+
opts.path = true
|
|
570
|
+
else if arg is '-q' or arg is '--query'
|
|
571
|
+
i++
|
|
572
|
+
opts.query = args[i]
|
|
573
|
+
else if arg is '-s' or arg is '--spacer'
|
|
574
|
+
opts.spacer = true
|
|
575
|
+
else if arg is '-t' or arg is '--tsv'
|
|
576
|
+
opts.tsv = true
|
|
577
|
+
else
|
|
578
|
+
paths.push arg
|
|
579
|
+
i++
|
|
580
|
+
|
|
581
|
+
if paths.length is 0
|
|
582
|
+
console.error "usage: rip-x12 [options] <file> <file> ..."
|
|
583
|
+
process.exit 1
|
|
584
|
+
|
|
585
|
+
# Build show args
|
|
586
|
+
showArgs = []
|
|
587
|
+
showArgs.push 'ansi' if opts.ansi
|
|
588
|
+
showArgs.push 'down' if opts.lower
|
|
589
|
+
noOpts = Object.keys(opts).length is 0
|
|
590
|
+
showArgs.push 'full' if opts.message or noOpts
|
|
591
|
+
showArgs.push 'hide' unless opts.fields
|
|
592
|
+
showArgs.push 'only' if opts.only
|
|
593
|
+
|
|
594
|
+
# Query setup
|
|
595
|
+
quer = null
|
|
596
|
+
seps = if opts.tsv then "\t" else "|"
|
|
597
|
+
if opts.query
|
|
598
|
+
quer = opts.query.split(',').map (s) -> s.trim()
|
|
599
|
+
|
|
600
|
+
# After date filter
|
|
601
|
+
afterTime = null
|
|
602
|
+
if opts.after
|
|
603
|
+
d = opts.after
|
|
604
|
+
iso = "#{d.slice(0,4)}-#{d.slice(4,6)}-#{d.slice(6,8)}"
|
|
605
|
+
afterTime = new Date iso
|
|
606
|
+
|
|
607
|
+
# File discovery
|
|
608
|
+
skipDirs = new Set ['.git', 'node_modules', '.rip-cache']
|
|
609
|
+
|
|
610
|
+
walkDir = (dir) ->
|
|
611
|
+
files = []
|
|
612
|
+
try
|
|
613
|
+
entries = readdirSync dir
|
|
614
|
+
catch
|
|
615
|
+
return files
|
|
616
|
+
for entry in entries
|
|
617
|
+
full = join dir, entry
|
|
618
|
+
try
|
|
619
|
+
stat = statSync full
|
|
620
|
+
catch
|
|
621
|
+
continue
|
|
622
|
+
if stat.isDirectory()
|
|
623
|
+
continue if entry.startsWith('.') or skipDirs.has entry
|
|
624
|
+
if opts.dive
|
|
625
|
+
files = files.concat walkDir full
|
|
626
|
+
else if stat.isFile()
|
|
627
|
+
if afterTime
|
|
628
|
+
files.push full if stat.mtime > afterTime
|
|
629
|
+
else
|
|
630
|
+
files.push full
|
|
631
|
+
files.sort()
|
|
632
|
+
|
|
633
|
+
# Build file list
|
|
634
|
+
list = []
|
|
635
|
+
for p in paths
|
|
636
|
+
unless existsSync p
|
|
637
|
+
console.error "unknown: #{p}"
|
|
638
|
+
continue
|
|
639
|
+
stat = statSync p
|
|
640
|
+
if stat.isDirectory()
|
|
641
|
+
list = list.concat walkDir p
|
|
642
|
+
else if stat.isFile()
|
|
643
|
+
if afterTime
|
|
644
|
+
list.push p if stat.mtime > afterTime
|
|
645
|
+
else
|
|
646
|
+
list.push p
|
|
647
|
+
else
|
|
648
|
+
console.error "unknown: #{p}"
|
|
649
|
+
|
|
650
|
+
# Process files
|
|
651
|
+
msgs = 0
|
|
652
|
+
|
|
653
|
+
for file in list
|
|
654
|
+
console.log() if opts.spacer and msgs > 0
|
|
655
|
+
|
|
656
|
+
if opts.path
|
|
657
|
+
if quer and quer.length is 1
|
|
658
|
+
process.stdout.write "#{file}:"
|
|
659
|
+
else
|
|
660
|
+
console.log "\n==[ #{file} ]==\n"
|
|
661
|
+
|
|
662
|
+
try
|
|
663
|
+
str = readFileSync file, 'utf-8'
|
|
664
|
+
catch
|
|
665
|
+
console.error "ERROR: unable to read: \"#{file}\""
|
|
666
|
+
continue
|
|
667
|
+
|
|
668
|
+
try
|
|
669
|
+
x12 = new X12 str
|
|
670
|
+
catch e
|
|
671
|
+
unless opts.ignore
|
|
672
|
+
console.error "ERROR: malformed X12: \"#{file}\" (#{e.message})"
|
|
673
|
+
continue
|
|
674
|
+
|
|
675
|
+
if quer
|
|
676
|
+
hits = x12.find ...quer
|
|
677
|
+
hits = [hits] unless Array.isArray hits
|
|
678
|
+
hits.unshift file if opts.path
|
|
679
|
+
console.log hits.join seps
|
|
680
|
+
console.log() if opts.path
|
|
681
|
+
continue
|
|
682
|
+
|
|
683
|
+
x12.show ...showArgs
|
|
684
|
+
msgs += 1
|
|
685
|
+
|
|
686
|
+
if opts.count and msgs > 0
|
|
687
|
+
console.log "\nTotal messages: #{msgs}"
|