3
\$\begingroup\$

I have a little experience working with .hl7 data (format for transmitting health data), so I decided to try and write an .hl7 parser using fparsec.

A standard .hl7 segment is a single line with:

  1. First 3 chars are the header
  2. Next 5 chars are the separators (typically |^~\&)
  3. Fields / repetitions delimited by pipes

A segment has fields or repetitions (repeating fields). A field can have components. A component can have sub-components.

Sample .hl7 message header:

MSH|^~\\&|Location|SendingApp|DateTimeOfMessage

I am looking for feedback on data design (the types) and the functions.

type Subcomponent = {value:string; position:int}
type Component = {subcomponents: Subcomponent list; position:int}
type SingleField = {components: Component list; position:int}
type Field = Repetitions of SingleField list | SingleField of SingleField
type Segment = { name:string; fields:Field list; }
type Hl7Message = { segments:Segment list }

let hl7Seps = "|^~\\&"

let normalChar = noneOf hl7Seps

let unescape c = match c with
                    | 'F' -> '|'
                    | 'R' -> '~'
                    | 'S' -> '^'
                    | 'T' -> '&'
                    | 'E' -> '\\'
                    | c -> c

let escapedChar = attempt (pchar '\\' >>. anyChar |>> unescape .>> skipChar '\\') <|> pchar '\\'

let pHl7Element = manyChars (normalChar <|> escapedChar)

let pcomp = sepBy pHl7Element (pchar '&') |>> (fun vals -> List.mapi (fun i s -> {value = s; position = i}) vals)

let pfield = sepBy pcomp (pchar '^') |>> (fun comps -> List.mapi (fun i c -> {subcomponents = c; position = i}) comps)

let pRepsOrField = sepBy pfield (pchar '~') |>> (fun fields -> match fields.Length with
                                                                | 0 | 1 -> SingleField {components = fields.Item 0; position = 0}
                                                                | _ -> Repetitions (List.mapi (fun i c -> {components = c; position = i}) fields))

let pheader = anyString 3 |>> (fun name -> name)

let pSegment = pipe2 pheader (sepBy pRepsOrField (pchar '|')) (fun name repsOrFields -> {name = name; fields = repsOrFields})

test pSegment "EVN|A&1^B^C|123~456~789"
\$\endgroup\$
  • \$\begingroup\$ Ok. First time on code-review. Will remove that question. \$\endgroup\$ – Joseph Evensen Sep 13 '16 at 13:26
2
\$\begingroup\$

I am not really familiar with Fparsec, but a few of your functions could be simplified, or written better:

For example :

let pcomp = sepBy pHl7Element (pchar '&') |>> (fun vals -> List.mapi (fun i s -> {value = s; position = i}) vals)

Using partial application, it could be written as :

let pcomp = sepBy pHl7Element (pchar '&') |>> (List.mapi (fun i s -> { value = s; position = i }))

And the one after it:

// Before
let pfield = sepBy pcomp (pchar '^') |>> (fun comps -> List.mapi (fun i c -> {subcomponents = c; position = i}) comps)
// After
let pfield = sepBy pcomp (pchar '^') |>> (List.mapi (fun i c -> { subcomponents = c; position = i }))

Also, (fun x -> x) could be simply replaced by id.

let pheader = anyString 3 |>> id

On other matters, there is a potential bug in your code : in your pRepsOrField function, you're matching field.Length with 0 then extracting the first item, which will crash your code. Using the Length property to find out the length is .. unusual. You don't even need it. Pattern matching with lists based on length in F# is usually done this way:

match fields with
| [] -> // do something
| [ c ] ->  SingleField { components = c; position = 0 }
| _ -> Repetitions(List.mapi (fun i c -> { components = c; position = i }) fields))

Now you don't have to use extra methods/properties to get the length and and the first item. It is all right there ...

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.