В жизни так случается, что не смотря на всю любовь к дифференциации данных и представления наступает день, когда возникает необходимость перенести часть логики в XSLT шаблон.
В моем случае ничего криминального на горизонте не предвиделось: требовалось провести расчет времени между двумя событиями в иерархическом XML логе. Дата и время хранились в формате частично совместимом с RFC 3339.
Эта совместимость обеспечивалась корректной нотацией даты
Шаблоны было решено собрать в extension с тем же namespace что и у exslt, так что вид контейнера
Теперь можно писать XPath вроде
В процессе разбора строки даты-времени, частенько приходилось проверять численные значения на совпадение с
Но такой код тоже не выгдядит опрятным. После не долгих раздумий был выбран вариант с автоформатом дробных чисел:
Интересно, что в Опере 10.53 функция
Следующий листинг — шаблон для преобразования даты-времени в миллисекундный timetamp:
@param Принимает единственный параметр
Обобщенный паттерн для параметра выглядит следующим образом:

@output Возвращает число миллисекунд прошедших с начала Unix-эпохи 1970-01-01T00:00:00Z.
Этот листинг — обратное преобразование, из числа в форматированную по RFC строку.
@output Строка вида
Я проверял шаблоны, как с отрицательными годами, так и с положительными — результат похож на правду. Тесты и исходник можно забрать на rapidshare. Если рапида не нравится, выложу куда-нибудь еще.
В моем случае ничего криминального на горизонте не предвиделось: требовалось провести расчет времени между двумя событиями в иерархическом XML логе. Дата и время хранились в формате частично совместимом с RFC 3339.
Эта совместимость обеспечивалась корректной нотацией даты
yyyy-MM-dd
и времени hh:mm:ss.SS
, но имели место следующие отступления от стандарта:- Дата и время разделялись пробелом, а не буквой
T
; - Число цифр, обозначающих миллисекунды могло варьироваться от «ниодной» до «много-много»;
- Часовой пояс не указывался вообще.
date:difference
, но от него пришлось отказаться. Дело в том, что разницу требовалось получать с точностью до миллисекунд, а этот алгоритм возвращал валидный xsd:duration
(ISO 8601), который миллисекунд не содержит. К тому же парсить чужой output, хоть и формализованный – дело не очень благодарное. Таким образом, покопавшись немного в exslt, я решил написать парсер сам, в надежде, что смогу сделать это быстро…Шаблоны было решено собрать в extension с тем же namespace что и у exslt, так что вид контейнера
<xsl:stylesheet/>
соответствующий:
- <?xml version="1.0" encoding="utf-8"?>
- <xsl:stylesheet version="1.0"
- xmlns:date="date"
- xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
- extension-element-prefixes="date">
- <!-- Source code goes here -->
- </xsl:stylesheet>
Задекларированное с помощью extension-element-prefixes
пространство имен расширения, будет использоваться шаблонами, а XML namespace date:*
будет использован только один раз для декларации следующего контейнера:
- <date:month>
- <january>31</january>
- <february>28</february>
- <march>31</march>
- <april>30</april>
- <may>31</may>
- <june>30</june>
- <july>31</july>
- <august>31</august>
- <september>30</september>
- <october>31</october>
- <november>30</november>
- <december>31</december>
- </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
вроде этого:
- <xsl:decimal-format name="date:NaN"
- NaN="0">
Т.е. вот такой XPath уронит шаблон: format-number($expression,0,'date:NaN')
Date-time to timestamp
Следующий листинг — шаблон для преобразования даты-времени в миллисекундный timetamp:
- <xsl:template name="date:timestamp">
- <xsl:param name="date-time"/>
- <xsl:variable name="compact"
- select="
- normalize-space(
- translate($date-time,'TZ ',''))"/>
- <xsl:variable name="year"
- select="
- translate(
- substring($compact,1,
- 4+(starts-with($compact,'+') or
- starts-with($compact,'-'))),
- '+','')"/>
- <xsl:variable name="date"
- select="substring-after($compact,$year)"/>
- <xsl:variable name="time"
- select="substring($date,7)"/>
- <xsl:variable name="month"
- select="format-number(substring($date,2,2)-1,0)"/>
- <xsl:variable name="utc-offset">
- <xsl:variable name="raw"
- select="
- concat(
- substring-after($time,'+'),
- substring-after($time,'-'))"/>
- <xsl:variable select="
- format-number(
- (contains($time,'-')-.5)
- *2*(substring($raw,1,2)*60
- +substring($raw,4,2)),0)"/>
- </xsl:variable>
- <xsl:variable select="
- format-number(
- 1000*(
- 24*3600*(
- $year*365-719527
- +floor($year div 4)
- -floor($year div 100)
- +floor($year div 400)
- +sum($date:month/*[$month>=position()])
- +format-number(substring($date,5,2)-1,0)
- -(2>$month and (($year mod 4=0 and
- $year mod 100!=0) or
- $year mod 400=0)))
- +format-number(
- concat(0,substring($time,7,
- (substring($time,6,1)=':')*2))
- +substring($time,1,2)*3600
- +substring($time,4,2)*60,0)
- +$utc-offset*60)
- +format-number(
- round(
- (substring($time,9,1)='.')
- *1000*substring-before(
- translate(
- concat('0.',substring-after($time,'.'),'_'),
- '+-','__'),'_')),0),0)"/>
- </xsl:template>
Не буду вдаваться в подробности рассчетов, только расскажу про его возможности.@param Принимает единственный параметр
$date-time
, в который передается форматированная строка. Наличие и число пробелов внутри строки значения не имеет — они все транслируются. Разелители даты могут быть любыми одиночными символами, кроме пробела.Обобщенный паттерн для параметра выглядит следующим образом:

yyyy
— годMM
— месяцdd
— деньT
— идентификатор датыhh
— часыmm
— минутыS
— дробная часть секунды, может содержать произвольное число цифр (в том числе ни одной)Z
— идентификатор UTC timezone[]
— содержимое скобок может присутствовать или нет;()
— содержимое скобок обязано присутствовать;|
— или@output Возвращает число миллисекунд прошедших с начала Unix-эпохи 1970-01-01T00:00:00Z.
Timestamp to date-time
Этот листинг — обратное преобразование, из числа в форматированную по RFC строку.
- <xsl:template name="date:date-time">
- <xsl:param name="timestamp"/>
- <xsl:if test="not(format-number($timestamp,0)='NaN')">
- <xsl:variable name="days"
- select="$timestamp div (24*3600000)"/>
- <xsl:variable name="time"
- select="
- $timestamp div 1000
- -floor($days)*24*3600"/>
- <xsl:variable name="year"
- select="
- 1970+floor(
- format-number($days div 365.24,'0.#'))"/>
- <xsl:variable name="year-offset"
- select="
- 719528-$year*365
- -floor($year div 4)
- +floor($year div 100)
- -floor($year div 400)
- +floor($days)"/>
- <xsl:variable name="month"
- select="
- count($date:month
- /*[$year-offset>=sum(preceding-sibling::*)][last()]
- /preceding-sibling::*)"/>
- <xsl:variable name="hours"
- select="floor($time div 3600)"/>
- <xsl:variable name="min"
- select="floor($time div 60-$hours*60)"/>
- <xsl:variable name="sec"
- select="floor($time -$hours*3600-$min*60)"/>
- <xsl:variable select="
- concat(
- format-number($year,'0000'),'-',
- format-number($month+1,'00'),'-',
- format-number(
- $year-offset
- -sum($date:month/*[$month>=position()])
- +(2>$month and (($year mod 4=0 and
- $year mod 100!=0) or
- $year mod 400=0)),
- '00'),'T',
- format-number($hours,'00'),':',
- format-number($min,'00'),':',
- format-number($sec,'00'),'.',
- format-number(
- 1000*($time
- -$hours*3600
- -$min*60-$sec),
- '000'),'Z')"/>
- </xsl:if>
- </xsl:template>
@param Как и предидущий шаблон, принимает единственный параметр. $timestamp
— число миллисекунд прошедшее с 1970-01-01T00:00:00Z. Если дата раньше начала 1970, то timestamp должет быть отрицательным.@output Строка вида
[-|+]yyyy-MM-ddThh:mm:ss.SSSZ
Я проверял шаблоны, как с отрицательными годами, так и с положительными — результат похож на правду. Тесты и исходник можно забрать на rapidshare. Если рапида не нравится, выложу куда-нибудь еще.