При создании приложения, активно взаимодействующего со сторонними сервисами и системами, часто требуется обеспечить обмен информацией с ними, односторонний или двусторонний
При этом зачастую сторонний сервис предоставляет единственный формат и структуры данных для такого взаимодействия.
Одним из таких форматов электронного документооборота является EDI ANSI ASC X12, достаточно подробное описание которого приведено по ссылке.
КДПВ была взята с этого сайта
Под катом приведен простой алгоритм парсера X12 и код на Clojure, реализующий парсер и пример обработки распарсенных данных.
Цитируя вышеприведенную ссылку:
Поэтому вы не увидите в нем никаких человекочитаемых красот, как в XML. И хотя стандарт позволяет создавать документы достаточно сложной иерархической структуры, с наличием блоков и так называемых loop-ов, тем не менее даже закрывающие теги для всех блоков (кроме ISA/GS/ST) не предусмотрены. По ссылке до ката желающие могут в деталях ознакомиться со структурой и описанием формата, далее мы будем касаться только необходимых вещей.
Каждый тип документа имеет свою структуру шаблона, в котором указывается смысл и назначение отдельных полей и сегментов, их типы и возможные значения, а также перечень обязательных и опциональных сегментов и блоков. Поддерживается версионирование шаблонов, информация о типе и номере версии передается в соответствующих полях документа. Предполагается, что именно с использованием шаблона конкретного типа документа должен производиться его разбор и валидация.
Ниже представлен пример документа, содержащего несколько транзакций типа 835 (документ типа claim response), на котором будет продемонстрирован парсинг и последующая обработка данных.
Детальное описание назначения каждого блока и сегмента можно узнать при анализе общей структуры 12 и структуры шаблона данного типа документа. Но базовая концепция общая для всех типов — содержимое посылки состоит из сегментов, разделенных символом ~ (в тексте примера каждый сегмент выведен с новой строки, для удобочитаемости). В свою очередь, каждый сегмент может содержать произвольное число полей, разделенных символом *.
Такие соглашения позволяют нам легко получить линейную структуру документа как списка сегментов с перечнем их полей. Однако, это недостаточно для восстановления иерархической структуры блоков документа. Для этого, как я уже упоминал, предполагается использовать схему, которая для большинства типов документов представляет собой достаточно объемный файл. Но, поскольку мы не будем рассматривать задачу валидации документа, а ограничимся только парсингом, то для наших целей вполне подойдет следующий алгоритм — для каждого сегмента, встретившегося при последовательном разборе линейного списка сегментов документа, нам необходимо получить ответ на единственный вопрос: формирует ли данный сегмент новый вложенный блок, является ли окончанием текущего блока (и одновременно началом следующего), или же (в остальных случаях) является внутренним сегментом текущего блока.
Ниже продемонстрирован код на Clojure, осуществляющий парсинг линейной структуры сегментов в иерархическую структуру блоков. Для задания структуры иерархии используется декларативный подход — простейшая структура данных loops, в которой для перечисленных сегментов отдельно задаются перечни сегментов, образующих вложенные блоки, и заканчивающих текущий блок. Разумеется, эта структура зависит от типа документа, она, собственно, и задает его иерархию. Но приведенная ниже функция парсинга является универсальной, и будет работать на любых корректно заданных таким образом шаблонах структур, конечно при условии соответствия типа разбираемого документа выбранному шаблону.
Под спойлером представлен
нашего исходного примера — иерархическая структура сегментов.
Собственно, на этом задачу уже можно считать решенной. Десяток строк Clojure-кода обеспечивают нам полнофункциональный парсер любых X12 документов в иерархическое AST. Но для полноты картины можно показать пример обхода данного AST для выполнения какой-нибудь полезной задачи — например, конструирования структур требуемого формата и записи этой информации в базу данных. Ниже представлен пример кода, который обходит распарсенную структуру, и создает на ее основе список объектов. Пара универсальных функций-хэлперов для удобного доступа к данным, как они представлены в AST, и обходчик дерева, формирующий объект с возможностью обращения при этом к исходным данным на любом уровне иерархии.
Под спойлером представлен
— можноподавать к столу писать в базу, визуализировать на UI или использовать как-либо еще
Подобный код и алгоритм парсинга X12 документов используется в моем рабочем проекте — разумеется, с кучей дополнительной функциональности. Примеры кода в статье — это минимальный рабочий прототип для демонстрации алгоритма и подхода. Сорри, что без абстрактных фабрик фабрик, комбинаторных парсеров, рекурсивных грамматик и прочих серьезных вещей — всего 3 десятка строк кода )
Желающие могут поиграться с данным парсером в любом онлайн-репле, поддерживающем Clojure — ideone/replit/etc. Из зависимостей требуется подключить только нэймспейс clojure.string, ну и возможно clojure.pprint для красивой печати результатов. Можно попробовать изменять код тестовой функции создания объекта, получать другие поля из распарсенной структуры и т.п. Примеры X12 документов типа 835 (claim response) можно найти в сети.
При этом зачастую сторонний сервис предоставляет единственный формат и структуры данных для такого взаимодействия.
Одним из таких форматов электронного документооборота является 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 hierarchical structure
(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" "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"]]]
нашего исходного примера — иерархическая структура сегментов.
Собственно, на этом задачу уже можно считать решенной. Десяток строк Clojure-кода обеспечивают нам полнофункциональный парсер любых X12 документов в иерархическое AST. Но для полноты картины можно показать пример обхода данного AST для выполнения какой-нибудь полезной задачи — например, конструирования структур требуемого формата и записи этой информации в базу данных. Ниже представлен пример кода, который обходит распарсенную структуру, и создает на ее основе список объектов. Пара универсальных функций-хэлперов для удобного доступа к данным, как они представлены в AST, и обходчик дерева, формирующий объект с возможностью обращения при этом к исходным данным на любом уровне иерархии.
;; util helpers for extracting information
(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)))
;; test function 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"}]})
— можно
Подобный код и алгоритм парсинга X12 документов используется в моем рабочем проекте — разумеется, с кучей дополнительной функциональности. Примеры кода в статье — это минимальный рабочий прототип для демонстрации алгоритма и подхода. Сорри, что без абстрактных фабрик фабрик, комбинаторных парсеров, рекурсивных грамматик и прочих серьезных вещей — всего 3 десятка строк кода )
Желающие могут поиграться с данным парсером в любом онлайн-репле, поддерживающем Clojure — ideone/replit/etc. Из зависимостей требуется подключить только нэймспейс clojure.string, ну и возможно clojure.pprint для красивой печати результатов. Можно попробовать изменять код тестовой функции создания объекта, получать другие поля из распарсенной структуры и т.п. Примеры X12 документов типа 835 (claim response) можно найти в сети.