Schaake.nu

XSLT Duration converter

Gepost in /XSLT stylesheets/Software op 26 Juni 2013
Deze blog is geschreven door Christiaan Schaake

Introduction

Within XML a number of datatypes are defined. The data, dateTime and time are easy to understand and convert since the exact location of each measurement unit is known. E.g. the month is always on character 6 and 7. The duration however is a much more complex datatype to represent date and or time. Espessially when we need to convert a complex duration of e.g. 1 year, 2 months and 4 minutes to seconds.

Duration datatype

The duration datatype is a ISO8601 extended datatype. The format is not as fixed as the dateTime datatype. This makes it impossible to directly select the right measures. The only way to analyse the duration is by going through almost character by character.

The duration is made up by defining each measurement unit is made up as value directly followed by its date or time element. The following date and time units may occur:

  • P is the duration designator (historically called "period") placed at the start of the duration representation.
  • Y is the year designator that follows the value for the number of years.
  • M is the month designator that follows the value for the number of months.
  • W is the week designator that follows the value for the number of weeks.
  • D is the day designator that follows the value for the number of days.
  • T is the time designator that precedes the time components of the representation.
  • H is the hour designator that follows the value for the number of hours.
  • M is the minute designator that follows the value for the number of minutes.
  • S is the second designator that follows the value for the number of seconds.

The first P is mandatory. After this any element may occur in the predefined order. There are some very nasty things we need to take into account.

  1. The designator M is used for both the Month and the Minute. The trick is the T designator for Time. This means the M before the T is the Month. And the M after the T the Minute.
  2. Not all designators have to be used. At least one must be present.
  3. The number of decimals used in the date and time units is not restricted. We could have PT1000M (1,000 minutes).
  4. Values can contain a decimal fraction. Officially the time elements must be a whole number. Both the comma as the full stop may be used as decimal point.

Examples for durations

Here are some examples to show the complexity of a duration datatype:

P1Y 1 year
P0.5Y half a year
P2MT3M 2 months and 3 minutes
PT1200S 1200 seconds
P2Y3W2D 2 years, 3 weeks and 2 days

Decoding the duration

The best approach is to analyze the duration element by element and recalculate the values to seconds. Next we can add up all the calculated seconds to have the total seconds of the duration.

With the number of seconds for the whole duration we can easily recalculate any other value, including a new duration format.

In a normal programming language we could simply decrypt the duration and store all the elements in variables. But we want to do it in XSLT 1.0. Ok, there are variables in XSLT 1.0, but the variable scope simply sucks.

A solution is to use a self calling template. The template selects the first element in the duration string and converts it into seconds. The result is added to the previous result and passed on to the next call of the same template. When the template is called again, the duration unit is stripped from the string. This way we simply walk through the duration string, converting every unit to seconds.

The duration can be stripped by the following XSLT function:

<xsl:with-param name="strippedDuration" select="substring-after($strippedDuration,'Y')"/>

The number of seconds is calculated:

<xsl:with-param name="seconds" select="substring-before($strippedDuration,'Y')*365*24*60*60"/>

The month introduces a new challenge since we need to detect the difference between the M for month and the M for minute. The month is however located before the T mark.
So we could use the following XSLT statement to select the month:

<xsl:when test="contains(substring-before($strippedDuration,'T'),'M')"> <xsl:call-template name="_durationStep"> <xsl:with-param name="strippedDuration" select="substring-after($strippedDuration,'M')"/> <xsl:with-param name="seconds" select="(substring-before($strippedDuration,'M')*30*24*60*60)+$seconds"/> </xsl:call-template> </xsl:when>

The month is the first unit in the duration string. After the check if the month exists, the substring functions will select the right values, there is no need to check the T again.

The time is a little more difficult. The time is located after the T mark. So we must include the T in the check. Simply removing the T in the hours will not work since we could have a string without hours, or even without minutes. So when calling the template again, the T mark must be placed back into the duration.

<xsl:when test="contains(substring-after($strippedDuration,'T'),'H')"> <xsl:call-template name="_durationStep"> <xsl:with-param name="strippedDuration" select="concat('T',substring-after(substring-after($strippedDuration,'T'),'H'))"/> <xsl:with-param name="seconds" select="(substring-before(substring-after($strippedDuration,'T'),'H')*60*60)+$seconds"/> </xsl:call-template> </xsl:when>

There is still one catch. The specification states that the decimal point could be a comma or a full-stop. XSLT math functional expect a full-stop as decimal point. So we need to convert the comma to the full-stop.

The complete solution will be:

<xsl:template name="durationToSeconds"> <xsl:param name="duration"/> <xsl:call-template name="_durationStep"> <xsl:with-param name="strippedDuration"><xsl:value-of select="substring-after(translate($duration,',','.'),'P')"/></xsl:with-param> </xsl:call-template> </xsl:template> <xsl:template name="_durationStep"> <xsl:param name="strippedDuration"/> <xsl:param name="seconds">0</xsl:param> <xsl:choose> <!-- Years (a standard year has 365 days --> <xsl:when test="contains($strippedDuration,'Y')"> <xsl:call-template name="_durationStep"> <xsl:with-param name="strippedDuration" select="substring-after($strippedDuration,'Y')"/> <xsl:with-param name="seconds" select="substring-before($strippedDuration,'Y')*365*24*60*60"/> </xsl:call-template> </xsl:when> <!-- Months (a standard month has 30 days --> <xsl:when test="contains(substring-before($strippedDuration,'T'),'M')"> <xsl:call-template name="_durationStep"> <xsl:with-param name="strippedDuration" select="substring-after($strippedDuration,'M')"/> <xsl:with-param name="seconds" select="(substring-before($strippedDuration,'M')*30*24*60*60)+$seconds"/> </xsl:call-template> </xsl:when> <!-- Weeks --> <xsl:when test="contains($strippedDuration,'W')"> <xsl:call-template name="_durationStep"> <xsl:with-param name="strippedDuration" select="substring-after($strippedDuration,'W')"/> <xsl:with-param name="seconds" select="(substring-before($strippedDuration,'W')*7*24*60*60)+$seconds"/> </xsl:call-template> </xsl:when> <!-- Days --> <xsl:when test="contains($strippedDuration,'D')"> <xsl:call-template name="_durationStep"> <xsl:with-param name="strippedDuration" select="substring-after($strippedDuration,'D')"/> <xsl:with-param name="seconds" select="(substring-before($strippedDuration,'D')*24*60*60)+$seconds"/> </xsl:call-template> </xsl:when> <!-- Hours --> <xsl:when test="contains(substring-after($strippedDuration,'T'),'H')"> <xsl:call-template name="_durationStep"> <xsl:with-param name="strippedDuration" select="concat('T',substring-after(substring-after($strippedDuration,'T'),'H'))"/> <xsl:with-param name="seconds" select="(substring-before(substring-after($strippedDuration,'T'),'H')*60*60)+$seconds"/> </xsl:call-template> </xsl:when> <!-- Minutes --> <xsl:when test="contains(substring-after($strippedDuration,'T'),'M')"> <xsl:call-template name="_durationStep"> <xsl:with-param name="strippedDuration" select="concat('T',substring-after(substring-after($strippedDuration,'T'),'M'))"/> <xsl:with-param name="seconds" select="(substring-before(substring-after($strippedDuration,'T'),'M')*60)+$seconds"/> </xsl:call-template> </xsl:when> <!-- Seconds --> <xsl:when test="contains(substring-after($strippedDuration,'T'),'S')"> <xsl:call-template name="_durationStep"> <xsl:with-param name="strippedDuration" select="concat('T',substring-after(substring-after($strippedDuration,'T'),'S'))"/> <xsl:with-param name="seconds" select="(substring-before(substring-after($strippedDuration,'T'),'S'))+$seconds"/> </xsl:call-template> </xsl:when> <xsl:otherwise> <xsl:value-of select="$seconds"/> </xsl:otherwise> </xsl:choose> </xsl:template>

Sample call of the template:

<xsl:call-template name="durationToSeconds"> <xsl:with-param name="duration" select="P3DT3H"/> </xsl:call-template>

Deze blog is getagd als Datatype Duration XML XSD xsd:duration XSLT

Google
facebook