Thursday, August 7, 2014

Localizing Time in HL7 CDA Rendering with XSLT

One of the questions that has come my way from several different sources is how to display times that appears in a CDA document in a way that is locally relevant.  Since the document narrative does very nothing with time that is under the viewer's control, there are really only two places where date time values might appear in CDA:

  1. The Document Header
  2. CDA Entries
While CDA entries do include date and time, rendering them as part of the display of document sections is rarely done.  The same techniques I mention for the header could also be applied to entries in the document sections though, if you happen to need something like that.

Date and Time values in the Header

There are numerous places within the document header where date and time could appear.  Perhaps the most visible one is the /ClinicalDocument/effectiveTime element, which indicates when the document was created.  Others include:
  • author/time (the date and time the author wrote the document)
  • legalAuthenticator/time (the date and time the document was legally signed)
  • documentationOf/serviceEvent/effectiveTime (the date and time associated with the documented service)
  • componentOf/encompassingEncounter/effectiveTime (the date and time associated with the encounter)

Time Zone or Locally Relevant Time?

Time zone is a function of politics, not math.  When the author writes a document at 201408041452-0700, what is the time zone?  Well, it depends upon where they are.  They could be in California, in which case the time zone is PDT.  But they could also be in Flagstaff, Arizona (where I will be next week for the HL7 Board Retreat), or in Zona Noroeste in the state of Baja California, in Mexico.  Time zone then, is a function of where you are, not when you are, and even if you have the when, it doesn't narrow the where down sufficiently.  A few years back, the US changed when it entered daylight savings time, so when you are, is also not just a function of time of day, but also of day, month and year.  Give up yet?  Good.  Don't try to show the time zone in these cases.  Just use the time offset.

Locally Relevant Time Zones

But people don't know what time 1200-0700 is, you say?  OK, so what you need is to convert that to a locally relevant time in your stylesheet.  So, how would you do that?  I'm not going to go into a lot of detail here, but I will make some design recommendations:
  • If possible, use XSLT 2.0 and the fn:adjust-dateTime-to-timeZone() function. In your transform.  Get the locale from the user agent.
  • If XSLT 2.0 is not available to you, try using EXSL date-time extensions with your XSLT processer.
  • To determine the local time zone offset from UTC (which is not the same as the time zone, but will serve for this purpose), try using the getTimezoneOffset() function on a JavaScript Date object.

XSLT 2.0

Let's look at how you might do this with XSLT 2.0 first.  You need to get the time zone from the browser. I'll assume you have a form somewhere, and that you submit that form at some point to the server.  Here is how you might pass the time zone as offset in minutes from UTC (note that getTimezoneOffset() returns offset of UTC from the current time zone, and we want the inverse, the offset of the time zone from UTC, so we just negate it).

<input type='hidden' id='TZ' name='TZ' value=''/>
<script type='text/javascript'>
  Date d = new Date();
  document.getElementById('tz').value = -d.getTimezoneOffset();
</script>
   
On the server size, you'd simply pass TZ as a parameter to your transform (you ARE executing this CDA transform server side, aren't you?  There are some very good reasons why you should do it that way).

In your stylesheet, you might do something like this to turn the offset in minutes into a time zone:
<xsl:variable name='tzString'>
   <xsl:if test='fn:number($TZ) &lt; 0'>-</xsl:if>
   <xsl:text>PT</xsl:text><xsl:value-of select='fn:abs(fn:number($TZ))'/>
   <xsl:text>M</xsl:text>
</xsl::variable>
<xsl:variable name='tzDuration' select='xs:dayTimeDuration($tzString)'/>

Then, when generating a time, you'd do something like this:
<xsl:variable name='timeValue'>
  <xsl:call-template name='reformatTimeFromHL7toXML'>
    <xsl:with-param name='time' select='...'/>
  </xsl:call-template/>
<xsl:variable>
<xsl:value-of select='format-time(xs:adjust-dateTime-to-timeZone($timeValue,$tzDuration),$myTimeFormat)'/>

Without XSLT 2.0

If you don't have an XSLT 2.0 parser, it gets a bit tricky.  There are a couple of different ways to handle date/time formatting, but you really don't want to write any of that code yourself in XSLT.  My favored way of handling it is by calling out to Java code to handle this sort of mess.  You can use standard date/time functions in Java in this case.  The java.text.SimpleDateFormat and java.util.GregorianCalendar classes provide just about everything you need to parse date/time values, and format them.

Below are a pair of templates using Java that are based on some work I used to convert CDA documents to XDS submission sets.  With a little bit of tweaking, you could use this to format date time values however you wanted.
  <!-- 
    Take a V3 date/time stamp with or without milliseconds, and with or 
    without timezone specification and convert it to a date/time stamp in 
    the specified zone precise to seconds, or less
  -->
  <xsl:template name="toZone">
    <xsl:param name="time"/>
    <xsl:param name="precision" select="14"/>
    <xsl:param name="length"
      select="string-length(substring-before(concat(
        translate($time, '-', '+'),'+'), '+'))"/>
    <xsl:param name="zone"/>
    <xsl:variable name="fmtString">
      <xsl:value-of select="substring('yyyyMMddHHmmss',1,$length)"/>
      <xsl:if test="contains($time, '+') or contains($time, '-')">Z</xsl:if>
    </xsl:variable>
    <xsl:variable name="inputFormatter" 
      select="java:java.text.SimpleDateFormat.new($fmtString)"/>
    <xsl:variable name="parsedDate" select="java:parse($inputFormatter, $time)"/>
    <xsl:call-template name="dateInZone">
      <xsl:with-param name="time" select="$parsedDate"/>
      <xsl:with-param name="precision" select="$precision"/>
      <xsl:with-param name="zone" select="$zone"/>
    </xsl:call-template>
  </xsl:template>

  <!-- 
    Take a Java Date and convert it to a date/time stamp in zone, precise
    to seconds, or less
  -->
  <xsl:template name="dateInZone">
    <xsl:param name="time"/>
    <xsl:param name="precision" select="14"/>
    <xsl:variable name="outputFormatter"
      select="java:java.text.SimpleDateFormat.new('yyyyMMddHHmmss')"/>
    <xsl:variable name="MyZone"
      select="java:java.util.TimeZone.getTimeZone($zone)"/>
    <xsl:variable name="void" 
      select="java:setTimeZone($outputFormatter, $MyZone)"/>
    <xsl:value-of 
      select="substring(java:format($outputFormatter, $time), 1, $precision)"/>
  </xsl:template>


Of course, this assumes your platform is Java.  What about all those .Net folk who are stuck with C#? The same principles apply, as C# has similar capabilities, I just don't know what they are ;-)

There is also a platform independent way to handle this, relying on EXSLT date and time extensions.  But, as mentioned there, no XSLT processors support that natively.  So you might just as well use a platform dependent implementation.

1 comment:

  1. Healthcare Education Faculty India helps the students to acquaint themselves with the knowledge of end to end processing the healthcare industry. They get to understand the operational side of healthcare center along with the medical terminologies. Institutes that offer healthcare training make their students to value the lives of their patients and respect the resources. This is how students get a chance to grasp entrepreneurial thinking abilities.

    ReplyDelete