Те, кому приходится верстать тексты посложнее служебок, знают, что латех —
удобная надстройка над техом — на данный момент является чуть ли не единственным
средством, позволяющим быстро верстать качественные тексты любой сложности.
Но не текстом единым… Латех можно использовать и для других целей, о которых
создатель Теха, Дональд Кнут, наверное и не думал.
Я уже писал, как можно верстать в латехе презентации. Теперь же я хочу рассказать
о том, как можно проводить несложные вычисления непосредственно силами латеха.
Для латеха разными людьми написана уйма всевозможных пакетов, расширяющих его
функционал и облегчающих работу верстальщику. Не забыты и средства, помогающие
работать с численными данными. Несложные вычисления с использованием чисел
с плавающей запятой (одинарной точности) можно выполнять непосредственно
силами теха. Для этого и предназначен пакет 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