Привет, коллеги. Иногда стоит задача написать заданное число предложений. Конечно, многое зависит от стиля: ведь можно умело комбинировать деепричастные и причастные обороты, склеивая вместе выражения, которые в ином случае можно было бы оформить как отдельные предложения, но в интересах сокращения полного их числа в тексте можно этого не делать без особого ущерба для понятности текста, хотя нельзя не отметить, что в ряде случаев подобный стиль выглядит, всё-таки, тяжеловесно.
Можно, поступать наоборот. Сокращать! Писать телеграфно. Чем короче - тем лучше. Больше предложений. Пусть каждое содержит меньше слов. Символов. Знаков. Главное - больше предложений. Даже! Так! Можно! Если очень надо.
Но наша задача - научиться подсчитывать. Я опишу три решения, и материал довольно сложный.
Есть команда ), которая двигается по предложениям. Можно использовать ее, особенно с повторителем. Это ручной режим: считать придётся самому.
Другой способ - использовать фиктивную замену. Мы знаем, что модификатор /n предписывает замену не делать, а только считать совпадения. Что нам и надо.
Осталось найти регулярное выражение, которое совпадает с предложением и больше ни с чем. Предложение мы определим так: оно начинается с заглавной буквы и оканчивается точкой либо одним из двух знаков: вопросительным или восклицательным. И затем должен идти либо пробел, либо конец строки. Остальное может быть чем угодно.
Попробуем:
\C[A-ZА-ЯЁ]\_.\{-}[.!?]\_s\+
Давайте разберем. Ключ \C включает учёт регистра, так как нам это важно. Класс [A-ZА-ЯЁ] содержит все заглавные буквы русского и английского языков. Для диакритики потребуются коррективы. Символ \_. означает "любой символ, включая конец строки". За ним идет ленивый квантификатор \{-} (нуль или более, чем меньше - тем лучше). Если поставить обычный жадный квантификатор *, то он спокойно заберет точку в конце предложения, сделав ее частью предложения, а нам это не надо. Далее идет класс, описывающий признак конца предложения: [.!?]. А после него идет пробел или конец строки: \_s. Может быть и табуляция, и какой-нибудь особый пробел из Юникода. Пробелов может быть много, но не менее одного: за этим проследит жадный квантификатор \+ (один или более, чем больше - тем лучше).
Кстати, символ "многоточие", по-английски "ellipsis", … можете добавить в класс [.!?] и тогда у вас предложение может заканчиваться им.
Итак, команда такая:
:%s/\C[A-ZА-ЯЁ]\_.\{-}[.!?]\_s\+//gn
Как вариант, можете просмотреть каждое предложение:
:%s/\C[A-ZА-ЯЁ]\_.\{-}[.!?]\_s\+/&/gc
Замена фиктивная, заменяет найденное на него же, но зато спрашивает и выделяет найденное, что позволяет посмотреть. Не забудьте выставить флаг hls. Есть небольшой недостаток, такой же, как у следующего варианта.
Ещё есть вариант
:%s/\C[A-ZА-ЯЁ]\_.\{-}[.!?]\_s\+/&/g
Он сообщает о числе замен, хотя сами замены тривиальны: меняют найденное на него же. Выставьте count в нуль, чтобы получить отчет даже об одном предложении. По умолчанию count равен 2, то есть про одно или два совпадения Вим не сообщает. Это команда, конечно, чуть более тяжеловесна, и на гигабайтном тексте это будет заметно. Ещё один недостаток в том, что на самом деле текст меняется: в конце добавляется ещё один символ конца строки. Это странное поведение, я не вижу разумного объяснения.
В итоге мы с коллегами выработали такой вариант:
\C\([.?!]\s*\|^\)\@<=[A-ZА-ЯЁ]\_.\{-}[.?!]\_s\+
Здесь задействована ретроспективная проверка \@<=: совпадение с [.?!]\s*\|^ должно быть перед предложением, но не является его частью. То есть, перед предложением должен быть конец предыдущего или начало строки.
Сейчас обсудим, почему такой вариант оптимален (но не идеален).
Но сначала посмотрим, что может пойти не так в целом.
В конце файла может не стоять символ конца строки, и тогда последнее предложение может не отловиться.
Может подвести многоточие. Если после него стоит имя с большой буквы, это будет сочтено новым предложением. А так бывает.
Может быть проблема другого рода: существует юникодный символ "многоточие", … (который отличается от трёх точек ...), и если вы его не предусмотрели в качестве завершающего, то предложение на нем не кончится и сольется со следующим. Это актуально для юникодного текста, конечно. Но в ТеХ может использоваться команда вроде \dots и тогда, конечно, она не распознается как конец предложения.
Если предложение начинается с маленькой буквы, оно не сосчитается. А так бывает.
Довольно запутанной бывает прямая речь. Например, "'Любимая!' - сказал он.". Я и сам не знаю, одно это предложение или два. Для нашего выражение одно, причем от Л до точки. А если "Сказал" с большой буквы, то два: одно от Л до восклицательного знака, другое от С до точки. А ведь может быть и так: "'Любимая!' - Пьер шагнул к девушке." Теперь для выражения два предложения.
Если в начале предложения стоит ТеХ-формула, то тоже предложение не зачтется. Так не надо делать, но это совет, а не запрет, и иногда так делают. Например,
$\sin$ is a function.
Если вы решили начать предложение с иностранного слова, и первая буква не входит в число разрешенных, совпадения не будет. Например,
Ω - это буква греческого алфавита.
Теперь экспертная тема. Я обнаружил в Виме если не баг, то особенность. Помимо тривиальной замены, которая оказалось не такой и тривиальной, о которой сказано выше. Если предложение растянуто на более, чем одну строку (то есть содержит символ конца строки внутри) и в нем есть заглавная буква, то команда s///n считает неправильно. Она увеличивает счетчик совпадений на каждой строке, даже если часть этой строки вошла в предыдущее совпадение. И получается лишнее предложение. Например:
Там ее уже
ждал Сильвер, который
только что пришел.
Первая команда (с флагом /gn) найдет ДВА предложения. В ручном режиме (с флагом /gc) будет найдено одно. Вариант с заменой (/&/g) считает верно, но он добавит лишнюю пустую строку в конец. Вариант с ретроспективной проверкой работает лучше всех.
Но и он может ошибиться, например в тексте
Сильвер был одним из самых
известных пиратов
XVII века.
найдет два предложения.
В принципе, можно развивать тему до бесконечности, учтя всё. Но не вижу смысла. Если вам действительно надо точно посчитать предложения и вариант с дополнительной пустой строкой не устраивает, то попробуйте перед подсчетом отформатировать текст так, чтобы одно предложение было на одной строке. Или весь текст был в одну строку. Для этого достаточно выполнить замену
:%s/[^.!?]\s*\zs\n/ /
либо просто
:%s/\n/ /
Первая команда заменит конец строки, который не конец предложения, на пробел, склеив части предложения вместе. Вторая просто заменит все концы строки на пробелы. Потом проведите подсчет любой из команд выше и отмените выполненное форматирование.
Удачи, коллеги!