How to get a random number within a range?

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.

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}

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$