null

Разбиваем большие числа на триады/История о форматирование цены

В один прекрасный день прилетела ко мне задача на форматирование отображения цены. Так как значение могло состоять из большого количества цифр, необходимо было разбить их на группы по три разряда в каждой, чтобы клиент сходу мог определить значение стоимости товара, не перечитывая строку в попытках понять написано ли там "девять миллионов" или же "девятьсот тысяч".

Ниже 2 изображения: что имеем и что хотим получить на выходе.

Это, хоть и небольшое изменение, сильно влияет на восприятие текста, а следовательно и на настроение пользователя. Сейчас оставим UX за бортом и сосредоточимся на решении конкретной описанной выше задаче.

Первая мысль, которая приходит в голову - это то, что здесь необходимо лишь небольшое форматирование текста и надо решать всё с помощью css. Оборачиваем число в тег <span>, навешиваем class и пишем некое css-правило, которое решают нашу проблему. 

Вообще, под 'вжух-вжух' я представлял некое подобие псевдокласса :nth-child(), которым я смогу у каждого 3-го символа изменять значение свойства letter-spacing. Решить проблему должен был псевдокласс :nth-letter, которого на самом деле нет. Имея такие псевдоклассы, как :nth-child, :nth-of-type, :first-letter, :first-line, закралась мысль и о существовании :nth-letter, но нет. Такого нам в css не завезли.

Так как css'ом задача не решалась. Переключился на формирование необходимой строки используя javascript. На входе я имел переменную price типа Number.
Алгоритм преобразования представлялся следующим образом:

  1. Переворачиваем число
  2. Разбиваем на массив строк, каждая длинной в три символа. Последняя может иметь длину от 1го до 3х
  3. Склеиваем массив, вставляя между элементами пробел и переворачиваем назад
     

Если, пункт первый и третий выглядели вполне понятно составляющимися из функций reverse(), split() и join(), то разбиение на тройки не было таким очевидным. Немного поиска в google и чтения документации на String и Array, познакомили с функцией match(). Вдаваться в подробности того что она делает не стану, т.к. понятно из названия. Используя match, передав в аргументы ему следующую регулярку '/.{1,3}/g', можно разбить строку на массивы по 3 символа в каждом (исключая последний, если length не кратна трём).

В итоге я получил следующее выражение:

price = price.toString().split("").reverse().join("").match(/.{1,3}/g).reverse().join(" ");


Выглядит это, конечно, пугающе. Виной всему многочисленные преобразования типов, т.к. одна функция работает с массивом, но не со строкой, другая же наоборот. Получилась следующая цепочка:

число -> строка -> массив -> переворот -> строка -> массив троек -> переворот -> склеивание в строку с пробелами.

Теперь же как и я присвоим price начальное значение в девяносто девять миллионов и выполним команду в интерпретаторе. Всё работает. Можно пересобирать приложение и радоваться жизни. Но я решил прогнать ещё на нескольких примерах своё творение. Как оказалось, разбивается на тройки число правильно и склеивается в нужных местах. Вот только значение в ходе этого выражение изменяется. Возьмём, например, число 12345678.

price = 12345678;
price = price.toString().split("").reverse().join("").match(/.{1,3}/g).reverse().join(" ");

>> '21 543 876'


Ну вот и первый баг, перевернув строку и разбив на группы, мы получили что в каждом тройки порядок цифр перевнут и надо бы после match() их переворачивать назад, только потом уже склеивая. В версии 2.0 наше выражение обзавелось уже своей анонимной функцией.

price = price.toString().split("").reverse().join("").match(/.{1,3}/g).map( function(item) {
                return item.split("").reverse().join("");
}).reverse().join(" ");

 

Если раньше было слегка пугающее выражение, то теперь оно превратилось в ужасающего монстра и вопрос о том,  не слишком ресурсоемки такие преобразования?

Но, так как была глубокая ночь, я остановился на этом варианте, как временном решении. Требование организма ко сну победило желание рефакторить и я оставил это дело ненадолго.

На следующий день, я решил заняться поисками более элегантного решения этой задачи. В конечном итоге я пришёл к такому варианту:

price = price.toString().replace(/(\d)(?=(\d{3})+$)/g, '$1 ');


В нём 10 вызовов функций были заменены на один вызов функции replace, которая принимает чудо-регулярку. Давайте попробуем разобраться, что же за магия такая творится в аргументах у ф-ции.

Код заключенный в '/code/' является регулярным выражением. Символ 'g' на конце делает поиск глобальным и позволяет обработать все совпадния с шаблоном. Второй аргумент функции '$1 ', определяет шаблон замены. Он возьмёт '$1' - первую скобочную подгруппу в выражении (по сути это будет (\d) ) и весь найденный шаблон заменит, на эту подгруппу + символ пробела.

Теперь о самом шаблоне:
'(\d)' -  найти любую цифру. Обозначим за A;
'(\d{3})+$' - группа из трех цифр, 0 или более совпадений, за ними обязательно конец строки. Это обозначим за B;
'A(?=B)'  - найти A, только если после него следует B.

Вместе имеем выражение следующего содержания: "Найти цифру, за которой следует группа из 0 или более троек цифр, заканчивающихся концом строки. После этого для всех совпадений произвести замену на первую скобочную подгруппу '(\d)' + пробел. Так как После в совпадения у нас включается только сама первая подгруппа (из-за выражения '?='), то замена эквивалента конкатенации с пробелом.

Напоследок давайте посмотрим работу функции замены на конкретном примере. Возьмём за входное значение цены - '12345678'.

Регулярное выражение даст 2 совпадения:

  1. Позиция 1, символ '2'. Он является цифрой и подходит под часть шаблона '(\d)', далее за ним следует 6 цифр, что является 2мя группами по 3 цифры и подходит под '(\d{3})+'. После никаких символов нет
  2. Позиция 4, символ '5'. Также является цифрой, а за ним следует 3 цифры и дальше ничего.

Итого 2 совпадения в строке '12345678'. Каждо из них заменится на себя + пробел. В результате получится строка '12 345 678'. Такой результат нам и нужен был.

На этой ноте я вас покину. Надеюсь, что эта заметка не только помогла найти решение проблемы, но и донесла принцип его работы.