7

How can I return a random number within a range? Here is a minimal (non-)working example of what I'm after:

\documentclass{article}

\newcommand{\thecmd}[1]{

% todo, set a to: #1-(#1/4)
% todo, set b to: #1+(#1/4)

\pgfmathrandominteger{\thenum}{\a}{\b}
\thenum

}

\begin{document}

\thecmd{10} % eg, this should return 1 number between 7.5-12.5, ideally rounded

\end{document}

The range should be calculated automatically as (#1-#1/4,#1+#1/4).

6
  • \usepackage{xparse}\ExplSyntaxOn\NewExpandableDocumentCommand \randomint { m m } { \int_random:nn { #1 } { #2 } }\ExplSyntaxOff
    – Skillmon
    Dec 30 '19 at 23:23
  • Thanks @skillmon, to clarify, will that code snippet automatically calculate ‘a’ and ‘b’ as described in the MWE or will I have to feed the range to the command? I’d rather give it one number and calculate the range. Dec 30 '19 at 23:29
  • 1
    @Justapigeon Considering the command has 2 parameters, you're going to have to feed the range. What do you mean by "give it one number and calculate the range"?
    – Alenanno
    Dec 30 '19 at 23:35
  • 1
    Please provide more information. E.g., should the random numbers be integer-valued, or should they be double-precision floating point numbers? Should the random numbers be uniformly distributed over their range?
    – Mico
    Dec 30 '19 at 23:45
  • 1
    I have never heard of \pdfrandominteger but if you meant \pgfrandominteger you were almost there. \documentclass{article} \usepackage{pgf} \newcommand{\thecmd}[1]{% \pgfmathrandominteger{\thenum}{round(#1-#1/4)}{round(#1+#1/4)}% \thenum} \begin{document} \thecmd{10} \thecmd{10} \end{document}.
    – user194703
    Dec 31 '19 at 1:51
5

It could be done with pgf/tikz package

\documentclass{article}

\usepackage{pgf} 

\pgfmathsetseed{\number\pdfrandomseed} % to ensure that it is randomized
% use \randomseed for xelatex

\newcommand{\thecmd}[1]{% 
\pgfmathsetmacro{\a}{int(#1-#1/4)}%
\pgfmathsetmacro{\b}{int(#1+#1/4)}% 
\pgfmathsetmacro{\thenum}{int(random(\a,\b))}%
\thenum%
}%

\begin{document}

\thecmd{10}

\end{document}

EDIT Despite the fact that it was accepted, I'll improve it with the help of the comments from @Schrödinger'scat

int() truncate the number. This lead to error. With the above code, I'll get a number between $\text{int}(10-\frac{10}{4})=\text{int}(7.5)=7$ and $\text{int}(10+\frac{10}{4})=\text{int}(12.5)=12$. So, a number between $7$ and $12$, but $7$ is not between $7.5$ and $12.5$, so shouldn't be a possibility. An improvement to the code could be:

\documentclass{article}

\usepackage{pgf} 

\pgfmathsetseed{\number\pdfrandomseed} % to ensure that it is randomized
% use \randomseed for xelatex

\newcommand{\thecmd}[1]{% 
\pgfmathsetmacro{\thenum}{int(random(ceil(#1-#1/4),floor(#1+#1/4)))}%
\thenum%
}%

\begin{document}

\thecmd{10}

\end{document}

Since ceil() round the number up and floor() round the number down, the previous code yield a number between $\text{ceil}(7.5)=8$ and $\text{floor}(12.5)=12$

2
  • 2
    When compiled with pdflatex, this throws an error. What is more, this defines some macros \a and \b that are global in your example. You do not need any of \a and \b, you can directly add this to the parser, \newcommand{\thecmd}[1]{% \pgfmathsetmacro{\thenum}{int(random(int(#1-#1/4),int(#1+#1/4)))}% \thenum% }%, see my comment above. Also, int just truncates the number, which is why I think round is better.
    – user194703
    Dec 31 '19 at 6:24
  • The solution with round throws: ! Missing number, treated as zero <to be read again> with pdflatex Dec 31 '19 at 18:20
9

You can use \fp_eval:n and rand() from expl3. According to interface3.pdf, rand() produces a pseudo-random floating-point number (actually, a multiple of 10-16) between 0 included and 1 excluded.

First interpretation of the question (\randA)

With a little affine transformation, we can transform the [0, 1) range into what you seem to want: [3/4*(#1), 5/4*(#1)) [see note 1]. Then we can use round() in order to round the intermediate result to the nearest integer. The final result is an integer in the [8, 12] range when #1 is 10.

Here, I intentionally used rand() instead of randint() because of the comment from your example, “this should return 1 number between 7.5-12.5, ideally rounded”. Indeed, adapting the method to, for instance, the range [7.2, 10.1], wouldn't give the same results as it would for, e.g., the range [7.4, 10.1]: the same integer values from range [7, 10] could be obtained in both cases, but not with the same frequencies. It seems to me this is what you want, given the comment in your example.

Note that this algorithm doesn't yield all possible integer values with the same probability (but see the \randB variant below). For instance, if \randA is called with the argument 12, the computed float range is [9, 15) because 3(/4)*12 = 9 and (5/4)*12 = 15, and the random uniform choice happens in this range. Let's call y the result of this random uniform choice. The final result (full expansion of \randA{12}) is:

  • 9 if y is in [9, 9.5)

  • 10 if y is in [9.5, 10.5]

  • 11 if y is in (10.5, 11.5)

  • 12 if y is in [11.5, 12.5]

  • 13 if y is in (12.5, 13.5)

  • 14 if y is in [13.5, 14.5]

  • 15 if y is in (14.5, 15).

All these intervals have length 1 except the first and last, which have length 0.5. Therefore, if we neglect the fact that the number of possible return values from rand() is finite and that some of the interval ends are open and others are closed, \randa{12} returns one of 10, 11, 12, 13, 14 with a probability of 1/6 and one of 9, 15 with a probability of 1/12. If you would rather obtain each of the values 9, 10, 11, 12, 13, 14, 15 with the same probability, you can use \randB instead, which we'll present now.

Second interpretation of the question (\randB)

\randB is a variant with different semantics. \randB{x} computes the same floating point interval [(3/4)*x, (5/4)*x), but its result (after full expansion) is a uniformly-chosen random integer between the ceil() of the interval's lower bound and the floor() of its upper bound. This implies that \randB{12} can expand to each of the integers 9, 10, 11, 12, 13, 14, 15 with the same probability, namely 1/7. This will be shown in the benchmark below.

The code

\documentclass{article}
\usepackage{xparse}

\ExplSyntaxOn

\NewExpandableDocumentCommand \randA { m }
  {
    \fp_eval:n { round( 0.5*(#1)*(rand() + 1.5) ) }
  }

\NewExpandableDocumentCommand \randB { m }
  {
    \fp_eval:n { randint( ceil(0.75*(#1)), floor(1.25*(#1)) ) }
  }

\ExplSyntaxOff

\begin{document}

\randA{10}
\randB{10}

\end{document}

Due to the randomness, the output will vary when you recompile this document.

A benchmark comparing both functions

\documentclass{article}
\usepackage{xparse}
\usepackage{xfp}
\usepackage{booktabs}
\usepackage{floatrow}
\usepackage{siunitx}
\sisetup{round-mode = places, round-precision=4}

\ExplSyntaxOn

\NewExpandableDocumentCommand \randA { m }
  {
    \fp_eval:n { round( 0.5*(#1)*(rand() + 1.5) ) }
  }

\NewExpandableDocumentCommand \randB { m }
  {
    \fp_eval:n { randint( ceil(0.75*(#1)), floor(1.25*(#1)) ) }
  }

% Code for the benchmark
\prop_new:N \l_my_prop          % nb of occurrences for each obtained result
\int_new:N \l_my_value_int
\int_new:N \l_my_count_int
\seq_new:N \l_my_values_seq
\tl_new:N \l_my_tabular_data_tl

\cs_generate_variant:Nn \prop_put:Nnn { NVx }

\cs_new_protected:Npn \my_benchmark:nn #1#2
  {
    \prop_clear:N \l_my_prop
    \int_step_inline:nn {#1}
      {
        \int_set:Nn \l_my_value_int {#2}

        \prop_if_in:NVTF \l_my_prop \l_my_value_int
          {
            \prop_get:NVN \l_my_prop \l_my_value_int \l_tmpa_tl
            \int_set:Nn \l_my_count_int { 1 + \l_tmpa_tl }
            \prop_put:NVx \l_my_prop \l_my_value_int
              { \int_use:N \l_my_count_int }
          }
          { \prop_put:NVn \l_my_prop \l_my_value_int { 1 } }
      }
  }

\NewDocumentCommand \runAndDisplayBenchmark { m m }
  {
    \my_benchmark:nn {#1} {#2}
    \seq_clear:N \l_my_values_seq

    \prop_map_inline:Nn \l_my_prop
      { \seq_put_right:Nn \l_my_values_seq { {##1} {##2} } }

    \seq_sort:Nn \l_my_values_seq
      {
        \int_compare:nNnTF { \use_i:nn ##1 } > { \use_i:nn ##2 }
          { \sort_return_swapped: }
          { \sort_return_same: }
      }

    \tl_clear:N \l_my_tabular_tl
    \seq_map_inline:Nn \l_my_values_seq
      {
        \tl_put_right:Nn \l_my_tabular_data_tl
          { \use_i:nn ##1 & \fp_eval:n { \use_ii:nn ##1 / (#1) } \\ }
      }

    \begin{tabular}{rS}
      \toprule
      { Value } & { Frequency } \\ \midrule
      \tl_use:N \l_my_tabular_data_tl
      \bottomrule
    \end{tabular}
  }

\ExplSyntaxOff

\newcommand{\numberOfTests}{10000}

\begin{document}

\begin{table}
  \floatsetup{rowfill=yes, captionskip=8pt}
  \begin{floatrow}[2]
    \hfil
    \ttabbox
      {\runAndDisplayBenchmark{\numberOfTests}{\randA{12}}}
      {\caption{\texttt{\string\randA}}}%
    \hfil
    \ttabbox
      {\runAndDisplayBenchmark{\numberOfTests}{\randB{12}}}
      {\caption{\texttt{\string\randB}}}%
    \hfil
  \end{floatrow}
\end{table}

\section*{\texttt{\string\randA}}

As explained earlier in the answer, the probabilities with \verb|\randA{12}|
are:
\begin{itemize}
\item $1/6 \approx \num{\fpeval{1/6}}$ for values 10, 11, 12, 13, and 14;
\item $1/12 \approx \num{\fpeval{1/12}}$ for values 9 and 15.
\end{itemize}

\section*{\texttt{\string\randB}}

If all possible values are chosen with the same probability, which should be
the case with \verb|\randB|, then the probability of each possible value
obtained with \verb|\randB{12}| is $1/7 \approx \num{\fpeval{1/7}}$.

\end{document}

Here is a result I obtained. Due to the randomness, you are of course likely to obtain slightly different results if you run this benchmark yourself.

Benchmark results


Footnote

  1. Parentheses denote open ends of an interval. For instance, [1,2] contains all floating point numbers x with 1 ≤ x ≤ 2, whereas [1,2) is the same set with number 2 removed, i.e.: all floating point numbers x with 1 ≤ x < 2.
6
  • In my test, randint (properly used) gives a more uniform distribution than your method with rand.
    – egreg
    Dec 31 '19 at 22:04
  • @egreg I'm not sure what you are testing, precisely. My goal was to uniformly pick a random floating point number in the [3*(#1)/4, 5*(#1)/4)) range and return the closest integer (with ties rounded to the closest even number). I believe the provided code does that, doesn't it?
    – frougon
    Dec 31 '19 at 22:14
  • if you use \thecmd{12}, the least value (that is, 9) is chosen half of the times with respect to the others, in the average. Not by chance: it happens whenever the argument is divisible by 4.
    – egreg
    Dec 31 '19 at 22:17
  • @egreg Unless I'm mistaken, I'd rather say that in this case, the probabilities to obtain values 9, 10, 11, 12, 13, 14 and 15 are 1/6 for each value except for 9 and 15, which have probability 1/12 each (disregarding the discrete nature of the range of rand()'s output). This is a feature, i.e., what I wanted to achieve. Otherwise, why would the question have mentioned 7.5-12.5? It would have been much clearer to say that a random integer uniformly chosen between 8 and 12, both inclusive, is desired. The question was imprecise, I treated it the way that seemed most likely/interesting to me.
    – frougon
    Dec 31 '19 at 22:26
  • @egreg I can add \fp_eval:n { randint( ceil(0.75*(#1)), floor(1.25*(#1)) ) } as a variant having the semantics you seem to desire, but as the OP doesn't seem to care, it is not very motivating. Happy new year!
    – frougon
    Dec 31 '19 at 23:20
6

My comment turned into an answer. Note that there was a typo in the name, it should have been \int_rand:nn. It will evaluate the expressions as pure integer expressions (so no floats allowed as input):

\documentclass[]{article}

\usepackage{xparse}
\ExplSyntaxOn
\NewExpandableDocumentCommand \randomint { m m }
  { \int_rand:nn { #1 } { #2 } }
\ExplSyntaxOff

\begin{document}
\randomint{8-8/4}{8+8/4}
\end{document}

So using just one argument (still with integer expression, as floats are already covered by @frougon) you could use:

\documentclass[]{article}

\usepackage{xparse}
\ExplSyntaxOn
\NewExpandableDocumentCommand \randomint { m }
  { \int_rand:nn { #1 - #1/4 } { #1 + #1/4 } }
\ExplSyntaxOff

\begin{document}
\randomint{8}
\end{document}
1

Possibly not very satisfying for you, but lcg package can be used as follows:

\documentclass{article}
\usepackage[]{lcg}
\newcommand{\thecmd}[2]{
\chgrand[first=#1]
\reinitrand[last=#2]
\rand\arabic{rand}
}
\begin{document}

\thecmd{7}{12}

\end{document}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.