Вычисления в латехе? Да запросто!

в 22:10, , рубрики: latex, метки:

Те, кому приходится верстать тексты посложнее служебок, знают, что латех —
удобная надстройка над техом — на данный момент является чуть ли не единственным
средством, позволяющим быстро верстать качественные тексты любой сложности.
Но не текстом единым… Латех можно использовать и для других целей, о которых
создатель Теха, Дональд Кнут, наверное и не думал.

Я уже писал, как можно верстать в латехе презентации. Теперь же я хочу рассказать
о том, как можно проводить несложные вычисления непосредственно силами латеха.

Для латеха разными людьми написана уйма всевозможных пакетов, расширяющих его
функционал и облегчающих работу верстальщику. Не забыты и средства, помогающие
работать с численными данными. Несложные вычисления с использованием чисел
с плавающей запятой (одинарной точности) можно выполнять непосредственно
силами теха. Для этого и предназначен пакет fp. Сразу хочу заметить,
что благодаря возможностям латеха выполнять внешние команды, можно даже выполнять
более сложные действия на этапе компиляции (например, что-нибудь вычислять
в Octave, затем строить в MathGL графики и вставлять их в текст), но об этом я,
возможно, расскажу в другой раз.

Я не задаюсь целью скопипастить мануал по пакету fp (тем более, что он достаточно
небольшой), а хочу рассказать, как я использую этот пакет для простых вычислений
при составлении всякого рода технических заданий, отчетов и т.п. А еще покажу,
как с его помощью реализовать элементарный функционал «электронных таблиц».

Для начала создадим команду, позволяющую вставлять различную варьирующуюся
информацию (т.е. какие-то параметры, которые в дальнейшем вы можете и изменить),
помечая в режиме черновой печати эти вставки красным надстрочным индексом и
заметкой на полях. Цвет в документе нам обеспечит пакет colorx.

Эта команда будет работать в трех вариантах:

  • без параметров (т.е. если нам нужно отметить место в тексте, где
    не хватает чего-то, что надо будет вставить в дальнейшем), в этом
    случае мы просто отметим пустое место в тексте красным квадратом с
    надстрочным индексом и вынесем на поля метку со знаком вопроса;
  • с одним параметром (т.е. мы вставили какие-то данные, которые в
    дальнейшем использоваться не будут), в этом случае вставленный текст
    отображается с надстрочным индексом и соответствующей пустой меткой
    на полях;
  • с двумя параметрами (т.е. наши данные будут использоваться дальше
    и мы хотим проинициализировать ими какую-то переменную), здесь также
    отображается инициализирующий текст с индексом, но на полях напротив
    этого индекса помещается имя переменной, в которую данное число заносится,
    плюс происходит инициализация этой переменной.

Итак, вот сам код:

newcounter{InsC@unt@r} 
defInd@x{stepcounter{InsC@unt@r}% 
			hbox to 0pt{raisebox{1ex}{tinyred{arabic{InsC@unt@r}}}}} 
defInsm@rgimp@r#1{marginpar{red{tinyarabic{InsC@unt@r}:#1}}} 
ifxTextOutundefined 
	defInsTxt@#1#2{#1Ind@xInsm@rgimp@r{#2}} 
else 
	defInsTxt@#1#2{#1} 
fi 
defIns{futureletnextIns@i} 
defIns@i{ifxnextbgroupexpandafterIns@iielseexpandafterIns@endfi} 
defIns@ii#1{deftemp@rg{#1}futureletnextIns@iii} 
defIns@iii{ifxnextbgroupexpandafterIns@two@rgs% 
	elseexpandafterIns@one@rgfi} 
defIns@two@rgs#1{InsWFP{temp@rg}{#1}} % Два аргумента 
defInsWFP#1#2{expandafterFPsetcsname #1endcsname{#2}InsTxt@{#2}{#1}} 
defIns@one@rg{InsTxt@{temp@rg}{}} % АРгумент один 
defIns@end{red{SquareInd@x}Insm@rgimp@r{?}xspace} % нет аргументов 

Поясню его. Сначала мы объявляем новый счетчик, который будет нами использоваться
для нумерации индексов вставок. Далее определяются различные вспомогательные
команды для основной команды Ins, которой мы и будем вставлять
свои данные в текст.

Команды Ind@x и Insm@rgimp@r нужны для того, чтобы отмечать
наши вставки в черновом режиме. Первая команда инкрементирует счетчик вставок,
создает не занимающий по ширине места бокс и помещает в него красный надстрочный
индекс с номером вставки. Вторая команда помещает отметку на поля.
Сам черновой режим регулируется командой
TextOut: если мы определим ее (defTextOut{}) до этого куска
кода, отметки на командой defInsTxt ставится не будут («беловой»
режим верстки), иначе они будут проставляться.

Дальше мы определяем собственно нашу команду Ins, принимающую переменное
количество аргументов. Она выполняет только одно: считывает следующий за ней
токен (futurelet) в макрос next, отбрасывает его обратно и
выполняет команду Ins@i. Та, в свою очередь, проверяет, что же за
токен следовал за нашей Ins: если это открывающаяся фигурная скобка
(bgroup), то выполняется команда Ins@ii с предварительным
считыванием следующего за ней параметра (иначе эта команда свой параметр «не
увидит»). Если же токеном является не скобка, значит, команда Ins не
имела аргументов и вызывается Ins@end, помещающая пустой квадратик в
текст и заметку на полях.

Команда Ins@ii помещает первый аргумент команды Ins во макрос
temp@rg и опять выполняет проверку наличия следующего аргумента
(Ins@iii): если он есть, выполняется команда Ins@two@rgs,
инициализирующая переменную и вставляющую ее значение и метки в текст; если же
его нет, выполняется команда Ins@one@rg, которая просто вставляет
в текст данные и делает отметку.

Чтобы облегчить вывод результатов вычислений, создадим команду FPrint,
похожую по написанию на FPprint из пакета fp, но делающую несколько
другое: ее аргумент имеет вид выражение:число, где выражение
— вычисления или команда, а число — количество цифр после запятой, до
которых нужно округлить результат. На экран выводится округленное число.
Здесь все достаточно просто реализовано, поэтому излишне комментировать не буду.

defFPrint#1{@@print@@#1} 
def@@print@@#1:#2{FPeval{res@lt}{round(#1:#2)}res@lt} 

Для хранения каких-то общих для всего текста данных (скажем, величины, которая
вычисляется на протяжении нескольких разделов документа, причем в каждом разделе
вычисляется какая-то ее часть, а сумму нам нужно будет вывести в конце текста)
определим команды Ini (инициализирует пользовательскую переменную, имеет
один обязательный аргумент — имя переменной, а также один необязательный —
значение, которым переменная инициализируется), Add (добавляет к
переменной из первого параметра выражение из второго), Sub (вычитает
из переменной число) и Show (отображает переменную в тексте, округляя
ее до двух цифр после запятой или до количества цифр, указанных в необязательном
параметре).

newcommand{Ini}[2][0]{expandaftergdefcsname cntr#2endcsname{#1}} 
defAdd#1#2{expandafterFPevalcsname cntr#1endcsname{cntr#1+(#2)}} 
defSub#1#2{expandafterFPevalcsname cntr#1endcsname{cntr#1-(#2)}} 
newcommand{Show}[2][2]{FPrint{cntr#2:#1}} 

Благодаря тому, что к имени переменной, определяемой пользователем, добавляется
префикс cntr, почти исчезает вероятность случайного переопределения каких-либо латеховских
макросов.

Далее перейдем к «электронным таблицам».
Для начала заведем автоматическую нумерацию:

newcount@row@num@row@num=15 
def@@nonum{} 
def@shownum{ifx@@nonumemptyglobaladvance@row@num1 the@row@numelse 
	gdef@@nonum{}fi} 
defNumIni{global@row@num=0gdef@@nonum{1}} 
defNoNum{gdef@@nonum{1}} 
newcolumntype{N}[1]{>{strut}#1<{@shownum}} 

Инициализацию счетчика надо выполнять вручную, ставя в начале таблицы
команду NumIni (либо придумать нечто наподобие everypage,
но для таблиц). При помощи пакета array мы будем выполнять определенные
нами команды для каждой ячейки нужного столбца. Этот же пакет позволяет
определить новые типы столбцов: тот столбец таблицы, в который мы хотим
вставить автонумерацию, надо будет пометить типом N. Если в какую-то
ячейку нам не нужно вставлять номер, будем помечать ее макросом NoNum.

Теперь перейдем к определению типов столбцов, позволяющих выполнять сложение,
вычитание, умножение и деление с числовыми переменными. Так как код всех этих
команд почти не отличается друг от друга (кроме выполняемой в FPeval
арифметической операции), для начала определим макрос, облегчающий определение
новых команд.

Заранее предупрежу: т.к. для однозначного определения окончания числа, находящегося
в ячейке таблицы, пришлось бы сканировать его посимвольно (что сильно увеличило
бы время компиляции текста), я пошел другим путем. В конце каждой ячейки перед
знаком амперсанда необходимо вставлять пробел, однозначно символизирующий окончание
данных внутри ячейки. Если забыть этот пробел, могут появиться «странные ошибки».

Подробно пояснять следующий макрос я не буду, ограничусь лишь поверхностным
описанием. Команда @def@cmd позволяет определить новый макрос (чье
имя указано в первом параметре), выполняющий арифметическое действие из
второго параметра, а также создать новый тип столбца таблицы (третий параметр).
Так как эта команда используется для инициализации других команд, да еще и
определяет новую команду по имени из параметра, используются такие теховские
конструкции, как expandafter (подавление раскрытия следующего токена,
пока не раскроется следующий через один токен), xdef (она идентична
globaledef и определяет новый макрос, предварительно раскрывая макросы
из своего тела), noexpand (запрет раскрытия токена «в первом прочтении»),
csname (преобразование текста в имя макроса).

def@@def@cmd#1#2#3{% 
	expandafterxdefcsname #1endcsname##1ignorespaces{% 
		xdefnoexpand@fst@rg{##1}futureletnoexpandnextexpandafternoexpandcsname 
			@@#1@endcsname} 
	expandafterxdefcsname @@#1@endcsname{% 
		noexpandifxnoexpandnextunskiprelax 
		noexpandelsenoexpandexpandafterexpandafternoexpandcsname 
		@testminus@#1endcsnamenoexpandfi} 
	expandafterxdefcsname @testminus@#1endcsname{% 
		noexpandifxnoexpandnext-noexpandexpandafterexpandafternoexpand 
			csname @@m@#1endcsnamenoexpandelse 
			noexpandexpandafterexpandafternoexpand 
			csname @@@#1endcsnamenoexpandfi} 
	expandafterxdefcsname @@@#1endcsname##1 {% 
		noexpandifnum1=1##1{}noexpandelse##1 noexpand#2{##1}noexpandfi} 
	expandafterxdefcsname @@m@#1endcsname##1 {##1 noexpand#2{##1}} 
	newcolumntype{#3}[2]{>{csname #1endcsname{##1}}##2} 
} 

Теперь, когда мы проделали грязную работу, можно определить основные команды:

def@SET#1{expandafterxdefcsname cntr@fst@rgendcsname{#1}} 
def@ADD#1{FPeval{res@lt}{cntr@fst@rg+(#1)}@SET{res@lt}} 
def@SUB#1{FPeval{res@lt}{cntr@fst@rg-(#1)}@SET{res@lt}} 
def@MUL#1{FPeval{res@lt}{cntr@fst@rg*(#1)}@SET{res@lt}} 
def@DIV#1{FPeval{res@lt}{cntr@fst@rg/(#1)}@SET{res@lt}} 
@@def@cmd{TAdd}{@ADD}{+} 
@@def@cmd{TSub}{@SUB}{-} 
@@def@cmd{TMul}{@MUL}{*} 
@@def@cmd{TDiv}{@DIV}{/} 
@@def@cmd{TSet}{@SET}{X} 

Теперь у нас есть следующие новые типы столбцов, принимающие два аргумента
(имя переменной и стандартный тип выравнивания ячейки). Типы +,
-, * и / выполняют соответствующие операции между переменной
из первого аргумента типа ячейки и содержащимся в ячейке числом.
Тип X устанавливает переменную в значение, содержащееся в ячейке.
Если переменная должна будет использоваться, не инициализируясь столбцом
X, ее надо до таблицы инициализировать командой Ini.
Содержимое ячейки проверяется: если там не число, то действия не выполняются.

Теперь нам остается объявить еще один тип столбца, который позволит отображать
результаты вычисления (а также при необходимости считать их сумму).

def@@plus#1#2{gdef@fst@rg{#1}@ADD{cntr#2}} 
def@Sho@@#1{ifx@@nonumempty@@plus{#1}{temp@rg}elsegdef@@nonum{}fi} 
def@Sh@{ifxnextbgroupexpandafter@Sho@@elsegdef@@nonum{}fi} 
defSho#1{ifx@@nonumemptyShow{#1}fideftemp@rg{#1}futureletnext@Sh@} 
newcolumntype{S}[3]{>{strut}#3<{Sho{#1}{#2}}} 

Мы создаем новый тип столбца — S, имеющий три параметра: имя отображаемой
переменной, имя переменной-аккумулятора и тип выравнивания. На эту команду
тоже действует NoNum. Команда Sho, реализующая тип S,
может выполняться и сама по себе. В этом случае если нам не нужно накапливать
результаты в какой-то переменной, мы просто даем этой команде только один аргумент.

Ну и напоследок — немного развлечений. Подключим пакет ifthen для реализации
сложных условных инструкций (тех не позволяет вложенных if) и сделаем реализацию
циклов и стека.

Простейший цикл for (без вложенных) можно определить так:

newcounter{f@rc@unter} 
newcommand{forplus}[4][1]{% 
	setcounter{f@rc@unter}{#2}ifthenelse{value{f@rc@unter} < #3}{#4% 
		addtocounter{f@rc@unter}{#1}% 
		forplus[#1]{value{f@rc@unter}}{#3}{#4}}{}} 
newcommand{forminus}[4][-1]{% 
	setcounter{f@rc@unter}{#2}ifthenelse{value{f@rc@unter} > #3}{#4% 
		addtocounter{f@rc@unter}{#1}% 
		forminus[#1]{value{f@rc@unter}}{#3}{#4}}{}} 
defiterator{arabic{f@rc@unter}} 
defLoop#1#2{forplus{0}{#1}{#2}} 

Здесь реализовано четыре команды. forplus позволяет выполнять цикл
с инкрементом от числа, указанного в первом параметре, до числа во втором, выполняя
каждый раз содержимое третьего параметра. Необязательный аргумент макроса — шаг
цикла. Команда forminus реализует цикл с декрементом. iterator
позволяет вывести содержимое итератора цикла, а Loop — выполнить
содержимое второго аргумента N раз (где N — первый аргумент).

Для работы со стеком определим команды push и pop, а также
stacklen (количество элементов в стеке), popall (вывод
всего содержимого стека) и popalldel (вывод содержимого с разделителем).

newcount@buflen@buflen=0 
newtoks@@stack 
@@stack={empty} 
defpush#1{advance@buflen1begingrouptoks0={{#1}}% 
   edefact{endgroupglobal@@stack={thetoks0 the@@stack}}act} 
defpop{ifnum@buflen>0advance@buflen-1fibegingroup% 
   edefact{endgroupnoexpandsplitListthe@@stack(tail)@@stack}act} 
defsplitList#1#2(tail)#3{ifx#1emptyred{Стек пуст!}else{#1}global#3={#2}fi} 
defstacklen{the@buflenxspace} 
defpopalldel#1{ifthenelse{the@buflen > 1}{pop#1popalldel{#1}}% 
	{ifnum@buflen=1popfi}} 
defpopall{popalldel{}} 

Стек реализован через простой список токенов. Помещение в стек просто добавляет к
началу списка новый токен, а выбор из стека разбивает список по первому токену
и возвращает его.

Примеры

Пример работы с таблицами:

Ini[1]{yy} 
Ini{zz} 
begin{table}[!th] 
begin{tabular}{|N{c}|X{xx}{c}|*{xx}{c}|S{xx}{zz}{c}|*{yy}{c}|} 
hline 
bf NumIni No{} п/п & A & B & A$cdot$BNoNum& C \ 
hline 
& -3.5 & 4.4 &&43.3 \ 
& 31.31 &200.21 &&3 \ 
& 1.23 &3.33 &&1.2 \ 
hline 
NoNum&&&NoNum $sum(Acdot B)=,$Show{zz}&$prod C=,$Show{yy} \ 
hline 
end{tabular} 
end{table} 

Вычисления в латехе? Да запросто!

Пример работы с инициализируемыми переменными и вычислениями:

Ini{totalmass} 
Пусть в переменной {tt totalmass} у нас хранится суммарная масса чего-то, массы частей чего мы 
будем вычислять на протяжении всего текста. Все эти элементы будем по мере вычисления к ней 
добавлять. 

Итак, здесь мы вычислим что-то, равное Ins{m1p}{45.1},FPeval{m1}{3*m1p} 
а потом умножим его на три: $m_1=3cdotFPrint{m1p:1}=FPrint{m1:1}$. Добавим это к общей 
массе.Add{totalmass}{m1} Получим: {tt totalmass}$,=Show[1]{totalmass}$. 

Далее, когда нам понадобится определить новую переменную, равную, скажем, 
Ins{moreaddtoa}{3.45}, и добавить ее к массе, опять воспользуемся тем же приемом. 
Получим: {tt totalmass}$,Add{totalmass}{moreaddtoa}=Show[2]{totalmass}$. 

Через некоторое время вычислим еще что-нибудь. Пусть плотность чего-то равна 
$rho=,$Ins{therho}{5.43}, а объем равен $V=,$Ins{theV}{12.44}, тогда его масса 
FPeval{mi}{therho*theV}$m_i=rhocdot V=FPrint{mi:3}$. Добавим и его к общей 
массе.Add{totalmass}{mi} В итоге получим: {tt totalmass}$,=Show[3]{totalmass}$. 

Команды verb'Ini', verb'Show' и verb'FPrint' можно использовать внутри групп. 
Команду verb'Ins' и команды вычислений пакета {tt fp} использовать внутри формул нельзя: 
первую из-за работы с полями, остальные "--- из-за того, что в них не используется команда 
verb'global' и вычисления, выполненные внутри группы, вне группы действия не возымеют. 

Вычисления в латехе? Да запросто!

Пример работы с циклами и стеком:

Пусть катеты треугольника равны a=Ins{a}{2.5}, b=Ins{b}{3}, тогда 
гипотенуза будет равна $c=sqrt{a^2+b^2}=FPrint{root(2, (a^2 + b^2)):2}$ 

Цикл от 1 до 4: forplus{1}{5}{iterator~} 

Цикл от 1 до 10 с шагом 3: forplus[3]{1}{11}{iterator~} 

Цикл от 4 до 1: forminus{4}{0}{iterator~} 

Цикл от 10 до 1 с шагом -3: forminus[-3]{10}{0}{iterator~} 

Loop5{Пять раз! } 

push{первый}push{второй}push{третий}push{последний} 
Содержимое стека через запятую: popalldel{, }. Осталось: stacklen записей. 

Вычисления в латехе? Да запросто!

Текст примера
Получившийся pdf

Автор: Eddy_Em


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


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