6

I am using the siunitx pacakge and trying to detect if a unit is defined. My initial assumption was that this would simply be:

\ifcsdef{#1}{#1 exists.}{#1 does NOT exist.}

This works fine if the unit declaration is not deferred until \AtBeginDocument. So, with the units defined as

\DeclareSIUnit\milliliter{\textnormal{mL}}%       <--- Can detect this just fine
\AtBeginDocument{%
    \DeclareSIUnit\centiliter{\textnormal{cL}}%   <--- But fails with this?
}%

detecting \milliliter is fine but detecting \centiliter is not so easy. The MWE below yields:

enter image description here

Notes:

Code:

\documentclass{article}
\usepackage{etoolbox}
\usepackage{siunitx}

\DeclareSIUnit\milliliter{\textnormal{mL}}% <--- Can detect this just fine

\AtBeginDocument{% \DeclareSIUnit\centiliter{\textnormal{cL}}% <--- But fails with this? }

\newcommand*{\TestIfUnitExists}[1]{% \par \ifcsdef{#1liter}{% #1liter exists. }{% #1liter does NOT exist. }% }%

\begin{document} \TestIfUnitExists{giga}%
\TestIfUnitExists{milli}% <--- milliliter exists \TestIfUnitExists{centi}% <--- centiliter also exists (but test fails) \TestIfUnitExists{}% <--- liter exists as well \end{document}

Peter Grill
  • 223,288
  • What I don't get is why you need to know if they are defined: your preamble should express exactly which units are available. – Joseph Wright Nov 11 '16 at 09:54
  • @JosephWright: Yes the preamble does express what is avaialble but I have code which typesets text based on an SI prefix and not all SI prefixes are applicable. – Peter Grill Nov 11 '16 at 15:55

3 Answers3

5

The reason for this different behavior seem to be how siunitx handles new units that aren't called in the scope of one of the \si/\SI commands. If we make LaTeX \show the definitions immediately after the beginning of the document, we get

> \milliliter=\protected\long macro:
->\ERROR .
l.27 \show\milliliter

> \centiliter=undefined.
l.28 \show\centiliter

So \milliliter is defined to an undefined control sequence, while \centiliter is actually undefined, strangely. This additional layer of indirection isn't recognized by your test.

A simple solution is to call your test in the context of the \si command when all the new units are in scope:

\newcommand*{\TestIfUnitExists}[1]{%
    \par
    \si{%
        \ifcsdef{#1liter}{%
            #1liter exists.
        }{%
            #1liter does NOT exist.
        }%
    }%
}%

enter image description here

siracusa
  • 13,411
  • Great explanation, but for me, the \si macro removes the spaces between the words. Accepting egreg's solution as I think it is better to set a global flag within the \si macro. – Peter Grill Nov 11 '16 at 15:59
3

The defined units are tracked internally by siunitx in a sequence. this is not public but as your use case seems somewhat odd anyway I guess you might do

\ExplSyntaxOn
\NewDocumentCommand \TestIfUnitExists { m }
  {
    \seq_if_in:NxTF \l__siunitx_declare_list_seq { \exp_not:c { #1 liter } }
      { #1liter~exists. }
      { #1liter~does NOT exist. }
  }
\ExplSyntaxOff
Joseph Wright
  • 259,911
  • 34
  • 706
  • 1,036
  • I'll look at the lack of definition in global scope for \centiliter in the question: something not quite right in terms of timing. However, one should not rely on the names being defined in global scope: this is only done as it's required by the s-type column, and to be honest with hindsight that was not a great plan. (If/when I finish v3 I'll likely not make that available as-standard.) – Joseph Wright Nov 11 '16 at 09:34
3

This is how the test ought to be defined:

\documentclass{article}
\usepackage{siunitx}

\DeclareSIUnit\milliliter{\textnormal{mL}}

\AtBeginDocument{%
    \DeclareSIUnit\centiliter{\textnormal{cL}}%
}

\ExplSyntaxOn
\NewDocumentCommand{\testifunitexistsTF}{smmm}
 {
  \IfBooleanTF{#1}
   { \grill_test_siunit:cTF {#2}{#3}{#4} }
   { \grill_test_siunit:NTF {#2}{#3}{#4} }
 }
\cs_new_protected:Nn \grill_test_siunit:NTF
 {
  \seq_if_in:NnTF \l__siunitx_declare_list_seq { #1 } { #2 } { #3 }
 }
\cs_generate_variant:Nn \grill_test_siunit:NTF { c }

\ExplSyntaxOff

\newcommand{\test}[1]{%
  \testifunitexistsTF*{#1liter}{#1liter exists}{#1liter doesn't exist}%
}

\begin{document}

\section{General command}

\texttt{\string\millimeter} \testifunitexistsTF{\milliliter}{exists}{doesn't exist}

\texttt{\string\millimeter} \testifunitexistsTF*{milliliter}{exists}{doesn't exist}

\section{Test}

\test{giga}%

\test{milli}% <--- milliliter exists

\test{centi}% <--- centiliter also exists (but test fails)

\test{}%      <--- liter exists as well

\end{document}

enter image description here

Unfortunately, the sequence variable where all the units are stored is not public, so this cannot be really used. Please, file a feature request to Joseph Wright for making the list of units searchable.

Here's a different implementation, based on another answer, but not typesetting anything in the scope of \si. Differently from the previous solution, the macro \testifunitexists only accepts a name as argument, not a control sequence.

\documentclass{article}
\usepackage{etoolbox}
\usepackage{siunitx}

\DeclareSIUnit\milliliter{\textnormal{mL}}

\AtBeginDocument{%
  \DeclareSIUnit\centiliter{\textnormal{cL}}%
}

\newtoggle{unitexists}

\newcommand{\testifunitexistsTF}[1]{%
  \global\togglefalse{unitexists}%
  \sbox0{\si{%
    \ifcsdef{#1}{\global\toggletrue{unitexists}}{}%
  }}%
  \iftoggle{unitexists}%
}

\newcommand{\test}[1]{%
  \testifunitexistsTF{#1liter}{#1liter exists}{#1liter doesn't exist}%
}

\begin{document}

\section{General command}

\texttt{\string\millimeter} \testifunitexistsTF{milliliter}{exists}{doesn't exist}

\section{Test}

\test{giga}%

\test{milli}% <--- milliliter exists

\test{centi}% <--- centiliter also exists (but test fails)

\test{}%      <--- liter exists as well

\end{document}
egreg
  • 1,121,712