В расширениях Хрома принято указывать версию скрипта в виде не более чем 4 чисел, разделённых точками, и величиной не более 32767 каждое и не начинающихся с цифры 0. Этого более чем достаточно, если в номер версии включены обычные данные: версия, подверсия, сборка. Если в версию хотим поместить дату в виде 3 чисел, то в наиболее удобной для чтения записи (версия.год.месяц.день) числа года, месяца и дня занимают 3 места из 4. На версию остаётся первое число (как более приоритетное перед датой), а на подверсию и минор ничего не остаётся. Задача: как расположить минорную версию, чтобы уложиться в формат, чтобы дата была читаемой, а версия с минором при сравнении 2 строк занимала правильное место в ряду версий? Кроме того, нужна процедура выделения даты и версии с минором из общей строки.
Очевидно, что единственное место для минора остаётся после дня месяца, с единственным возможным разделителем — цифрами. И, в общем случае, из-за ограничения величины числа, на минор нельзя отвести более 3 цифр: например, 31-го числа, дописав 3 цифры, получаем 31999, что всё ещё меньше 32767.
В общем, этой системы «версия + минор» вполне хватит для нумерации минорных версий скрипта, сделанных в один день. Версия в таком случае представляет собой почти что номер сборки, поставленный в начале, но с возможностью не включать в нумерацию малозначимые сборки. Это тоже удобно: мелкие исправления могут различаться или датой, или, в крайнем случае, минором, смешанным с днём месяца. К сожалению, натуральный номер версии с минором перестанет правильно сортироваться, но не будем же мы ради гипотетической сортировки дописывать обязательный минор «00000» в конце каждой даты? Проще потом, в базе или там, где надо сортировать, считать версии без миноров имеющими минор «00000». С задачей разобрались, поехали.
Пример
Представим, что версию 200 понадобилось обновить 1 апреля, не меняя номер старшей версии. Получаем 2 номера версий в один день:
200.2013.4.1
200.2013.4.101
Следующее обновление в тот же день:
200.2013.4.102
А на следующий день достаточно изменить дату:
200.2013.4.2
Чтобы не было лишних цифр, а номер не был привязан к жёсткому формату, придумаем систему распознавания любого числа минора, отличающегося от написания дня месяца. Так, в примере надо понять, записали ли мы минор 01 1-го числа, или это — минор 1, записанный 10-го числа. Учтём возможность написания 0 перед датой и месяцем, чтобы система работала не только для Хрома, в котором лидирующие нули запрещены. Так, пусть будут допустимы версии вида
200.2013.04.01
Сравнение строк версий при этом не страдает — единственное, что тогда нельзя смешивать 2 варианта написания: версии 200.2013.04.01 и 200.2013.04.2 расположатся в неправильном порядке. Но это — в общем, бонус для формата записи, потому что в первую очередь интересует Хромовская система без лидирующих нулей.
Распознавание вариантов минорной версии
Посмотрим, как разработчик может записать минорную версию после даты и как её лучше интерпретировать при разделении даты и версии.
Имеем возможные даты: 1,2, 3,… 31. К ним добавлены 01,02,… 09. Всё, что выходит за рамки этого формата, будет считаться днём месяца с номером минорной версии.
Например, 32 — это день 3, минорная версия 2. (Но в результате, для простоты парсинга, НЕ будем распосзнавать числа, начинающиеся с 3, а будем определять их как 32-е число. Ведь такого числа всё равно не будет в данных. Зато 42 и выше будем распознавать как 4-е число, минор 2).
А что будет означать 156? Это 15-е число и 6-я версия минора или 1-е число? Исходя из требования сравнения версий как строк, выводим, что эта запись — именно 15-е число, потому что иначе порядок версий выстроится неправильно в таком примере:
1
15
156 //15-е число, 6-я версия, а не 1-е число, 56-я версия
Лучше, видимо, придумать другое правило сравнения (Правило 1). Числа, начинающиеся с 1,2,3 — дополнять нулями перед сравнением версий, если они распознаны как 1,2,3, а не 1x,2x,3x. Тогда останется придумать правило различения дат. Можно принять за правило, что минор в неоднозначных случаях должен отделяться от даты нулём:
101 //число 1, разделитель 0, минор 1
1001 //число 10, разделитель 0, минор 1
111 //число 1, минор 11
Чтобы отличать 1 от 10, добавим ещё одно правило (Правило 2), что даты с 0 в конце должны всегда иметь разделитель в виде 0, т.е. всегда будут записаны с 2 нулями подряд. Это сохранит непротиворечивость нумерации и определит простое правило распознавания — отделения даты от версии.
Правда, оказывается, что 10-го, 20-го и 30-го числа число версий не может стать больше 99, потому что 100100 больше 32767, а 10100 — это 1-е число, 100-я версия, когда остальные даты позволят иметь 999 версий и больше. Впрочем, это ограничение для системы нумерации версий — не очень критичное.
Помним также, что имеется конфликт старшинства для процедуры простого сравнения строк. Если 1-го числа начинаем писать миноры, то строка вида '101','1001','10234' будет всегда больше строки '10', которая означает 10-е число.
Для дат, начинающихся с чисел 4-9 конфликтов вообще не имеем и можем даже не пользоваться нулями-разделителями (но никто не запрещает пользоваться): 62, 405, 435 всегда можно распознать как 6-е число, 2-я версия, 4-е число, 05-я версия и т.д. Можно и для чисел 32...-39… придумать то же, но это будет усложнением идеи и скриптов.
Вся трудность остаётся в неоднозначностях: что есть числа 301, 3002, 2145? Уже договорились в том, что наличие 1 нуля в конце даты означает однозначное число даты, а двух нулей — двузначное. Для чисел без нулей условимся, по соображениям сравнения строк, что (Правило 3) распознаваться будет максимально возможная дата, т.е. двузначная, если первая цифра меньше 4. Будем считать правильной 2-значной датой любую, начинающуюся с 1, 2 или 3 (иначе, надо анализировать месяцы и високосные годы, а это сложновато для шаблонов, хотя и выполнимо).
Правила для людей
Итого, при написании минора нужно помнить 2 неочевидных правила — для дат 1,2,3 — писать только 1 нуль-разделитель. Для дат 10,20,30 — писать после нуля ещё один нуль (разделитель).
Сложностей с другими числами нет: 2134 — это 21.34, а 20034 — это 20.34. При желании, можно записать и 21034 как 21.34, но нельзя опускать 0-разделитель для 1,2,3,10,20,30.
Теперь осталось записать регулярные выражения для с таким трудом полученных правил.
Регулярное выражение распознавания версии, даты и минора
С началом строки — всё просто.
d+ — старшая версия
d{4} — год
d{1,2} — месяц
Как разделить день и версию?
([4-9]|[1-3][1-9]) — все простые случаи дней. После них — пусто или минор с лидирующими нулями. Остаётся 6 сложных случаев.
[4-9]|[1-3][1-9]|[1-3]0?$ — всё, что без версии
([1-3])0([1-9]d*|$) — первые 3 дня
(10|20|30)0([1-9]d*|$) — 10-,20- и 30-е
Единое выражение (добавляются точки-разделители в виде ".", а "" удваиваются по правилам записи строк):
rVerMinor = RegExp('(\d+)\.'
+'(\d{4})\.' //по JS "\" - это "" в строке
+'(\d{1,2})\.'
+'('
+'([4-9]|[1-3][1-9]|[1-3]0?$)(\d*|$)'
+'|([1-3])0([1-9]\d*|$)'
+'|(10|20|30)0([1-9]\d*|$)'
+')');
a = s.match(rVerMinor);
Запишем его исполняемым и с рядом тестов: jsfiddle.net/spmbt/dk346/
Здесь имеются вложенные скобки. В соответствии с правилами поиска значений в скобках, после значения месяца в результате появится столько значений, сколько скобок имеется в выражении. И после разбора нужно ещё дополнительно постараться отыскать среди них непустые значения. Удобнее объединить позиции массива, имеющие одинаковый смысл, переписав выражение с меньшим числом скобок.
rVerMinor = RegExp('(\d+)\.'
+'(\d{4})\.'
+'(\d{1,2})\.'
+'('
+'([4-9]|[1-3][1-9]|[1-3]0?$)(\d*|$)'
+'|([1-3]|10|20|30)0([1-9]\d*|$)' //объединили
+')');
Наконец, устраним случай с лидирующими нулями: 200.2013.4.10001
rVerMinor = RegExp('(\d+)\.'
+'(\d{4})\.'
+'(\d{1,2})\.'
+'('
+'([4-9]|[1-3][1-9]|[1-3]0?$)(\d*|$)'
+'|(10|20|30|[1-3])0+([1-9]\d*|$)' //изменения здесь
+')');
Осталось 2 варианта результатов, которые придётся разбирать скриптами. На местах a[5],a[6] — случаи без нулей-разделителей, на a[7],a[8] — остальные. Хорошо бы их объединить. Но пока есть непокрытые случаи лидирующих нулей в датах. Это не нужно для Гугла, но не Гуглом единым… Ставим везде, где надо, «0?», плюс выявился случай, когда надо писать |0[1-3] и далее — без разделителя.
rVerMinor = RegExp('(\d+)\.'
+'(\d{4})\.'
+'(\d{1,2})\.'
+'('
+'(0?[4-9]|[1-3][1-9]|0?[1-3]0?$|0[1-3])(\d*|$)' //добавили 0? и |0[1-3]
+'|(10|20|30|0?[1-3])0+([1-9]\d*|$)' //"0?"
+')');
То, что в найденные варианты попадают лидирующие нули — не страшно, но важно помнить, что числовая запись с нулём впереди — это восьмеричное число, чтобы парсить его потом правильно.
Тесты пройдены, jsfiddle.net/spmbt/dk346/1/. Теперь всё готово к Великому Объединению. Видно, что оба выражения похожи, но чтобы объединить в одно, нужно немного переосмыслить ряд правил. Прежнее построение работает, но будем считать, что нам лень писать JS и хочется ещё немного покрутить регекспы.
Пишем такой ряд правил:
'('+ //скобки альтернатив
[1-3]0(?=0)'+ //10|20|30, за которыми идёт 0
' |0?[1-3]0$'+ //10|20|30 и пусто
' |[1-3][1-9](?!0)'+ //11-31 кроме 20,30, за кот.нет 0
' |0?[1-9]'+ //от 1 до 9
')(\d*|$)' //все остальные цифры или конец строки
Оно охватывает все требуемые правила и имеет только 1 ряд альтернатив. Дата и версия всегда попадают в a[4] и a[5]. JS станет проще и само выражение стало содержать меньше строк-образцов. Существенно использовано то, что разделитель — 0, и его отнесли ко 2-му числу во всех случаях. Тесты: jsfiddle.net/spmbt/dk346/2/.
Результат
Осталось дописать JS.
var s ='200.2013.04.123'
,rVerMinor = RegExp('(\d+)\.'
+'(\d{4})\.'
+'(\d{1,2})\.'
+'([1-3]0(?=0)|0?[1-3]0$|[1-3][1-9](?!0)|0?[1-9])(\d*|$)');
for(var i =0, iL = s.length; i < iL; i++){
a = s[i].match(rVerMinor);
console.log(s[i], a && a.slice(1))
document.body.innerHTML += '<i style=color:#999>'+ s[i] +'</i>'
+' =>' +(a &&'<br> v.'
+ a[1] + (a[5] ?'.'+ a[5].replace(/^0+/,''):'')
+', дата '+ a[2] +'-'
+(a[3].length==1?'0':'')+ a[3]
+'-'+(a[4].length==1?'0':'') + a[4])+'<br>';
}
Финальные тесты по 20 строкам с оформлением версии и даты: jsfiddle.net/spmbt/dk346/3/
Результаты:
200.2013.4.1 =>
v.200, дата 2013-04-01
200.2013.04.12 =>
v.200, дата 2013-04-12
200.2013.04.123 =>
v.200.3, дата 2013-04-12
200.2013.4.103 =>
v.200.3, дата 2013-04-01
200.2013.4.20 =>
v.200, дата 2013-04-20
200.2013.4.1053 =>
v.200.53, дата 2013-04-01
200.2013.4.10253 =>
v.200.253, дата 2013-04-01
200.2013.4.10001 =>
v.200.1, дата 2013-04-10
200.2013.4.2003 =>
v.200.3, дата 2013-04-20
200.2013.4.10015 =>
v.200.15, дата 2013-04-10
200.2013.4.300199 =>
v.200.199, дата 2013-04-30
200.2013.4.30199 =>
v.200.199, дата 2013-04-03
200.2013.4.31199 =>
v.200.199, дата 2013-04-31
200.2013.4.3199 =>
v.200.99, дата 2013-04-31
200.2013.4.03 =>
v.200, дата 2013-04-03
200.2013.4.031 =>
v.200.1, дата 2013-04-03
200.2013.4.0301 =>
v.200.1, дата 2013-04-03
200.2013.4.00 =>null
200.2013.12.56 =>
v.200.6, дата 2013-12-05
200.2013.4.501 =>
v.200.1, дата 2013-04-05
Регексп в 1 строчку:
/d+).(d{4}).(d{1,2}).([1-3]0(?=0)|0?[1-3]0$|[1-3][1-9](?!0)|0?[1-9])(d*|$)/
Что осталось читателям
Можно попрактиковаться в отрезании недопустимых дат, месяцев, дней. Сделать проверку 2 форм версии: с датой и без (вида ЧЧЧ.ЧЧЧ). Попроверять выражение на других тестовых примерах строк, убедиться, что работает, как задумано, или найти ошибку. Использовать запись версии программы в формате с датой в своих проектах.
Автор: spmbt