Pull to refresh

4-числовая система нумерации версий с датой и минорами

Reading time8 min
Views8.7K
В расширениях Хрома принято указывать версию скрипта в виде не более чем 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','200.2013.04.1002']
,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*|$)/


UPD: для тестирования регулярного выражения версия тех же примеров выложена на сервисе из соседней статьи: regexponline.com/s/we#sthash.GpUQK42k.92zmz1RF.dpuf

Что осталось читателям


Можно попрактиковаться в отрезании недопустимых дат, месяцев, дней. Сделать проверку 2 форм версии: с датой и без (вида ЧЧЧ.ЧЧЧ). Попроверять выражение на других тестовых примерах строк, убедиться, что работает, как задумано, или найти ошибку. Использовать запись версии программы в формате с датой в своих проектах.
* Пагинатор (постраничная навигация) на XSLT
Tags:
Hubs:
+6
Comments6

Articles

Change theme settings