Showing posts with label CDA. Show all posts
Showing posts with label CDA. Show all posts

Tuesday, March 28, 2023

Translating CDA to FHIR: SNOMED CT Codes


One of the challenges in translating from CDA (or C-CDA) to FHIR is related to variations in how CDA and FHIR store complex SNOMED-CT codes using qualifiers.  As you might expect, these differences are especially significant when the concept being encoded uses the SNOMED CT Expressions

In CDA, these codes are stored using the ConceptDescriptor (CD) data type, and unpacks the concept using the Qualifier component of the concept description when the code is a SNOMED CT expression.  This is arguably a flaw in HL7 Version 3 standards, as the qualifier concept is variably expressed in SNOMED CT and other terminology systems differently through their own expression languages, and shouldn't be syntactically unpacked in the CD data type.  It's overengineering the terminology component to meet the needs of a data model, and overgeneralizing (in the favor of SNOMED) how this may be handled in other terminologies.

HL7 FHIR uses the CodableConcept data type, and doesn't unpack the expression (fixing this flaw).  If we take the SNOMED CT example provided at that link and shown below:

Example

And look at it in CDA, we would see:

<code xsi:type="CD" code="284196006" codeSystem="2.16.840.1.113883.6.96" 
      displayName="burn of skin">
    <qualifier>
        <name code="363698007" codeSystem="2.16.840.1.113883.6.96" displayName="finding site" />
        <value code="770850006" codeSystem="2.16.840.1.113883.6.96" 
               displayName="Skin structure of left index finger"/>
    </qualifier>
</code>

But in FHIR:

<code>
  <coding>
    <code value='284196006|burn of skin|:363698007|finding site|=770850006|Skin structure of left index finger|' 
          system='http://snomed.info/sct'/>
  </coding>
</code>
                    <!-- OR Better yet (IMNSHO*) -->
<code>
  <coding>
    <code value='284196006:363698007=770850006' system='http://snomed.info/sct'
          display='|burn of skin|:|finding site|=|Skin structure of left index finger|'/>
  </coding>
</code>
NOTE, the latter example is just a simplified version of the former without any display name values.

This use of SNOMED CT Expressions is advanced, and doesn't show up often in the real world, but when it does, it is a real challenge for implementers.

To convert from a SNOMED CT code with qualifiers in CDA to a SNOMED CT Code in FHIR, apply the following algorithm:

  1. Generate new nested <fhir:code>, <fhir:coding> and <fhir:code> elements.
  2. If there is a cda:code/@code attribute, generate a value attribute in the final fhir:code element as follows:
    1. Set fhir:code/@value attribute in FHIR to the value of cda:code/@code
    2. (Optional and legal, but not recommended): If there is a cda:code/@displayName attribute in the CDA, append "|" + @displayName + "|" to fhir:code/@value.  NOTE: This captures displayNames, which you should only trust from your own terminology service, and it also appends a string to fhir:code/@value which will complicate FHIR Search operations in most FHIR implementations.
    3. For each cda:qualifier element from SNOMED CT in the cda:code element. 
      1. Append ":" + cda:qualifer/cda:name/@code to fhir:code/@value
      2. Again, optional, legal and not recommended: append "|" + @displayName + "|" to fhir:code/@value
      3. Append "=" + cda:qualifer/cda:value/@code to fhir:code/@value
  3. If there isn't a code/@code attribute, generate an appropriate uncoded value in FHIR (possibly using null flavors depending on the IG).  NOTE: If there's NO @code, but there are cda:qualifier elements, this is bogus, and should be reported as a validation error on input, at least for SNOMED CT.  You can't qualify an uncoded value.
Some comments:
  1. SNOMED CT expressions allow display name values to be appended to a code between vertical bars | (we call these pipes in HL7 V2).  I don't recommend putting the display names into the code even though it's legal and semantically correct.  Pragmatically, it's going to cause your FHIR implementation to have problems in search.
    1. This is legal:
      <code value='284196006|burn of skin|:363698007|finding site|=770850006|Skin structure of left index finger|'  system='http://snomed.info/sct'/>
    2. But this is MUCH better:
      <code value='284196006:363698007=770850006'
            display='|burn of skin|:|finding site|=|Skin structure of left index finger|'
            system='http://snomed.info/sct'/>

  2. Don't trust display name values from external systems.  Use the display name values from your own, validated terminology service.

  3. Consider how incorporating qualifiers into codes will impact your search.  In general, qualifiers reflect refinement in SNOMED CT, which implies subclasses of the core code.  You might consider coding it twice, once without the qualifiers, and a second time with the qualifiers:
    <code>
      <coding>
    
        <code value='284196006' system='http://snomed.info/sct'
              display='burn of skin'/>
        <code value='284196006:363698007=770850006' system='http://snomed.info/sct'
    display='|burn of skin|:|finding site|=|Skin structure of left index finger|'/> </coding> </code>
    I like this better because a search for fhir/Condition?code=
    http://snomed.info/sct|284196006 is going to find what the end user expects, all Condition resources representing a burn of skin.

  4. If you go with #3 above, you may be concerned (and rightly so) about negation.  There are a couple of recommendations you might consider:
    1. Insert a human into the mix and ensure that qualifiers related to negation or other situations with explicit context are correctly interpreted in workflows that involve importing data from CDA into the patient chart.
    2. Read through and understand SNOMED CT documentation on this topic.
    3. Few systems today use SNOMED CT expressions.  Fewer yet use them with negation.  That doesn't mean you can safely ignore it. Make it an exceptional process in your conversions, AND don't try to automate processing it, just detect it and call for (human) assistance in interpretation.  The number of times that happens is likely be small enough to avoid user complaint, yet remain safe for patient care.  And note, my expert opinion does not excuse you from making your own risk assessments and code accordingly.

Keith

P.S. To make it even uglier, technically, the FHIR representation would also be legal in CDA, if about as rare as a snowball in Central Africa.

<code xsi:type="CD" value='284196006:363698007=770850006'
      displayName='|burn of skin|:|finding site|=|Skin structure of left index finger|'
      codeSystem="2.16.840.1.113883.6.96">
* IMNSHO = In My Not So Humble Opinion

Thursday, February 27, 2020

Using the HL7 FHIR IG Builder for other Standards

As I mentioned earlier this week, there's a way that one can use the IG Builder to profile other content.  It all depends on having a FHIR Structure Definition for that content, as exists for CDA 2.1 and HL7 Version 2 Messages.  These two examples demonstrate that other structured content (such as that used for XDS-b, or NCPDP and X12) can also be documented using the StructuredDefinition resource.  This was one of Grahame's desiderata for constructing that resource, not that it was a requirement at all for FHIR, just a generally good idea he implemented because it was the right thing to do.

Using this structure definitions as the foundation, one can apply other FHIR tools, such as Sushi, or the IG Builder to create an implementation guide.  Sushi would need some work to create example instances using features of the StructureDefinition resource it probably doesn't deal well with right now (e.g., using XML attributes to describe output).

And IG Builder right now defaults to using a set of canonical StructureDefinitions for different versions of FHIR, although I'm sure there's a way to get it to look at others, although it might require additional work to expose that as an option.

Even so, imagine if everyone in healthcare could use the same set of tools to build an IG using standards from multiple SDOs.  It's a nice pipe dream.





Thursday, September 26, 2019

Strange Dreams and Context Controls in HL7 CDA

One of the challenges of a job that requires a lot of thinking is that it's really hard to escape it sometimes.  Last night, just before I head off for a short trip to the cape, I was dreaming about context controls in CDA documents.


Did I just lose you?  Yeah, I was wondering about that myself.

The CDA Document enumerates a number of participations, and for many of these participations, fixes the contextControlCode value to "OP", a code that means Overriding and Propagating, as in the example below from the CDA R-MIM.

What the heck does this mean?

In RIM Speak, context controls describe how the participation applies to not just the act the author participation is attached to (the CDA Document), but also the acts descending downwards (in the direction of the arrows) from the act in which the participation is attached.

So, if you have a CDA Document, it can have sections (a descendant act), which can have entries (a descendant act), and those entries can reference other things (like reference ranges) and so on.  And if, at the top of the document (/ClinicalDocument/author), you say that the author is Alex, then that propagates through the act and all related acts (in the direction of the act relationship arrows) until that same type of participation is again encountered.  At that time, the new participant (Bobbie) overrides the existing one, and propagates downward as the author of all attached and decedent acts.

This behavior is crucial to the way that participations work in CDA, and fortunately, all essential participations work that way.  The way it is crucial is because you can, using XSLT  (yeah, I know, who the hell else do you that dreams in XSLT...) create an XPath expression that identifies the author (or recorder, or performer) of a particular event.

Say you are looking at an allergy in a CCDA Document.  Who's the author of this thing?  Basically, you look at the act (in this case the observation using the allergy template).  If that has an author, you stop there.  Otherwise you look to the act above it to see if it has an author, and so on, all the way to the top of the document (which is /ClinicalDocument), and which, is guaranteed (according to the CDA R-MIM and Schema) to contain at least one author.  Once you find an act that has at least ONE author, you then accumulate all authors as the author of the act.  Assuming your current context is an act that you want to find the author of, here's the XPath expression that identifies all authors of the current act.

ancestor-or-self::cda:*[cda:author][1]/cda:author

What does this say?

ComponentMeaning
ancestor‑or‑self::Starting with the current node and ascending to all of its ancestors in order through the root of the document.
cda:*Find any element ... note, that while this expression will allow traversal through both act and act-relationship links, the next element will only ever be true on acts (because only acts can have author children).
[cda:author]That contains an cda:author element 
[1]Stop after you find the first one
/cda:authorCollect ALL of the authors.

So, this will find the first act that specified an author, and collect all of the authors.  If you are trying to translate from CDA to FHIR, this is an essential tool you can use to find author, informant, subject, or other participant.

You cannot assume that just because Alex authored the document that he / she authored all acts within it.  You have to look.

One important adjustment to this.  The subject of every act is assumed to be the patient, unless subject is otherwise specified, but the patient participation is actually identified through the record target participation in CDA.  This is inference from semantics, rather than true semantics, but that's OK.  The way to adjust for that is to use the following to get to the "subject" of the act:

ancestor-or-self::cda:*[cda:subject|cda:recordTarget][1]/(cda:subject|cda:recordTarget)

You may have to fiddle a bit around the use of parentheses depending on whether you are working in XSLT 1.0 or 2.0 (or even 3.0), but you should be able to make it work.

     Keith

Wednesday, November 29, 2017

How to say no ... 2017 Edition

RochesterBestiary detail Griffin

Thanks goes out to Brett Marquard for suggesting this update!

Negation has always been a beast of a problem.  In 2011 I described a bestiary of negated concepts used in CDA, and how to relate them using the standard.  As time goes by and the standard becomes more widely used, our understanding of the best practices also changes.

So here is the original bestiary with links to examples to the CURRENTLY ACCEPTED best weapons for slaying these beasts when known and provided by the CDA Examples task force.  And I'll be posting a link to this post in the original.

  1. Patient is not on THIS drug.
    Example pending, check back in a bit
  2. Patient is not on ANY drugs.
    No Medications.
  3. Patient does not have THIS ailment.
    No Known Problems
  4. Patient does not have ANY ailment.
    No Known Problems
  5. Patient does not have THIS allergy.
    Example pending, check back in a bit
  6. Patient does not have ANY allergy.
    No Known Allergies
  7. Patient does not have ANY MEDICATION allergy.*
    No Known Medication Allergies
* New in this edition

Examples for all of the above and more can be found at http://cdasearch.hl7.org/

   Keith



Monday, September 18, 2017

Comparing Dynamically Generated Documents

Sometimes to see if two things are similar, you have to ignore some of the finer details.  When applications dynamically generate CDA or FHIR output, a lot of details are necessary, but you cannot control always control all the values.  So, you need to ignore the detail to see the important things.  Is there a problem here?  Ignore the suits, look at the guns.

Creating unit tests against a baseline XML can be difficult because of detail. What you can do in these cases is remove the stuff that doesn't matter, and enforce some rigor on other stuff in ways you control rather than your XML parser, transformer or generation infrastructure.

The stylesheet below is an example of just such a tool.  If you run it over your CDA document, it will do a few things:

  1. Remove some content (such as the document id and effective time) which are usually unique and dynamically determined.
  2. Clean up ID attributes such that every ID attribute is numbered in document order in the format ID-1. 
  3. Ensure that internal references to those ID attributes still point to the thing that they originally did.

This stylesheet uses the identity transformation with some little tweaks to "clean up" the things we don't care to compare.  It's a pretty simple tool so I won't go into great detail about how to use it.

   Keith


<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:cda="urn:hl7-org:v3">
  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
  </xsl:template>
  
  <xsl:template match='@ID'>
    <xsl:attribute name="ID">
      <xsl:text>ID-</xsl:text>
      <xsl:number count='//*[@ID]'/>
    </xsl:attribute>
  </xsl:template>
  
  <xsl:template match='/cda:ClinicalDocument/cda:id|/cda:ClinicalDocument/cda:effectiveTime|/cda:ClinicalDocument/cda:*/cda:time'>
    <xsl:copy>Ignored for Comparison</xsl:copy>
  </xsl:template>
  
  <xsl:template match="cda:reference/@value[starts-with(.,'#')]">
    <xsl:attribute name="value">
      <xsl:text>#ID-</xsl:text>
      <xsl:value-of select='count(//*[@ID=substring-after(current(),"#")]/preceding::*/@ID)+1'/>
    </xsl:attribute>
  </xsl:template>
  
  <xsl:template match='@ID' mode='count'>
    <xsl:attribute name="ID">
      <xsl:text>#ID-</xsl:text>
      <xsl:number count='//*[@ID]'/>
    </xsl:attribute>
  </xsl:template>
  
</xsl:stylesheet>


Wednesday, September 6, 2017

The Good, the bad and the ugly (HL7 Ballots)

Polling StationHL7 Balloting just closed this last hour.  Here's my recap of what I looked at, how I felt about it, and where I think the ballot will wind up from worst to best.  Note: My star ratings aren't just about the quality of the material, its a complex formula involving the quality of the material, the likelyhood of it being implemented, the potential value to end users and the phase of the moon on the first Monday in the third week of August in the year the material was balloted.

VMR (Virtual Medical Record) 
  1. HL7 Implementation Guide: Decision Support Service, Release 1 - US Realm (PI ID: 1018)
  2. HL7 Version 2 Implementation Guide: Implementing the Virtual Medical Record for Clinical Decision Support (vMR-CDS), Release 1 (PI ID: 184)
  3. HL7 Version 3 Standard: Decision Support Service (DSS), Release 2 (PI ID: 1015)
  4. HL7 Virtual Medical Record for Clinical Decision Support (vMR-CDS) Logical Model, Release 2 (PI ID: 1017)
  5. HL7 Virtual Medical Record for Clinical Decision Support (vMR-CDS) Templates, Release 1 - US Realm (PI ID: 1030)
  6. HL7 Virtual Medical Record for Clinical Decision Support (vMR-CDS) XML Specification, Release 1 - US Realm (PI ID: 1016)

This had a total of six artifacts on the ballot.  Together they get 1 star for being able to pass muster to go to ballot.  As a family of specifications, this collection of material looks like it was written by a dozen different people across multiple workgroups with three different processes. What is sad here is that the core group of people who have been working on this material for some time (including me) is the same across much of this work, and it all comes out of the same place.  VMR was always an ugly stepchild in HL7, and these specifications don't make it much better.  Don't lose hope though, because QUICK and CQL are significant improvements, and the FHIR-based clinical decision support work such as CDS Hooks is much more promising. All appear to have achieved quorum and seem likely to pass once through reconciliation.

Release 2: Functional Profile; Work and Health, Release 1 - US Realm (PI ID: 1202)   
Yet another functional model.  Decent stuff if that is what excites you.  I find functional models boring mostly because they aren't being used as intended where it matters.  Pretty likely to pass.

HL7 Version 2.9 Messaging Standard (PI ID: 773) 
The last? of a dying breed of standard.  Maybe? Please? Not enough votes to pass yet, but could happen after reconciliation (which is where V2 usually passes).

Pharmacist Care Plan  
  1. HL7 CDA® R2 Implementation Guide: Pharmacist Care Plan Document, Release 1 - US Realm (PI ID: 1232)
  2. HL7 FHIR® Implementation Guide: Pharmacist Care Plan Document, Release 1 - US Realm (PI ID: 1232)
Another duo, missing the overweight architectural structure of VMR, but certainly adequate for what it is trying to accomplish.  The question I have hear is about its relevance.  Except in inpatient settings, I find the notion of a pharmacist care plan for a patient to be of very little value at this stage.  In fact, we need more attention on care planning in the ambulatory setting.

These are for comment only ballots and the voting reflects it.  While not likely to "pass", the comment only status guarantees that these will go back through another cycle.  Based on the voting, the material needs it.

HL7 Guidance: 
Project Life Cycle for Product Development (PLCPD), Release 2 (PI ID: 1328)  
HL7 continues to ballot its own processes.  What makes this one funny is that this particular ballot comes out of a workgroup in the Technical and Support Services steering division, which previously rejected another group in that divisions balloting a document because T3SD (their acronym) doesn't do ballots (BTW: That's a completely inadequate summary of what really happened, some day if you buy me a beer I'll get _ and _ to tell you the story.  Better yet, buy them beers).

It's a decent document, and likely to "pass".

HL7 CDA® R2 Implementation Guide: 
International Patient Summary, Release 1 (PI ID: 1087) 
I could get more excited about this particular piece of work if it weren't for the fact that it's all about getting treatment internationally, rather than being an international standard that would eliminate some of the need to deal with cross border issues.  But, it's the former rather than the latter, so only three stars.  A lot of the work spends time dealing with all the tiny little details about making everyone happy on every end instead of getting someone to make some decent decisions that enable true international coordination.

This one is tight, will likely pass in reconciliation, and is getting a lot of international eyes on it.  It's good stuff.

UDI Implementation: 

  1. HL7 Domain Analysis Model: Unique Device Identifier (UDI) Implementation Guidance, Release 1 (PI ID: 1238)
  2. HL7 CDA® R2 Implementation Guide: Consolidated CDA Templates for Clinical Notes; Unique Device Identifier (UDI) Templates, Release 1 - US Realm

By itself, neither one of these might have gotten four stars.  Together they do.  UDI needs a lot of explaining for people.  These documents help.

While the balloting looks tough (the second document is "failing" to pass by a 2/3 majority), it's all about doing what DOD, VA, and others want to ensure interoperability between them.

HL7 CDA® R2 Implementation Guide: 
Consolidated CDA Templates for Clinical Notes; Advance Directives Templates, 
Release 1 - US Realm (PI ID: 1323)  

This is a useful addition to what we can do today with Advance Directives, and a great example of how to deal with backwards compatibility right, and they almost nailed it perfectly (my one negative comment on this item is a fine point).

Not a lead-pipe cinch but surely the issues in this one will be resolved during reconciliation.

HL7 CDA® R2 Implementation Guide: 
Quality Reporting Document Architecture Category I (QRDA I) Release 1, 
STU Release 5 - US Realm (PI ID: 210) 
Useful, necessary, and boring, but of great value.  Sometimes it pays to be boring.
Definitely a lead-pipe cinch to pass.  Third highest in positive votes, with 0 negatives.

HL7 Cross-Paradigm Specification: 
Allergy and Intolerance Substance Value Set(s) Definition, Release 1 (PI ID: 1272) 
ABOUT. DAMN. TIME. An allergy value set we can all use. Nuf said.
The interesting back story here is who is voting negative (who cares) about this.  It looks like a lot of VA/DOD interoperability is going to get decided through standards. I'm pretty certain this stuff is going to get worked out, which has tremendous value to the rest of us.

HL7 FHIR® IG: SMART Application Launch Framework, Release 1 (PI ID: 1341) 
I spend the most time commenting on this one.  I'm looking forward to this seeing this published as an HL7 Standard and in getting some overall improvements to what I've been implementing for the past year or so.

There's definitely some good feedback on this ballot (which means likely to take a while in reconciliation), even though it seems very likely to pass.

HL7 Clinical Document Architecture, Release 2.1 (PI ID: 1150) 
This was the surprise of the lot for me.  I expected to be bored, having said CDA is Dead not quite four years ago.  I was, pleasantly so.  There was only one contentious issue for me (the new support added for tables in tables). They got to four stars by making sure all the issues we've encountered over the past decade and more were addressed. They got an extra star by making it easy to find what had changed in the content since CDA R2.  All in all, a pleasant surprise. CDA R2 still reigns supreme, but I think CDA R2.1 might very well become regent until CDA on FHIR is of age.
Oh yeah.  It passed, so very likely to go normative, which will make discussions about the standard in the next round of certification VERY interesting.

   Keith










Wednesday, February 15, 2017

An XSLT Design Pattern (and not just for FHIR to CDA conversions)

I have a couple of XSLT design patterns that I've been using and improving over the past decade, which I pulled out again last night to do some FHIR to CDA transformations.  XSLT isn't what you'd call a strongly object oriented language, nor a strongly typed one.  However, the design patterns I used borrow from experiences I've had with strongly typed, object oriented languages such as Java.

The design pattern is a general one which I've used for a number of strongly typed schema to schema conversions.  FHIR to CDA is just one example.  V2 XML to CDA (or back) is another one, and I've also done this for CDA to ebXML (XDS related work), and at one point for CCR and CDA, even though I'd never really call CCR strongly typed.

The first design pattern is in how I define my transformations.  There are two ways in which you can call a template, and they have different benefits.  Sometimes you want to use one, and sometimes you want to use the other.  When going from one format to another in XML, usually there is a target element that you are trying to create, and one or more source elements you are trying to create it from.

My first method uses apply-templates.
<xsl:apply-templates select='...' mode='target-element-name'/>

You can also call apply-templates with parameters.
<xsl:apply-templates select='...' mode='target-element-name'>
  <xsl:with-param name='some-param' select='some-value'/>
</xsl:apply-templates>

This is pretty straight forward.  I use mode on my transformation templates for a variety of reasons, one of which is that it allows you to build transformation trees that the XSLT engine automates processing with.  More often that not, the name of the mode is the output element I'm targeting to generate.

The second method works more like a traditional function call, using call-template:
<xsl:call-template name='target-element-name'>
  <xsl:with-param name='this' select='the-source-element-to-transform'/>
  <xsl:with-param name='some-param' select='some-value'/>
</xsl:call-template>

Now, if the source element is always the same for a given target element (e.g., a FHIR address going to a CDA addr element), you create your actual template signature thus:

<xsl:template name='addr' mode='addr' match='address'>
  <xsl:param name='this' select='.'/>
   ...

Note that name and mode are the same.  What you want to create here is an <addr> element in CDA. The input you want to convert is a FHIR address element (I checked several dozen places, just about everywhere the Address datatype is used, it is always called address in FHIR resources).  So that explains the first line.  The name of the template, and its mode, identity what you are trying to create.

The second line is critical, and it has some serious impact on how you write your templates.  Instead of assuming a default context for your transformation, the <xsl:param name='this' select='.'/> sets up a parameter in which you can explicitly pass the transformation context, but which will implicitly use the current context if no such parameter is passed.  When you write your transformation, you have to be careful to use $this consistently, or you'll have some hard to find bugs, because sometimes it might work (e.g., when called using apply-templates), but other times not (e.g., when called using call-template).  It takes some diligence to write templates this way, but after a while you get used to it.

Here's the example for converting a FHIR Address to a CDA addr element.  It's pretty obvious:

<xsl:template name='addr' mode='addr' match='address'>
  <xsl:param name='this' select='.'/>
  <xsl:variable name='use'><xsl:choose>
    <xsl:when test='$this/use/@value="home"'>H</xsl:when>
    <xsl:when test='$this/use/@value="work"'>WP</xsl:when>
    <xsl:when test='$this/use/@value="temp"'>TMP</xsl:when>
  </xsl:choose></xsl:variable>
  <addr>
    <xsl:if test='$use!=""'>
      <xsl:attribute name='use'><xsl:value-of select='$use'/></xsl:attribute>
    </xsl:if>
    <xsl:for-each select='$this/line'>
      <streetAddressLine><xsl:value-of select='@value'/></streetAddressLine>
    </xsl:for-each>
    <xsl:for-each select='$this/city'>
      <city><xsl:value-of select='@value'/></city>
    </xsl:for-each>
    <xsl:for-each select='$this/state'>
      <state><xsl:value-of select='@value'/></state>
    </xsl:for-each>
    <xsl:for-each select='$this/postalCode'>
      <postalCode><xsl:value-of select='@value'/></postalCode>
    </xsl:for-each>
    <xsl:for-each select='$this/country'>
      <country><xsl:value-of select='@value'/></country>
    </xsl:for-each>
  </addr>
</xsl:template>

[There's a little XSLT shorthand nugget in the above transform, another design pattern I use a lot:
  <xsl:for-each select='x'> ... </xsl:for-each>

 is a much simpler way to say:
  <xsl:if test='count(x) != 0'> ... transform each x ... </xsl:if>

especially when x is a collection of items.]

Now, if you want to drop an address in somewhere, you can do something like this:
  <xsl:apply-templates select='address' mode='addr'/>

Or, you can also do it this way:
  <xsl:call-template name='addr'>
    <xsl:with-param name='this' select='$patient/address'/>
  </xsl:call-template>

Either works, and in developing large scale transformations, I often find myself using the same template in different ways.  The elegance of this design pattern in XSLT extends further when you have two or more source elements going to the same target element.

In that case, you have two templates with the same mode, but different match criteria.  AND, you have a named template.  Let's presume in the FHIR case, you want to map Resource.id and Resource.identifier to an id data type (it's not to far-fetched an idea, even if not one I would use). You then write:

  <xsl:template mode='id' match='id'>
    ...
  </xsl:template>
  <xsl:template mode='id' match='identifier'>
    ...
  </xsl:template>

  <xsl:template name='id'>
    <xsl:param name='this'/>
    <xsl:apply-templates select='$this' mode='id'/>
  </xsl:template>

And the final named template simply uses the match type to automatically select the appropriate template to use based on your input value.

Sometimes you want to create an output element but it isn't always called the same thing, even though it uses the same internal structure.  Using default parameters, you can set this up.  Let's look into example above.  Not EVERY id element is named id.  Sometimes in CDA the have a different name depending on what is being identified.  For example, in the CDA header, you have setId (in fact it's nearly the only case for id).  A more obvious case is code.  Most of the time, code is just code, but sometimes it's ____Code (e.g., priorityCode), but the general structure of a priorityCode (CE CWE [0..1]) is pretty much the same as for code (CD CWE [0..1]).  So, if you were going to convert a FHIR CodableConcept or Coding to a CDA CD/CE data type, you might use the same transformation.

  <xsl:template mode='code' match='code' name='code'>
    <xsl:param name='element' select='"code"'/>
    <xsl:element name='$name'>
      ...
    </xsl:element>
  </xsl:template>

You get the idea.  Usually, you want to generate <code>, and so you say nothing.  Sometimes you want to generate something different, and so you add a parameter.
  <xsl:call-template name='code'>
    <xsl:with-param name='this' select='$that/code'/>
    <xsl:with-param name='element' select='"priorityCode"'/>
  </xsl:call-template>

Remember you can also pass parameters using apply-templates, so this also works:
  <xsl:apply-templates select='$that/code'>
    <xsl:with-param name='element' select='"priorityCode"'/>
  </xsl:apply-templates>
  
Enough chatter, back to work.  HIMSS is only a couple of days away.

   - Keith

P.S.  I've missed being able to post while I've been heads down working towards HIMSS and the Interop showcase.  Hopefully I'll get more time when I get back.

Thursday, January 12, 2017

What is it?

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:cda='urn:hl7-org:v3' 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  version="1.0">
  <xsl:template match="/|cda:*|@xsi:type|text()|@*">
    <xsl:copy>
      <xsl:apply-templates select="cda:*|@*|text()"/>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

What is this small but useful beast?

Tuesday, January 10, 2017

What is in a name? CDA Document Types revisited

The CCD (Continuity of Care Document) was originally envisioned as being the HL7 version of the Continuity of Care Record, which originated from a paper form originally used in the State of Massachusessets.  Since then the name has become synonymous with Meaningful Use, and in many ways, burdensome communication.

Because after all, if what you are after is Continuity of Care, what information would you possibly omit?

Recently I'd been asked by John Moehrke what other document could be used to represent a summary of a single visit.  Providers have many names for these:

Visit Note is the most generic, and basically could mean any level of detail from a single line of text to a three page report on the patient's current condition.  It could mean any of the different kinds of notes physicians use.

History and Physical Note describes an encounter (ambulatory visit) in which a History and Physical Examination is performed.  This could represent something like what your provider would report for your annual physical examination, or prior to undertaking some specific health-related activity.  Often surgeons perform an H&P prior to surgery.  Newly expectant mothers undergo one often when they first learn of their pregnancy.  The H&P principally is describing the results of a physical examination where either something specific is being looked for (such as the reason for an undetermined illness), or where more generally an assessment of a patient's overall health is being done.  More specifically, H&P relates to a specific service performed and billed by physicians.  H&P is pretty typically used in cases where a healthy patient is having an encounter with a physician, and may also be used in other cases where nothing is obvious.  It is in some ways a fishing expedition.

A consult note represents a different kind of service.  In this case, an opinion is being sought about a specific situation.  Rendering that opinion may require a history and physical, or it may just require some very specific examinations.  When my wife was being evaluated for arthritis, the physician didn't perform all of the steps one would expect in a physical examination.  He didn't need to look at different body systems, he was just interested in two or three.  He applied a stethoscope to my wife's knee and promptly upon listening when she bent it indicated that she did indeed have arthritis,and so rendered his opinion.  After we had already gone to the trouble to procure expensive imaging of said joint which he didn't even need to look at (although fortunately it later did come in handy).

A progress note is simply an update about a patient's progress.  In ambulatory settings it is used to report on a patient's progress with particular treatment or disease that is being followed by a physician.  Often it is used in various ancillary "therapy" settings, such as Physical Therapy, Respiratory Therapy, Cardiac Rehab, et cetera.

If you want a note that summarizes what happened in an encounter, think about the principal service provided during that encounter.  The note will name it, and the provider will also bill for it.  If I had to pick just one of the three above, Consult note would be my choice, because a consultation can always include an H&P, but an H&P doesn't cover the wide variety of healthcare situations.

Then again, maybe we should stop trying to shoe-horn every healthcare visit into the same documentation template.  There's more to this world than nails, and not every problem needs a hammer.



   Keith

P.S.  I think its funny that almost nine years later I'm still talking about hammers.

Thursday, October 27, 2016

Partial Rejection and Levels of Validity in CDA (or anything else for that matter)

One of the things that I learned in my Informatics education was that there were many different ways to evaluate validity of something.  It's not a yes/no question, but rather a multi-faceted scale.  Most informaticists are familiarity with a 5 or even 7 point scale used to evaluate quality of evidence.  This is essentially the same idea.

In a recent Structured Documents discussion, the topic of what it means to "reject" an "invalid" CDA document was discussed.  When you look at these terms, they seem like yes/no, binary decisions. But here is how you can turn this into shades of gray:

Level 0: Totally bogus content.  Is this even XML?
Level 1: The CDA Header is valid.
Level 2a: Level 1 + the narrative content is valid according to the CDA Schema
Level 2b: Level 2 + the LOINC codes for documents and sections are recognized as valid.
Level 3a: Level 2 + the entries are schema valid according to CDA.
Level 3b: Level 3a + the codes are recognized.

Here's how a system can respond after making these assessments (note that possibly available actions at higher level include those at the level below):

Level 3b: Discrete data can be imported into the system.
Level 3a: Some data can be coded based on string matching, and for that data which has matching codes, that data can be imported into the system after validation by a healthcare provider.
Level 2b: Narrative only sections (such as Reason for Visit or HPI) can be imported, but no discrete data.
Level 2a: After performing some simple pattern matching, some narrative only sections can be imported, after validation by a healthcare provider.  The document can safely be displayed to the end user.
Level 1: The CDA Header is valid.  The fact that a document has been received for a patient from a particular organization can be displayed to the end user.
Level 0: You might be able to look at some magic numbers in the data to figure out what the heck someone sent you, but there's no way to even assess what patient it was for unless you have that stored in some other metadata (thus Direct+XDM evolves as a good solution for sending files).  You might be able to figure out an appropriate viewer for the content, but even then, there are no guarantees it is safe.

Validity?  It's not a switch.  It's a dial.  And mine goes to 11.  Rejection? That's just a pre-amp filter.




Friday, July 29, 2016

Round tripping identifiers from CDA to FHIR and back

I think I solved this problem for myself last year or so, or else the answer wouldn't be so readily available in my brain.

The challenge is this: You have an identifier in a CDA document.  You need to convert it to FHIR. And then you need to get it back into CDA appropriately.

There are four separate cases to consider if we ignore degenerate identifiers where nullFlavor is non-null.
  1. <id root='OID'/>
  2. <id root='UUID'/>
  3. <id root='OIDorUUID' extension='value'/>
For case 1 and 2, the FHIR identifier will have system =  urn:ietf:rfc:3986, and the value will be urn:oid:OID, or urn:uuid:UUID.
For case 3, it gets a tiny bit messy.  You need a lookup table to map a small set of OIDs to FHIR URLs for FHIR defined identifier systems.  If the OID you have matches one of the FHIR OIDs in that registry, use the specified URL.  Otherwise, convert the OID to its urn:oid form.  If it is a UUID, you simply convert it to its urn:uuid form.

Going backwards:
If system is urn:ietf:rfc:3986, then it must have been in root-only format, and the value is the OID or UUID in urn: format.  Simply convert back to the unprefixed OID or UUID value, and stuff that into the root attribute.

Otherwise, if it is a URL that is not in urn:oid or urn:uuid format, then look up the identifier space in the FHIR identifier system registry, and reverse it to an OID, and put that into root.  Otherwise, you just convert back to the unprefixed OID or UUID value, and stuff that into root.  In that case, the extension attribute should contain whatever is in value.

So now then, you might ask, how do I represent a FHIR identifier that is NOT one of these puppies in HL7 CDA format.  In other words, I have a native FHIR identifier, and CDA had nothing to do with generating it.  So, there's a system and a value, but no real way to tell CDA how to deal with it.  To do that, we need a common convention or a defined standard.

So, pick an OID to define the convention, and a syntax to use in value to represent system and value when system cannot be mapped to an OID or UUID based on the above convention.  In this manner you can represent a FHIR identifier in CDA without loss of fidelity because CDA does not provide any limits on value.  Oh, and modify the algorithm above to handle that special OID in case four.

I'll let HL7 define the standard, select the OID, and specify the syntax.  I have better things to do with $100 than register an OID for this purpose.  But clearly, it could be done.

   Keith





Wednesday, June 15, 2016

Apple IOS10 to support HL7 CDA in HealthKit

This is pretty big news from Apple for HL7, and something I'm rather feeling a bit proud of myself, even if I only played a small part in getting us to this stage.

Thanks to Andy Stechishin of the HL7 Mobile Health Workgroup for spotting this one and bringing it to the attention of HL7 Membership.

   Keith

P.S. Now if we can get the technology giant to pay attention to FHIR, my day will be made.

Friday, March 25, 2016

I really think we should ... Oh look, squirrel

So the question came up last working group meeting about whether to pursue use of the FHIR StructureDefinition as a mechanism to capture CDA Templates in some meeting somewhere.  I wasn't at that meeting or I would have shown my distaste for the idea then.  Grahame's been playing with StructureDefinition and has demonstrated quite successfully I think that he can use it to represent models for everything from HL7 Version 3, CDA (being a version 3 derived spec, that should be no surprise), to I would guess, V2, X12 and even arbitrary XML schema and other models provided they follow a few fairly simple rules.

Thus, I introduce to you the squirrel in question.  It's a cute squirrel, and even a rather powerful one.

Why do we need this?  We have a perfectly good standard for representing CDA templates if we would just use it in the tools HL7 presently uses to publish so much of its own documentation in.

But it isn't FHIR related, and loses to the flavor of the month (year? decade?) I think.  I suspect chasing this squirrel will only distract any further work on something else that might benefit the HL7 community at large ... for example, liberating CDA Templates from Trifolia in a standard format.

The only benefit I see to this distraction may have is to keep people tied up in a harmless activity, at least for anyone but the squirrel.  But I think StructureDefinition will survive that race.

   Keith

Wednesday, September 9, 2015

CDA -> FHIR : Working Backwards to go Forwards

I've been spending quite a bit of time on various FHIR and CDA related topics.  As I mentioned a bit ago, I got distracted by conversions of FHIR JSON to XML and back.  There was a reason for that, as there was a piece of FHIR JSON I wanted in XML so that I could convert it to CDA.

So why am I working backwards on the CDA -> FHIR side?  It could be that you start with what you know.  Or it could be that I figured it would be easier to reverse the process once I figured out how to do it from a different direction.  As the case may be, I've made some progress.

Here's what I did:

  1. I download the CDA Templates from the C-CDA DSTU 2.1 Release using Trifolia.
  2. I selectively annotated some of the entry templates (section and document templates are boring for the most part) with markup explaining how to map the CDA entry to a FHIR Resource or component thereof.
  3. I wrote some utility stuff to map certain CDA basic data types to the FHIR equivalents (I have a similar set for the reverse mapping already done).
  4. I wrote a code generator (in XSLT) that generated an XSLT stylesheet from the Template XML I downloaded.
  5. I wrote another XSLT that managed the basic parts of the CDA Document to FHIR Bundle conversion, and invoked templates in the stylesheet I automatically generated.
The end result is that I have a model driven transform of CDA to FHIR, and I can probably take the same stuff and figure out how to reverse it.

There's some things that don't quite work because they appear in different orders in CDA and FHIR. For example, the identifiers in a Problem Observation appear AFTER some data in the Problem Act. Since my transform processes things in CDA order (at least for now), it generates some not-quite-correct output.  However, that output is close enough for me to figure out how to address that problem later.  I could probably even use my JSON/XML translator to help with the cleanup [oh damn, there went more sleep as I just came up with that].

   -- Keith



Monday, June 8, 2015

The case against a negationInd extension for supply and encounter in CDA and QRDA

It's been proposed recently that the prohibition in CDA Release 2.0 against extensions altering the semantics of the information doesn't apply to RIM based extensions.  What the standard has to say in section 1.4 on Extensibility is [emphasis mine]:

Locally-defined markup may be used when local semantics have no corresponding representation in the CDA specification. CDA seeks to standardize the highest level of shared meaning while providing a clean and standard mechanism for tagging meaning that is not shared. In order to support local extensibility requirements, it is permitted to include additional XML elements and attributes that are not included in the CDA schema. These extensions should not change the meaning of any of the standard data items, and receivers must be able to safely ignore these elements. Document recipients must be able to faithfully render the CDA document while ignoring extensions.
The rationale for including this extension in QRDA is to enable one measure (of 93) to be able to report that something was not supplied to the patient.

We did a risk analysis of this today on a special Structured Documents call.  Here's the scenario:

Data is captured for quality reporting and quality improvement activities.
Within that context, rules are created to ensure that patients are followed up on if they aren't getting appropriate treatment as evidenced by supply records.

  1. A patient with DVT risk is ordered prophylaxis.
  2. However, that prophylaxis is not supplied for some reason (e.g., patient couldn't afford to pay).
  3. This is recorded using the extension.
  4. A system that uses the proposed extension will correctly detect that the supply did not occur, and can initiate followup.  However, a system that does not use the proposed extension and is developed with the understanding that unrecognized extensions can safely be ignored will not recognize that the supply did not occur.  In fact, it will instead recognize the opposite, that supply did occur.
  5. As a result of this, no followup on the necessary intervention is performed.  
  6. Due to the resulting delay to detect that the prophylaxis was not given, a life threatening health event occurs.

We need to assess three things:

  1. The severity of harm, which we evaluated as critical/life threatening [different organizations may use different terms].
  2. Probability of occurrence of the hazard: Care management systems today are looking for the absence of the supply signal.  Presenting a supply signal using QRDA with the extension will always cause this trigger to fail.  Due to the way it interferes with followup activities, is unlikely to be noticed by a provider, so the probability of occurrence is high that it will occur.
  3. Likelyhood of harm: This would have to be ratedMechanical prophylaxis can reduce the risk of DVT from 27% to 13% alone, or when used with medications, from 15% to 2% [see http://www.ncbi.nlm.nih.gov/pmc/articles/PMC1925160/].
If you work out the rest, you realize this is not really a workable solution by itself.  Something else has to be done.  Documenting the extension and putting some clear warnings around it is something we know doesn't work well.  Trying to create patient safety in a design through labeling is the least effective method according to the FDA.

If I were the product manager for the 93 quality measures, I'd pull the one in question and release it later when CDA Release 2.1 (or as seems more likely, CDA on FHIR) has the capacity to address the need.





Thursday, October 16, 2014

On a Process for Rapid Template Development

Recently a question crossed the Structured Documents Workgroup List about how to record information about a patient's recent travel.  You can probably guess what recent media events motivated that question.

IHE had long ago developed a template for foreign travel, as part of XPHR. Since this section wasn't required in the HITSP C32, it was not further developed in C-CDA.  However, that doesn't stop anyone from using it, even under Meaningful Use.  The Foreign Travel template is simply a section containing a narrative description of travel.  Narrative capture of travel history is what most EHR systems today support.  This usually appears somewhere in the social history section of the patient chart, and is accessible to any provider caring for the patient in most EHR systems.

For cases of communicable disease, if you want the EHR to be able to apply clinical decision support to recent foreign travel, you would need coded entries, or natural language processing over the narrative.  To code the travel information, you will need to do more in this section.  The basic activity being documented is travel, and so you could readily capture that in an act, with location participants for each place visited.

<act classCode='ACT' moodCode='EVN'>
  <code code='420008001' displayName='Travel'
      codeSystem='2.16.840.1.113883.6.96' codeSystemName='SNOMED-CT'/>
  <effectiveTime><!-- This might be optional, see participant/time below -->
    <low value='Starting Date for Travel History'/>
    <high value='Ending Date for Travel History'/>
  </effectiveTime>

  <participant typeCode='LOC'>
    ...
  </participant>
</act>

The participant would represent the various locations visited during the time period described in the travel act.  We need not go into the entity level since participant.role can capture what we need.

<participant typeCode='LOC'>
  <time>
    <low value='Starting Date for this Location'/>
    <high value='Ending Date for this Location'/>
  </time>
  <participantRole classCode='TERR'>
    <!-- This might be optional, and could identify locations using a value
         set such as Geographic Location History -->
    <code code='Code identifying location
      codeSystem='Code System for Locations (e.g., ISO-3166)'/>
    <addr>
      <city>...</city>
      <county>...</county>
      <state>...</state>
      <country>...</country>
    </addr>
  </participantRole>
</participant>

We would probably want to constrain participantRole so that only one addr element was present (and was mandatory), and had at least one element of city, county, state or country.  I would also recommend that country always be present, and that if city or county is present, that state also be present.  For disambiguation purposes, you might need to know which of the twelve New London's in the US your patient was recently in.

Some have suggested that location could be rolled up into a code, as I have shown above in participantRole/code.  While I agree that would make certain kinds of decision support easier, it is something that could be done within the clinical decision support module, rather than being specified within the EHR.  The Geographic Location Value set referenced above shows why this might be a problem, as it contains codes describing locations at different levels.

So, now back to the main point.  We quickly went through this model on the Structured Documents workgroup list service in less than three days.  It would take us several months to role this out as a new template.  We need a model of development and consensus building somewhat like what OpenEHR does for archetypes, allowing for quicker development and deployment of these sorts of artifacts.  I also think that this is the way some of these templates should be developed in the future. We should develop a model that can be approved through a general call for consensus, and then periodically, we can roll up several of these templates into a release which gets a more thorough review through the HL7 ballot process.

This would allow HL7 to be responsive to rapidly developing health issues, without having to make it something that we have to panic about.  Note that foreign travel is relevant only for some cases. There are plenty of other ways to be exposed to disease, including everyday activities like going to school or work, or shopping.  For that you might want to be looking at other information in the patient health record, such as their home address, and workplace and school contacts.  IHE also included entries for those contacts in XPHR.

   -- Keith

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.