Очевидно, что единственное место для минора остаётся после дня месяца, с единственным возможным разделителем — цифрами. И, в общем случае, из-за ограничения величины числа, на минор нельзя отвести более 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 форм версии: с датой и без (вида ЧЧЧ.ЧЧЧ). Попроверять выражение на других тестовых примерах строк, убедиться, что работает, как задумано, или найти ошибку. Использовать запись версии программы в формате с датой в своих проектах.
ссылка на оригинал статьи http://habrahabr.ru/post/175187/
Добавить комментарий