Problem with array( function used inside a custom function

I have a not-particularly-efficient function, NumberRange, which takes two parameters: a pre-sorted text array of integers, and the array separator. It contracts any consecutive sequences between x to y to the form xy (separated by an en rule, U+2013), removes any leading zeros and passes the resulting array through commalist(:

functionvalue commalist(regexreplace(arrayfilter(parameter(1),parameter(2),“?(val(import())=val(array(parameter(1),seq()-1,parameter(2)))+1 and val(import())=val(array(parameter(1),seq()+1,parameter(2)))-1,"–",val(import()))”),"("+regexliteral(parameter(2))+"–)+"+regexliteral(parameter(2)),"–"),parameter(2))

This works fine when defined as a procedure, NumberRange, within a database:

call("","NumberRange","01-03-04-06-07-08-09-10-11-13-14-15-17-19-20","-")

correctly returns the result:

1, 3, 4, 6–11, 13–15, 17, 19 and 20

However, when I substitute •1 and •2 for parameter(1) and parameter(2) and define it as a custom function, NUMBERRANGE:

registercustomfunction "NUMBERRANGE",2,{commalist(regexreplace(arrayfilter(•1,•2,“?(val(import())=val(array(•1,seq()-1,•2))+1 and val(import())=val(array(•1,seq()+1,•2))-1,"–",val(import()))”),"("+regexliteral(•2)+"–)+"+regexliteral(•2),"–"),•2)}

when I try to use it the result is the error: array( function: ARRAY parameter must be text or binary.

Attempting to track down the problem, if I substitute literal string constants for •1 in each occurrence of the array( function, that error goes away but instead I get array( function: SEPARATOR parameter must be text. Substituting a further literal string for •2 in the two array( functions then makes that error go away. So it appears for some reason that parameter substitution isn’t working properly for the two string parameters for the array( function, when it works fine with every other function I’ve tried. Very odd. Or am I missing something blindingly obvious?

After further thought . . .

Note that the parameter substitution within the arrayfilter( and regexreplace( functions works as it should. Unlike them, the array( functions here are within the arrayfilter( formula, so that must be the problem. If the way parameter substitution works in custom functions precludes using them inside an arrayfilter( formula that’s a pity, because arrayfilter( lends itself especially to one-line functions. But I suppose the way round it would be to use the first function above as a custom statement, then create a custom function to call( that statement, as I have seen done in several instances elsewhere. A pity though, when a one-line function definition is more elegant and efficient. Who doesn’t like a one-liner?

Panorama 10.2.0.b14 (3643)
MacOS 10.12.6

(I should add this takes advantage of Panorama’s unusual but useful feature of returning an empty string when accessing a numbered array element outside the bounds of the array!)

Custom function parameters like •1, •2 etc can only be used as a complete operand. From the point of view of the registercustomfunction statement, the formula specified in the arrayfilter is just a text constant. – in other words the whole thing is an operand. The components of the formula inside that text constant are completely invisible to the registercustomfunction statement. So you cannot use •1, *2 etc. inside that formula.

When you use the parameter( functions, that works completely differently, it’s not a substitution. When the formula is evaluated (once per array element) the parameters for the subroutine are valid, sot that works.

I think there might be a way to get what you want using the cache( function. You could use this at the start of the custom function to set up formula local variables for your values. Combine this with the ignore( function so that the output from your formula is unaffected. Once the variable values are set up you should be able to use them in your arrayfilter. I haven’t tried this so I won’t absolutely promise that it will work, but I think it probably will work. If not, then I can’t think of any other way to do it.

Thanks. In that case a combination of custom statement and custom formula looks the best option, I think:

Custom statement numberrange_f:

functionvalue commaampersandlist(regexreplace(arrayfilter(parameter(1),parameter(2),“?(val(import())=val(array(parameter(1),seq()-1,parameter(2)))+1 and val(import())=val(array(parameter(1),seq()+1,parameter(2)))-1,"–",import())”),"("+regexliteral(parameter(2))+"–)+"+regexliteral(parameter(2)),"–"),parameter(2))

and corresponding custom function NUMBERRANGE:

registercustomfunction "NUMBERRANGE",2,{call("Peter Newble's Custom Statements","numberrange_f",•1,•2)}

That works.

Don’t forget that now there is a callwithin( function. That would allow you to tuck your function code inside some other procedure. I’ve used that technique frequently, especially when there is a custom statement and a custom function that do the same thing. Or in your case, you could put it into the same procedure that defines the custom functions. (Or just leave it as is – it works, so why change it?)

Yes, good idea. If as much as possible is kept inside the .InitializeFunctions procedure, it makes it easier to keep track of the custom functions.

The only thing that caught me out at first was that I have been accustomed to calling

callwithin("","","FunctionName",parameters)

for subroutines appended to the end of a procedure, whereas in this case both the user’s custom-functions database and the .InitializeFunctions procedure must be named explicitly, otherwise somewhat unintuitive errors occur when using the custom functions.

Is this really the desired output? 3 and 4 are obviously consecutive values and I would expect they be separated by a dash as well or a " and " as in the 19 and 20 at the end.

For my initial purposes I actually wanted a list such as 1, 3, 4, 6 to retain pairs of consecutive numbers as discrete items. However, later yesterday I thought the same as you about the general case, and amended it to:

functionvalue commalist(regexreplace(arrayfilter(parameter(1),parameter(2),“?(seq()=1,import(),val(import())=val(array(parameter(1),seq()-1,parameter(2)))+1,?(val(import())=val(array(parameter(1),seq()+1,parameter(2)))-1,"–","–"+parameter(2)+import()),import())”),"("+regexliteral(parameter(2))+"–)+"+regexliteral(parameter(2)),"–"),parameter(2))

which changes the result of the previous example to:

1, 3–4, 6–11, 13–15, 17 and 19–20

Using your example (“01-03-04-06-07-08-09-10-11-13-14-15-17-19-20”) for the starting array I was able to generate this output using only a formula that could be easily converted to a custom function:

1, 3–4, 6–11, 13–15, 17 and 19–20

Here is the formula I put together using the variable myArray for the starting array and theSep for the separator used in that array:

commalist(arrayfilter(replace(arrayfilter(myArray,theSep,{?(val(import())=
val(array(myArray,seq()-1,theSep))+1,import()[“≠0”,-1],“,”+import()[“≠0”,-1])}),
theSep+“,”,“, “),”, “,{?(arraysize(import(),theSep)=1,import(),yoke(arrayfirst(import(),
theSep),”–”,arraylast(import(),theSep)))}),", ")

I don’t know if you can use this or not, but it was a nice little challenge to start the week.