Парсим X12 «на коліні»
При створенні програми, активно взаємодіє зі сторонніми сервісами і системами, часто потрібно забезпечити обмін інформацією з ними, односторонній або двосторонній
При цьому найчастіше сторонній сервіс надає єдиний формат і структури даних для такої взаємодії.
Одним з таких форматів електронного документообігу є EDI ANSI ASC X12, досить докладний опис якого наведено за посиланням.
Під катом наведено простий алгоритм парсера X12 та код на Clojure, що реалізує парсер і приклад обробки распарсенных даних.
Трохи про формат
Цитуючи вищенаведену посилання: Стандарт електронного обміну документами ANSI ASC X12 (American National Standards Institute Accredited Standards Committee X12) був розроблений у 70-х роках, коли був важливий малий розмір електронного документа (для модемів зі швидкостями 300-1200 мм. біт в секунду) і кожен байт повинен був нести максимум інформації. Таким чином, від «читаності» електронного документа відмовилися на користь «щільності інформації».
Тому ви не побачите в ньому ніяких человекочитаемых красот, як XML. І хоча стандарт дозволяє створювати документи досить складної ієрархічної структури, з наявністю блоків і так званих loop-ів, тим не менше навіть закриваючі теги для всіх блоків (крім ISA/GS/ST) не передбачені. За посиланням до ката бажаючі можуть в деталях ознайомитися зі структурою і описом формату, далі ми будемо стосуватися тільки необхідних речей.
Кожен тип документа має свою структуру шаблону, в якому зазначається зміст і призначення окремих полів і сегментів, їх типи і можливі значення, а також перелік обов’язкових і опціональних сегментів і блоків. Підтримується версіонування шаблонів, інформація про тип і номер версії передається у відповідних полях документа. Передбачається, що саме з використанням шаблону конкретного типу документа повинен проводитися його розбір і валідація.
Нижче представлений приклад документа, що містить декілька транзакцій типу 835 (документ типу claim response), на якому буде продемонстровано парсинг і подальша обробка даних.
Приклад X12
ISA*00* *00* *ZZ*EMEDNYBAT *ZZ*ETIN *140305*0929*^*00501*111111123*0*P*:~
GS*HP*EMED*ETIN*20140301*09304100*111111123*X*005010X221A1~
ST*835*35681~
BPR*I*810.8*C*CHK************20140331~
TRN*1*12345*1512345678~
REF*EV*XYZ CLEARINGHOUSE~
N1*PR*DENTAL OF ABC~
N3*225 MAIN STREET~
N4*CENTERVILLE*PA*17111~
PER*BL*JANE DOE*TE*9005555555~
N1*PE*BAN DDS LLC*FI*999994703~
LX*1~
CLP*7722337*1*226*132**12*119932404007801~
NM1*QC*1*DOE*SANDY****MI*SJD11112~
NM1*82*1*BAN*ERIN****XX*1811901945~
AMT*AU*132~
SVC*AD:D0120*46*25~
DTM*472*20140324~
CAS*CO*131*21~
AMT*B6*25~
SVC*AD:D0220*25*14~
DTM*472*20140324~
CAS*CO*131*11~
AMT*B6*14~
SVC*AD:D0230*22*10~
DTM*472*20140324~
CAS*CO*131*12~
AMT*B6*10~
SVC*AD:D0274*60*34~
DTM*472*20140324~
CAS*CO*131*26~
AMT*B6*34~
SVC*AD:D1110*73*49~
DTM*472*20140324~
CAS*CO*131*24~
AMT*B6*49~
LX*2~
CLP*7722337*1*119*74**12*119932404007801~
NM1*QC*1*DOE*SALLY****MI*SJD11111~
NM1*IL*1*DOE*JOHN****MI*123456~
NM1*82*1*BAN*ERIN****XX*1811901945~
AMT*AU*74~
SVC*AD:D0120*46*25~
DTM*472*20140324~
CAS*CO*131*21~
AMT*B6*25~
SVC*AD:D1110*73*49~
DTM*472*20140324~
CAS*CO*131*24~
AMT*B6*49~
LX*3~
CLP*7722337*1*226*108*24*12*119932404007801~
NM1*QC*1*SMITH*SALLY****MI*SJD11113~
NM1*82*1*BAN*ERIN****XX*1811901945~
AMT*AU*132~
SVC*AD:D0120*46*25~
DTM*472*20140324~
CAS*CO*131*21~
AMT*B6*25~
SVC*AD:D0220*25*0~
DTM*472*20140324~
CAS*PR*3*14~
CAS*CO*131*11~
AMT*B6*14~
SVC*AD:D0230*22*0~
DTM*472*20140324~
CAS*PR*3*10~
CAS*CO*131*12~
AMT*B6*10~
SVC*AD:D0274*60*34~
DTM*472*20140324~
CAS*CO*131*26~
AMT*B6*34~
SVC*AD:D1110*73*49~
DTM*472*20140324~
CAS*CO*131*24~
AMT*B6*49~
LX*4~
CLP*7722337*1*1145*14*902*12*119932404007801~
NM1*QC*1*SMITH*SAM****MI*SJD11116~
NM1*82*1*BAN*ERIN****XX*1811901945~
AMT*AU*14~
SVC*AD:D0220*25*14~
DTM*472*20140324~
CAS*CO*131*11~
AMT*B6*14~
SVC*AD:D2790*940*0~
DTM*472*20140324~
CAS*PR*3*756~
CAS*CO*131*184~
SVC*AD:D2950*180*0~
DTM*472*20140324~
CAS*PR*3*146~
CAS*CO*131*34~
LX*5~
CLP*7722337*1*348*16.8*44.2*12*119932404007801~
NM1*QC*1*JONES*SAM****MI*SJD11122~
NM1*82*1*BAN*ERIN****XX*1811901945~
AMT*AU*28~
SVC*AD:D4342*125*0~
DTM*472*20140313~
CAS*CO*131*125~
SVC*AD:D4381*43*0~
DTM*472*20140313~
CAS*PR*3*33~
CAS*CO*131*10~
SVC*AD:D2950*180*16.8~
DTM*472*20140313~
CAS*PR*3*11.2~
CAS*CO*131*152~
AMT*B6*28~
LX*6~
CLP*7722337*1*226*132**12*119932404007801~
NM1*QC*1*JONES*SALLY****MI*SJD11133~
NM1*82*1*BAN*ERIN****XX*1811901945~
AMT*AU*132~
SVC*AD:D0120*46*25~
DTM*472*20140321~
CAS*CO*131*21~
AMT*B6*25~
SVC*AD:D0220*25*14~
DTM*472*20140321~
CAS*CO*131*11~
AMT*B6*14~
SVC*AD:D0230*22*10~
DTM*472*20140321~
CAS*CO*131*12~
AMT*B6*10~
SVC*AD:D0274*60*34~
DTM*472*20140321~
CAS*CO*131*26~
AMT*B6*34~
SVC*AD:D1110*73*49~
DTM*472*20140321~
CAS*CO*131*24~
AMT*B6*49~
LX*7~
CLP*7722337*1*179*108**12*119932404007801~
NM1*QC*1*DOE*SAM****MI*SJD99999~
NM1*82*1*BAN*ERIN****XX*1811901945~
AMT*AU*108~
SVC*AD:D0120*46*25~
DTM*472*20140324~
CAS*CO*131*21~
AMT*B6*25~
SVC*AD:D0274*60*34~
DTM*472*20140324~
CAS*CO*131*26~
AMT*B6*34~
SVC*AD:D1110*73*49~
DTM*472*20140324~
CAS*CO*131*24~
AMT*B6*49~
LX*8~
CLP*7722337*1*129*82**12*119932404007801~
NM1*QC*1*DOE*SUE****MI*SJD88888~
NM1*82*1*BAN*ERIN****XX*1811901945~
AMT*AU*82~
SVC*AD:D0120*46*25~
DTM*472*20140324~
CAS*CO*131*21~
AMT*B6*25~
SVC*AD:D1120*54*37~
DTM*472*20140324~
CAS*CO*131*17~
AMT*B6*37~
SVC*AD:D1208*29*20~
DTM*472*20140324~
CAS*CO*131*9~
AMT*B6*20~
LX*9~
CLP*7722337*1*221*144**12*119932404007801~
NM1*QC*1*DOE*DONNA****MI*SJD77777~
NM1*82*1*BAN*ERIN****XX*1811901945~
AMT*AU*144~
SVC*AD:D0120*46*25~
DTM*472*20140324~
CAS*CO*131*21~
AMT*B6*25~
SVC*AD:D0330*92*62~
DTM*472*20140324~
CAS*CO*131*30~
AMT*B6*62~
SVC*AD:D1120*54*37~
DTM*472*20140324~
CAS*CO*131*17~
AMT*B6*37~
SVC*AD:D1208*29*20~
DTM*472*20140324~
CAS*CO*131*9~
AMT*B6*20~
SE*190*35681~
GE*1*111111123~
IEA*1*111111123~
Детальний опис призначення кожного блоку та сегмента можна дізнатися при аналізі загальної структури 12 і структури шаблону даного типу документа. Але базова концепція загальна для всіх типів — вміст посилки складається з сегментів, розділених символом ~ (у тексті прикладу кожен сегмент виведений з нового рядка, для зручності читання). У свою чергу, кожен сегмент може містити довільне число полів, розділених символом *.
Такі угоди дозволяють нам легко отримати лінійну структуру документа списку сегментів з переліком їх полів. Однак, це недостатньо для відновлення ієрархічної структури блоків документа. Для цього, як я вже згадував, передбачається використовувати схему, яка для більшості типів документів являє собою досить об’ємний файл. Але, оскільки ми не будемо розглядати задачу валідації документа, а обмежимося тільки парсингом, то для наших цілей цілком підійде наступний алгоритм — для кожного сегмента, який зустрівся при послідовному розборі лінійного списку сегментів документа, нам необхідно одержати відповідь на єдине питання: чи формує даний сегмент новий вкладений блок, є закінченням поточного блоку (і одночасно початком наступного), або ж (в інших випадках) є внутрішнім сегментом поточного блоку.
Парсер
Нижче продемонстровано код на Clojure, здійснює парсинг лінійної структури сегментів в ієрархічну структуру блоків. Для задання структури ієрархії використовується декларативний підхід — найпростіша структура даних loops, в якій для перерахованих сегментів окремо визначаються переліки сегментів, що утворюють вкладені блоки, і закінчують поточний блок. Зрозуміло, ця структура залежить від типу документа, вона, власне, й задає його ієрархію. Але наведена нижче функція парсинга є універсальною, і буде працювати на будь-яких коректно заданих таким чином шаблонах структур, звичайно за умови відповідності типу разбираемого документа вибраного шаблону.
;; Parse 835 x12 string to structure hierarchical
(def loops
{"835" {:nested #{"ISA"}}
"ISA" {:nested #{"GS"}}
"GS" {:nested #{ST} :end #{"IEA"}}
"ST" {:nested #{"N1" "LX"} :end #{"GE" "ST"}}
"N1" {:end #{"SE" "LX" "N1"}}
"LX" {:nested #{"CLP"} :end #{"SE" "LX"}}
"CLP" {:nested #{"SVC"} :end #{"SE" "LX" "CLP"}}
"SVC" {:end #{"SE" "LX" "CLP" "SVC"}}})
(defn parser-core [id ss acc]
(let [seg-id (first (first ss))
{:keys [nested end]} (loops id)]
(if (or (empty? ss) (and (contains? end seg-id) (not (empty? acc))))
[acc ss]
(let [[v ss-] (if (contains? nested seg-id)
(parser-core seg-id ss [])
[(first ss) (rest ss)])]
(recur id ss- (conj acc v))))))
(defn segments [s] (str/split (str/trim s) #"~"))
(defn elements [s] (str/split (str/trim s) #"*"))
(defn x12 [s] (first (parser-core "835" (mapv elements (segments (or s ""))) [])))
Під спойлером представлений
Результат парсинга
[[["ISA"
"00"
""
"00"
""
"ZZ"
"EMEDNYBAT "
"ZZ"
"ETIN "
"140305"
"0929"
"^"
"00501"
"111111123"
"0"
"P"
":"]
[["GS" "HP" "EMED" "ETIN" "20140301" "09304100" "111111123" "X" "005010X221A1"]
[["ST" "835" "35681"]
["BPR" "I" "810.8" "C" "CHK" "" "" "" "" "" "" "" "" "" "" "" "20140331"]
["TRN" "1" "12345" "1512345678"]
["REF" "EV" "XYZ CLEARINGHOUSE"]
[["N1" "PR" "OF DENTAL ABC"]
["N3" "225 MAIN STREET"]
["N4" "CENTERVILLE" "PA" "17111"]
["PER" "BL" "JANE DOE" "TE" "9005555555"]]
[["N1" "PE" "BAN DDS LLC" FI "999994703"]]
[["LX" "1"]
[["CLP" "7722337" "1" "226" "132" "" "12" "119932404007801"]
["NM1" "QC" "1" "DOE" "SANDY" "" "" "" "MI" "SJD11112"]
["NM1" "82" "1" "BAN" "ERIN" "" "" "" "XX" "1811901945"]
["AMT" "AU" "132"]
[["SVC" "AD:D0120" "46" "25"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "21"]
["AMT" B6 "25"]]
[["SVC" "AD:D0220" "25" "14"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "11"]
["AMT" B6 "14"]]
[["SVC" "AD:D0230" "22" "10"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "12"]
["AMT" B6 "10"]]
[["SVC" "AD:D0274" "60" "34"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "26"]
["AMT" B6 "34"]]
[["SVC" "AD:D1110" "73" "49"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "24"]
["AMT" B6 "49"]]]]
[["LX" "2"]
[["CLP" "7722337" "1" "119" "74" "" "12" "119932404007801"]
["NM1" "QC" "1" "DOE" "SALLY" "" "" "" "MI" "SJD11111"]
["NM1" "IL" "1" "DOE" "JOHN" "" "" "" "MI" "123456"]
["NM1" "82" "1" "BAN" "ERIN" "" "" "" "XX" "1811901945"]
["AMT" "AU" "74"]
[["SVC" "AD:D0120" "46" "25"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "21"]
["AMT" B6 "25"]]
[["SVC" "AD:D1110" "73" "49"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "24"]
["AMT" B6 "49"]]]]
[["LX" "3"]
[["CLP" "7722337" "1" "226" "108" "24" "12" "119932404007801"]
["NM1" "QC" "1" "SMITH" "SALLY" "" "" "" "MI" "SJD11113"]
["NM1" "82" "1" "BAN" "ERIN" "" "" "" "XX" "1811901945"]
["AMT" "AU" "132"]
[["SVC" "AD:D0120" "46" "25"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "21"]
["AMT" B6 "25"]]
[["SVC" "AD:D0220" "25" "0"]
["DTM" "472" "20140324"]
["CAS" "PR" "3" "14"]
["CAS" "CO" "131" "11"]
["AMT" B6 "14"]]
[["SVC" "AD:D0230" "22" "0"]
["DTM" "472" "20140324"]
["CAS" "PR" "3" "10"]
["CAS" "CO" "131" "12"]
["AMT" B6 "10"]]
[["SVC" "AD:D0274" "60" "34"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "26"]
["AMT" B6 "34"]]
[["SVC" "AD:D1110" "73" "49"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "24"]
["AMT" B6 "49"]]]]
[["LX" "4"]
[["CLP" "7722337" "1" "1145" "14" "902" "12" "119932404007801"]
["NM1" "QC" "1" "SMITH" "SAM" "" "" "" "MI" "SJD11116"]
["NM1" "82" "1" "BAN" "ERIN" "" "" "" "XX" "1811901945"]
["AMT" "AU" "14"]
[["SVC" "AD:D0220" "25" "14"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "11"]
["AMT" B6 "14"]]
[["SVC" "AD:D2790" "940" "0"]
["DTM" "472" "20140324"]
["CAS" "PR" "3" "756"]
["CAS" "CO" "131" "184"]]
[["SVC" "AD:D2950" "180" "0"]
["DTM" "472" "20140324"]
["CAS" "PR" "3" "146"]
["CAS" "CO" "131" "34"]]]]
[["LX" "5"]
[["CLP" "7722337" "1" "348" "16.8" "44.2" "12" "119932404007801"]
["NM1" "QC" "1" "JONES" "SAM" "" "" "" "MI" "SJD11122"]
["NM1" "82" "1" "BAN" "ERIN" "" "" "" "XX" "1811901945"]
["AMT" "AU" "28"]
[["SVC" "AD:D4342" "125" "0"]
["DTM" "472" "20140313"]
["CAS" "CO" "131" "125"]]
[["SVC" "AD:D4381" "43" "0"]
["DTM" "472" "20140313"]
["CAS" "PR" "3" "33"]
["CAS" "CO" "131" "10"]]
[["SVC" "AD:D2950" "180" "16.8"]
["DTM" "472" "20140313"]
["CAS" "PR" "3" "11.2"]
["CAS" "CO" "131" "152"]
["AMT" B6 "28"]]]]
[["LX" "6"]
[["CLP" "7722337" "1" "226" "132" "" "12" "119932404007801"]
["NM1" "QC" "1" "JONES" "SALLY" "" "" "" "MI" "SJD11133"]
["NM1" "82" "1" "BAN" "ERIN" "" "" "" "XX" "1811901945"]
["AMT" "AU" "132"]
[["SVC" "AD:D0120" "46" "25"]
["DTM" "472" "20140321"]
["CAS" "CO" "131" "21"]
["AMT" B6 "25"]]
[["SVC" "AD:D0220" "25" "14"]
["DTM" "472" "20140321"]
["CAS" "CO" "131" "11"]
["AMT" B6 "14"]]
[["SVC" "AD:D0230" "22" "10"]
["DTM" "472" "20140321"]
["CAS" "CO" "131" "12"]
["AMT" B6 "10"]]
[["SVC" "AD:D0274" "60" "34"]
["DTM" "472" "20140321"]
["CAS" "CO" "131" "26"]
["AMT" B6 "34"]]
[["SVC" "AD:D1110" "73" "49"]
["DTM" "472" "20140321"]
["CAS" "CO" "131" "24"]
["AMT" B6 "49"]]]]
[["LX" "7"]
[["CLP" "7722337" "1" "179" "108" "" "12" "119932404007801"]
["NM1" "QC" "1" "DOE" "SAM" "" "" "" "MI" "SJD99999"]
["NM1" "82" "1" "BAN" "ERIN" "" "" "" "XX" "1811901945"]
["AMT" "AU" "108"]
[["SVC" "AD:D0120" "46" "25"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "21"]
["AMT" B6 "25"]]
[["SVC" "AD:D0274" "60" "34"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "26"]
["AMT" B6 "34"]]
[["SVC" "AD:D1110" "73" "49"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "24"]
["AMT" B6 "49"]]]]
[["LX" "8"]
[["CLP" "7722337" "1" "129" "82" "" "12" "119932404007801"]
["NM1" "QC" "1" "DOE" "SUE" "" "" "" "MI" "SJD88888"]
["NM1" "82" "1" "BAN" "ERIN" "" "" "" "XX" "1811901945"]
["AMT" "AU" "82"]
[["SVC" "AD:D0120" "46" "25"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "21"]
["AMT" B6 "25"]]
[["SVC" "AD:D1120" "54" "37"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "17"]
["AMT" B6 "37"]]
[["SVC" "AD:D1208" "29" "20"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "9"]
["AMT" B6 "20"]]]]
[["LX" "9"]
[["CLP" "7722337" "1" "221" "144" "" "12" "119932404007801"]
["NM1" "QC" "1" "DOE" "DONNA" "" "" "" "MI" "SJD77777"]
["NM1" "82" "1" "BAN" "ERIN" "" "" "" "XX" "1811901945"]
["AMT" "AU" "144"]
[["SVC" "AD:D0120" "46" "25"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "21"]
["AMT" B6 "25"]]
[["SVC" "AD:D0330" "92" "62"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "30"]
["AMT" B6 "62"]]
[["SVC" "AD:D1120" "54" "37"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "17"]
["AMT" B6 "37"]]
[["SVC" "AD:D1208" "29" "20"]
["DTM" "472" "20140324"]
["CAS" "CO" "131" "9"]
["AMT" B6 "20"]]]]
["SE" "190" "35681"]]
["GE" "1" "111111123"]]
["IEA" "1" "111111123"]]]
нашого вихідного прикладу — ієрархічна структура сегментів.
Власне, на це завдання можна вважати вирішеною. Десяток рядків Clojure-коду забезпечують нам повнофункціональний парсер будь-яких X12 документів в ієрархічне AST. Але для повноти картини можна показати приклад обходу даного AST для виконання якої-небудь корисної завдання — наприклад, конструювання структур необхідного формату і запису цієї інформації в базу даних. Нижче представлений приклад коду, який обходить распарсенную структуру, і створює на її основі списку об’єктів. Пара універсальних функцій-помічники для зручного доступу до даних, як вони представлені в AST, і обхідник дерева, формує об’єкт з можливістю звернення при цьому до вихідним даним на будь-якому рівні ієрархії.
;; util helper for information extracting
(defn v-prefix? [v p]
(and
(vector? v)
(= p (if (vector? p) (subvec v 0 (min (count v) (count p))) (get v 0)))))
(defn items [v p & path] (filter #(v-prefix? (get in % (vec path)) p) v))
(defn item [v p & path] (first (apply items v p path)))
;; function test for extracting human-readable structure
(defn tst [x12-string]
(for [isa (items (x12 x12-string) "ISA" 0)
gs (items isa "GS" 0)
st (items gs ST 0)
lx (items st "LX" 0)
clp (items lx "CLP" 0)]
(let [bpr (item st "BPR")]
{:message {:received (get in isa [0 9])
:created (get in gs [0 4])}
:transaction {:check (get (item st "TRN") 2)
:payed (get bpr 16)
:total (read-string (get bpr 2))}
:insurer (get in (item st ["N1" "PR"] 0) [0 2])
:organization (get in (item st ["N1" "PE"] 0) [0 2])
:claim {:patient (if-let [x (item clp ["NM1" "QC"])]
(str (get x 3) "" (get x 4)))
:total (read-string (get in clp [0 4]))}
:services (mapv
(fn [svc]
{:code (get in svc [0 1])
:amount (read-string (get in svc [0 3]))
:date (get (item svc ["DTM" "472"]) 2)})
(items clp "SVC" 0))})))
Під спойлером представлений
Результат роботи функції
({:message {:received "140305", :created "20140301"},
:transaction {:check "12345", :payed "20140331", :total 810.8},
:insurer "DENTAL OF ABC",
:organization "BAN DDS LLC",
:claim {:patient "DOE SANDY", :total 132},
:services
[{:code "AD:D0120", :amount 25, :date "20140324"}
{:code "AD:D0220", :amount 14, :date "20140324"}
{:code "AD:D0230", :amount 10, :date "20140324"}
{:code "AD:D0274", :amount 34, :date "20140324"}
{:code "AD:D1110", :amount 49, :date "20140324"}]}
{:message {:received "140305", :created "20140301"},
:transaction {:check "12345", :payed "20140331", :total 810.8},
:insurer "DENTAL OF ABC",
:organization "BAN DDS LLC",
:claim {:patient "DOE SALLY", :total 74},
:services
[{:code "AD:D0120", :amount 25, :date "20140324"}
{:code "AD:D1110", :amount 49, :date "20140324"}]}
{:message {:received "140305", :created "20140301"},
:transaction {:check "12345", :payed "20140331", :total 810.8},
:insurer "DENTAL OF ABC",
:organization "BAN DDS LLC",
:claim {:patient "SMITH SALLY", :total 108},
:services
[{:code "AD:D0120", :amount 25, :date "20140324"}
{:code "AD:D0220", :amount 0, :date "20140324"}
{:code "AD:D0230", :amount 0, :date "20140324"}
{:code "AD:D0274", :amount 34, :date "20140324"}
{:code "AD:D1110", :amount 49, :date "20140324"}]}
{:message {:received "140305", :created "20140301"},
:transaction {:check "12345", :payed "20140331", :total 810.8},
:insurer "DENTAL OF ABC",
:organization "BAN DDS LLC",
:claim {:patient "SMITH SAM", :total 14},
:services
[{:code "AD:D0220", :amount 14, :date "20140324"}
{:code "AD:D2790", :amount 0, :date "20140324"}
{:code "AD:D2950", :amount 0, :date "20140324"}]}
{:message {:received "140305", :created "20140301"},
:transaction {:check "12345", :payed "20140331", :total 810.8},
:insurer "DENTAL OF ABC",
:organization "BAN DDS LLC",
:claim {:patient "JONES SAM", :total 16.8},
:services
[{:code "AD:D4342", :amount 0, :date "20140313"}
{:code "AD:D4381", :amount 0, :date "20140313"}
{:code "AD:D2950", :amount 16.8, :date "20140313"}]}
{:message {:received "140305", :created "20140301"},
:transaction {:check "12345", :payed "20140331", :total 810.8},
:insurer "DENTAL OF ABC",
:organization "BAN DDS LLC",
:claim {:patient "JONES SALLY", :total 132},
:services
[{:code "AD:D0120", :amount 25, :date "20140321"}
{:code "AD:D0220", :amount 14, :date "20140321"}
{:code "AD:D0230", :amount 10, :date "20140321"}
{:code "AD:D0274", :amount 34, :date "20140321"}
{:code "AD:D1110", :amount 49, :date "20140321"}]}
{:message {:received "140305", :created "20140301"},
:transaction {:check "12345", :payed "20140331", :total 810.8},
:insurer "DENTAL OF ABC",
:organization "BAN DDS LLC",
:claim {:patient "DOE SAM", :total 108},
:services
[{:code "AD:D0120", :amount 25, :date "20140324"}
{:code "AD:D0274", :amount 34, :date "20140324"}
{:code "AD:D1110", :amount 49, :date "20140324"}]}
{:message {:received "140305", :created "20140301"},
:transaction {:check "12345", :payed "20140331", :total 810.8},
:insurer "DENTAL OF ABC",
:organization "BAN DDS LLC",
:claim {:patient "DOE SUE", :total 82},
:services
[{:code "AD:D0120", :amount 25, :date "20140324"}
{:code "AD:D1120", :amount 37, :date "20140324"}
{:code "AD:D1208", :amount 20, :date "20140324"}]}
{:message {:received "140305", :created "20140301"},
:transaction {:check "12345", :payed "20140331", :total 810.8},
:insurer "DENTAL OF ABC",
:organization "BAN DDS LLC",
:claim {:patient "DOE DONNA", :total 144},
:services
[{:code "AD:D0120", :amount 25, :date "20140324"}
{:code "AD:D0330", :amount 62, :date "20140324"}
{:code "AD:D1120", :amount 37, :date "20140324"}
{:code "AD:D1208", :amount 20, :date "20140324"}]})
— можна подавати до столу писати в базу, візуалізувати на UI або використовувати як-небудь ще
Подібний код та алгоритм парсинга X12 документів використовується у моєму робочому проекті — зрозуміло, з купою додаткової функціональності. Приклади коду в статті — це мінімальний робочий прототип для демонстрації алгоритму і підходу. Сорі, що без абстрактних фабрик фабрик, комбінаторних парсерів, рекурсивних граматик та інших серйозних речей — усього 3 десятка рядків коду )
Бажаючі можуть погратися з даними парсером в будь-якому онлайн-репле, підтримує Clojure — ideone/replit/etc. Із залежностей потрібно підключити лише нэймспейс clojure.string, ну і можливо clojure.pprint для красивої друку результатів. Можна спробувати змінювати код тестової функції створення об’єкта, отримувати інші поля з распарсенной структури і т. п. Приклади X12 документів типу 835 (claim response) можна знайти в мережі.