Как стать автором
Обновить

Timestamp из даты-времени с помощью XSLT

Время на прочтение 13 мин
Количество просмотров 9.5K
В жизни так случается, что не смотря на всю любовь к дифференциации данных и представления наступает день, когда возникает необходимость перенести часть логики в XSLT шаблон.

В моем случае ничего криминального на горизонте не предвиделось: требовалось провести расчет времени между двумя событиями в иерархическом XML логе. Дата и время хранились в формате частично совместимом с RFC 3339.

Эта совместимость обеспечивалась корректной нотацией даты yyyy-MM-dd и времени hh:mm:ss.SS, но имели место следующие отступления от стандарта:
  1. Дата и время разделялись пробелом, а не буквой T;
  2. Число цифр, обозначающих миллисекунды могло варьироваться от «ниодной» до «много-много»;
  3. Часовой пояс не указывался вообще.
Сначала я хотел воспользоваться готовым решением с exslt.org – date:difference, но от него пришлось отказаться. Дело в том, что разницу требовалось получать с точностью до миллисекунд, а этот алгоритм возвращал валидный xsd:duration (ISO 8601), который миллисекунд не содержит. К тому же парсить чужой output, хоть и формализованный – дело не очень благодарное. Таким образом, покопавшись немного в exslt, я решил написать парсер сам, в надежде, что смогу сделать это быстро…

Шаблоны было решено собрать в extension с тем же namespace что и у exslt, так что вид контейнера <xsl:stylesheet/> соответствующий:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <xsl:stylesheet version="1.0"
  3.                 xmlns:date="date"
  4.                 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  5.                 extension-element-prefixes="date">
  6.     <!-- Source code goes here -->
  7. </xsl:stylesheet>
Задекларированное с помощью extension-element-prefixes пространство имен расширения, будет использоваться шаблонами, а XML namespace date:* будет использован только один раз для декларации следующего контейнера:
  1. <date:month>
  2.     <january>31</january>
  3.     <february>28</february>
  4.     <march>31</march>
  5.     <april>30</april>
  6.     <may>31</may>
  7.     <june>30</june>
  8.     <july>31</july>
  9.     <august>31</august>
  10.     <september>30</september>
  11.     <october>31</october>
  12.     <november>30</november>
  13.     <december>31</december>
  14. </date:month>
Для удобства получения числа дней по номеру или имени месяца введем переменную:

<xsl:variable name="date:month"
              select="document('')//date:month"/>

Теперь можно писать XPath вроде sum($date:month/*[$i>=position()])+($i>2) — полное число дней високосного года по $iтый месяц включительно.

В процессе разбора строки даты-времени, частенько приходилось проверять численные значения на совпадение с NaN и в случае положительного сравнения заменять их на 0. Это порождало бы множество <xsl:if/> что сильно загромождало код. Поэтому я начал использовать translate вот такого вида:

translate($expression,'NaN',0)

Но такой код тоже не выгдядит опрятным. После не долгих раздумий был выбран вариант с автоформатом дробных чисел:

<xsl:decimal-format NaN="0"/>

Интересно, что в Опере 10.53 функция format-number не умеет работать с тремя аргументами и порождает unknown error, что не дает возможности использовать именованные форматы чисел decimal-format вроде этого:
  1. <xsl:decimal-format name="date:NaN"
  2.                     NaN="0">
Т.е. вот такой XPath уронит шаблон: format-number($expression,0,'date:NaN')

Date-time to timestamp

Следующий листинг — шаблон для преобразования даты-времени в миллисекундный timetamp:
  1. <xsl:template name="date:timestamp">
  2.     <xsl:param name="date-time"/>
  3.     <xsl:variable name="compact"
  4.                   select="
  5.          normalize-space(
  6.              translate($date-time,'TZ ',''))"/>
  7.     <xsl:variable name="year"
  8.                   select="
  9.          translate(
  10.              substring($compact,1,
  11.                  4+(starts-with($compact,'+') or
  12.                     starts-with($compact,'-'))),
  13.              '+','')"/>
  14.     <xsl:variable name="date"
  15.                   select="substring-after($compact,$year)"/>
  16.     <xsl:variable name="time"
  17.                   select="substring($date,7)"/>
  18.     <xsl:variable name="month"
  19.                   select="format-number(substring($date,2,2)-1,0)"/>
  20.     <xsl:variable name="utc-offset">
  21.         <xsl:variable name="raw"
  22.                       select="
  23.              concat(
  24.                  substring-after($time,'+'),
  25.                  substring-after($time,'-'))"/>
  26.         <xsl:variable select="
  27.              format-number(
  28.                  (contains($time,'-')-.5)
  29.                  *2*(substring($raw,1,2)*60
  30.                     +substring($raw,4,2)),0)"/>
  31.     </xsl:variable>
  32.     <xsl:variable select="
  33.          format-number(
  34.              1000*(
  35.                  24*3600*(
  36.                      $year*365-719527
  37.                          +floor($year div 4)
  38.                          -floor($year div 100)
  39.                          +floor($year div 400)
  40.                      +sum($date:month/*[$month>=position()])
  41.                      +format-number(substring($date,5,2)-1,0)
  42.                      -(2>$month and (($year mod 4=0 and
  43.                                       $year mod 100!=0) or
  44.                                       $year mod 400=0)))
  45.                  +format-number(
  46.                      concat(0,substring($time,7,
  47.                              (substring($time,6,1)=':')*2))
  48.                      +substring($time,1,2)*3600
  49.                      +substring($time,4,2)*60,0)
  50.                  +$utc-offset*60)
  51.              +format-number(
  52.                  round(
  53.                      (substring($time,9,1)='.')
  54.                      *1000*substring-before(
  55.                          translate(
  56.                              concat('0.',substring-after($time,'.'),'_'),
  57.                              '+-','__'),'_')),0),0)"/>
  58. </xsl:template>
Не буду вдаваться в подробности рассчетов, только расскажу про его возможности.

@param Принимает единственный параметр $date-time, в который передается форматированная строка. Наличие и число пробелов внутри строки значения не имеет — они все транслируются. Разелители даты могут быть любыми одиночными символами, кроме пробела.

Обобщенный паттерн для параметра выглядит следующим образом:

RFC 3339 date-time pattern

yyyy — год
MM — месяц
dd — день
T — идентификатор даты
hh — часы
mm — минуты
S — дробная часть секунды, может содержать произвольное число цифр (в том числе ни одной)
Z — идентификатор UTC timezone
[] — содержимое скобок может присутствовать или нет;
() — содержимое скобок обязано присутствовать;
| — или

@output Возвращает число миллисекунд прошедших с начала Unix-эпохи 1970-01-01T00:00:00Z.

Timestamp to date-time

Этот листинг — обратное преобразование, из числа в форматированную по RFC строку.
  1. <xsl:template name="date:date-time">
  2.     <xsl:param name="timestamp"/>
  3.     <xsl:if test="not(format-number($timestamp,0)='NaN')">
  4.         <xsl:variable name="days"
  5.                       select="$timestamp div (24*3600000)"/>
  6.         <xsl:variable name="time"
  7.                       select="
  8.              $timestamp div 1000
  9.             -floor($days)*24*3600"/>
  10.         <xsl:variable name="year"
  11.                       select="
  12.              1970+floor(
  13.                  format-number($days div 365.24,'0.#'))"/>
  14.         <xsl:variable name="year-offset"
  15.                       select="
  16.              719528-$year*365
  17.              -floor($year div 4)
  18.              +floor($year div 100)
  19.              -floor($year div 400)
  20.              +floor($days)"/>
  21.         <xsl:variable name="month"
  22.                       select="
  23.              count($date:month
  24.                    /*[$year-offset>=sum(preceding-sibling::*)][last()]
  25.                    /preceding-sibling::*)"/>
  26.         <xsl:variable name="hours"
  27.                       select="floor($time div 3600)"/>
  28.         <xsl:variable name="min"
  29.                       select="floor($time div 60-$hours*60)"/>
  30.         <xsl:variable name="sec"
  31.                       select="floor($time -$hours*3600-$min*60)"/>
  32.         <xsl:variable select="
  33.              concat(
  34.                  format-number($year,'0000'),'-',
  35.                  format-number($month+1,'00'),'-',
  36.                  format-number(
  37.                      $year-offset
  38.                      -sum($date:month/*[$month>=position()])
  39.                      +(2>$month and (($year mod 4=0 and
  40.                                       $year mod 100!=0) or
  41.                                       $year mod 400=0)),
  42.                      '00'),'T',
  43.                  format-number($hours,'00'),':',
  44.                  format-number($min,'00'),':',
  45.                  format-number($sec,'00'),'.',
  46.                  format-number(
  47.                      1000*($time
  48.                      -$hours*3600
  49.                      -$min*60-$sec),
  50.                      '000'),'Z')"/>
  51.     </xsl:if>
  52. </xsl:template>
@param Как и предидущий шаблон, принимает единственный параметр. $timestamp — число миллисекунд прошедшее с 1970-01-01T00:00:00Z. Если дата раньше начала 1970, то timestamp должет быть отрицательным.

@output Строка вида [-|+]yyyy-MM-ddThh:mm:ss.SSSZ

Я проверял шаблоны, как с отрицательными годами, так и с положительными — результат похож на правду. Тесты и исходник можно забрать на rapidshare. Если рапида не нравится, выложу куда-нибудь еще.
Теги:
Хабы:
+7
Комментарии 7
Комментарии Комментарии 7

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн