Разделение числа на разряды с помощью регулярного выражения
Интуитивно понятно как решить эту задачу вручную, на бумаге. Нужно просто идти справа налево и через каждые три цифры ставить запятую.
А можно ли сформулировать это решение на языке регулярных выражений?
Сразу же возникает затык - регулярные выражения не умеют ходить справа налево. Значит, нужно переформулировать решение так, чтобы задача решалась в обычном для регулярных выражений направлении - слева направо.
Но так вот, запросто, идти слева и отсчитывать тройки цифр не получится. Ведь разряды по определению отсчитываются справа, супротив математики не попрешь. Значит, идти, как велят регулярные выражения, нужно слева, а разряды считать - справа.
Что эта фигня значит?
Щас разберемся. Для простоты возьмем число 1234.
Идем слева направо. Первая цифра слева - 1. Следует ли после этой цифры поставить запятую? Как понять, является ли место за цифрой 1 местом разделения разрядов?
Посчитаем цифры справа, от конца числа. От конца числа насчитывается три цифры до места после цифры 1. Три цифры - это разряд, значит, после цифры 1 нужно поставить запятую:
Здесь разряд обозначен квадратными скобками.
Но высказывание "от конца числа насчитывается три цифры до места после цифры 1" направлено справа налево, а для написания регулярного выражения нам нужно высказывание, направленное слева направо. Для получения такого высказывания просто развернем предыдущее высказывание задом наперед.
Как несложно догадаться, результатом будет:
"от места после цифры 1 насчитывается три цифры до конца числа".
Значит, если от места после цифры 1 насчитывается три цифры до конца числа, значит, место после цифры 1 является местом разделения разрядов.
Разберем это высказывание по частям и запишем на языке регулярных выражений.
Видно, что высказывание состоит из следующих частей:
1. "от места после цифры 1" 2. "насчитывается три цифры" 3. "до конца числа"
"от места после цифры 1" - это, очевидно, заглядывание вперед. На языке регулярных выражений заглядывание вперед записывается так:
Впереди "насчитываются три цифры". Три цифры записываются так:
Плюс, впереди находится "конец числа". Конец числа записывается так:
Тут требуется пояснение. Дословно эта конструкция означает "место, после которого нет цифры", и, вообще говоря, эта конструкция не является концом числа в общем случае. Но в нашем конкретном случае эта конструкция подходит. За подробностями отсылаю к Фридлу.
Соединим все три части в одно выражение:
Получившееся выражение находит место разделения разрядов. Ну, а вставить на это место запятую проще простого, для этого у нас есть оператор s/// - стандартный оператор замены.
Однострочник для проверки:
$ perl -e '$a=1234; $a=
Результатом выполнения будет 1,234.
Регулярное выражение, однако, пока еще не закончено. Если вместо числа 1234 взять число подлиннее, например, 1234567890, то результатом выполнения будет 1234567,890.
А где остальные запятые? Почему выделен только один разряд?
Легко догадаться, что один разряд выделен потому, что регулярное выражение находит ровно три цифры - \d . Три цифры соответствуют одному разряду. Чтобы выделить все разряды, нужно найти все имеющиеся тройки цифр.
Высказывание "все имеющиеся" - это квантификатор +. Соответственно, высказывание "все имеющиеся тройки цифр" записывается так:
А полностью регулярное выражение теперь будет выглядеть так:
Запомним это выражение, оно пригодится дальше.
$ perl -e '$a=1234567890; $a=
Опаньки. Результат 1,234567890.
В первую очередь возникает мысль о том, что жадный квантификатор + захватил все возможные тройки. Это действительно так, но причина, все-таки, не в жадности квантификатора. Причина в том, что по умолчанию регулярное выражение ищет только одно совпадение из всех возможных. Ну, а то, что это совпадение из-за жадности квантификатора оказывается максимально длинным, это уже не существенно.
А какие вообще есть возможные совпадения? Давайте разберемся. Запишем в квадратных скобках тройки цифр:
1, [ 234 ][ 567 ][ 890 ]
Поскольку квантификатор + является жадным, то выражение (\d)+ совпадает со всеми тройками цифр. Обозначим совпадение круглыми скобками:
1. 1, ( [ 234 ][ 567 ][ 890 ] )
Вот это первое максимально длинное совпадение и отделяется запятой. Но ведь есть и другие совпадения, менее длинные, которые тоже нужно отделить запятыми:
2. 1 [ 234 ] , ( [ 567 ][ 890 ] ) 3. 1 [ 234 ][ 567 ] , ( [ 890 ] )
Для этого надо заставить регулярное выражение не останавливаться после нахождения первого совпадения и найти два оставшихся возможных совпадения.
Чтобы регулярное выражение искало все возможные совпадения, нужно к оператору s/// добавить модификатор g.
$ perl -e '$a=1234567890; $a=
Ага, теперь результатом выполнения будет 1,234,567,890.
Это - правильный результат. Победа? Увы, нет.
Проведем контрольную проверку с числом 123456789:
$ perl -e '$a=123456789; $a=
Результатом выполнения будет ,123,456,789.
Как видите, впереди числа появилась лишняя запятая. Откуда она взялась? Она взялась из-за еще одного возможного совпадения, которого не было в числе 1234567890, но которое есть в числе 123456789:
Тут все число состоит из троек цифр. Поэтому регулярное выражение дает совпадение со всем числом целиком.
Формально все правильно, но здравый смысл подсказывает, что слева от числа ставить запятую бессмысленно. А почему, собственно? Очевидно, потому, что запятыми мы разделяем цифры, а слева от этой лишней запятой ни одной цифры нет.
Значит, в регулярное выражение нужно добавить условие, требующее ставить запятые только в тех местах, слева от которых цифра есть.
Высказывание "место, слева от которого есть цифра" состоит из следующих частей:
1. "место, слева от которого" 2. "есть цифра"
"место, слева от которого" - это, очевидно, заглядывание назад. На языке регулярных выражений заглядывание назад записывается так:
Соединим эти две части:
Теперь присоединим получившееся к ранее составленному (и запомненному) регулярному выражению. Присоединяем слева, ведь наличие цифры мы проверяем именно слева:
$ perl -e '$a=123456789; $a=
Отлично, получился результат 123,456,789.
Все, окончательное решение найдено. Регулярное выражение для разделения числа на разряды выглядит так: