This is a general framework that can be used to define any production level function.
I am going to use the ThrowFailure
mechanism and the SetUsage
function so I need GeneralUtilities`
Needs["GeneralUtilities`"]
First thing: documentation!
SetUsage[movingAverage,
"
movingAverage[vec$, n$] computes the moving average of the numerical vector vec$ \
by taking the mean of subsets of length n$.
"
];
The function may need options, we define them here and sort them properly
Options[movingAverage] = {
} // SortBy[ToString @* First];
Some internal options that we may not want to expose can go here
movingAverageOptions = {
} // SortBy[ToString @* First];
The main entry point for our function. This will do basic argument parsing and counting (no validation) and return either the result or unevaluated depending on the input or the execution. The key piece here is Arguments
: it returns a list with two sub-list (or whatever you specify in the optional third argument), one for the arguments and one for the options. Arguments are counted and the proper message is issued, options are verified against Options[] and an optional list in the fourth argument.
If some arguments are expected to be rules, use ArgumentsWithRules
.
movingAverage[args___] :=
Module[{a, res},
a = System`Private`Arguments[movingAverage[args], 2, List, movingAverageOptions];
res /; a =!= {} && !FailureQ[res = imovingAverage @@ a]
]
Now the main code for the function. I tend to define a getOption
utility not to have to write that longish piece of code over and over.
imovingAverage[args_, opts_] :=
CatchFailureAsMessage[movingAverage,
Module[
{getOption, vec, n},
getOption[name_] := OptionValue[{movingAverage, movingAverageOptions}, opts, name];
n = argumentTest["PositiveInteger", "sizeinv"][args[[2]]];
vec = argumentTest[{"NumericList", n}, "numinv"][args[[1]]];
Mean[Transpose[Partition[vec, n, 1]]]
]]
Argument validation can be done in countless ways, here's a sub-values based one I just came up with. It can be saved in a separate file (Common.wl, Test.wl, ...) to be used in many places. If the tests are standard one can probably avoid passing the message name.
argumentTest["PositiveInteger", message_] :=
Function[
If[Internal`PositiveIntegerQ[#],
#,
ThrowFailure[message, #]
]
]
argumentTest[{"NumericList", n_Integer}, message_] :=
Function[
If[VectorQ[#, Internal`RealValuedNumericQ] && Length[#] >= n,
#,
ThrowFailure[message, #, n]
]
]
And now the messages. For these kind of standard tests, one can define General
messages that can be used with any head.
movingAverage::numinv = "Expecting a list of numerical quantities of length greater of equal then `2` instead of `1`.";
movingAverage::sizeinv = "Expecting a positive integer instead of `1`.";