среда, 4 февраля 2009 г.

Инкремент даты в XSLT 1.0.

Пусть в XSLT преобразовании требуется к заданной дате прибавить заданное количество дней. Другими словами, у насть есть три числа - год, месяц, день. Нужно прибавить к ним произвольное количество дней и получить новые год, месяц, день... Вопрос - как это сделать?

В XSLT 1.0 встроенных средств нет. В XSLT 2.0, насколько я понимаю,
можно выкрутится встроенными средствами, но мне требуется XSLT 1.0.

Можно подключить JavaScript и написать скрипт для инкремента даты. Однако, как оказалось, можно обойтись и без JavaScript'а.

Вот здесь приведены исходные коды двух функций, позволяющих преобразовать дату в количество дней, прошедших с 31-декабря 0-ого года, и обратно. Если у нас в распоряжении есть такие функции, то выполнить инкремент даты не составит проблем:
int date; //количество дней
itod(&date, 2009, 2, 3); //получаем количество дней для даты 3.02.2009

int y, m, d;
dtoi(date+1, &y, &m, &d); //получаем год, месяц, число для 4.02.2009.


Приложение на С выглядит так:
void dtoi (int date, int &year, int &month, int &day) {
// the date specified as the number of days elapsed since 
//31 Dec of year 0. So 1 Jan 0001 is 1.
  int Z = date + 306;
  int H = 100*Z - 25;
  int A = H/3652425;
  int B = A - A/4;
  year = (100*B + H) / 36525;
  int C = B + Z - 365*year - year / 4;
  month = (5*C + 456) / 153;
  day = C - (153*month - 457) / 5;
  if (month > 12) { year += 1; month -= 12; }
}

int itod (int y, int m, int d) {
  if (m < 3) { m += 12; y -= 1; }
  return d + (153*m - 457) / 5 + 365*y + y/4 - y/100 + y/400 - 306;
}

int _tmain(int argc, _TCHAR* argv[])
{
  int date = itod(2009, 12, 31);
  int y, m, d;
  dtoi(date+1, y, m, d);
}



Перевел код на XSLT. Получилось следующее:
<xsl:template name="date_to_int">
  <xsl:param name="y" />
  <xsl:param name="m" />
  <xsl:param name="d" />
  <xsl:choose>
    <xsl:when test="number($m) < 3">
    <xsl:call-template name="date_to_int_auxilary">
        <xsl:with-param name="y" select="number($y)-1"/>
        <xsl:with-param name="m" select="number($m)+12"/>
        <xsl:with-param name="d" select="$d"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:call-template name="date_to_int_auxilary">
        <xsl:with-param name="y" select="$y"/>
        <xsl:with-param name="m" select="$m"/>
        <xsl:with-param name="d" select="$d"/>
      </xsl:call-template>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<xsl:template name="date_to_int_auxilary">
  <xsl:param name="y" />
  <xsl:param name="m" />
  <xsl:param name="d" />

  <xsl:value-of select="number($d) + floor((153.0*number($m) - 457.0) div 5.0) + 365*number($y) + floor(number($y) div 4 ) - floor(number($y) div 100) + floor(number($y) div 400) - 306"/>
</xsl:template>


<!-- Convert number of days to date (year, month, day - required part of the date is selected by 'required_value' parameter -->
<xsl:template name="int_to_date">
  <xsl:param name="g"/>
  <xsl:param name="required_value" select="'year'"/> <!-- 'year' or 'month' or 'day'-->

  
  <xsl:variable name="Z" select="floor(number($g) + 306)"/>
  <xsl:variable name="H" select="floor(100*number($Z) - 25)"/>
  <xsl:variable name="A" select="floor(number($H) div 3652425)"/>
  <xsl:variable name="B" select="floor(number($A) - number($A) div 4)"/>
  <xsl:variable name="year" select="floor((number($B)*100 + number($H)) div 36525)"/>
  <xsl:variable name="C" select="floor(-365.0*number($year) + number($B) + number($Z)- floor(number($year) div 4))"/>
  <xsl:variable name="month" select="floor((number($C)*5 + 456) div 153)"/>
  <xsl:variable name="day" select="number($C) - floor(( floor(153*number($month)) - 457) div 5)"/>

  <xsl:choose>
    <xsl:when test="number($month) > 12">
  <xsl:call-template name="int_to_date_helper">  
    <xsl:with-param name="year" select="number($year)+1"/>
    <xsl:with-param name="month" select="number($month)-12"/>
    <xsl:with-param name="day" select="number($day)"/>
    <xsl:with-param name="required_value" select="$required_value"/>
  </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
  <xsl:call-template name="int_to_date_helper">  
    <xsl:with-param name="year" select="number($year)"/>
    <xsl:with-param name="month" select="number($month)"/>
    <xsl:with-param name="day" select="number($day)"/>
    <xsl:with-param name="required_value" select="$required_value"/>
  </xsl:call-template>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>
  
<xsl:template name="int_to_date_helper">
  <xsl:param name="year"/>
  <xsl:param name="month"/>
  <xsl:param name="day"/>
  <xsl:param name="required_value"/> <!-- 'year' or 'month' or 'day'-->

  <xsl:choose>
    <xsl:when test="$required_value='year'">
       <xsl:value-of select="round($year)"/>
    </xsl:when>
    <xsl:when test="$required_value='month'">
       <xsl:value-of select="round($month)"/>
    </xsl:when>
    <xsl:when test="$required_value='day'">
       <xsl:value-of select="round($day)"/>
    </xsl:when>
  </xsl:choose>
</xsl:template>


Функция int_to_date соответствует C-шной функции itod, функция date_to_int - функции dtoi. Функция date_to_int принимает два параметра - количество дней и название требуемой части даты - 'year', 'month' или 'day' (передавать прямо в апострофах). Таким образом, если вы хотите напечатать дату в виде "Год.Месяц.День", то функцию date_to_int придется вызвать трижды - один раз, чтобы получить год, второй - месяц, третий - день. Впрочем, совсем несложно изменить шаблон так, чтобы функция возвращала дату целиком, в виде строки в подходящем формате.

Полный текст тестового преобразования с примером вызова можно взять здесь.

P.s. самой заковыристой частью трансляции кода оказалось правильное округление чисел в алгоритме. Дело в том, что в алгоритме используются целые числа и во всех расчетах нужно использовать именно целые, а не вещественные числа. Отсюда многочисленные floor в преобразовании.

Комментариев нет:

Отправить комментарий