Stuart Byrne · Mar 16, 2023 go to post

Hi Mike,
Could you use an XLST to parse the XML and pass the value fields by reference into COS variables?
I've done something similar on a smaller scale for some IHE transactions (Albeit in HealthShare), but I've used XLST in IRIS as well.
I can't get to the Caché 2018.1 docs to check.
 

Stuart Byrne · Nov 9, 2022 go to post

Hi @Kathy Ward 
Are you trying to transform from HL7 2.x to HL7 2.x?
If that's the case you should be able to map the OBX()-5 from target to source.

Regards

Stuart

Stuart Byrne · Sep 7, 2022 go to post

The use of sub transforms is always subjective subject.
Go what feels best for you if your having many lines for a specific section / function then a sub-transform is suitable.

Stuart Byrne · Sep 7, 2022 go to post

Your getting that error as your class is named %BI.Adaptor.

Create a custom name and extend %BI.Adaptor

Stuart Byrne · Aug 19, 2022 go to post

The key (k1)  is missing from your targetObj:
<subtransform class='ADT.DTL.SDAToPatientIdentifierInfo' targetObj='target.PatientIdentifierInfo.()' sourceObj='source.{Patient.Extension.PatientIdentifiers(k1)}' />

In your master transform is this housed within a foreach loop and k1 defined?

Stuart Byrne · Mar 15, 2021 go to post

Thanks for the advice @Vitaly Furman, I used  square bracket syntax as I was iterating over an array.

I did try my code using {} brackets and my code still isn't ending the nested IF loops upon finding the field and value that match.

Regards

Stuart

Stuart Byrne · Mar 14, 2021 go to post

Source JSON:

{
  "patient": [
    {
      "age": 51,
      "name": {
        "forename": "Bill",
        "surname": "Brighthouse"
      }
    },
    {
      "age": 2,
      "name": {
        "forename": "Dickie",
        "surname": "Bird"
      }
    }
  ]
}

Stuart Byrne · Mar 14, 2021 go to post

Hi Joey, 

@Arun Tiriveedhi is correct that you need to use square bracket syntax, as this returns a comma delimited list.

I had a case where I had a whitelist of OBR-4.1 values for filtering result messages to a particular system.

@Stephen Renshaw made a great suggestion to create a function to loop through the list and query the lookup table here: https://community.intersystems.com/post/using-lookup-or-exists-repeating-fields-ensemble-business-rule

Regards

Stuart
 

Stuart Byrne · Nov 28, 2020 go to post

Thanks David, your suggestion and some advice via the WRC enabled me to connect over FTPS and collect the text reports from the 3rd party.

Stuart Byrne · Nov 28, 2020 go to post

Thank you Stephen, I used your advice on square brackets and your method as a template to write my own.

Thank you for your guidance.

Stuart

Stuart Byrne · Jun 23, 2020 go to post

I've tried testing in the console using the %Net.FTP Command.

I ran the following (server name substituted):

do ftpcall.Connect("sftp.ldh.nhs.uk",username,pwd,"990")

I did a zwrite and found the connection was sucessful and on:

I got the following for the Return message:

write !, ftpcall.ReturnMessage
 
Features:
 MDTM
 REST STREAM
 SIZE
 MLST type*;size*;modify*;
 MLSD
 AUTH SSL
 AUTH TLS
 PROT
 PBSZ
 UTF8
 CLNT
 MFMT
 EPSV
 EPRT
End

This says it supports PROT protocol, but I am not sure from the %Net.FTP documentation how to set this feature.

Stuart Byrne · Jun 6, 2020 go to post

Hi Kevin,

I had gone down your route in the end. 

I had been using the FindSegment method from the EnsLib.HL7.Message, which uses the 2nd input variable, however I was having issues implementing the key byref.

Creating my own counter using the same as you suggested with   

"set obxCounter=source.GetValueAt("PIDgrpgrp("_(1)_").ORCgrp("_(1)_").OBXgrp("_("*")_")") "  and then using the full HL7 segment structure to retrieve the fields removed all woes.

Thank you Vic for your help too, I think my use of the FindSegment method was causing my issues.

Stuart Byrne · Jun 4, 2020 go to post

Thanks Vic,

The incoming messages have these set already:

  DocTypeCategory 2.4
  DocTypeSecondary ORU_R01
  DocTypeName

ORU_R01

pMsg is set to  EnsLib.HL7.Message, which  information should be populated when the method is called right?

Stuart

Stuart Byrne · Jun 4, 2020 go to post

Thanks Vic,

I fixed the mistake, but am still having issues.

I tried set obxcnt = pMSg.GetValueAt("OBX(*)"), but the counter doesn't get populated.

Stuart Byrne · Mar 10, 2020 go to post

Hi Edward,

I've posted an excerpt of the code as I want to save the classmethod for another post.

Memory/speed shouldn't be an issue as the size of the lists should be <30 elements.

Thanks

Stuart

Stuart Byrne · Mar 10, 2020 go to post

Excerpt of Code as Promised:

set list = "ABC~DEF~GHI~JKL~MNO~[~PQR~]"

set SegmentList = $LISTFROMSTRING(list,"~")
Set SegmentCount = $LISTLENGTH(SegmentList)
FOR i = 1:1:SegmentCount{
IF ($LISTGET(SegmentList,i,i)= "[") {
set $LIST(SegmentList,i,i)=""
set OptionalSegment = "Y" 
}
ELSEIF ($LISTGET(SegmentList,i,i)= "]") {
set $LIST(SegmentList,i,i)=""
}
ELSEIF ($LISTGET(SegmentList,i,i)= "") {
QUIT
}
Write !, "Current Segment: "_$LISTGET(SegmentList,i,i)
Set SegmentFields = $LISTFROMSTRING(object.SegmentSubStructure,"~")
Set SegFieldCount = $LISTLENGTH(SegmentFields)
Stuart Byrne · Feb 21, 2020 go to post

Hi  Evgeny Shvarov ,

Please can you assist.

The reason for the change is that my existing email address is with my current employer.

I am leaving my organisation and will lose the access to this address with it.

I do not wish to lose my content or at least the ownership of it.

Thanks in advance.

Kind Regards

Stuart

Stuart Byrne · Feb 21, 2020 go to post

I am also having issues changing my email address for the developer community.

I don't have access to the WRC either.

Please could you advise on how to proceed.

Stuart Byrne · Dec 18, 2019 go to post

Thanks Jeffrey.

I've spoken to our Caché ODBA and he concurs there are orphaned records in the Ens.Message* globals.

We're on 2017.1 currently, but have been live since Caché 2012, which may explain todays issues.

Looking at the purge tasks we have records with no timestamp, which would escape the purging process.

Our Caché ODBA is going to open a WRC to confirm the best course of action for a one off purge of these Ens.Message* globals.

Thank you so much for your advice as it has helped immensely identifying this issue.

Stuart Byrne · Dec 17, 2019 go to post

Hi Jeffrey,

SELECT COUNT(ID) & SELECT COUNT (*) are timing out via the System>SQL pages.

I had a look at our purge settings and we have "BodiesToo" unchecked and "KeepIntegrity" checked.

The next way forward would be to investigate the effect of purging with the "BodiesToo" and see if this reduces the EnsAlertRequest global size.

Stuart Byrne · Dec 17, 2019 go to post

Please find my method below which is used to drive a Business Operation.  It's activated by a schedule on a daily basis at 6am:

Method OnInit(Output pResponse As Ens.Response) As %Status
{
//set the DST variable
set DST = $SYSTEM.Util.IsDST()
// set pre-DST edited timestamps for the email header
set emailSummaryStartDAT = $ZDATE($H-1,3)_" "_(..SummaryStartTime)
set emailSummaryFinishDAT = $ZDATE($H,3)_" "_(..SummaryFinishTime)
// ..SummaryStartTime DST handling IF DST =1 adjust SummaryStartTime & SummaryFinishTime Property's H-1
IF DST = 1 {
set ..SummaryStartTime = ($P(..SummaryStartTime,":",1)-1)_":"_$P(..SummaryStartTime,":",2,3)
set ..SummaryFinishTime = ($P(..SummaryFinishTime,":",1)-1)_":"_$P(..SummaryFinishTime,":",2,3)
$$$LOGINFO("Daylight Savings is being applied to timestamps")
}
// IF the hour part of timestamp is <10 then add a leading 0 to make a full UTC timestamp
IF ($P(..SummaryStartTime,":",1)<10){
set ..SummaryStartTime = "0"_..SummaryStartTime
}
IF ($P(..SummaryFinishTime,":",1)<10){
set ..SummaryFinishTime = "0"_..SummaryFinishTime
}
//Set Search Start Date/Time of yesterday with SummaryStartTime
set SummaryStartDAT = $ZDATE($H-1,3)_" "_(..SummaryStartTime)
//Set Search Finish Date/Time for today with SummaryFinishtime
set SummaryFinishDAT = $ZDATE($H,3)_" "_(..SummaryFinishTime) //Obtain the ID of the first and last message in the search timeframe
&sql(SELECT MAX(ID),MIN(ID),COUNT(ID)INTO :LastID,:FirstID,:ErrorCount FROM Ens.AlertRequest WHERE AlertTime BETWEEN :SummaryStartDAT AND :SummaryFinishDAT) /// Log Ens.AlertRequest ID range and the no. of errors
$$$LOGINFO("AlertRequest ID's identified between: "_FirstID_" & "_LastID_" No. of Errors is: "_ErrorCount)
  //build the email subject line
  Set Subject = "Automatic alert from AIE: "_$NAMESPACE_" Email Alert Summary between:"_emailSummaryStartDAT_" & "_emailSummaryFinishDAT
  //build the email Body
  Set emailBody = "This is a summary of alerts generated by the CUH Application Integration Engine (AIE)."
  set emailBody = emailBody_"<br><br>"
  set emailBody = emailBody_"This email covers alerts between: "_emailSummaryStartDAT_" & "_emailSummaryFinishDAT
  set emailBody = emailBody_"<br><br>"
  Set emailBody = emailBody_"Namespace: "_$NAMESPACE
  Set emailBody = emailBody_"<br><br>"
  set emailBody = emailBody_"No of errors is: "_ErrorCount
  Set emailBody = emailBody_"<br><br>"
  // Check if any errors have been identified
  //If errors identified create table
  IF ErrorCount '= 0 {
  // set table header
  set errortbl = "<head><style>table, th, td { border: 0px solid black;}</style></head><table style='width:100%'><tr><th>Alert ID</th><th>AlertText</th><th>AlertTime</th><th>SourceConfigName</th> </tr>"
  //Populate the error table using sql cursor query to loop through the SQL resultset.
  &sql(DECLARE C1 CURSOR FOR SELECT ID, AlertText, AlertTime, SourceConfigName INTO :ID, :AlertText, :AlertTime, :SourceConfigName  FROM Ens.AlertRequest WHERE AlertTime BETWEEN :SummaryStartDAT AND :SummaryFinishDAT ORDER BY SourceConfigName, AlertTime ASC)
  &sql(OPEN C1)
  IF SQLCODE '= 0 {
  $$$LOGERROR("SQL Error Code: "_SQLCODE)
  }
QUIT:(SQLCODE'=0)
&sql(FETCH C1)
//Loop through Alert fields from the time period specified.
While SQLCODE = 0 {
IF DST = 1 {
set AlertHour = $P(AlertTime," ",2)
set $P(AlertHour,":",1) = ($P(AlertHour,":",1)+1)
// Check if updated time is before 10:00am if so a "0" prefix needs adding
IF ($P(AlertHour,":",1)<10){
set $P(AlertHour,":",1) = "0"_$P(AlertHour,":",1)
}
set AlertTime = ($P(AlertTime," ",1)_" "_AlertHour)
  }
Set errortbl = errortbl_"<tr> <td>"_ID_"</td> <td>"_AlertText_"</td> <td>"_AlertTime_"</td>"
set errortbl = errortbl_"<td>"_SourceConfigName_"</td></tr>"
&sql(FETCH C1)
}
&sql(CLOSE C1)
//End of HTML Table
set errortbl = errortbl_"</table>"
Set errortbl = errortbl_"<br>"
set emailBody = emailBody_errortbl}
//handling for no errors. No table is created and comment below written
ELSE {set emailBody = emailBody_"No errors have been identified between: "_emailSummaryStartDAT_" & "_emailSummaryFinishDAT_"<br><br>"}
  // Set Email Footer
  set emailBody = emailBody_"Do NOT reply to this email."
  Set emailBody = emailBody_"<br>"
Set emailBody = emailBody_"<br>Application Integration Engine (AIE) | eHospital | "
Set emailBody = emailBody_"<br>Cambridge University Hospitals NHS Foundation Trust | Addenbrookes Hospital | Box 117 |"
Set emailBody = emailBody_"<br>"
Set emailBody = emailBody_"This email is confidential, see www.cuh.org.uk/email_disclaimer.html"
  // Build new mail structure.
$$$LOGINFO("Attempting to send Alert Summary email")
IF (..From = "") {
SET From = "noreply@addenbrookes.nhs.uk"
}
IF (..To = "") {
$$$LOGWARNING("Warning - No email provided => No email sent")
}
ELSE {
SET mail = ##class(%Net.MailMessage).%New()
// Define sender.
    SET mail.From = ..From
    // Define recipients.
    // Converts String into list of string.
    DO mail.To.InsertList($lfs(..To,";"))
// Define subject.
    SET mail.Subject = Subject
//Char set to "utf-8" to allow for html manipulation
SET mail.Charset = "utf-8"
SET mail.ContentType = "text/html"
SET mail.IsHTML = 1
// Define body.
DO mail.TextData.WriteLine(emailBody)
// Check if high priority is needed.
set Priority = 1
IF Priority = 1 {
DO mail.Headers.SetAt(1,"X-Priority")
  DO mail.Headers.SetAt("High","X-MSMail-Priority")
  DO mail.Headers.SetAt("High","Importance")
}
// Build the smtp server to send the email.
SET server=##class(%Net.SMTP).%New()
SET server.smtpserver=..SMTPServer
SET server.port=..SMTPPort
SET status=server.Send(mail)
   If $$$ISERR(status) {
QUIT $$$ISERR(status)
}
$$$LOGINFO("Alert Summary Email has been sent")
}
QUIT $$$OK
} }
Stuart Byrne · Jul 23, 2019 go to post

Have you tried applying a schedule to it, so it's only active when you want to send out the critical alerts summary.

Stuart Byrne · Jul 23, 2019 go to post

We are currently implementing something similar to Jeff's code above.

We're trying to reduce the amount of alert spam

For the out of hours alerts, I have them sent to a custom BO (business operation) where we send a summary in a single email.

This way you don't lose what's alerted overnight and take action if necessary.

Stuart Byrne · Apr 15, 2019 go to post

The object of part 2 is to compare the strings from the same input as part 1 and identify which 2 strings differ by 1 character in the same position.

Use the links above to find the exercise. 

My code goes:

ClassMethod Part2SQL()
{
//initialising varibles
 set rowcount = ""
 set line = ""
  // Get the highest row number in the table WDA
 &sql(SELECT TOP 1 ID INTO :rowcount FROM AOC2018.Day2 ORDER BY ID DESC)
 Write !, "Row count is: "_rowcount
 // Loop through each row in the table
 FOR i=1:1:rowcount{
 // initialise looping variables
 set mismatchcount = "0"
 set complinelength = ""
 set compline = ""
 set compchar = ""
 // run SQL query to look at last number for row loop.
 &sql(SELECT WholeLine INTO :line FROM AOC2018.Day2 WHERE ID = :i)
 set linelength = $LENGTH(line)
 set complinelength = linelength
 //set currrentrow = ##class(AOC2018.Day2).%OpenId(i)
 FOR comprow = i+1:1:rowcount{
 set mismatchchar1 = ""
 set mismatchchar2 = ""
 set mismatchpos = ""
 //IF comprow = rowcount+1 {QUIT}
 &sql(SELECT WholeLine INTO :compline FROM AOC2018.Day2 WHERE ID = :comprow)
 IF compline = "" {QUIT}
 set mismatchcount = "0"
 For char=1:1:linelength{
 /// get current character being looked for
 set currentchar = $EXTRACT(line,char,char)
 set compchar = $EXTRACT(compline,char,char)
 IF compchar '= currentchar{
 set mismatchcount = $INCREMENT(mismatchcount)
 set mismatchchar1 = currentchar
 set mismatchchar2 = compchar
 set mismatchpos = char
 }
 QUIT:(mismatchcount >1)
 }
 QUIT:(mismatchcount =1)
 }
 QUIT:(mismatchcount =1)
 }
 write !, "The lines with only one mismatch are:"
 write !, "1st line: "_line_" On row: "_i
 write !, "2nd Line: "_compline_" On row: "_comprow
 write !, "The char in the mismatch in line: "_i_". The char is: "_mismatchchar1
 write !, "The char in the mismatch in line: "_comprow_". The char is: "_mismatchchar2
 write !, "With character position: "_mismatchpos
 set answerfirsthalf = $EXTRACT(line,1,mismatchpos-1)
 set answersecondhalf = $EXTRACT(line,mismatchpos+1,linelength)
 write !, "The matching text is: "_answerfirsthalf_answersecondhalf
}
Stuart Byrne · Mar 2, 2019 go to post

AOC 2018 Day1 Part 2

As promised in the post above, I have posted part 2 in the comments.

The summary of part 2 is to find the first frequency that repeats twice.

In this published method rather than looping through a string, I looped through the first set of sequence changes (990) via a sql query.

This version was much quicker than parsing the string of frequency changes through each loop.  I added a kill global to the first method, so I could use this to generate the first loop of values before testing/debugging this script.

In hindsight I didn't need to do this and could do something inside this method.

Let me know what you think:

 ClassMethod FreqPart2(Input As %String)
{
//This needs to be run after ImportFreqPI method as expects Global to be populated
// set previous record counter to 0 
    set prevrecord = 0 
    //Find the last record to append to in AOC2018.Day1 global
    &sql(SELECT TOP 1 ID INTO :prevrecord FROM AOC2018.Day1 ORDER BY ID DESC)
    //Set Prev frequency to 0 then get value via SQL query
    set prevfreq = 0
    &sql(SELECT CurrentFrequency INTO :prevfreq FROM AOC2018.Day1 WHERE ID = :prevrecord)
    write prevfreq
    set end = 990
   
PILOOP
    // Loop through list
    FOR i=1:1:end{
    //create new object to save to Global
    set newrecord = ##class(AOC2018.Day1).%New()
    // Set Freqency counter to 0, so when used later is only value from current loop
    set FreqCount = 0
    //set freqcount variable to newrecord.Freqcount so can be saved correctly .
    set FreqCount = newrecord.FreqCount
    //set current value in list
    &sql(SELECT FrequencyChange INTO :insertitem FROM AOC2018.Day1 WHERE ID = :i )
    //write list value
    //write insertitem
    //set Frequency change value = to insertitem variable
    set newrecord.FrequencyChange = insertitem
    //set current frequency by adding prevfreq to newrecord.FrequencyChange
    set newrecord.CurrentFrequency = prevfreq + newrecord.FrequencyChange
    //set prev freq variable to newrecord.CurrentFrequency
    // so can be used to calculate newrecord.CurrentFrequency in next loop
    set prevfreq = newrecord.CurrentFrequency
    //save record
    set status = newrecord.%Save()
    // Check if there is any entry in the global already for the frequency and input into FreqCount variable
    &sql(SELECT COUNT (CurrentFrequency) INTO :FreqCount FROM AOC2018.Day1 WHERE CurrentFrequency = :newrecord.CurrentFrequency)
    //write FreqCount
    set newrecord.FreqCount = FreqCount 
    set status = newrecord.%Save()
    //write !,"Frequency Change Is:"_insertitem_" Current Frequency is:"_newrecord.FrequencyChange
    //write !,"Frequency counter IS: "_FreqCount
    //write status
    IF FreqCount > 1 {
        write "First repeat = "_newrecord.CurrentFrequency
        QUIT}
        set prevrecord = prevrecord+1
        }
    IF FreqCount < 2 {
    write !, "Relooping" 
    GOTO PILOOP
            }

    return status
    }