Running scripts in Google Sheets doesn't work exactly like Excel, which can be both good and bad. Coming from an Excel VBA background, there is a mindset shift you have to make.
- When writing a user defined function (UDF) in Excel, you can pass
Range parameters like this: =MyUDF(A1,C3). The function signature will give you two Range objects as parameters. In Google Sheets, this isn't the case. ALL range parameters are converted to values (or an array of values). This means that in a Sheets cell if you type =MyUDF(A1,C3 your script code will get the value of A1 and the value of C3. This is not what you want.
Based on this answer, your best option is to parse the original cell formula and extract the range parameters. Then you can create a Range object for your UDF. This is a roundabout set of logic, but it is consistent with how Sheets implements the underlying functions and scripts. The following function will pick up the formula from the active cell and parse it to give you the expected range object.
NOTE: you'll have to modify this function to pick out your UDF call if it's inside a compound formula. For example, it works if your
formula is =MyUDF(A1,C3). But is DOES NOT work if your formula is
=If(A2='Yes','',MyUDF(A1,C3)). You can modify the function to detect
this, but it's not provided here.
function GetParamRange(paramNumber) {
var activeSheet = SpreadsheetApp.getActiveSheet();
var formula = SpreadsheetApp.getActiveRange().getFormula();
//--- expecting two parameters, split here and check
const MAXARGS = 2;
var args = formula.match(/=\w+\((.*)\)/i)[1].split(',');
if (args.length != MAXARGS) {
throw new Error('in GetParamRange: ' + args.join(',') + '- invalid number of arguments, expecting ' + MAXARGS);
}
if ((paramNumber < 1) || (paramNumber > MAXARGS)) {
throw new Error('in GetParamRange: invalid parameter requested, must be ' + MAXARGS + ' or less');
}
//--- is the worksheet specified?
var paramRange;
var parts = args[paramNumber-1].split('!');
if (parts.length == 2) {
//--- use worksheet and cell to get the range
Logger.log('activeSheet = ' + activeSheet.getName());
ss = SpreadsheetApp.getActive().getSheetByName(parts[0].replace(/\'/g, ''));
paramRange = ss.getRange(parts[1]);
}
else {
//--- get the range from the active sheet
paramRange = activeSheet.getRange(parts[0]);
}
return(paramRange);
}
- Before I list the actual function, there is an important aspect to coding UDFs you must understand. In Excel, you can explicitly tag a UDF as
Volatile. In Google Sheets this is not possible. Honestly, I haven't worked with Google Sheet enough to learn all the details so you'll have to search around.
- The user defined function itself accepts two parameters, but those parameters are actually created by calling
GetParamRange. From there, it's a simple loop to add your running sum (with an added safety limit).
It will throw an error if the safety limit is exceeded.
/**
* ParentChildCalc1
* @customfunction ParentChildCalc1
* @param {string} critCell Critical Cell
* @param {number} valCell Value Cell
* @returns {number} ParentChildCalc1
*/
function ParentChildCalc1(critCell, valCell) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var critCell = GetParamRange(1);
var valCell = GetParamRange(2);
const safetyNet = 5000;
var runningSum = 0;
var numYes = 0;
for (var i = 0; !valCell.offset(i,0).isBlank(); i++) {
if (critCell.offset(i,0).getValue() == 'Yes') {
numYes++;
if (numYes == 2) break;
if (i == safetyNet) {
throw new Error('exceeded safety net limit in ParentChildCalc1, searching ' + safetyNet + ' rows.');
}
}
runningSum = runningSum + valCell.offset(i,0).getValue();
}
return (runningSum);
}
Here's the whole code in a single block:
/**
* ParentChildCalc1
* @customfunction ParentChildCalc1
* @param {string} critCell Critical Cell
* @param {number} valCell Value Cell
* @returns {number} ParentChildCalc1
*/
function ParentChildCalc1(critCell, valCell) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var critCell = GetParamRange(1);
var valCell = GetParamRange(2);
const safetyNet = 5000;
var runningSum = 0;
var numYes = 0;
for (var i = 0; !valCell.offset(i,0).isBlank(); i++) {
if (critCell.offset(i,0).getValue() == 'Yes') {
numYes++;
if (numYes == 2) break;
if (i == safetyNet) {
throw new Error('exceeded safety net limit in ParentChildCalc1, searching ' + safetyNet + ' rows.');
}
}
runningSum = runningSum + valCell.offset(i,0).getValue();
}
return (runningSum);
}
function GetParamRange(paramNumber) {
var activeSheet = SpreadsheetApp.getActiveSheet();
var formula = SpreadsheetApp.getActiveRange().getFormula();
//--- expecting two parameters, split here and check
const MAXARGS = 2;
var args = formula.match(/=\w+\((.*)\)/i)[1].split(',');
if (args.length != MAXARGS) {
throw new Error('in GetParamRange: ' + args.join(',') + '- invalid number of arguments, expecting ' + MAXARGS);
}
if ((paramNumber < 1) || (paramNumber > MAXARGS)) {
throw new Error('in GetParamRange: invalid parameter requested, must be ' + MAXARGS + ' or less');
}
//--- is the worksheet specified?
var paramRange;
var parts = args[paramNumber-1].split('!');
if (parts.length == 2) {
//--- use worksheet and cell to get the range
Logger.log('activeSheet = ' + activeSheet.getName());
ss = SpreadsheetApp.getActive().getSheetByName(parts[0].replace(/\'/g, ''));
paramRange = ss.getRange(parts[1]);
}
else {
//--- get the range from the active sheet
paramRange = activeSheet.getRange(parts[0]);
}
return(paramRange);
}
So, to use this function just put the formula =ParentChildCalc1(D7,C7) in cell E7.