How to link 2 lengths
This is a similar approach that I used in my xassoccnt
package for counters: associating a length to a 'master' length, say '\foo'.
If \foo
is assigned a new value with \setlength
, all lengths associated with it will get the same value.
I defined \addtolength
etc, as well as removal and synchronization.
The starred version of \setlength
and \addtolength
will manipulate the master length only!
However, a direct manipulation of \foo
will not work for \baz
etc.
\documentclass{article}
\usepackage{xparse}
\makeatletter
\let\latex@setlength\setlength
\let\latex@addtolength\addtolength
\newcommand{\stripslash}[1]{%
\expandafter\@gobble\string#1
}
\ExplSyntaxOn
\cs_new:Nn \strongbad_associate_lengths:nn {%
\seq_set_from_clist:cn {g_strongbad_#1_lengths_seq} {#2} % Populate unexpanded
\seq_remove_duplicates:c {g_strongbad_#1_lengths_seq}
\seq_remove_all:cn {g_strongbad_#1_lengths_seq} {#1}% Prevent self - association!
\seq_map_inline:cn {g_strongbad_#1_lengths_seq} {
\dim_if_exist:NF { ##1 } {% Preventing complaining about already existing length variables
\newlength{##1}
}
}
}
\NewDocumentCommand{\RemoveAssociatedLengths}{mm}{%
\seq_if_exist:cT { g_strongbad_ \stripslash{#1} _lengths_seq } {
\seq_set_from_clist:Nn \l_tmpa_seq {#2}
\seq_map_inline:Nn \l_tmpa_seq {
\seq_remove_all:cn { g_strongbad_ \stripslash{#1} _lengths_seq } {##1}
}
}
}
\NewDocumentCommand{\DeclareAssociatedLength}{mm}{%
\strongbad_add_associated_length:nn {#1}{#2}
}
\cs_new:Nn \strongbad_add_associated_length:nn {%
\seq_if_exist:cF { g_strongbad_\stripslash{#1}_lengths_seq} {
\seq_new:c {g_strongbad_\stripslash{#1}_lengths_seq}
}
\strongbad_associate_lengths:nn{\stripslash{#1}}{#2}
}
\NewDocumentCommand{\AddAssociatedLengths}{mm}{%
\strongbad_associate_lengths:nn{\stripslash{#1}}{#2}
}
\RenewDocumentCommand{\addtolength}{smm}{%
\IfBooleanF{#1}{% No starred command
\seq_if_exist:cT { g_strongbad_ \stripslash{#2} _lengths_seq } {
\seq_map_inline:cn { g_strongbad_ \stripslash{#2} _lengths_seq } {
\latex@addtolength{##1}{#3}
}% End of \seq_map_inline
}% End of \seq_if_exist
}% End of \IfBooleanF
\latex@addtolength{#2}{#3}
}
\NewDocumentCommand{\synclengths}{m}{%
\seq_if_exist:cT { g_strongbad_ \stripslash{#1} _lengths_seq } {
\dim_set:Nn \l_tmpa_dim {\the#1}
\setlength{#1}{\l_tmpa_dim}
}
}
\NewDocumentCommand{\syncaddtolength}{mm}{%
\addtolength*{#1}{#2}%
\synclengths{#1}%
}
\RenewDocumentCommand{\setlength}{smm}{%
\IfBooleanF{#1}{% No starred command
\seq_if_exist:cT { g_strongbad_ \stripslash{#2} _lengths_seq } {
\seq_map_inline:cn { g_strongbad_ \stripslash{#2} _lengths_seq } {
\latex@setlength{##1}{#3}
}% End of \seq_map_inline
}% End of \seq_if_exist
}% End of \IfBooleanF
\latex@setlength{#2}{#3}
}
\makeatother
\ExplSyntaxOff
\begin{document}
\newlength{\foo}
\newlength{\boz}
\DeclareAssociatedLength{\foo}{\baz,\buz,\boz,\biz}
\setlength{\foo}{100pt}
foo: \the\foo
baz: \the\baz
buz: \the\buz
boz: \the\boz
biz: \the\biz
\setlength{\baz}{\dimexpr200pt+\foo}
Foo again: \the\foo
Baz now: \the\baz
buz: \the\buz
boz: \the\boz
biz: \the\biz
Now using the starred version:
\setlength*{\foo}{5000pt}
Foo after starred version: \the\foo
Baz after starred version: \the\baz
Adding some value:
\addtolength{\foo}{100pt}
Foo after adding some value: \the\foo
Baz after adding some value to foo: \the\baz
Adding some value with synchronization first:
\syncaddtolength{\foo}{100pt}
Foo after adding some value: \the\foo
Baz after adding some value to foo: \the\baz
Buz after adding some value to foo: \the\buz
Removing buz and biz:
\RemoveAssociatedLengths{\foo}{\buz,\biz}
\addtolength{\foo}{-1000pt}
New foo: \the\foo
New biz: \the\biz
New buz: \the\buz
\end{document}
\baz
expands to \foo
, which is a length, and therefore 2\baz
works in the second length setting. However, letting \def\baz{400pt}
doesn't make \baz
a length anymore. Yes, technically it expands to a valid length, but it can't be used as-is in a length product. You'll have to explicitly use \dimexpr
, otherwise the values are concatenated rather than multiplied:
\documentclass{article}
\let\bar\relax% Just for this example
\newlength{\foo}% \foo is a length
\newlength{\bar}\setlength{\bar}{\foo}% \bar is a length
\newlength{\qux}% \qux is a length
\def\baz{\foo}% \baz expands to \foo, which is a length
\begin{document}
\setlength{\foo}{100pt}\bigskip
foo: \the\foo\par bar: \the\bar\par baz: \the\baz\par
\setlength{\foo}{2\baz}\bigskip
foo: \the\foo\par bar: \the\bar\par baz: \the\baz\par
\def\baz{400pt}% \baz is no longer a length
\setlength{\qux}{2\dimexpr\baz}\bigskip
foo: \the\foo\par bar: \the\bar\par baz: \the\dimexpr\baz\par
qux: \the\qux\par
\end{document}
On the somewhat sly side, you could
\def\baz{\dimexpr400pt}
You can create linked lengths, but you need different commands (or to override the standard ones). The syntax is similar to \newcounter
.
\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\NewLength}{mo}
{
\newlength{#1}
\seq_new:c { g_strongbad_length_ \cs_to_str:N #1 _seq }
\IfValueT{#2}
{
\seq_gput_right:cn { g_strongbad_length_ \cs_to_str:N #2 _seq } { #1 }
}
}
\NewDocumentCommand{\SetLength}{mm}
{
\setlength{#1}{#2}
\seq_map_inline:cn { g_strongbad_length_ \cs_to_str:N #1 _seq }
{
\setlength{##1}{#2}
}
}
\ExplSyntaxOff
\NewLength{\foo}
\NewLength{\baz}[\foo]
\NewLength{\qux}
\begin{document}
\SetLength{\foo}{100pt}\bigskip
foo: \the\foo\par baz: \the\baz\par
\SetLength{\foo}{2\baz}\bigskip
foo: \the\foo\par baz: \the\baz\par
\SetLength{\baz}{10pt}\bigskip
foo: \the\foo\par baz: \the\baz\par
\SetLength\baz{400pt}
\SetLength{\qux}{2\baz}\bigskip
qux: \the\qux\par
\end{document}
Every defined length has an associated list of lengths, which are set to the same value as the master length as soon as this is modified (with \SetLength
). I leave to you as an exercise to implement \AddToLength
.