yegor256
yegor256

Reputation: 105193

How to convert ISO 8601 date/time into milliseconds in XSLT 1.0?

How can I convert ISO 8601 2013-08-13T17:57:55Z date/time text into "milliseconds since epoch" using XSLT 1.0? More specifically, Google Chrome's version of XSLT.

Upvotes: 1

Views: 1281

Answers (2)

C. M. Sperberg-McQueen
C. M. Sperberg-McQueen

Reputation: 25054

Extending an algorithm for Julian dates taught me by my old manager (hi, George!), who got it in turn from the Collected Algorithms of the ACM (although it does not appear to be included in the online version of CALGO and I have not been able to find the algorithm number, the date of publication, or the author [but see Postscript below]), the following template calculates, for a given ISO 8601 timestamp in time zone Z, the number of milliseconds since the beginning of Julian Day 0, excluding leap seconds. Julian Day 0 begins at noon on 1 January 4713 BC in the Julian calendar; that's noon on 24 November 4714 BCE in the Gregorian calendar.

Checking the input for sanity, adjustment to use a different epoch, extension to handle time zones other than Z, and extension to handle leap seconds and dates before the common era are all left as an exercise for the reader. (Or you could call out to Javascript, but where's the fun in that?)

<xsl:template name="ts2i">
  <!--* timestamp to integer:  convert an ISO 8601 time stamp
      * to the number of milliseconds since an epoch.
      * Our epoch is 1 January 4713 BCE (Julian!),
      * which is Julian day 0.
      * To use 1970-01-01T00:00:00Z as the epoch,
      * subtract 210866760000000 ms.
      *-->
  <xsl:param name="ts"/>
  <!--* checking the timestamp for bad data is left as an
      * exercise of the reader.  Our contract is simpler:
      * give me a correct timestamp in time zone Z, for a 
      * date on or after 0001-01-01, and I'll
      * give you a correct answer (modulo overflow).
      *-->
  <!--* yyyy-mm-ddThh:mm:ss.sss...Z 
      * ....|....|....|....|...   | 
      * 1   5   10   15   20      n
      *-->

  <!--* Parse out c, y, m, d, hh, mm, ss (for century, 
      * years in current century, months since February, 
      * days in current month, hours, minutes, seconds).
      * the y and m values are adjusted to make the 
      * year begin 1 March (so leap day is always the last
      * day of the year).
      *-->
  <xsl:variable name="y0" select="substring($ts,1,4)"/>
  <xsl:variable name="m0" select="substring($ts,6,2)"/>
  <xsl:variable name="d"  select="substring($ts,9,2)"/>
  <xsl:variable name="y1">
    <xsl:choose>
      <xsl:when test="$m0 &lt; 3"><xsl:value-of select="$y0 - 1"/></xsl:when>
      <xsl:otherwise><xsl:value-of select="$y0"/></xsl:otherwise>
    </xsl:choose>
  </xsl:variable>
  <xsl:variable name="m" select="($m0 + 9) mod 12"/>
  <xsl:variable name="c" select="floor($y1 div 100)"/>
  <xsl:variable name="y" select="($y1 mod 100)"/>
  <xsl:variable name="hh" select="substring($ts,12,2)"/>
  <xsl:variable name="mm" select="substring($ts,15,2)"/>
  <xsl:variable name="s0" select="substring($ts,18)"/>
  <xsl:variable name="ss">
    <xsl:choose>
      <xsl:when test="contains($s0,'Z')">
        <xsl:value-of select="substring-before($s0,'Z')"/>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$s0"/>    
      </xsl:otherwise>
    </xsl:choose>
  </xsl:variable>
  <!--* H holds the offset in days between Julian day 0
      * and the beginning of the common era.
      * J holds the offset in ms between midnight and
      * noon, when Julian day 0 actually began.
      *-->
  <xsl:variable name="H" select="1721119"/>
  <xsl:variable name="J" select="43200000"/>

  <!--* Calculate the Julian day that begins on the 
      * given date.  There are 146097 days in each
      * 400-year period (including 25 leap days),
      * 1461 in each 4-year period, and there are
      * (($m * 153) + 2) div 5 days in $m months 
      * elapsed since 1 March.
      * This is straight from the Collected Algorithms.
      *-->
  <xsl:variable name="j" select="floor(($c * 146097) div 4) 
                                 + floor(($y * 1461) div 4) 
                                 + floor((($m * 153) + 2) div 5) 
                                 + $d + $H"/>

  <!--* Calculate the milliseconds since the beginning
      * of Julian day 0.  This is my extension, and 
      * it could have an off-by-one error.
      *-->
  <xsl:value-of select="$j   * 86400000
                        + $hh * 3600000
                        + $mm *   60000
                        + $ss *    1000
                        - $J"/>
</xsl:template>

When given the input value "2013-08-13T17:57:55Z", the template given returns the numeric value 2.12243176675e+14, i.e. 212243176675000.

As noted in one of the comments, I was worried about overflow when I wrote this, but XSLT 1.0 numbers are IEEE doubles, and thus have (I believe) mantissas of 52 bits. So overflow is not likely to be a problem in the next several thousand years; your milliseconds will not be subject to rounding errors until 15 April 138001 or so.

Postscript: A little research in the ACM Digital Library turned up what I believe must have been the source from which my manager George Yanos learned this algorithm: Robert G. Tantzen, "Algorithm 199: conversions between calendar date and Julian day number", Communications of the ACM 6.8 (Aug 1963): 444. I notice that Tantzen does not explain any of the magic constants, which gives his laconic Algol code an air of mystery.

Upvotes: 2

Michael Kay
Michael Kay

Reputation: 163625

The only way is by calling out to Javascript code.

Upvotes: 1

Related Questions