
У меня есть файл, содержащий список, например такой:
apple
orange
pear
pineapple
Как создать макрос, который проверяет, есть ли элемент в этом списке. Например:
\isfruit{pear}
возвращает «1».\isfruit{carrot}
возвращает «0».
решение1
Если файл не слишком длинный, можно обойтись
\documentclass{article}
\makeatletter
\newread\@readisfruit
\newcommand\isfruit[3][]{%
\begingroup\endlinechar=\m@ne
\openin\@readisfruit=#2
\def\@tempa{#3}%
\def\@result{0}%
\loop\unless\ifeof\@readisfruit
\read\@readisfruit to \@tempb
\ifx\@tempa\@tempb\def\@result{1}\fi
\repeat
\closein\@readisfruit
\edef\x{\endgroup\if!\noexpand#1!\@result\else\edef\noexpand#1{\@result}\fi}\x}
\makeatother
\begin{document}
\isfruit{village.dat}{pear}
\isfruit[\result]{village.dat}{carrot}\show\result
\end{document}
Однако его нельзя расширить, поэтому я предоставил необязательный аргумент: имя управляющей последовательности, где следует сохранить результат.
Альтернативное определение
Как замечает Ахмед Муса, если файл длинный, его чтение может занять очень много времени, особенно если тест выполняется много раз. Мы можем использоватьфайл захвата:
\usepackage{catchfile}
\CatchFileDef{\village}{village.dat}{\endlinechar=`| }
\makeatletter
\newcommand{\isfruitA}[3][]{%
\ifcsname loaded@#2\endcsname\else
\expandafter\CatchFileDef\csname loaded@#2\endcsname{#2}{\endlinechar=`| }%
\fi
\begingroup\expandafter\let\expandafter\@tempa\csname loaded@#2\endcsname
\edef\x{\endgroup\noexpand\in@{\unexpanded{#3}|}{\unexpanded\expandafter{\@tempa}}}\x
\ifin@ \def\@result{1}\else \def\@result{0}\fi
\if!\noexpand#1!\@result\else\edef#1{\@result}\fi}
\makeatother
Теперь \isfruitA{village.dat}{pear}
выведет 1 (после определения макроса, расширяющегося до содержимого village.dat
, со строками, разделенными |
, которые, как мы предполагаем, не появляются в строках). Если мы вызовем
\isfruitA[\result]{village.dat}{orange}
результат теста (0 или 1) будет помещен в макрос \result
. Файл будет прочитан только один раз. Некоторые пакеты, такие какxstringsможно использовать, чтобы избежать громоздкого теста с \ifin@
.
решение2
Я попробую ответить на этот вопрос. Может быть, кто-то сможет улучшить мой ответ.
Я создал макрос \isfruit
таким образом, что макрос читает файл во время того, как enlinechar
установлено значение -1
. Каждая строка будет прочитана и сохранена в списке. После этого я сравниваю список с аргументом.
\documentclass{article}
\usepackage{filecontents}
\begin{filecontents*}{fruit.tex}
apple
orange
pear
pineapple
\end{filecontents*}
\usepackage{etoolbox}
\newread\InputFruit
\newcommand*\isfruit[2]{%
\begingroup%
\def\MyList{}
\openin\InputFruit=#1
\endlinechar=-1%
\loop\unless\ifeof\InputFruit
\read\InputFruit to \reserveda
\listxadd\MyList{\reserveda}
\repeat
\closein\InputFruit
\xifinlist{#2}{\MyList}{in list}{not in list}
\endgroup%
}%
\begin{document}
\isfruit{fruit.tex}{apple}
\isfruit{fruit.tex}{foo}
\end{document}
решение3
@Marco Daniel: В вашем макросе есть пробелы, которые не аннулируются внутренним \endlinechar=-1
. Они будут отображаться в документе в горизонтальном режиме. Более того, назначение \endlinechar=-1%
должно было быть \endlinechar=-1 %
. Кроме того, я вынесу ветвление за \xifinlist
пределы \isfruit
команды, чтобы дать пользователю \isfruit
возможность изменить два обратных вызова. И ваш разделитель списка (используемый по умолчанию в \xifinlist
) может присутствовать в исходном списке для тестирования.
\documentclass{article}
\usepackage{filecontents}
\begin{filecontents*}{fruit.tex}
apple
orange
pear
pineapple
\end{filecontents*}
\begingroup
\catcode`\|=3
\endlinechar=-1
\makeatletter
\gdef\ifinfruitlist#1#2{%
\begingroup
\endlinechar=-1
\def\MyList{}
\openin\@inputcheck=#1 %
\loop\unless\ifeof\@inputcheck
\read\@inputcheck to \reserveda
\edef\MyList{\ifx\MyList\@empty\else
\unexpanded\expandafter{\MyList}|
\fi\unexpanded\expandafter{\reserveda}}
\repeat
\closein\@inputcheck
\@expandtwoargs\in@{|#2|}{|\unexpanded\expandafter{\MyList}|}
\expandafter\endgroup
\ifin@\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
}
\endgroup
\begin{document}
\ifinfruitlist{fruit.tex}{apple}{in list}{not in list}
\ifinfruitlist{fruit.tex}{foo}{in list}{not in list}
\end{document
Вышеуказанное решение неприемлемо неэффективно. Если список состоит из десяти страниц, а интересующий нас токен — первый в списке, нам придется сначала прочитать весь документ, прежде чем проверять наличие тестовой строки. Вот более эффективная реализация:
\documentclass{article}
\begin{filecontents*}{fruit.tex}
apple
orange
pear
pineapple
\end{filecontents*}
\makeatletter
\gdef\ifinfruitlist#1#2{%
\begingroup
\def\reserved@b{#2}%
\endlinechar=-1 %
\openin\@inputcheck=#1 %
\@tempswafalse\@testfalse
\def\do{%
\if@tempswa
\closein\@inputcheck
\else
\ifeof\@inputcheck
\@tempswatrue
\else
\read\@inputcheck to\reserved@a
\ifx\reserved@a\reserved@b
\@testtrue\@tempswatrue
\fi
\fi
\expandafter\do
\fi
}%
\do
\expandafter\endgroup
\if@test\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi
}
\makeatother
\begin{document}
\ifinfruitlist{fruit.tex}{pear}{in list}{not in list}
\ifinfruitlist{fruit.tex}{foo}{in list}{not in list}
\end{document}
решение4
LuaTeX идеально подходит для такой задачи. Ниже представлено решение на основе luatex в ConTeXt.
\startbuffer[fruits]
apple
orange
pear
pineapple
\stopbuffer
%% Save the contents of fruits buffer in \jobname-fruits.tmp
\savebuffer[fruits][fruits]
\startluacode
local data = {}
local find = string.find
function commands.doiffruitelse(file, fruit)
if not data[file] then
data[file] = io.loaddata(file) or ""
end
return commands.testcase(find(data[file], "%f[%a]"..fruit.."%f[%A]") ~= nil)
end
\stopluacode
\def\doiffruitelse#1%
{\ctxcommand{doiffruitelse("\jobname-fruits.tmp", "#1")}}
\def\isFruit#1%
{\doiffruitelse{#1}{1}{0}}
\starttext
\startlines
\isFruit{pear}
\isFruit{carrot}
\stoplines
\stoptext
Это использует io:loaddata()
функцию из l-io.lua
для загрузки содержимого файла, commands
таблицу для разделения пространства имен команд lua и commands.testcase
функцию для предоставления функциональности do-if-else. Фактическое соответствие выполняется с помощью string.find
функции. Я используюПограничный узордля соответствия границам слов.