Date and time processing

1. Date conversions
2. Adding dates
3. Dates: milliseconds since 1970
4. Date addition
5. Date difference
6. Comparing within a date range
7. Last n in date order
8. Date Time addition
9. Date comparisons
10. Adjust time zone
11. First Saturday in the month!
12. Testing for dates, xslt 2
13. Subtract two dates

1.

Date conversions

Michael Kay




> My problem is that I receive for example a date in a concatenated 
> format:

> Ex: 20041207

> And I have to retrieve it in a readable format through my xslt-fo 
> transformation.

> Ex. 12 July 2004

Are you sure that date is 12 July and not 7 December? Probably a stupid question, it's just that I've never seen dates written as YYYYDDMM before.

I'll assume the value is in $d.

XSLT 2.0:

format-date(
  xs:date(
    concat(
      substring($d,1,4),
      '-',
      substring($d,7,2),
      '-',
      substring($d,5,2))),
    '[D01] [MNn] [Y0001]')

2.

Adding dates

Mike Kay and Jeni Tennison



>I want to add 6 months to a date (in xslt2.0) but cant find any 
>references on how to do this.  After much googling I  tried something 
>like:
>
>   <xsl:value-of select="xs:date($reviewDate) + 
>xs:duration('P0Y0M6DT0H00M')"/>
>   
>But the processor (Saxon 8.0)  says that date arithmetic is not 
>supported on xs:duration.  

Mike Kay replied:

xs:date($reviewDate) + xdt:yearMonthDuration('P6D')

Jeni expanded.

xs:duration is defined by XML Schema as a number of years, months, days, hours, minutes and seconds. The trouble is that it's not possible to give definitive answers to comparisons when you have durations that contain both years or months and days, hours, minutes or seconds. For example, which is longer, one month or 30 days? In technical terms, xs:duration is "partially ordered".

XPath 2.0 dodges the question by only supporting two "totally ordered" subtypes of xs:duration: durations that only involve years and months (xdt:yearMonthDuration) and durations that only involve days, hours, minutes and seconds (xdt:dayTimeDuration).

If you want to add a duration that combines the two subtypes, you need to add one and then the other, as in:

  (current-date() + xdt:yearMonthDuration('P1Y')) +
  xdt:dayTimeDuration('P1D')

(Of course, the order in which you add them makes a difference to the result...)

Note that you don't have to include a component if there are 0 of them: if you have 0 days in a duration, you don't need to put '0D' in it.

3.

Dates: milliseconds since 1970

Michael Kay




The main thing I need the Javascript for is the formatting of dates.
The XML produced by our database expresses dates as milliseconds since
January 1, 1970.  The only formatting technique suggested by the
documentation for our database is to use Javascript's Date class.  If
there's a way to format these dates in native XSLT, or anything
besides embedding the Javascript into the code, I haven't found it
yet.

Easy enough in XPath 2.0:

<xsl:value-of select='xs:dateTime("1970-01-01T00:00:00") + 
 $in * xs:dayTimeDuration("PT0.001S")'/>

where $in is the current datetime.

4.

Date addition

Chris Ferrier

This function adds a number of days to a given date

  xmlns:dp="http://www.dpawson.co.uk"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns:xdt="http://www.w3.org/2005/02/xpath-datatypes"
 <xsl:template match="/">
   
        <xsl:value-of select="dp:date-add($date1,3)"/>

  </xsl:template>
<xsl:variable name="one-day" select="xdt:dayTimeDuration('P1D')"/>

<xsl:variable name="date1" select="xs:dateTime('2005-03-01T12:00:00')"/>


  <xsl:function name="dp:date-add">
    <xsl:param name="date" as="xs:dateTime"/>
    <xsl:param name="days" as="xs:integer"/>
    <xsl:variable name="day" select="$date + ($one-day * $days)"/>
    <xsl:value-of select="format-dateTime($day,'[Y,*-2]-[M,2]-[D,2]')"/>

  </xsl:function>

5.

Date difference

Jeni Tennison



> I have two elements:
> <due date>2005-04-05</due date>
> <actual arrival>2005-04-11T22:21:30</actual arrival>
>
> what is the function to display the day-time difference?

Assuming you're using XSLT 2.0 and that the elements are actually called <due-date> and <actual-arrival>, you can use the minus operator as follows:

  xs:dateTime(actual-arrival) - xs:dateTime(xs:date(due-date))

to get the xdt:dayTimeDuration P6DT22H21M30S (6 days, 22 hours, 21 minutes, 30 seconds). You can then use the days-from-duration(), hours-from-duration() etc. functions to extract the values of the individual components from that duration in order to make something readable.

Note that the xs:date() constructor constructs an xs:date from the due date, and the xs:dateTime() constructor casts this to a xs:dateTime by adding 00:00:00 as the time.

6.

Comparing within a date range

Michael Kay


> I want to know how to get particular employees covered  between 
> two different  joining dates (for example Fromdate and Todate ).

> What is the  Xpath / XSL expression.

If the source documents have a schema, and the attributes are defined in the schema as being of type xs:date, and you are using a schema-aware XSLT 2.0 processor, then you can just write

employee[@FromDate le $d and @ToDate ge $d]

If you are not using a schema, but you are using XSLT 2.0, and your dates are stored in the ISO 8601 format that XML Schema uses, then you can still do the same kind of comparison but you need to do a little more work:

employee[xs:date(@FromDate) le $d and xs:date(@ToDate) ge $d]

If you are not using a schema and your dates are in some other format, or if you are using XSLT 1.0, then you will need to do even more work, but this then depends entirely on the format you are starting with.

7.

Last n in date order

David Carlisle

I write a weblog using the atom format. Each entry is dated. I wanted a recent index of the last 12 entries, by date order. I had found half a solution. David C showed me the way to the rest, which I think has a couple of learning points. I certainly learned from it. My source document had a feed element as the dcoument element, and numerous entry elements which needed sorting by date.

Firstly, the context of a template. The context is the set of all matching nodes, the position that of the current node within the context. This is helpful here.

Next, I needed to calculate if an entry was within the last thirty days. Then I needed to determine if its position() was greater than that of the last 12 entries.

Firstly the date difference



<xsl:variable name="now" select="current-dateTime()"/>

 <xsl:function name="d:dateDiff" as="xs:boolean">
  <xsl:param name="oldDate" as="xs:dateTime"/>
   <xsl:param name="newDate" as="xs:dateTime"/>
   <xsl:param name="count" as="xs:integer"/>

   <xsl:variable name="diff" select="xs:integer(days-from-duration($newDate - $oldDate))"/>
   <xsl:variable name="diffx" select="days-from-duration($newDate - $oldDate)"/>

<!--
   <xsl:message>
     diff   <xsl:value-of select="$diff"/> [<xsl:value-of select="$diff &lt; $count"/>]
   </xsl:message>
-->
   <xsl:sequence select="$diff &lt; $count"/>
 </xsl:function>

The date parameter is taken from the entry, the current date from a global variable. The function returns true if the difference in dates is less than the number passed as the third parameter.

Next, I needed a template. David suggested.


 <xsl:template 
    match="a:entry[d:dateDiff(a:published,$now,30)]
           [position() &gt; (last() - 12)]">
      <xsl:copy>
        <xsl:copy-of select="@*"/>
        <xsl:apply-templates select="a:title | a:link| a:summary|a:updated|a:id"/>
      </xsl:copy>      
 </xsl:template>
 

 <xsl:template match="a:entry"/>


All it does is copy input to output, with the processing being done in the match pattern The first predicate uses the above function to determine if the entry is 'recent' (within the last 30 days). The second checks that the position() of the node within all entry nodes is within the last 12. Simple! For some! Nice one David

8.

Date Time addition

Abel Braaksma

(In xslt2.0)I would like to increment a xs:dateTime variable dt by one month. And have the result in (or somehow converted to) xs:dateTime. How can I achieve this.

I think that you should explicitly convert the first argument to xs:dateTime, or it must be explicitly declared. This works, and returns "2004-12-15T15:12:10" (which is 1 day and 1 hour later than the supplied date time):

<xsl:value-of select="xs:dateTime('2004-12-14T14:12:10') +
xs:dayTimeDuration('P1DT1H')"></xsl:value-of>

9.

Date comparisons

Abel Braaksma



> < So I'm dealing with a schema which has an attribute:
> <
> <   <xs:simpleType  name="time">
> <     <xs:union  memberTypes="
> <           xs:gYear xs:gYearMonth xs:date xs:dateTime"  />
> <   </xs:simpleType>
> <
> < I had to write a function to let someone compare two of these 'time'
> < simple types.  It appeared as though the only way to do comparisions
> < was with equal times, so that mean converting everything to xs:dateTime.
>

True, but that does not need to be as hard as you think. I tried applying date:parse-date from EXSLT, but it was not supported by Saxon (not sure you use Saxon, though). But I may have a simpler solution for you.

> The details of what I need are:
>
>   'time' may be xs:dateTime, xs:date, xs:gYear, xs:gYearMonth
>    $a is a 'time' in a document
>    $b is a 'time' provided by a user
>
>    Given $a and $b I want to allow for eq, lt, le, gt, ge
>    comparision of $a and $b)
>

You created quite a complex function. I today pondered a bit about your question and I came up with the following solution. I requires that you do not add gMonth, gDay etc to the list, because it employs padding the date.

What I do is, in fact, the same as you: make the value a dateTime and compare on the resulting value. However, I don't believe you need so many, long functions. In fact, you only need 8 lines, including the declaration. Here's what I tried:

1) Take a "picture" string, I call it a "null-date": 0001-01-01T00:00:00.000

2) Apply that to your stringized date/datetime/year/yearmonth

3) Convert the result.

For instance, take a gYear value "2006", use the null-date as overlay (see function below) and you get "2006-01-01T00:00:00.000", which is a comparable dateTime. The same applies to gYearMonth "2006-06" which would become "2006-06-01T00:00:00.000", and so on.

Here's the whole template. Call it on itself and it will show you that it works for your four datatypes. You can use any operation on the result of running date:parse-date. I used the EXSLT date namespace, but you are of course free to choose your own.

<xsl:stylesheet version="2.0"
   xmlns:xs = "http://www.w3.org/2001/XMLSchema";
   xmlns:date="http://exslt.org/dates-and-times";
   xmlns:xsl="http://www.w3.org/1999/XSL/Transform";>

   <xsl:output method="text"/>

   <xsl:template match="/" >
       <xsl:variable name="dateTime"
as="xs:dateTime">2006-12-30T16:12:22.234</xsl:variable>
       <xsl:variable name="date" as="xs:date">2006-12-30</xsl:variable>
       <xsl:variable name="yearMonth"
as="xs:gYearMonth">2006-12</xsl:variable>
       <xsl:variable name="year" as="xs:gYear">2006</xsl:variable>

       dateTime  : <xsl:value-of select="$dateTime" />
       date      : <xsl:value-of select="$date" />
       yearMonth : <xsl:value-of select="$yearMonth" />
       year      : <xsl:value-of select="$year" />

       Conv dateTime  : <xsl:value-of
select="date:parse-date($dateTime)" />
       Conv date      : <xsl:value-of select="date:parse-date($date)" />
       Conv yearMonth : <xsl:value-of
select="date:parse-date($yearMonth)" />
       Conv year      : <xsl:value-of select="date:parse-date($year)" />

       $year lt $date : <xsl:value-of select="date:parse-date($year) gt
date:parse-date($date)" />

   </xsl:template>

   <xsl:function name="date:parse-date" as="xs:dateTime">
       <xsl:param name="parsable-date" />
       <xsl:variable
name="null-date">0001-01-01T00:00:00.000</xsl:variable>
       <xsl:variable name="padded-date" select="
           concat(xs:string($parsable-date),
               substring($null-date
               , string-length(xs:string($parsable-date)) + 1
               , 40))" />
       <xsl:value-of select="xs:dateTime(substring($padded-date, 1,
23))" />
   </xsl:function>

</xsl:stylesheet>

10.

Adjust time zone

Abel Braaksma


I am using XSLT 2and trying to call the function
"adjust-time-to-timezone"

Here's code that works with XSLT 2.0

<xsl:variable name="MY_TIME" select=" '20:30:10' "/>
<xsl:variable name="DURATION" select=" '-PT6H' "/>
<xsl:value-of
      select="adjust-time-to-timezone(xs:time($MY_TIME),
                  xs:dayTimeDuration($DURATION))"/>

The namespace 'xs' must be bound to "http://www.w3.org/2001/XMLSchema";

11.

First Saturday in the month!

David Carlisle

This for example returns the first saturday of the current month.

$ saxon8 -it main saturday.xsl

<?xml version="1.0" encoding="UTF-8"?>
today:  2007-01-30Z
1st of month   2007-01-01Z
1st day month   Monday
1st saturday of month : 2007-01-06Z
<xsl:stylesheet version="2.0"
       xmlns:xs="http://www.w3.org/2001/XMLSchema";
       xmlns:xsl="http://www.w3.org/1999/XSL/Transform";>


<xsl:template name="main">
today:  <xsl:value-of  select="current-date()"/>
<xsl:variable name="today" select="current-date()"/>
<xsl:variable name="fom"
 select="current-date()-
xs:dayTimeDuration(concat('P',day-from-date(current-date())-1,'DT0S'))"/>
1st of month   <xsl:value-of  select="$fom"/>
1st day month   <xsl:value-of  select="format-date($fom,'[F]')"/>
1st saturday of month : <xsl:value-of select="(for $d in  0 to 6
 return
  $fom + xs:dayTimeDuration(concat('P',$d,'DT0S')))[format-date(.,'[F]')='Saturday']
 "/>
</xsl:template>
</xsl:stylesheet>

12.

Testing for dates, xslt 2

Florent Georges


> <xsl:variable name="is-date" select="if(DATE_PACKED_XSD
> castable as xs:date) then '1' else('0')" />

Why not simply:

   <xsl:variable name="is-date" as="xs:boolean"
                 select="DATE_PACKED_XSD castable as xs:date"/>


> <xsl:choose>
>   <xsl:when test="$is-date = '1'">
>     ... some processing here
>   </xsl:when>
>   <xsl:otherwise>
>     ... some different processing here
>   </xsl:otherwise>
> </xsl:choose>

> What I expected to happen was that when the stylesheet
> processed a DATE_PACKED_XSD element that could not be cast
> as a date ($is-date = 0), processing would pass to the
> <xsl:otherwise> branch.

> What did happen was this:

> stylesheet.xslt:423: Fatal Error! Invalid date "208 2-01-1
> 2".  Non-numeric component


> What am I misapprehending about "castable as"?

Nothing IMHO. Could you please show a little example that reproduces the problem?

   ~> cat xslt/tests/castable-as.xsl
   <xsl:transform
       xmlns:xsl="http://www.w3.org/1999/XSL/Transform";
       xmlns:xs="http://www.w3.org/2001/XMLSchema";
       version="2.0">

     <xsl:output method="text"/>

     <xsl:template name="main">
       <xsl:value-of select="
           for $d in ('2006-08-29', '208 2-01-1 2') return
             $d castable as xs:date"/>
     </xsl:template>

   </xsl:transform>

   ~> saxon -it main xslt/tests/castable-as.xsl
   true false

13.

Subtract two dates

Michael Kay



>I want to know if it is possible to calculate the number of months
>between two dates. For example xs:duration(xs:date('2009-09-23') -
>current-date())

One possible algorithm, which you can easily implement as a function, is

(year-from-date($d1) - year-from-date($d2))*12
+ (month-from-date($d1) - month-from-date($d2))
- if (days-from-date($d1) ge days-from-date($d2)) then 0 else 1

That works reasonably for $d2 < $d1. To handle $d1 < $d2 one approach is to use the same formula as above, another is to return -($d2 - $d1). For 2007-05-31 - 2007-06-01 one approach gives you -1, the other gives you 0.

Another algorithm is to ignore the day entirely, and just use the year and month components. Or you can divide the day by the number of days in that month.