Элегантные строки

в 19:49, , рубрики: .net, fluent interface, library, string, строки, метки: , , , ,

Представим, что нам нужно что-нибудь сделать со строками в .net. Что-то не очень сложное, но и не совсем простое. Например, для правильного форматирования, расставить пробелы после запятых в тексте. Что же предлагает .net из коробки?
Что-то такое:

string str = "...";
str.Replace(",", ", ");

Постойте, но мы же хотели расставлять пробелы, а не заменять запятые!..

Хорошо, пойдем дальше.
Давайте, введем цензуру. Не будем разрешать в наших текстах, скажем, слово «медведь». Вот так вот запросто. Будем подменять каждого «медведя» многоточием.
Ага, подменять. Значит логично использовать все тот же метод Replace. Сказано — сделано:

string str = "Лев и медведь добыли мясо и стали за него драться. Медведь не хотел уступить, и лев не уступал.";
var result = str.Replace("медведь", "...");

Хм, многоточие вместо первого медведя появилось, а вот второй был слишком горд и начинался с большой буквы. И наш метод перед ним спасовал. Придется пробежаться второй раз и поменять еще и гордых «Медведей».

string str = "Лев и медведь добыли мясо и стали за него драться. Медведь не хотел уступить, и лев не уступал.";
var result = str.Replace("медведь", "...").Replace("Медведь", "...");

Фух, получилось. Не очень красиво, но работает. Но работает ли? Вдруг придет такой «меДВедь»?
Мы подумали, напряглись и отсекли таких наглецов тоже. Но какой ценой!

string str = "Лев и медведь добыли мясо и стали за него драться. Медведь не хотел уступить, и лев не уступал.";
int index = str.IndexOf("медведь", StringComparison.CurrentCultureIgnoreCase);
while (index >= 0)
{
    str = str.Remove(index, "медведь".Length);
    str = str.Insert(index, "...");
    index = str.IndexOf("медведь", StringComparison.CurrentCultureIgnoreCase);
}

Что-то в этом коде не так. И проблемы две:

  1. Медленное выполнение из-за пересоздания строк на каждом шаге благодаря иммутабельности
  2. Низкоуровневый кусок утилитарного кода, который обычно ссылают в класс с названием Util и забывают, посреди прелестного семантично-выверенного проекта (ну, хотя бы фантазиях же можно?)

При этом, решение для улучшения быстродействия есть — переписать, используя StringBuilder.
Но что делать с тихо ворчащим эстетическим чувством?

Согласитесь, существующий интерфейс работы со строками в .net морально устарел. Он архаичен, недостаточно гибок и заставляет писать много странного кода для, казалось бы, обычных и простых операций раз за разом.
Еще не забудьте проверки на null. Проверки граничных значений индекса. И извольте правильно обойтись с длинами строк.

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

Посмотрим, что же из этого получилось.

Пример операции вставки:

string t = "Строка будет вставлена после второго слова маркер. Я тот самый маркер! А этот маркер будет проигнорирован"
           .Insert(", а тут был Вася").After(2, "Маркер").IgnoringCase().From(The.Beginning);
t.Should().Be("Строка будет вставлена после второго слова маркер. Я тот самый маркер, а тут был Вася! А этот маркер будет проигнорирован");

Читается как Insert " а тут был Вася" after second "маркер" ignoring case from the beginning. Хотя, что это я? И так же все понятно.

Что-нибудь удалим:

string t = "Эта строчка будет удалена ->ТЕСТ и эта тоже ->ТЕСТ, а эта останется ->ТЕСТ"
           .Remove(2, "ТЕСТ");
transformed.Should().Be("Эта строчка будет удалена -> и эта тоже ->, а эта останется ->ТЕСТ");

А теперь удалим все, учитывая регистр:

string t = "Строка ТЕСТ будет удалена с обоих концов ТЕСТ".RemoveAll("тЕСт").IgnoringCase();
t.Should().Be("Строка  будет удалена с обоих концов ");

Или даже так:

string t = "Some very long string".RemoveChars('e', 'L', 'G').IgnoringCase();
t.Should().Be("Som vry on strin");

И так:

string t = "Очень длинная строка с русскими буквами, ё".RemoveVowels().For("ru");
t.Should().Be("чнь длнн стрк с рсскм бквм, ");

Нашлось место и для расширения стандартной логики:

bool isEmptyOrWhiteSpace = "  ".IsEmpty().OrWhiteSpace();
isEmptyOrWhiteSpace.Should().Be(true);

Проход туда:

var indexes = "Текст в котором есть маркер, потом еще один маркер и напоследок МАРКЕР большими буквами"
              .IndexesOf("МаРкЕр").IgnoringCase();
indexes.Should().ContainInOrder(21, 44, 64);

И обратно:

var indexes = "Текст в котором есть маркер, потом еще один маркер и напоследок МАРКЕР большими буквами"
              .IndexesOf("маркер").From(The.End);
indexes.Should().ContainInOrder(44, 21);

А пример с медведями получается компактным и легко читаемым:

string str = "Лев и медведь добыли мясо и стали за него драться. Медведь не хотел уступить, и лев не уступал.";
var result = str.ReplaceAll("медведь").With("...").IgnoringCase();

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

Быстро попробовать можно при помощи NuGet.
А помочь проекту на GitHub или CodePlex.

Автор: dokable

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js