Chapter 4. Dates and Times

Does anyone really know what time it is? Does anyone really care?

Chicago

Introduction

Native XSLT 1.0 does not know what time it is and does not seem to care. However, dates and times are a necessary aspect of everyday life. The need to manipulate them arises frequently in computing, especially in web development. Therefore, it was surprising and unfortunate that standard XSLT 1.0 did not have any built-in date and time support.

The situation in XSLT 2.0 has improved substantially. XPath 2.0 has numerous functions for manipulating dates, times, and durations. In fact, the only thing 2.0 leaves us wanting for, with respect to date and time, is functions with shorter names! I am told names like subtract-dates-yielding-yearMonthDuration() were provided partly to appease the XQuery database weenies. The alternation between dashes and camelcase follows from the differing conventions adopted by XPath and XML Schema committees.

The examples in this section can help compensate for XSLT 1.0’s lack of support for dates and times. Unfortunately, one of the most crucial date and time capabilities cannot be implemented in XSLT 1.0—that is, getting the current date and time. For that, you need to call out to another language whose library supports interacting with the hardware’s real-time clock. Both Java and JavaScript have this capability. If your application just needs to format dates and times that already exist in a document, then the routines here should cover most needs. In 2.0, use current-date(), current-time(), and current-dateTime().

Doing date and time manipulation and conversion without native support in the language can be tricky, but it is almost purely an exercise in intricate integer arithmetic involving what are essentially base conversions in a mixed radix system. Working with non-Gregorian calendars and determining holidays also requires quite a bit of historical, religious, and cultural knowledge. Readers with no application for date and time routines may wish to skip this chapter because little by way of XSLT technique is unique to these algorithms. Those who are curious about the theory behind these calculations should definitely look at the papers cited in the See Also section, next.

I am grateful to Jason Diamond for graciously contributing many of the templates dealing with Gregorian time. The XSLT code for dealing with non-Gregorian calendars was adapted from Edward M. Reingold’s public domain Lisp implementation. Some of the algorithms were adapted to better suit XSLT and build upon the existing foundation provided by Jason’s code.

Do not be confused by the technique used to pass a date into most templates. It is designed for maximum convenience. You can pass in the date in two ways: by using a string formatted using ISO date-time rules and by using the individual parameters for the year, month, and day. The following example should clarify usage:

<xsl:call-template name="ckbk:calculate-day-of-the-week">
     <xsl:with-param name="date-time" select="'2002-01-01T01:00:00'"/>
</xsl:call-template>
   
<xsl:call-template name="ckbk:calculate-day-of-the-week">
     <xsl:with-param name="date" select="'2002-01-01'"/>
</xsl:call-template>
   
<xsl:call-template name="ckbk:calculate-day-of-the-week">
     <xsl:with-param name="year" select="2002"/>
     <xsl:with-param name="month" select="01"/>
     <xsl:with-param name="day" select="01"/>
</xsl:call-template>

Each of the calls evaluates the day of the week for January 1, 2002. The first two variations are convenient when a date is already ISO formatted. The last variation is convenient when the components of the date are stored separately. You can also override parts of an ISO date string as follows:

<xsl:call-template name="ckbk:calculate-day-of-the-week">
     <xsl:with-param name="date" select="'2002-01-01'"/>
     <xsl:with-parm name="day" select="25"/>
</xsl:call-template>

In all templates, unless otherwise stated, dates are Gregorian by default. This civil calendar system is used by most of the Western world.

See Also

Claus Tøndering’s calendars FAQ (available at http://www.pauahtun.org/CalendarFAQ/cal/calendar24.pdf) contains the illuminating theory behind many calculations in this chapter.

Jason Diamond’s original XSLT implementation can be found at http://xsltsl.sourceforge.net/date-time.html.

Calendrical Calculations (http://emr.cs.iit.edu/~reingold/calendar.ps) by Nachum Dershowitz and Edward M. Reingold provides additional insight. Their paper discusses the non-Gregorian systems covered here.

I do not cover the Chinese or Indian calendars in this chapter because they are more complex. Information about the mathematics of the Chinese and Indian calendars can be found at http://www.math.nus.edu.sg/aslaksen/calendar/chinese.shtml and http://www.math.nus.edu.sg/aslaksen/calendar/indian.shtml, respectively.

If, for some reason, you have an application for the Mayan, French Revolutionary, or Old Hindu calendars, then you should investigate Calendrical Calculations II (http://emr.cs.iit.edu/~reingold/calendar2.ps).

EXSLT.org has a date-time module that provides much of the same functionality as the Gregorian date-time templates implemented in this chapter. They also provide templates for working durations (differences between dates and times).

XForms (http://www.w3.org/MarkUp/Forms/) provides some date-time functionality. It uses XML Schema (ISO) lexical representations (with durations subdivided into dateTime and yearMonth flavors). The new functions are: now(), days-from-date(), seconds-from-dateTime(), seconds(), and months( ).

4.1. Calculating the Day of the Week

Problem

Given the year, month, and day, you want to determine the day of the week.

Solution

XSLT 1.0

The following calculation does the trick and returns an integer in the range of 0-6, where 0=Sunday.

  <xsl:template name="ckbk:calculate-day-of-the-week">
    <xsl:param name="date-time"/>
    <xsl:param name="date" select="substring-before($date-time,'T')"/>
    <xsl:param name="year" select="substring-before($date,'-')"/>
    <xsl:param name="month" 
          select="substring-before(substring-after($date,'-'),'-')"/>
    <xsl:param name="day" select="substring-after(substring-after($date,'-'),'-')"/>
    
    <xsl:variable name="a" select="floor((14 - $month) div 12)"/>
    <xsl:variable name="y" select="$year - $a"/>
    <xsl:variable name="m" select="$month + 12 * $a - 2"/>
   
    <xsl:value-of select="($day + $y + floor($y div 4) - floor($y div 100) 
    + floor($y div 400) + floor((31 * $m) div 12)) mod 7"/>
   
  </xsl:template>

XSLT 2.0

Use format-date to get the day of the week as a number or a language-dependent string.

Discussion

You will notice that these equations and those in other examples make judicious use of the XPath floor() function. This is the only way to emulate integer arithmetic in XSLT 1.0, since all numbers are represented in floating point internally. The reason why this calculation works has to do with intricacies of the Gregorian calendar that are not particularly relevant to XSLT. For example, the fact that 97 leap years occur every 400 years so that every year divisible by 4 is a leap year, except if it is divisible by 100 and not divisible by 400, explains the final calculation. For further information, see Sidebar 1.

4.2. Determining the Last Day of the Month

Problem

Given a month and a year, determine the last day of the month.

Solution

XSLT 1.0

<xsl:template name="ckbk:last-day-of-month">
     <xsl:param name="month"/>
     <xsl:param name="year"/>
     <xsl:choose>
       <xsl:when test="$month = 2 and 
          not($year mod 4) and 
          ($year mod 100 or not($year mod 400))">
         <xsl:value-of select="29"/>
          </xsl:when>
       <xsl:otherwise>
         <xsl:value-of 
          select="substring('312831303130313130313031',
                         2 * $month - 1,2)"/>
       </xsl:otherwise>
     </xsl:choose>
</xsl:template>

XSLT 2.0

You can translate the 1.0 recipe to a function or take advantage of date math and subtract a day from the first of the following month. This solution is easier to understand but the former is probably faster:

<xsl:function name="ckbk:last-day-of-month" as="xs:integer">

    <xsl:param name="month" as="xs:integer"/>
    <xsl:param name="year" as="xs:integer"/>

    <!--First of the month in given year -->
    <xsl:variable name="date" 
            select="xs:date(concat(xs:string($year),
                         '-',format-number($month,'00'),'-01'))"
            as="xs:date"/>

    <xsl:variable name="m" 
            select="xdt:yearMonthDuration('P1M')" 
            as="xdt:yearMonthDuration"/>

    <xsl:variable name="d" 
            select="xdt:dayTimeDuration('P1D')" 
            as="xdt:dayTimeDuration"/>
 
    <xsl:sequence select="day-from-date(($date + $m) - $d)"/>

</xsl:function>

Discussion

This function has potential application for constructing pages of a calendar. It is simple enough to understand once you know the rules governing a leap year. This function was translated from Lisp, in which the number of days in each month was extracted from a list. I choose to use substring to accomplish the same task. You may prefer to place the logic in additional when elements. You might also store data about each month, including its length, in XML.

See Also

See Recipe 4.5 for ways to store calendar metadata in XML.

4.3. Getting Names for Days and Months

Problem

You want to convert from numerical values for days and months to symbolic values.

Solution

XSLT 1.0

If internationalization is not important to your application, then the following simple code will do:

  <xsl:template name="ckbk:get-day-of-the-week-name">
    <xsl:param name="day-of-the-week"/>
   
    <xsl:choose>
      <xsl:when test="$day-of-the-week = 0">Sunday</xsl:when>
      <xsl:when test="$day-of-the-week = 1">Monday</xsl:when>
      <xsl:when test="$day-of-the-week = 2">Tuesday</xsl:when>
      <xsl:when test="$day-of-the-week = 3">Wednesday</xsl:when>
      <xsl:when test="$day-of-the-week = 4">Thursday</xsl:when>
      <xsl:when test="$day-of-the-week = 5">Friday</xsl:when>
      <xsl:when test="$day-of-the-week = 6">Saturday</xsl:when>
      <xsl:otherwise>
          error: <xsl:value-of select="$day-of-the-week"/>
        </xsl:otherwise>
    </xsl:choose>
   
  </xsl:template>
   
   <xsl:template name="ckbk:get-day-of-the-week-abbreviation">
    <xsl:param name="day-of-the-week"/>
   
    <xsl:choose>
      <xsl:when test="$day-of-the-week = 0">Sun</xsl:when>
      <xsl:when test="$day-of-the-week = 1">Mon</xsl:when>
      <xsl:when test="$day-of-the-week = 2">Tue</xsl:when>
      <xsl:when test="$day-of-the-week = 3">Wed</xsl:when>
      <xsl:when test="$day-of-the-week = 4">Thu</xsl:when>
      <xsl:when test="$day-of-the-week = 5">Fri</xsl:when>
      <xsl:when test="$day-of-the-week = 6">Sat</xsl:when>
      <xsl:otherwise>
          error: <xsl:value-of select="$day-of-the-week"/>
        </xsl:otherwise>
    </xsl:choose>
   
  </xsl:template>
   
  <xsl:template name="ckbk:get-month-name">
    <xsl:param name="month"/>
   
    <xsl:choose>
      <xsl:when test="$month = 1">January</xsl:when>
      <xsl:when test="$month = 2">February</xsl:when>
      <xsl:when test="$month = 3">March</xsl:when>
      <xsl:when test="$month = 4">April</xsl:when>
      <xsl:when test="$month = 5">May</xsl:when>
      <xsl:when test="$month = 6">June</xsl:when>
      <xsl:when test="$month = 7">July</xsl:when>
      <xsl:when test="$month = 8">August</xsl:when>
      <xsl:when test="$month = 9">September</xsl:when>
      <xsl:when test="$month = 10">October</xsl:when>
      <xsl:when test="$month = 11">November</xsl:when>
      <xsl:when test="$month = 12">December</xsl:when>
      <xsl:otherwise>error: <xsl:value-of select="$month"/></xsl:otherwise>
    </xsl:choose>
   
  </xsl:template>
   
  <xsl:template name="ckbk:get-month-abbreviation">
    <xsl:param name="month"/>
   
    <xsl:choose>
      <xsl:when test="$month = 1">Jan</xsl:when>
      <xsl:when test="$month = 2">Feb</xsl:when>
      <xsl:when test="$month = 3">Mar</xsl:when>
      <xsl:when test="$month = 4">Apr</xsl:when>
      <xsl:when test="$month = 5">May</xsl:when>
      <xsl:when test="$month = 6">Jun</xsl:when>
      <xsl:when test="$month = 7">Jul</xsl:when>
      <xsl:when test="$month = 8">Aug</xsl:when>
      <xsl:when test="$month = 9">Sep</xsl:when>
      <xsl:when test="$month = 10">Oct</xsl:when>
      <xsl:when test="$month = 11">Nov</xsl:when>
      <xsl:when test="$month = 12">Dec</xsl:when>
      <xsl:otherwise>error: <xsl:value-of select="$month"/></xsl:otherwise>
    </xsl:choose>
   
  </xsl:template>

XSLT 2.0

XSLT 2.0 has the format-date() function, which solves this recipe and much more. Depending on your implementation, it may also handle internationalization:

<xsl:function name="ckbk:get-day-of-the-week-name" as="xs:string">
  <xsl:param name="day-of-the-week" as="xs:integer"/>

  <!-- Any old Sunday will do as the base. Here I arbitrarily picked 
       Sunday, Aug 14 2005 as my base date because it happens to be the 
       day I am writing this-->

  <xsl:variable name="date" 
        select="concat('2005-08-', string(14 + $day-of-the-week))" as="xs:date"/>
   
  <xsl:sequence select="format-date($date,"[F]"/>
</xsl:function>
   
<xsl:function name="ckbk:get-day-of-the-week-name-abbr" as="xs:string">
  <xsl:param name="day-of-week" as="xs:integer"/>

  <xsl:variable name="date" 
        select="concat('2005-08-', string(14 + $day-of-the-week))" as="xs:date"/>

  <xsl:sequence select="format-date($date, '[FNn,3-3]')"/>
    
</xsl:function>   

<xsl:function name="ckbk:get-month-name" as="xs:string">
  <xsl:param name="month" as="xs:integer"/>

  <xsl:variable name="jan01" select="xs:date('2005-01-01')" as="xs:date"/>

  <!-- Here we use date+duration math rather than string manipulation to get
       the date we need for format-date --> 
  <xsl:sequence select="format-date($jan01 +
       xdt:yearMonthDuration(concat('P',$month - 1,'M')), '[MNn]')"/>
    
</xsl:function>   

<xsl:function name="ckbk:get-month-name-abbr" as="xs:string">
  <xsl:param name="month" as="xs:integer"/>

  <xsl:variable name="jan01" select="xs:date('2005-01-01')" as="xs:date"/>

  <xsl:sequence 
     select="format-date($jan01 + 
               xdt:yearMonthDuration(concat('P',$month - 1,'M')), '[MNn,3-3]')"/>    

</xsl:function>

The second parameter to format-date is a picture string that surrounds formatting codes in square brackets (e.g., [D01]). There is a rich variety of formatting options available. See http://www.w3.org/TR/xslt20/#function-format-date for more details.

Discussion

XSLT 1.0

These templates are just fine if your application will never be used outside of the English-speaking world. However, you might consider using a table-driven approach for added portability:

<xsl:stylesheet
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:date="http://www.ora.com/XSLTCookbook/NS/dates">
   
<!-- United States : us -->
<ckbk:month country="us" m="1"  name="January" abbrev="Jan" />
<ckbk:month country="us" m="2"  name="February" abbrev="Feb"/>
<ckbk:month country="us" m="3"  name="March" abbrev="Mar"/>
<ckbk:month country="us" m="4"  name="April" abbrev="Apr"/>
<ckbk:month country="us" m="5"  name="May" abbrev="May"/>
<ckbk:month country="us" m="6"  name="June" abbrev="Jun"/>
<ckbk:month country="us" m="7"  name="July" abbrev="Jul"/>
<ckbk:month country="us" m="8"  name="August" abbrev="Aug"/>
<ckbk:month country="us" m="9"  name="September" abbrev="Sep"/>
<ckbk:month country="us" m="10" name="October" abbrev="Oct"/>
<ckbk:month country="us" m="11" name="November" abbrev="Nov"/>
<ckbk:month country="us" m="12" name="December" abbrev="Dec"/>
   
<!-- Germany : de -->
<ckbk:month country="de" m="1"  name="Januar" abbrev="Jan"/>
<ckbk:month country="de" m="2"name=";Februar" abbrev="Feb"/>
<ckbk:month country="de" m="3"  name="März" abbrev="Mär"/>
<ckbk:month country="de" m="4"  name="April" abbrev="Apr"/>
<ckbk:month country="de" m="5"  name="Mai" abbrev="Mai"/>
<ckbk:month country="de" m="6"  name="Juni" abbrev="Jun"/>
<ckbk:month country="de" m="7"  name="Juli" abbrev="Jul"/>
<ckbk:month country="de" m="8"  name="August" abbrev="Aug"/>
<ckbk:month country="de" m="9"  name="September" abbrev="Sep"/>
<ckbk:month country="de" m="10" name="Oktober" abbrev="Okt"/>
<ckbk:month country="de" m="11" name="November" abbrev="Nov"/>
<ckbk:month country="de" m="12" name="Dezember" abbrev="Dez"/>
<!-- You get the idea ... -->
   
<!-- Store element in variable for easy access -->
<xsl:variable name="ckbk:months" select="document('')/*/ckbk:month"/>
   
</xsl:stylesheet>
   
<xsl:stylesheet
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:date="http://www.ora.com/XSLTCookbook/dates">
   
<xsl:include href="date-conversion.xsl"/>
   
<xsl:template name="ckbk:get-month-name">
     <xsl:param name="month"/>
     <xsl:param name="country" select=" 'us' "/>
   
     <xsl:value-of select="$ckbk:months[@country=$country and 
          @m=$month]/@name"/>
</xsl:template>

XSLT 2.0

The format-date(), format-time(), and format-dateTime() functions are quite rich. In addition to the capabilities demonstrated by these recipes, it can format all components of dates and times in multiple ways. They also allow country, calendar, and language codes to be specified, so internationalization is free (subject to the limits of your implementation).

See Also

The full specification of the XSLT 2.0 date formatting functions can be found in Recipe 4.10.

4.4. Calculating Julian and Absolute Day Numbers from a Specified Date

Problem

You have a date and would like to know the corresponding Julian day number and/or absolute day number.

Solution

XSLT 1.0

This template will give you the Julian day, given the year, month, and day:

<xsl:template name="ckbk:calculate-julian-day">
     <xsl:param name="year"/>
     <xsl:param name="month"/>
     <xsl:param name="day"/>
   
    <xsl:variable name="a" select="floor((14 - $month) div 12)"/>
    <xsl:variable name="y" select="$year + 4800 - $a"/>
    <xsl:variable name="m" select="$month + 12 * $a - 3"/>
   
    <xsl:value-of select="$day + floor((153 * $m + 2) div 5) + $y * 365 + 
          floor($y div 4) - floor($y div 100) + floor($y div 400) - 
          32045"/>
   
  </xsl:template>

Once you have a way to calculate the Julian day number, it is easy to create a template for determining the number of days between any two dates:

<xsl:template name="ckbk:date-difference">
     <xsl:param name="from-year"/>
     <xsl:param name="from-month"/>
     <xsl:param name="from-day"/>
     <xsl:param name="to-year"/>
     <xsl:param name="to-month"/>
     <xsl:param name="to-day"/>
   
     <xsl:variable name="jd1">
        <xsl:call-template name="ckbk:calculate-julian-day">
         <xsl:with-param name="year" select="$from-year"/>
           <xsl:with-param name="month" select="$from-month"/>
           <xsl:with-param name="day" select="$from-day"/>
       </xsl:call-template>
     </xsl:variable>
   
     <xsl:variable name="jd2">
        <xsl:call-template name="ckbk:calculate-julian-day">
         <xsl:with-param name="year" select="$to-year"/>
           <xsl:with-param name="month" select="$to-month"/>
           <xsl:with-param name="day" select="$to-day"/>
       </xsl:call-template>
     </xsl:variable>
   
     <xsl:value-of select="$jd1 - $jd2"/>
</xsl:template>

The following templates convert from a Julian day to a Gregorian date in the form YYYY/MM/DD. Use substring-before, substring-after, and translate to parse or convert to the conventions of a particular locale:

<xsl:template name="ckbk:julian-day-to-julian-date">
     <xsl:param name="j-day"/>
   
     <xsl:call-template name="ckbk:julian-or-gregorian-date-elem">
          <xsl:with param name="b" select="0"/>
          <xsl:with param name="c" select="$j-day + 32082"/>
     </xsl:call-template>
   
</xsl:template>
   
<xsl:template name="ckbk:julian-day-to-gregorian-date">
     <xsl:param name="j-day"/>
   
     <xsl:variable name="a" select="$j-day + 32044"/>
     <xsl:variable name="b" select="floor((4 * $a + 3) div 146097)"/>
     <xsl:variable name="c" select="$a - 146097 * floor($b div 4)"/>
   
     <xsl:call-template name="ckbk:julian-or-gregorian-date-elem">
          <xsl:with param name="b" select="$b"/>
          <xsl:with param name="c" select="$c"/>
     </xsl:call-template>
     
   
</xsl:template>
   
<!-- A utility that is used for both Gregorian and Julian calendars. -->
<xsl:template name="ckbk:julian-or-gregorian-date-elem">
     <xsl:param name="b"/>
     <xsl:param name="c"/>
   
     <xsl:variable name="d" select="floor((4 * $c + 3) div 1461)"/>
     <xsl:variable name="e" select="$c - floor((1461 * $d) div 4)"/>
     <xsl:variable name="m" select="floor((5 * $e + 2) div 153)"/>
   
     <xsl:variable name="day" 
          select="$e - floor((153 * $m + 2) div 5) + 1"/>
   
     <xsl:variable name="month" 
          select="$m + 3 - (12 * floor($m div 10))"/>
   
     <xsl:variable name="year" 
          select="100 * $b + $d - 4800 + floor($m div 10)"/>
   
     <xsl:value-of select="concat($year,'/',$month,'/',$day)"/>
     
</xsl:template>

You can easily convert between Julian days and absolute days with the following templates:

<xsl:template name="ckbk:julian-day-to-absolute-day">
     <xsl:param name="j-day"/>
     <xsl:value-of select="$j-day - 1721425"/>
</xsl:template>
   
<xsl:template name="ckbk:absolute-day-to-julian-day">
     <xsl:param name="abs-day"/>
     <xsl:value-of select="$abs-day + 1721425"/>
</xsl:template>

You can then express absolute day/Gregorian conversions in terms of the existing Julian day/Gregorian conversions:

<xsl:template name="ckbk:date-to-absolute-day">
    <xsl:param name="year"/>
    <xsl:param name="month"/>
    <xsl:param name="day"/>
    
    <xsl:call-template name="ckbk:julian-day-to-absolute-day">
      <xsl:with-param name="j-day">
        <xsl:call-template name="ckbk:date-to-julian-day">
          <xsl:with-param name="year" select="$year"/>
          <xsl:with-param name="month" select="$month"/>
          <xsl:with-param name="day" select="$day"/>
        </xsl:call-template>
      </xsl:with-param>
  </xsl:call-template>
</xsl:template>
   
<xsl:template name="ckbk:absolute-day-to-date">
  <xsl:param name="abs-day"/>
  
  <xsl:call-template name="ckbk:julian-day-to-date">
       <xsl:with-param name="j-day">
            <xsl:call-template name="ckbk:absolute-day-to-julian-day">
                 <xsl:with-param name="abs-day" select="$abs-day"/>
            </xsl:call-template>
       </xsl:with-param>
  </xsl:call-template>
</xsl:template>

XSLT 2.0

The need for Julian and absolute day functions in XSLT 2.0 is diminished because date math is directly supported. For example,

<dateDiff> 
<xsl:value-of select="xs:date('2005-02-21') - xs:date('2005-01-01')"/> 
</dateDiff>

results in:

<dateDiff>P51D</dateDiff>

However, some applications may need these values for other purposes. Here we choose to implement the 1.0 templates in terms of functions that take an xs:date rather than the individual components:

<xsl:function name="ckbk:calculate-julian-day">
   <xsl:param name="date" as="xs:date"/>
     
   <xsl:variable name="year" select="year-from-date($date)"  as="xs:integer"/>
   <xsl:variable name="month" select="month-from-date($date)" as="xs:integer"/>
   <xsl:variable name="day" select="month-from-date($date)" as="xs:integer"/>
   
   <xsl:variable name="a" select="(14 - $month) idiv 12" as="xs:integer"/>
   <xsl:variable name="y" select="$year + 4800 - $a" as="xs:integer"/>
   <xsl:variable name="m" select="$month + 12 * $a - 3" as="xs:integer"/>
   
   <xsl:sequence select="$day + ((153 * $m + 2) idiv 5) + $y * 365 + 
        floor($y div 4) - ($y idiv 100) + ($y idiv 400) - 32045"/>
   
</xsl:function>

Discussion

The Julian day and absolute day are useful because they greatly simplify other date algorithms. Other examples in this chapter reuse these conversions extensively. These numbering schemes act as a common currency for all the calendar systems in this chapter. Should you ever find yourself needing to convert a Hebrew date to a Muslim date, the sequence Muslim to Absolute to Hebrew will do the trick.

4.5. Calculating the Week Number for aSpecified Date

Problem

You want to convert from a date to the number of the week within the year.

Solution

XSLT 1.0

The week number ranges from 1 to 53. Although most years have 52 weeks, years containing 53 Thursdays have 53.

The solution reuses the Julian day template:

  <xsl:template name="ckbk:calculate-week-number">
    <xsl:param name="year"/>
    <xsl:param name="month"/>
    <xsl:param name="day"/>
   
    <xsl:variable name="ckbk:j-day">
      <xsl:call-template name="ckbk:calculate-julian-day">
        <xsl:with-param name="year" select="$year"/>
        <xsl:with-param name="month" select="$month"/>
        <xsl:with-param name="day" select="$day"/>
      </xsl:call-template>
    </xsl:variable>
   
    <xsl:variable name="d4" 
          select="($j-day + 31741 - ($j-day mod 7)) 
               mod 146097 mod 36524 mod 1461"/>
   
    <xsl:variable name="L" select="floor($d4 div 1460)"/>
   
    <xsl:variable name="d1" select="(($d4 - $L) mod 365) + $L"/>
   
    <xsl:value-of select="floor($d1 div 7) + 1"/>
   
  </xsl:template>

Warning

This function assumes that Monday is the first day of the week. Most of the other functions in this chapter use the more popular convention in which weeks start on Sunday. See the ISO calendar recipes for an explanation of this idiosyncrasy.

XSLT 2.0

Use format-date():

<xsl:function name="ckbk:calculate-week-number" as="xs:integer">
  <xsl:param name="date" as="xs:date"/>
  <xsl:sequence select="xs:integer(format-date($date,'[W]'))"/>
</xsl:function>

Discussion

The week number is the number assigned to each week of the year. Week 1 of any year is the week that contains January 4 or, equivalently, the week that contains the first Thursday in January. A week that overlaps the end of one year and the beginning of the next is assigned to the year when most of the week’s days lie. This will occur when the year starts on Thursday or Wednesday in a leap year. The U.S. does not currently use this numbering system.

See Also

See Recipe 4.8, later in this chapter.

4.6. Working with the Julian Calendar

Problem

You need to work in the old Julian system of dates.

Solution

<xsl:template name="ckbk:julian-date-to-julian-day">
    <xsl:param name="year"/>
    <xsl:param name="month"/>
    <xsl:param name="day"/>
   
    <xsl:variable name="a" select="floor((14 - $month) div 12)"/>
    <xsl:variable name="y" select="$year + 4800 - $a"/>
    <xsl:variable name="m" select="$month + 12 * $a - 3"/>
   
    <xsl:value-of 
        select="$day + floor((153 * $m + 2) div 5) + 365 * $y 
        + floor($y div 4) - 32083"/>
   
  </xsl:template>

Once you have the Julian day, you can use other recipes in this chapter for date formatting, date math, or conversion to other calendar systems.

Discussion

The Julian system is rarely used in modern times. (One exception is the Russian Orthodox Church.) The Julian calendar was abandoned in favor of the Gregorian calendar due to its slightly inaccurate estimate of a year containing 365 1/4 days. The mean length is actually 365.2425 days, and over time, the seasons began to shift under the Julian system.

4.7. Working with the ISO Calendar

Problem

You need to work with dates in the International standard ISO-8601 calendar.[1]

Warning

Readers familiar with XML Schema should not confuse the ISO calendar with the ISO format for standard Gregorian dates (e.g., 2002-04-12 or 2002-04-12T09:26:00). Both the calendar and the formatting standard are covered in the 8601 standard. This recipe is concerned with the ISO calendar.

Solution

A basic facility you need to work with ISO dates (and, later, for determining certain holidays) is a function for finding the absolute day of the k th day on or before a specific absolute day. For example, the first Monday (k = 1) on or before January 4, 2004 (absolute day 731,584) is December 29, 2003 (absolute day 731,578):

<xsl:template name="ckbk:k-day-on-or-before-abs-day">
     <xsl:param name="abs-day"/>
     <xsl:param name="k"/>
     <xsl:value-of select="$abs-day - (($abs-day - $k) mod 7)"/>
</xsl:template>

You can now convert ISO dates to absolute days, which is a simple matter of determining the number of absolute days in prior years and adding in the remaining days in the given ISO date:

<xsl:template name="ckbk:iso-date-to-absolute-day">
     <xsl:param name="iso-week"/>
     <xsl:param name="iso-day"/>
     <xsl:param name="iso-year"/>
     
     <xsl:variable name="jan-4-of-year">
        <xsl:call-template name="ckbk:date-to-absolute-day">
         <xsl:with-param name="year" select="$iso-year"/>
          <xsl:with-param name="month" select="1"/>
          <xsl:with-param name="day" select="4"/>
         </xsl:call-template>
     </xsl:variable>
   
     <xsl:variable name="days-in-prior-yrs">
       <xsl:call-template name="ckbk:k-day-on-or-before-abs-day">
         <xsl:with-param name="abs-day" select="$jan-4-of-year"/>
         <xsl:with-param name="k" select="1"/>
       </xsl:call-template>
     </xsl:variable>
   
     <xsl:variable name="days-in-prior-weeks-this-yr"     
          select="7 * ($iso-week - 1)"/>
   
     <xsl:variable name="prior-days-this-week" select="$iso-day - 1"/>
   
     <xsl:value-of select="$days-in-prior-yrs + 
          $days-in-prior-weeks-this-yr + $prior-days-this-week"/>     
</xsl:template>

To convert from absolute days to an ISO date, the code will first try to establish the year by making a guess that it is the same as the Gregorian minus 3 days. This guess can be wrong only if the absolute day is actually on Jan 1 to Jan 3 of the following year. To correct for the possible off-by-one mistake, a comparison is made to Jan 1 of the following year using the iso-date-to-absolute-day code already on hand. Having firmly established the ISO year, the week and day follow by computing the offset from Jan 1 of that year. We return the ISO date formatted as year-week-day. This format is an ISO convention to prevent ISO dates from being confused with Gregorian dates:

<xsl:template name="ckbk:absolute-day-to-iso-date">
     <xsl:param name="abs-day"/>
     
     <xsl:variable name="d">
       <xsl:call-template name="ckbk:absolute-day-to-date">
         <xsl:with-param name="abs-day" select="$abs-day - 3"/>
       </xsl:call-template>
     </xsl:variable>
     
     <xsl:variable name="approx" select="substring-before($d,'/')"/>
     
     <xsl:variable name="iso-year">
       <xsl:variable name="a">
         <xsl:call-template name="ckbk:iso-date-to-absolute-day">
          <xsl:with-param name="iso-week" select="1"/>
          <xsl:with-param name="iso-day" select="1"/>
          <xsl:with-param name="iso-year" select="$approx + 1"/>
         </xsl:call-template>
       </xsl:variable>
       <xsl:choose>
         <xsl:when test="$abs-day >= $a">
          <xsl:value-of select="$approx + 1"/>
         </xsl:when>
         <xsl:otherwise>
          <xsl:value-of select="$approx"/>
         </xsl:otherwise>
       </xsl:choose>
     </xsl:variable>
   
     <xsl:variable name="ckbk:iso-week">
       <xsl:variable name="a">
         <xsl:call-template name="ckbk:iso-date-to-absolute-day">
           <xsl:with-param name="iso-week" select="1"/>
           <xsl:with-param name="iso-day" select="1"/>
           <xsl:with-param name="iso-year" select="$iso-year"/>
         </xsl:call-template>
      </xsl:variable>
       <xsl:value-of select="1 + floor(($abs-day - $a) div 7)"/>
     </xsl:variable>
     
     <xsl:variable name="iso-day">
       <xsl:variable name="a" select="$abs-day mod 7"/>
         <xsl:choose>
          <xsl:when test="not($a)">
            <xsl:value-of select="7"/>
          </xsl:when>
          <xsl:otherwise>
                    <xsl:value-of select="$a"/>
               </xsl:otherwise>
          </xsl:choose>
     </xsl:variable>
   
     <xsl:value-of select="concat($iso-year,'-W',$iso-week,'-',$iso-day)"/>
          
</xsl:template>

Discussion

In European commercial and industrial applications, reference to a week of a year is often required. The ISO calendar specifies dates by using the Gregorian year, the week number (1-53) within the year, and the ordinal day of the week (1-7, where ISO mandates the first day of the week is Monday). A week that overlaps successive years is assigned to the year with the most days in that week. According to this rule, the first week of the ISO calendar year can begin as late as January 4 and as early as December 29 of the previous year. Likewise, the last week of the ISO calendar year can end as early as December 28 and as late as January 3 of the following year. For example, in 2004, ISO week 1 actually started on December 29, 2003![2] To determine the start of the ISO week, then, you need to find the Monday on or before January 4.

See Also

You can see the ISO calendar in action at http://personal.ecu.edu/mccartyr/isowdcal.html.

4.8. Working with the Islamic Calendar

Problem

You need to work with dates in the Islamic system.

Warning

It is difficult if not impossible to devise universally accepted algorithms for the Islamic calendar. This is because each month starts when the lunar crescent is first seen (by an actual human being) after a new moon. New moons can be calculated quite precisely, but the actual visibility of the crescent depends on factors such as weather and the location of the observer. It is therefore very difficult to compute accurately in advance when a new month will start. Furthermore, some Muslims depend on a local sighting of the moon, whereas others depend on a sighting by authorities somewhere in the Muslim world. Saudi Arabia is an exception, since they use astronomical calculation rather than visual sightings. The algorithms provided here can be off by a few days when computing Islamic dates far in advance.

Solution

The last day of an Islamic month can be estimated quite accurately by assigning 30 days to odd months and 29 days to even months, except during a leap year:

<xsl:template name="ckbk:last-day-of-islamic-month">
     <xsl:param name="month"/>
     <xsl:param name="year"/>
     
     <xsl:variable name="islamic-leap-year" 
          select="(11 * $year + 14) mod 30 &lt; 11"/>
     
     <xsl:choose>
       <xsl:when test="$month mod 2 or ($month = 12 and $islamic-leap-year)">
         <xsl:value-of select="30"/>
       </xsl:when>
       <xsl:otherwise>
         <xsl:value-of select="29"/>
       </xsl:otherwise>
     </xsl:choose>
</xsl:template>

The Islamic calendar began with the Hijra, Muhammad’s emigration to Medina. For most Muslims, this date occurred on sunset of July 15, 622 AD (Julian calendar). This corresponds to absolute day 227,015, hence the offset of 227,014 in the otherwise straightforward calculation of the absolute day from the Islamic date:

<xsl:template name="ckbk:islamic-date-to-absolute-day">
    <xsl:param name="year"/>
    <xsl:param name="month"/>
    <xsl:param name="day"/>
    
    <xsl:value-of select="$day + 29 * ($month - 1) + floor($month div 2) + 354 
        * ($year - 1) + floor((11 * $year + 3) div 30) + 227014"/>
</xsl:template>

The absolute day to Islamic date conversion is different from the original Lisp code from which it was adapted. I use floating-point math to avoid a search technique employed in the Lisp implementation. The authors of the Lisp code desired to keep all calculations within 24-bit integer limits while retaining the greatest accuracy. However, their techniques did not translate well to XSLT. Given that XSLT 1.0 uses only floating-point math, it does not make sense to go through pains to avoid it. The numbers used stem from the average lunar month having 29.530555... days. The month number is approximated and then adjusted if it would result in a day with value less than 1. Once the year and month are established, the day can be computed as an offset from the first day of the year:

<xsl:template name="ckbk:absolute-day-to-islamic-date">
    <xsl:param name="abs-day"/>
   
     <xsl:variable name="year" 
          select="floor(($abs-day - 227014) div 354.36667) + 1"/>
     
     <xsl:variable name="month">
       <xsl:variable name="a" 
          select="$abs-day - 227014 - floor((11 * $year + 3) div 30) -
                 354 * ($year - 1)"/>
       <xsl:variable name="approx" select="floor($a div 29.53056)+1"/>
       <xsl:choose>
         <xsl:when test="(29 * ($approx - 1) + floor($approx div 2)) - 
          $a &lt; 1">
          <xsl:value-of select="$approx - 1"/>
         </xsl:when>
         <xsl:otherwise>
          <xsl:value-of select="$approx"/>
         </xsl:otherwise>
       </xsl:choose>
     </xsl:variable>
     
     <xsl:variable name="day">
       <xsl:variable name="a">
         <xsl:call-template name="ckbk:islamic-date-to-absolute-day">
          <xsl:with-param name="year" select="$year"/>
          <xsl:with-param name="month" select="$month"/>
          <xsl:with-param name="day" select="1"/>
         </xsl:call-template>
       </xsl:variable>
       <xsl:value-of select="$abs-day - $a + 1"/>
     </xsl:variable>
     
     <xsl:value-of select="concat($year,'/',$month,'/',$day)"/>
     
</xsl:template>

Discussion

The Hijri or Islamic calendar is interesting because it is based on purely lunar cycles, and thus the Muslim months are not fixed within the seasons. An Islamic year is approximately 354.36 days. The first year of the Islamic calendar is denoted 1 A.H. (After Hijra), Muhammad’s flight from Mecca to Medina. The Islamic calendar has deep religious significance to devout Muslims and is almost always based on visual observations of the moon. Approximate calendars can be computed in advance by using algorithms, but they should generally be used only for rough planning.

See Also

You can see the Muslim calendar in action at http://www.sufisattari.com/calendar.html.

Differences in convention among various Islamic counties are described at http://www.math.nus.edu.sg/aslaksen/calendar/islamic.shtml.

4.9. Working with the Hebrew Calendar

Problem

You need to work with dates in the Hebrew system.

Solution

You need to build up some basic utilities to work effectively with the Hebrew calendar. Hebrew years have 12 months in a regular year and 13 in a leap year. Leap years occur on the 3rd, 6th, 8th, 11th, 14th, 17th, and 19th years of the Metonic cycle (see the “Discussion” section). A concise means of making this determination is given by the relation 7y + 1 mod 19 < 7. From this, you can easily devise a function to determine the last month of any Hebrew year:

<xsl:template name="ckbk:last-month-of-hebrew-year">
     <xsl:param name="year"/>
     <xsl:choose>
          <xsl:when test="(7 * $year + 1) mod 19 &lt; 7">
               <xsl:value-of select="13"/>
          </xsl:when>
          <xsl:otherwise>
               <xsl:value-of select="12"/>
          </xsl:otherwise>
     </xsl:choose>
</xsl:template>

As a prerequisite to determining the number of days in any given month or year, you need to encapsulate the complex rules that determine when the Hebrew new year starts. See the paper by Dershowitz and Reingold for detailed explanation:

<!-- Number of days elapsed from the Sunday prior to the start of the 
Hebrew calender to the mean conjunction of Tishri of Hebrew year. -->
   
<xsl:template name="ckbk:hebrew-calendar-elapsed-days">
     <xsl:param name="year"/>
   
     <xsl:variable name="hebrew-leap-year" 
          select="(7 * $year + 1) mod 19 &lt; 7"/>
     <xsl:variable name="hebrew-leap-year-last-year" 
          select="(7 * ($year - 1) + 1) mod 19 &lt; 7"/>
     
     <xsl:variable name="months-elapsed" 
          select="235 * floor(($year -1) div 19) +
               12 * (($year -1) mod 19) +
               floor((7 * (($year - 1) mod 19) + 1) div 19)"/>
   
     <xsl:variable name="parts-elapsed" 
          select="13753 * $months-elapsed + 5604"/>
   
     <xsl:variable name="day" select="1 + 29 * $months-elapsed + 
                    floor($parts-elapsed div 25920)"/>
   
     <xsl:variable name="parts" select="$parts-elapsed mod 25920"/>
     
     <xsl:variable name="alternative-day">
       <xsl:choose>
         <xsl:when test="$parts >= 19440">
          <xsl:value-of select="$day + 1"/>
         </xsl:when>
         <xsl:when test="$day mod 7 = 2 and $parts >= 9924 and 
               not($hebrew-leap-year)">
          <xsl:value-of select="$day + 1"/>
         </xsl:when>
         <xsl:when test="$day mod 7 = 1 and $parts >= 16789 and 
               $hebrew-leap-year-last-year">
          <xsl:value-of select="$day + 1"/>
         </xsl:when>
         <xsl:otherwise>
          <xsl:value-of select="$day"/>
         </xsl:otherwise>
       </xsl:choose>
     </xsl:variable>
   
     <xsl:choose>
       <xsl:when test="$alternative-day mod 7 = 0">
         <xsl:value-of select="$alternative-day + 1"/>
       </xsl:when>
       <xsl:when test="$alternative-day mod 7 =3">
         <xsl:value-of select="$alternative-day + 1"/>
       </xsl:when>
       <xsl:when test="$alternative-day mod 7 = 5">
         <xsl:value-of select="$alternative-day + 1"/>
       </xsl:when>
       <xsl:otherwise>
         <xsl:value-of select="$alternative-day"/>
       </xsl:otherwise>
     </xsl:choose> 
     
</xsl:template>

The number of days in a Hebrew year is calculated as the difference between the elapsed days in successive years:

<xsl:template name="ckbk:days-in-hebrew-year">
     <xsl:param name="year"/>
     
     <xsl:variable name="e1">
       <xsl:call-template name="ckbk:hebrew-calendar-elapsed-days">
         <xsl:with-param name="year" select="$year + 1"/>
       </xsl:call-template>
     </xsl:variable>
   
     <xsl:variable name="e2">
       <xsl:call-template name="ckbk:hebrew-calendar-elapsed-days">
         <xsl:with-param name="year" select="$year"/>
       </xsl:call-template>
     </xsl:variable>
     
     <xsl:value-of select="$e1 - $e2"/>
</xsl:template>

Heshvan and Kislev are the eighth and ninth months of the Hebrew year and their number of days can vary. You need to know when Heshvan is long and Kislev is short, so you create two predicates:

<xsl:template name="ckbk:long-heshvan">
     <xsl:param name="year"/>
     
     <xsl:variable name="days">
          <xsl:call-template name="ckbk:days-in-hebrew-year">
               <xsl:with-param name="year" select="$year"/>
          </xsl:call-template>
     </xsl:variable>
   
     <xsl:if select="$days mod 10 = 5">
          <xsl:value-of select="true()"/>
     </xsl:if>
</xsl:template>
   
<xsl:template name="ckbk:short-kislev">
     <xsl:param name="year"/>
   
     <xsl:variable name="days">
          <xsl:call-template name="ckbk:days-in-hebrew-year">
               <xsl:with-param name="year" select="$year"/>
          </xsl:call-template>
     </xsl:variable>
   
     <xsl:if select="$days mod 10 = 3">
          <xsl:value-of select="true()"/>
     </xsl:if>
</xsl:template>

Warning

If you write predicate templates in XSLT 1.0, you should code them to return true() (or alternatively, 'true') for true but '' (null string) for false. The problem here is that templates return trees, and any tree, even one whose only node contains false() or '', will evaluate to true. The advantage of a tree containing '' is that it can be effectively evaluated as a Boolean by using the string() conversion. This is one of the many awkward facts of XSLT. In XSLT 2.0 you can use functions instead as they can return Booleanvalues.

Most of the machinery is now in place to tackle standard date-time functions provided in other recipes. The first standard function gives the last day in a month for a specified Hebrew month and year:

<xsl:template name="ckbk:last-day-of-hebrew-month">
     <xsl:param name="month"/>
     <xsl:param name="year"/>
     
     <xsl:variable name="hebrew-leap-year" 
          select="(7 * $year + 1) mod 19 &lt; 7"/>
     
     <xsl:variable name="long-heshvan">
       <xsl:call-template name="ckbk:long-heshvan">
         <xsl:with-param name="year" select="$year"/>
       </xsl:call-template>
     </xsl:variable>
   
     <xsl:variable name="short-kislev">
       <xsl:call-template name="ckbk:short-kislev">
         <xsl:with-param name="year" select="$year"/>
       </xsl:call-template>
     </xsl:variable>
     
     <xsl:choose>
       <xsl:when test="$month=12 and $hebrew-leap-year">
         <xsl:value-of select="30"/>
       </xsl:when>
       <xsl:when test="$month=8 and string($long-heshvan)">
         <xsl:value-of select="30"/>
       </xsl:when>
       <xsl:when test="$month=9 and string($short-kislev)">
         <xsl:value-of select="29"/>
       </xsl:when>
       <xsl:when test="$month=13">
         <xsl:value-of select="29"/>
       </xsl:when>
       <xsl:when test="$month mod 2 = 0">
         <xsl:value-of select="29"/>
       </xsl:when>
       <xsl:otherwise>
         <xsl:value-of select="30"/>
       </xsl:otherwise>
     </xsl:choose>
</xsl:template>

This recursive utility lets you sum the last days in a range of Hebrew months for a given year. It is used when converting a Hebrew date to an absolute year:

<xsl:template name="ckbk:sum-last-day-in-hebrew-months">
     <xsl:param name="year"/>
     <xsl:param name="from-month"/>
     <xsl:param name="to-month"/>
     <xsl:param name="accum" select="0"/>
   
     <xsl:choose>
       <xsl:when test="$from-month &lt;= $to-month">
         <xsl:call-template name="ckbk:sum-last-day-in-hebrew-months">
          <xsl:with-param name="year" select="$year"/>
          <xsl:with-param name="from-month" select="$from-month+1"/>
          <xsl:with-param name="to-month" select="$to-month"/>
          <xsl:with-param name="accum">
            <xsl:variable name="temp">
              <xsl:call-template name="ckbk:last-day-of-hebrew-month">
               <xsl:with-param name="year" select="$year"/>
               <xsl:with-param name="month" select="$from-month"/>
              </xsl:call-template>
            </xsl:variable>
            <xsl:value-of select="$temp + $accum"/>
          </xsl:with-param>
         </xsl:call-template>
       </xsl:when>
       <xsl:otherwise>
         <xsl:value-of select="$accum"/>
       </xsl:otherwise>
     </xsl:choose>     
</xsl:template>
   
<xsl:template name="ckbk:hebrew-date-to-absolute-day">
    <xsl:param name="year"/>
    <xsl:param name="month"/>
    <xsl:param name="day"/>
   
     <xsl:variable name="prior-months-days">
       <xsl:choose>
         <xsl:when test="7 > $month"> <!-- before Tishri -->
          <xsl:variable name="last-month-of-year">
            <xsl:call-template name="ckbk:last-month-of-hebrew-year">
              <xsl:with-param name="year" select="$year"/>
            </xsl:call-template>
          </xsl:variable>
          <!-- Add days before and after Nisan -->
          <xsl:variable name="days-before-nisan">
            <xsl:call-template name="ckbk:sum-last-day-in-hebrew-months">
              <xsl:with-param name="year" select="$year"/>
              <xsl:with-param name="from-month" select="7"/>
              <xsl:with-param name="to-month" 
               select="$last-month-of-year"/>
            </xsl:call-template>
          </xsl:variable>
          <xsl:call-template name="ckbk:sum-last-day-in-hebrew-months">
            <xsl:with-param name="year" select="$year"/>
            <xsl:with-param name="from-month" select="1"/>
            <xsl:with-param name="to-month" select="$month - 1"/>
            <xsl:with-param name="accum" select="$days-before-nisan"/>
          </xsl:call-template>
         </xsl:when>
         <xsl:otherwise>
          <!-- days in prior months this year-->
          <xsl:call-template name="ckbk:sum-last-day-in-hebrew-months">
            <xsl:with-param name="year" select="$year"/>
            <xsl:with-param name="from-month" select="7"/>
            <xsl:with-param name="to-month" select="$month - 1"/>
          </xsl:call-template>
         </xsl:otherwise>
       </xsl:choose>    
     </xsl:variable>
     
     <xsl:variable name="days-in-prior-years">
       <xsl:call-template name="ckbk:hebrew-calendar-elapsed-days">
         <xsl:with-param name="year" select="$year"/>
       </xsl:call-template>
     </xsl:variable>
   
     <!--      1373429 days before absolute day 1 -->
     <xsl:value-of select="$day + $prior-months-days + 
          $days-in-prior-years - 1373429"/>
</xsl:template>

Before implementing absolute-day-to-hebrew-date, you need two more recursive summation utilities that will help search for the actual year and month corresponding to an absolute day from approximations of the same year and month:

<xsl:template name="ckbk:fixup-hebrew-year">
     <xsl:param name="start-year"/>
     <xsl:param name="abs-day"/>
   
     <xsl:param name="accum" select="0"/>
   
     <xsl:variable name="next">
       <xsl:call-template name="ckbk:hebrew-date-to-absolute-day">
         <xsl:with-param name="month" select="7"/>
         <xsl:with-param name="day" select="1"/>
         <xsl:with-param name="year" select="$start-year + 1"/>
       </xsl:call-template>
     </xsl:variable>
     
     <xsl:choose>
       <xsl:when test="$abs-day >= $next">
         <xsl:call-template name="ckbk:fixup-hebrew-year">
          <xsl:with-param name="start-year" select="$start-year+1"/>
          <xsl:with-param name="abs-day" select="$abs-day"/>
          <xsl:with-param name="accum" select="$accum + 1"/>
         </xsl:call-template>
       </xsl:when>
       <xsl:otherwise>
         <xsl:value-of select="$accum"/>
       </xsl:otherwise>
     </xsl:choose>     
</xsl:template>
   
<xsl:template name="ckbk:fixup-hebrew-month">
     <xsl:param name="year"/>
     <xsl:param name="start-month"/>
     <xsl:param name="abs-day"/>
   
     <xsl:param name="accum" select="0"/>
   
     <xsl:variable name="next">
       <xsl:call-template name="ckbk:hebrew-date-to-absolute-day">
         <xsl:with-param name="month" select="$start-month"/>
         <xsl:with-param name="day">
           <xsl:call-template name="ckbk:last-day-of-hebrew-month">
            <xsl:with-param name="month" select="$start-month"/>
            <xsl:with-param name="year" select="$year"/>
           </xsl:call-template>
         </xsl:with-param>
         <xsl:with-param name="year" select="$year"/>
       </xsl:call-template>
     </xsl:variable>
   
     <xsl:choose>
       <xsl:when test="$abs-day > $next">
         <xsl:call-template name="ckbk:fixup-hebrew-month">
          <xsl:with-param name="year" select="$year"/>
          <xsl:with-param name="start-month" select="$start-month + 1"/>
          <xsl:with-param name="abs-day" select="$abs-day"/>
          <xsl:with-param name="accum" select="$accum + 1"/>
         </xsl:call-template>
       </xsl:when>
       <xsl:otherwise>
         <xsl:value-of select="$accum"/>
       </xsl:otherwise>
     </xsl:choose>     
</xsl:template>
   
<xsl:template name="ckbk:absolute-day-to-hebrew-date">
    <xsl:param name="abs-day"/>
   
     <xsl:variable name="year">
       <xsl:variable name="approx" 
          select="floor(($abs-day + 1373429) div 366)"/>
       <xsl:variable name="fixup">
         <xsl:call-template name="ckbk:fixup-hebrew-year">
           <xsl:with-param name="start-year" select="$approx"/>
           <xsl:with-param name="abs-day" select="$abs-day"/>
         </xsl:call-template>
       </xsl:variable>
       <xsl:value-of select="$approx + $fixup"/>
     </xsl:variable>
   
     <xsl:variable name="month">
       <xsl:variable name="first-day-of-year">
       <xsl:call-template name="ckbk:hebrew-date-to-absolute-day">
         <xsl:with-param name="month" select="1"/>
          <xsl:with-param name="day" select="1"/>
          <xsl:with-param name="year" select="$year"/>
         </xsl:call-template>
       </xsl:variable>
          
       <xsl:variable name="approx">
         <xsl:choose>
          <xsl:when test="$abs-day &lt; $first-day-of-year">
            <xsl:value-of select="7"/>
          </xsl:when>
          <xsl:otherwise>
            <xsl:value-of select="1"/>
          </xsl:otherwise>
         </xsl:choose>
       </xsl:variable>
   
       <xsl:variable name="fixup">
         <xsl:call-template name="ckbk:fixup-hebrew-month">
          <xsl:with-param name="year" select="$year"/>
          <xsl:with-param name="start-month" select="$approx"/>
          <xsl:with-param name="abs-day" select="$abs-day"/>
         </xsl:call-template>
       </xsl:variable>
   
       <xsl:value-of select="$approx + $fixup"/>
     </xsl:variable>
   
     <xsl:variable name="day">
       <xsl:variable name="days-to-first-of-month">
         <xsl:call-template name="ckbk:hebrew-date-to-absolute-day">
           <xsl:with-param name="month" select="$month"/>
           <xsl:with-param name="day" select="1"/>
           <xsl:with-param name="year" select="$year"/>
         </xsl:call-template>
       </xsl:variable>
     
       <xsl:value-of select="$abs-day - ($days-to-first-of-month - 1)"/>
     </xsl:variable>
   
     <xsl:value-of select="concat($year,'-',$month,'-',$day)"/>
          
</xsl:template>

Tip

In the previous example, you create three unique yet similar recursive templates that compute the sum of a function. Creating a generic utility that can sum an arbitrary function is highly desirable. Readers who are familiar with Lisp could probably guess that the original Lisp code from which this XSLT was derived used a Lisp macro to accomplish just that. Chapter 13 demonstrates how generic programming can be achieved in XSLT and how this recipe can be greatly simplified as a result.

Discussion

The Hebrew calendar is the most complex calendar covered in this chapter. Hence, the Hebrew date-time code is correspondingly complicated. The Hebrew calendar is intricate because its months are strictly lunar, yet it mandates that Passover must always occur in the spring. While most other calendars have a fixed number of months, the Hebrew calendar has 12 during a conventional year and 13 during a leap year.

4.10. Formatting Dates and Times

Problem

You want to format dates and times based on a format string.

Solution

These templates reuse many of the templates already presented in this chapter. The format-date-time uses a format string where %x is a formatting directive (see later) and all other text is output literally. The default format is the ISO date-time format for Gregorian dates:

<xsl:template name="ckbk:format-date-time">
    <xsl:param name="year"/>
    <xsl:param name="month"/>
    <xsl:param name="day"/>
    <xsl:param name="hour"/>
    <xsl:param name="minute"/>
    <xsl:param name="second"/>
    <xsl:param name="time-zone"/>
    <xsl:param name="format" select="'%Y-%m-%dT%H:%M:%S%z'"/>
   
     <xsl:choose>
       <xsl:when test="contains($format, '%')">
        <xsl:value-of select="substring-before($format, '%')"/>
      </xsl:when>
       <xsl:otherwise>
        <xsl:value-of select="$format"/>
       </xsl:otherwise>
     </xsl:choose>
   
    <xsl:variable name="code"
                  select="substring(substring-after($format, '%'), 1, 1)"/>
    <xsl:choose>
   
      <!-- Abbreviated weekday name -->
      <xsl:when test="$code='a'">
        <xsl:variable name="day-of-the-week">
          <xsl:call-template name="ckbk:calculate-day-of-the-week">
            <xsl:with-param name="year" select="$year"/>
            <xsl:with-param name="month" select="$month"/>
            <xsl:with-param name="day" select="$day"/>
          </xsl:call-template>
        </xsl:variable>
        <xsl:call-template name="ckbk:get-day-of-the-week-abbreviation">
          <xsl:with-param name="day-of-the-week" 
             select="$day-of-the-week"/>
        </xsl:call-template>
      </xsl:when>
   
      <!-- Full weekday name -->
      <xsl:when test="$code='A'">
        <xsl:variable name="day-of-the-week">
          <xsl:call-template name="ckbk:calculate-day-of-the-week">
            <xsl:with-param name="year" select="$year"/>
            <xsl:with-param name="month" select="$month"/>
            <xsl:with-param name="day" select="$day"/>
          </xsl:call-template>
        </xsl:variable>
        <xsl:call-template name="ckbk:get-day-of-the-week-name">
          <xsl:with-param name="day-of-the-week" 
                          select="$day-of-the-week"/>
        </xsl:call-template>
      </xsl:when>
   
      <!-- Abbreviated month name -->
      <xsl:when test="$code='b'">
        <xsl:call-template name="ckbk:get-month-abbreviation">
          <xsl:with-param name="month" select="$month"/>
        </xsl:call-template>
      </xsl:when>
   
      <!-- Full month name -->
      <xsl:when test="$code='B'">
        <xsl:call-template name="ckbk:get-month-name">
          <xsl:with-param name="month" select="$month"/>
        </xsl:call-template>
      </xsl:when>
   
      <!-- Date and time representation appropriate for locale -->
      <xsl:when test="$code='c'">
        <xsl:text>[not implemented]</xsl:text>
      </xsl:when>
   
      <!-- Day of month as decimal number (01 - 31) -->
      <xsl:when test="$code='d'">
        <xsl:value-of select="format-number($day,'00')"/>
      </xsl:when>
   
      <!-- Hour in 24-hour format (00 - 23) -->
      <xsl:when test="$code='H'">
        <xsl:value-of select="format-number($hour,'00')"/>
      </xsl:when>
   
      <!-- Hour in 12-hour format (01 - 12) -->
      <xsl:when test="$code='I'">
        <xsl:choose>
          <xsl:when test="$hour = 0">12</xsl:when>
          <xsl:when test="$hour &lt; 13">
            <xsl:value-of select="format-number($hour,'00')"/>
           </xsl:when>
          <xsl:otherwise>
            <xsl:value-of select="format-number($hour - 12,'00')"/>
         </xsl:otherwise>
        </xsl:choose>
      </xsl:when>
   
      <!-- Day of year as decimal number (001 - 366) -->
      <xsl:when test="$code='j'">
         <xsl:variable name="diff"> 
         <xsl:call-template name="ckbk:date-difference">
           <xsl:with-param name="from-year" select="$year"/>
            <xsl:with-param name="from-month" select="1"/>
            <xsl:with-param name="form-day" select="1"/>
            <xsl:with-param name="to-year" select="$year"/>
            <xsl:with-param name="to-month" select="$month"/>
            <xsl:with-param name="to-day" select="$day"/>
         </xsl:call-template>
         </xsl:variable> 
        <xsl:value-of select="format-number($diff + 1, '000')"/>
      </xsl:when>
   
      <!-- Month as decimal number (01 - 12) -->
      <xsl:when test="$code='m'">
        <xsl:value-of select="format-number($month,'00')"/>
      </xsl:when>
   
      <!-- Minute as decimal number (00 - 59) -->
      <xsl:when test="$code='M'">
        <xsl:value-of select="format-number($minute,'00')"/>
      </xsl:when>
   
      <!-- Current locale's A.M./P.M. indicator for 12-hour clock -->
      <xsl:when test="$code='p'">
        <xsl:choose>
          <xsl:when test="$hour &lt; 12">AM</xsl:when>
          <xsl:otherwise>PM</xsl:otherwise>
        </xsl:choose>
      </xsl:when>
   
      <!-- Second as decimal number (00 - 59) -->
      <xsl:when test="$code='S'">
        <xsl:value-of select="format-number($second,'00')"/>
      </xsl:when>
   
      <!-- Week of year as decimal number, 
           with Sunday as first day of week (00 - 53) -->
      <xsl:when test="$code='U'">
        <!-- add 1 to day -->
        <xsl:call-template name="ckbk:calculate-week-number">
          <xsl:with-param name="year" select="$year"/>
          <xsl:with-param name="month" select="$month"/>
          <xsl:with-param name="day" select="$day + 1"/>
        </xsl:call-template>
      </xsl:when>
   
      <!-- Weekday as decimal number (0 - 6; Sunday is 0) -->
      <xsl:when test="$code='w'">
        <xsl:call-template name="ckbk:calculate-day-of-the-week">
          <xsl:with-param name="year" select="$year"/>
          <xsl:with-param name="month" select="$month"/>
          <xsl:with-param name="day" select="$day"/>
        </xsl:call-template>
      </xsl:when>
   
      <!-- Week of year as decimal number, 
           with Monday as first day of week (00 - 53) -->
      <xsl:when test="$code='W'">
        <xsl:call-template name="ckbk:calculate-week-number">
          <xsl:with-param name="year" select="$year"/>
          <xsl:with-param name="month" select="$month"/>
          <xsl:with-param name="day" select="$day"/>
        </xsl:call-template>
      </xsl:when>
   
      <!-- Date representation for current locale -->
      <xsl:when test="$code='x'">
        <xsl:text>[not implemented]</xsl:text>
      </xsl:when>
   
      <!-- Time representation for current locale -->
      <xsl:when test="$code='X'">
        <xsl:text>[not implemented]</xsl:text>
      </xsl:when>
   
      <!-- Year without century, as decimal number (00 - 99) -->
      <xsl:when test="$code='y'">
        <xsl:value-of select="format-number($year mod 100,'00')"/>  
      </xsl:when>
   
      <!-- Year with century, as decimal number -->
      <xsl:when test="$code='Y'">
         <xsl:value-of select="format-number($year,'0000')"/>
      </xsl:when>
   
      <!-- Time-zone name or abbreviation; -->
      <!-- no characters if time zone is unknown -->
      <xsl:when test="$code='z'">
        <xsl:value-of select="$time-zone"/>
      </xsl:when>
   
      <!-- Percent sign -->
      <xsl:when test="$code='%'">
        <xsl:text>%</xsl:text>
      </xsl:when>
   
    </xsl:choose>
   
    <xsl:variable name="remainder" 
                  select="substring(substring-after($format, '%'), 2)"/>
   
    <xsl:if test="$remainder">
      <xsl:call-template name="ckbk:format-date-time">
        <xsl:with-param name="year" select="$year"/>
        <xsl:with-param name="month" select="$month"/>
        <xsl:with-param name="day" select="$day"/>
        <xsl:with-param name="hour" select="$hour"/>
        <xsl:with-param name="minute" select="$minute"/>
        <xsl:with-param name="second" select="$second"/>
        <xsl:with-param name="time-zone" select="$time-zone"/>
        <xsl:with-param name="format" select="$remainder"/>
      </xsl:call-template>
    </xsl:if>
   
</xsl:template>
   
<xsl:template name="ckbk:format-julian-day">
    <xsl:param name="julian-day"/>
    <xsl:param name="format" select="'%Y-%m-%d'"/>
   
    <xsl:variable name="a" select="$julian-day + 32044"/>
    <xsl:variable name="b" select="floor((4 * $a + 3) div 146097)"/>
    <xsl:variable name="c" select="$a - floor(($b * 146097) div 4)"/>
   
    <xsl:variable name="d" select="floor((4 * $c + 3) div 1461)"/>
    <xsl:variable name="e" select="$c - floor((1461 * $d) div 4)"/>
    <xsl:variable name="m" select="floor((5 * $e + 2) div 153)"/>
   
    <xsl:variable name="day" select="$e - floor((153 * $m + 2) div 5) + 1"/>
    <xsl:variable name="month" select="$m + 3 - 12 * floor($m div 10)"/>
    <xsl:variable name="year" select="$b * 100 + $d - 4800 + floor($m div 10)"/>
   
    <xsl:call-template name="ckbk:format-date-time">
      <xsl:with-param name="year" select="$year"/>
      <xsl:with-param name="month" select="$month"/>
      <xsl:with-param name="day" select="$day"/>
      <xsl:with-param name="format" select="$format"/>
    </xsl:call-template>
   
</xsl:template>

XSLT 2.0

Use the XSLT 2.0 date formatting functions. In the following fragment, I assume you have to deal with a date as a string in the form YYYYMMDD, which is often more common in practice than the ISO date format required by format-date:

xsl:param name="date" as="xs:string"/>

<invoiceDate><xsl:value-of 
    select="format-date(
     xs:date(concat(substring($date,1,4),
                    '-',
                     substring($date,5,2),
                    '-',
                     substring($date,7,2))), 
              '[MNn] [D01], [Y0001]')"/></invoiceDate>

This would output the following for $date = '20050811':

<invoiceDate>August 11, 2005</invoiceDate>

Discussion

XSLT 1.0

This example was made possible by all the date work done in the prior examples. The options requiring locale are not implemented, but could be implemented using extension functions (see Chapter 12).

XSLT 2.0

I provide an excerpt from the W3C specification for your convenience.

Three functions are provided to represent dates and times as a string, using the conventions of a selected calendar and locale. Each has two variants:

format-dateTime($value as xs:dateTime?, 
                $picture as xs:string, 
                $date-format-name as xs:string) as xs:string?

format-dateTime($value as xs:dateTime?, $picture as xs:string) as xs:string? 

format-date($value as xs:date?,
            $picture as xs:string,
            $date-format-name as xs:string) as xs:string?

format-date($value as xs:date?, $picture as xs:string) as xs:string? 

format-time($value as xs:time?,
            $picture as xs:string,
            $date-format-name as xs:string) as xs:string?

format-time($value as xs:time?, $picture as xs:string) as xs:string?

The format-dateTime, format-date, and format-time functions format $value as a string using the picture string specified by the $picture argument and the date-format named by the $date-format-name argument, or the default date-format, if there is no $date-format-name argument. The result of the function is the formatted string representation of the supplied dateTime, date, or time value.

The three functions, format-dateTime, format-date, and format-time, and are referred to collectively as the date formatting functions.

It is a dynamic error if the name specified as the $date-format-name argument is not a valid QName, or if its prefix has not been declared in an in-scope namespace declaration, or if the stylesheet does not contain a declaration of a date-format with a matching expanded-QName. The processor must either signal the error, or must recover by ignoring the $date-format-name argument. If the processor is able to detect the error statically (for example, when the argument is supplied as a string literal), then the processor may optionally signal this as a static error.

If $value is the empty sequence, the empty sequence is returned.

The date-format declaration

<!-- Category: declaration -->
<xsl:date-format name = qname
                 language = nmtoken
                 calendar = qname />

The xsl:date-format element declares a date-format, which provides information used by the date formatting functions. If there is a name attribute, then the element declares a named date format; otherwise, it declares the default date format. The value of the name attribute is a QName. It is a static error to declare either the default date-format or a date-format with a given name more than once (even with different import precedence), unless it is declared every time with the same value for all attributes (taking into account any default values). If a stylesheet does not contain a declaration of the default date-format, a declaration equivalent to an xsl:date-format element with no attributes is implied.

The language attribute specifies the language to be used for the result string of the format-date function. The effective value of the attribute must be a value that would be valid for the xml:lang attribute. If the language attribute is omitted, then the default is implementation-defined.

The language is used to select the appropriate language-dependent forms of:

  • Names (for example, of months)

  • Ordinal form of numbers

  • Hour convention (0-23 vs 1-24, 0-11 vs 1-12)

  • First day of week, first week of year

The set of languages that are supported is implementation-defined.

The calendar attribute specifies that the dateTime, date, or time supplied in the $value argument must be converted to a value in that calendar and then converted to a string using the conventions of that calendar.

A calendar value must be a valid QName. If the QName does not have a prefix, then it identifies a calendar with the designator specified below. If the QName has a prefix, then the QName is expanded into an expanded-QName ; the expanded-QName identifies the calendar; the behavior in this case is not specified by this document.

If the calendar attribute is omitted, a locale-specific value is used.

It is a static error if an implementation does not support the language specified in the language attribute, or the calendar specified in the calendar attribute, or the combination of the two. The processor must either signal the error, or must recover by using a locale-specific value of the two attributes instead of the values specified. If a different calendar is used from that requested, the name of this calendar must be included in the result string.

Tip

The calendars listed next were known to be in use during the last hundred years. Many other calendars have been used in the past, and in many cases these cannot be fully supported without additional parameters. Such parameters may be defined using additional namespace-prefixed attributes on the xsl:date-format element; the semantics of such attributes are not defined by this specification.

This specification does not define any of these calendars, nor the way that they map to the value space of the xs:date data type in XML Schema. There is only an approximate equivalence between dates recorded using different calendars. For example, the start of a new day is not simultaneous in different calendars, and may also vary geographically. Implementations supporting calendars other than the Gregorian calendar may therefore produce different results.

Designator

Calendar

AD

Anno Domini (Christian Era)

AH

Anno Hegirae (Muhammedan Era)

AME

Mauludi Era (solar years since Mohammed’s birth)

AM

Anno Mundi (Jewish Calendar)

AP

Anno Persici

AS

Aji Saka Era (Java)

BE

Buddhist Era

CB

Cooch Behar Era

CE

Common Era

CL

Chinese Lunar Era

CS

Chula Sakarat Era

EE

Ethiopian Era

FE

Fasli Era

ISO

ISO 8601 calendar

JE

Japanese Calendar

KE

Khalsa Era (Sikh calendar)

KY

Kali Yuga

ME

Malabar Era

MS

Monarchic Solar Era

NS

Nepal Samwat Era

OS

Old Style (Julian Calendar)

RS

Rattanakosin (Bangkok) Era

SE

Saka Era

SH

Mohammedan Solar Era (Iran)

SS

Saka Samvat

TE

Tripurabda Era

VE

Vikrama Era

VS

Vikrama Samvat Era

At least one of the preceding calendars must be supported. It is implementation-defined which calendars are supported.

The ISO 8601 calendar, which is included in the previous list and designated ISO, is essentially the same as the Gregorian calendar designated AD, but it prescribes the use of particular numbering conventions as defined in ISO 8601, rather than allowing these to be localized on a per-language basis. Specifically, in the ISO calendar, the days of the week are numbered from 1 (Monday) to 7 (Sunday), and week 1 in any calendar year is the week (from Monday to Sunday) that includes the first Thursday of that year. The numeric values of the components year, month, day, hour, minutes, and seconds are the same in this calendar as the values used in the lexical representation of the date and time as defined in XML Schema. The ISO calendar is intended primarily for applications that need to produce dates and times in formats to be read by other software, rather than by human users.

Tip

The value space of the date and time data types, as defined in XML Schema, is based on absolute points in time. The lexical space of these data types defines a representation of these absolute points in time using the proleptic Gregorian calendar — that is, the modern Western calendar extrapolated into the past and the future — but the value space is calendar-neutral. The date-formatting functions produce a representation of the same absolute point in time as denoted in a possibly different calendar. So, for example, the date whose lexical representation in XML Schema is 1502-01-11 (the day on which Pope Gregory XIII was born) might be formatted using the Old Style (Julian) calendar as 1 January 1502. This reflects the fact that there was at that time a ten-day difference between the two calendars. It would be incorrect, and would produce incorrect results, to represent this date in an element or attribute of type xs:date as 1502-01-01, even though this might reflect the way the date was recorded in contemporary documents.

The picture string

The picture consists of a sequence of variable markers and literal substrings. A substring enclosed in square brackets is interpreted as a variable marker; substrings not enclosed in square brackets are taken as literal substrings. The literal substrings are optional and if present are rendered unchanged, including any whitespace. If an opening or closing square bracket is required within a literal sub-string, it must be doubled. The variable markers are replaced in the result by strings representing aspects of the date and/or time to be formatted. These are described in detail next.

A variable marker consists of a component specifier followed optionally by one or two presentation modifiers and/or optionally by a length modifier. Whitespace within a variable marker is ignored.

The component specifier indicates the component of the date or time that is required, and takes the following values:

Specifier

Meaning

Default presentation modifier

Y

Year

1

M

Month in year

1

D

Day in month

1

D

Day in year

1

F

Day of week

n

W

Week in year

1

W

Week in month

1

H

Hour in day (24 hours)

1

H

Hour in half-day (12 hours)

1

P

a.m./p.m. marker

n

M

Minute in hour

1

S

Second in minute

1

F

Fractional seconds

1

Z

Timezone as a time offset from UTC, or if an alphabetic modifier is present, the conventional name of a timezone (such as PST)

1

Z

Timezone as a time offset using GMT; for example, GMT+1

1

C

Calendar: the name or abbreviation of a calendar name

n

E

Era: the name of a baseline for the numbering of years; for example, the reign of a monarch

n

It is a dynamic error if a component specifier within the picture refers to components that are not available in the given $value, or which are not supported in the chosen calendar. This is a recoverable error . The processor may signal the error, or may recover by ignoring the offending component specifiers .

The first presentation modifier indicates the style in which the value of a component is to be represented, and takes the following values:

Modifier

Meaning

A

Alphabetic, upper ase

A

Alphabetic, lowercase (may start with initial uppercase if so used in language)

N

Name, uppercase

N

Name, lowercase (may start with initial uppercase if so used in language)

digit 1

Decimal representation

I

Lower-case Roman numeral

I

Upper-case Roman numeral

Any character that has a decimal digit value of 1 (as specified in the Unicode character property database) generates a decimal representation of the number using the appropriate set of Unicode digits.

Any other character may be used to indicate a numbering sequence that starts with that character, if the implementation supports such a numbering sequence.

If the implementation does not support the use of the requested presentation modifier, it must use the default presentation modifier for that component.

If the first presentation modifier is present, then it may optionally be followed by a second presentation modifier as follows:

Modifier

Meaning

T

Traditional numbering. This has the same meaning as letter-value="traditional" in xsl:number.

O

Ordinal form of a number; for example, 3rd or 8º.

Whether or not a presentation modifier is included, a width modifier may be supplied. This indicates the number of characters or digits to be included in the representation of the value.

The width modifier takes the form:

min-width ("-" max-width)?

where min-width is either an unsigned integer indicating the minimum number of characters to be output, or * indicating that there is no explicit minumum, and max-width is either an unsigned integer indicating the maximum number of characters to be output, or * indicating that there is no explicit maximum. If max-width is omitted, then * is assumed. Both integers, if present, must be greater than zero.

If there is no width modifier, then the output uses as many characters as are required to represent the value of the component without truncation and without padding: this is referred to below as the full representation of the value.

If the full representation of the value exceeds the specified maximum width, then the processor should attempt to use an alternative shorter representation that fits within the maximum width. Where the presentation modifier is n or N, this is done by abbreviating the name, using either conventional abbreviations if available, or crude right-truncation if not. For example, setting max-width to 4 indicates that four-letter abbreviations should be used, though it would be acceptable to use a three-letter abbreviation if this is in conventional use. (For example, “Tuesday” might be abbreviated to “Tues”, and “Friday” to “Fri”.) In the case of the year component, setting max-width requests omission of high-order digits from the year; for example, if max-width is set to 2, then the year 2003 will be output as 03. If no mechanism is available for fitting the value within the specified maximum width (for example, when roman numerals are used), then the value should be output in its full representation.

If the full representation of the value is shorter than the specified minimum width, then the processor should pad the value to the specified width. For decimal representations of numbers, this should be done by prepending zero digits from the appropriate set of digit characters. In other cases, it should be done by prepending spaces.

The choice of the names and abbreviations used in any given language is implementation-defined. For example, one implementation might abbreviate July as Jul while another uses Jly. In German, one implementation might represent Saturday as Samstag while another uses Sonnabend. Implementations may provide mechanisms allowing users to control such choices.

The following examples show a selection of dates and times and the way they might be formatted. These examples assume the use of the Gregorian calendar, and assume that the name of xsl:date-format declarations in the stylesheet is the same as the value of their language attribute. (For example, <date-format name="sv" language="sv"/>.)

Required output

Expression

2002-12-31

format-date($d,"[Y]-[M]-[D]")

12-31-2002

format-date($d,"[M]-[D]-[Y]")

31-12-2002

format-date($d,"[D]-[M]-[Y]")

31 XII 2002

format-date($d,"[D1] [MI] [Y]")

31st December, 2002

format-date($d,"[Do] [Mn], [Y]", "en")

31 DEC 2002

format-date($d,"[D] [MN,*-3] [Y]", "en")

December 31, 2002

format-date($d,"[Mn] [D], [Y]", "en")

31 Dezember, 2002

format-date($d,"[D] [Mn], [Y]", "de")

Tisdag 31 December 2002

format-date($d,"[Fn] [D] [Mn] [Y]", "sv")

[2003-04-07]

format-date($t,"[[[Y]-[M]-[D]]]")

3:58 PM

format-time($t,"[h]:[m] [PN]", "en")

3:58:45 pm

format-time($t,"[h]:[m]:[s] [Pn]", "en")

3:58:45 PM PDT

format-time($t,"[h]:[m]:[s] [PN] [ZN,*-3]", "en")

3:58:45 o'clock PM PDT

format-time($t,"[h]:[m]:[s] o'clock [PN] [ZN,*-3]", "en")

15:58

format-time($t,"[H]:[m]")

15:58:45

format-time($t,"[H]:[m]:[s]")

15:58:45 GMT+02:00

format-time($t,"[H]:[m]:[s] [z]", "en")

15.58 Uhr GMT+02:00

format-time($t,"[H]:[m]:[s] Uhr [z]", "de")

The following examples use calendars other than the Gregorian calendar:

<!-- Example: Thai --> 
<xsl:date-format name="modern_Thai" language="th" calendar="BE"/>
format-date($d, "[D&#x0E51;] [Mn] [Y&#x0E51;]", "modern_Thai")
<!-- Result:  -->

<!--Example: Islamic--> 
<xsl:date-format name="Islamic" language="ar" calendar="AH"/>
format-date($d, "[D&#x0661;] [Mn] [Y&#x0661;]", "Islamic")
<!-- Result:  -->

<!--Example: Jewish--> 
<xsl:date-format name="Jewish

" language="he" calendar="AM"/>
format-date($d, "[D] [Mn] [Y]", "Jewish")
<!--Result: 26 5763-->

4.11. Determining Secular and Religious Holidays

Problem

You would like to know if a given date is a holiday.

Solution

The first type of holiday includes those that fall on the same day every year. For example, a function to determine the absolute day of American Independence for any year is simply:

<xsl:template name="ckbk:independence-day">
     <xsl:param name="year"/>
     <xsl:call-template name="ckbk:date-to-absolute-day">
          <xsl:with-param name="month" select="7"/>
          <xsl:with-param name="day" select="4"/>
          <xsl:with-param name="year" select="$year"/>
     </xsl:call-template>
</xsl:template>

The second type of holiday falls on the same day of the week relative to the start or end of a month. You can compute those days with the help of the following utility, which wraps the k-day-on-or-before-abs-day template contained in Recipe 4.8:

<xsl:template name="ckbk:n-th-k-day">
    <!-- The n'th occurance of k in the given month -->
     <!-- Postive n counts from beginning of month; negative from end. -->
     <xsl:param name="n"/>   
   
    <!-- k = the day of the week (0 = Sun) -->     
     <xsl:param name="k"/>   
   
     <xsl:param name="month"/>
     <xsl:param name="year"/>
   
     <xsl:choose>
       <xsl:when test="$n > 0">
         <xsl:variable name="k-day-on-or-before">
          <xsl:variable name="abs-day">
            <xsl:call-template name="ckbk:date-to-absolute-day">
              <xsl:with-param name="month" select="$month"/>
              <xsl:with-param name="day" select="7"/>
              <xsl:with-param name="year" select="$year"/>
            </xsl:call-template>
          </xsl:variable>
          <xsl:call-template name="ckbk:k-day-on-or-before-abs-day">
            <xsl:with-param name="abs-day" select="$abs-day"/>
            <xsl:with-param name="k" select="$k"/>
          </xsl:call-template>
         </xsl:variable>
         <xsl:value-of select="$k-day-on-or-before + 7 * ($n - 1)"/>
       </xsl:when>
       <xsl:otherwise>
         <xsl:variable name="k-day-on-or-before">
          <xsl:variable name="abs-day">
            <xsl:call-template name="ckbk:date-to-absolute-day">
              <xsl:with-param name="month" select="$month"/>
              <xsl:with-param name="day">
               <xsl:call-template name="ckbk:last-day-of-month">
                 <xsl:with-param name="month" select="$month"/>
                 <xsl:with-param name="year" select="$year"/>
               </xsl:call-template>
              </xsl:with-param>
              <xsl:with-param name="year" select="$year"/>
            </xsl:call-template>
          </xsl:variable>
          <xsl:call-template name="ckbk:k-day-on-or-before-abs-day">
            <xsl:with-param name="abs-day" select="$abs-day"/>
            <xsl:with-param name="k" select="$k"/>
          </xsl:call-template>
         </xsl:variable>
         <xsl:value-of select="$k-day-on-or-before + 7 * ($n + 1)"/>
       </xsl:otherwise>
     </xsl:choose>
  </xsl:template>

This function assumes Gregorian dates. If you need to determine relative dates within other calendar systems, you need to write equivalent routines within those systems.

It is now easy to handle holidays like American Labor and Memorial days (the first Monday of September and the last Monday of May, respectively):

<xsl:template name="ckbk:labor-day">
     <xsl:param name="year"/>
     <xsl:call-template name="ckbk:n-th-k-day ">
          <xsl:with-param name="n" select="1"/>
          <xsl:with-param name="k" select="1"/>
          <xsl:with-param name="month" select="9"/>
          <xsl:with-param name="year" select="$year"/>
     </xsl:call-template>
</xsl:template>
   
<xsl:template name="ckbk:memorial-day">
     <xsl:param name="year"/>
     <xsl:call-template name="ckbk:n-th-k-day ">
          <xsl:with-param name="n" select="-1"/>
          <xsl:with-param name="k" select="1"/>
          <xsl:with-param name="month" select="5"/>
          <xsl:with-param name="year" select="$year"/>
     </xsl:call-template>
</xsl:template>

Although not a holiday, American daylight savings time can also be handled:

<xsl:template name="ckbk:day-light-savings-start">
     <xsl:param name="year"/>
     <xsl:call-template name="ckbk:n-th-k-day ">
          <xsl:with-param name="n" select="1"/>
          <xsl:with-param name="k" select="0"/>
          <xsl:with-param name="month" select="4"/>
          <xsl:with-param name="year" select="$year"/>
     </xsl:call-template>
</xsl:template>
   
<xsl:template name="ckbk:day-light-savings-end">
     <xsl:param name="year"/>
     <xsl:call-template name="ckbk:n-th-k-day ">
          <xsl:with-param name="n" select="-1"/>
          <xsl:with-param name="k" select="0"/>
          <xsl:with-param name="month" select="10"/>
          <xsl:with-param name="year" select="$year"/>
     </xsl:call-template>
</xsl:template>

Discussion

Covering every secular and religious holiday in each country and year is impossible. However, you can classify most holidays into two types: those that fall on the same day every year (e.g., U.S. Independence Day) in their respective calendars and those that are on a particular day of the week relative to the start or end of a month (e.g., U.S. Labor Day). Religious holidays are often simple within their native calendrical system, but can be more difficult to determine within another system. For example, Eastern Orthodox Christmas always falls on December 25 of the Julian calendar. Thus, in a Gregorian year, Eastern Orthodox Christmas can fall in the beginning, the end, or not appear at all. Since we cannot cover every religious holiday in every faith, please explore the references mentioned in this chapter’s introduction.



[1] ISO is the International Organization for Standardization. (Not only does ISO roll off the tongue better than IOS but it comes from the Greek isos meaning equal.)

[2] And you thought standards were supposed to simplify life!

Get XSLT Cookbook, 2nd Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.