Alternative to array.includes() in google apps script? - google-apps-script

I'm using range.getValues() to load an array in Google Apps Script.
var array = SpreadsheetApp.getActive().getActivesheet().getRange('E10:E30').getValues();
> array = [["val1"],["val2"],["val3"],["val4"]]
I then want to loop through a separate list to see whether its elements exist in the array, using array.prototype.includes():
if(array.includes("val4")) {doSomething;}
Using array.prototype.includes() like this does not work for two reasons: 1) since each element in array is an array in itself and 2) because the method isn't available in Google Apps Script:
TypeError: Cannot find function includes in object
Then wanted to use array.prototype.flat() to solve 1) but it seems it isn't available in Google Apps Script either. I get the following error:
TypeError: Cannot find function flat in object
This is something I could do using brute force, but surely there's a neat way of doing it?

EDIT(2019-04-07): Please be advised. With the expected V8 upgrade (ECMAScript 2017) for App Script, the language will natively support Array.prototype.includes and many other modern Javascript features in the near future.
The simplest solution for array.prototype.includes is to use the following polyfill from MDN in your apps script project. Just create a script file and paste the following code - the code/polyfill will add the function directly to the Array prototype object:
// https://tc39.github.io/ecma262/#sec-array.prototype.includes
if (!Array.prototype.includes) {
Object.defineProperty(Array.prototype, 'includes', {
value: function(searchElement, fromIndex) {
if (this == null) {
throw new TypeError('"this" is null or not defined');
}
// 1. Let O be ? ToObject(this value).
var o = Object(this);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// 3. If len is 0, return false.
if (len === 0) {
return false;
}
// 4. Let n be ? ToInteger(fromIndex).
// (If fromIndex is undefined, this step produces the value 0.)
var n = fromIndex | 0;
// 5. If n ≥ 0, then
// a. Let k be n.
// 6. Else n < 0,
// a. Let k be len + n.
// b. If k < 0, let k be 0.
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
function sameValueZero(x, y) {
return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
}
// 7. Repeat, while k < len
while (k < len) {
// a. Let elementK be the result of ? Get(O, ! ToString(k)).
// b. If SameValueZero(searchElement, elementK) is true, return true.
if (sameValueZero(o[k], searchElement)) {
return true;
}
// c. Increase k by 1.
k++;
}
// 8. Return false
return false;
}
});
}
For array.prototype.flat the MDN site also provides alternative solutions. One of which leverages array.prototype.reduce and array.prototype.concat:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat#Alternative

Replace the .includes() with .indexOf()+1 (it will yield 0 if the element is not present, else it will yield an integer between 1 and the length of your array). It works in Google Script.
if(array.indexOf("val4")+1) {doSomething;}

Went with if(array.indexOf("val4") > -1) {doSomething}; as suggested in the comments
I first decided to go with array.filter():
var test = array.filter(element => element == "val4");
if(test != null) {doSomething};
But as noted below, the arrow functions don't work in Google Apps Script
But while looking for an answer I found this to solve 1):
function flatten(arrayOfArrays){
return [].concat.apply([], arrayOfArrays);
}
Definitely better than I would have been able to come up with.

Related

Returning instance numbers, not values, of a dataset that passes a filter function

How do you write a filter function in GAS that, rather than returning values of a dataset that pass the condition, returns the instance numbers of the data that passes the condition?
For example, let's say our condition is divisibility by 10.
(value % 10 = 0)
And our dataset is
[1,5,10,20,7,40]
Items # 0, 1, and 4 fail the condition; items # 2, 3, and 5 pass.
Desired result:
[2,3,5]
EDIT: Cooper's answer below solves the problem as a single instance console command. In my Comments I have parsed his answer as a function that can take the array and condition as variables. For sake of better readability, I'll post that here:
function indicesOfEntriesPassingCondition (array,condition) {
return array.map((value, i) => (condition(value))? i:'').filter(value => value !== '')
}
function testIndicesOfEntriesPassingCondition () {
var condition = value => (value % 10 == 0);
var array = [1,5,10,20,7,40]
var results = indicesOfEntriesPassingCondition (array,condition);
console.log(results)
}
function lfunko() {
Logger.log([1,5,10,20,7,40].map((e,i) => (e % 10 == 0)? i: '').filter(e => e !== '').join(','))
}
Execution log
4:42:39 PM Notice Execution started
4:42:40 PM Info 2,3,5
4:42:40 PM Notice Execution completed
Array.map
Alternate Solution
Try this code using simple JavaScript loop and indexOf:
function indexOfDivisibleByTen(){
var arr = [1,5,10,20,7,40];
var numberIndex = [];
//loop through array to find those divisible by 10
for (i = 0; i < arr.length; i++){
if (arr[i] % 10 ==0){
//enter the index of the value in array instead of the actual value
numberIndex.push(arr.indexOf(arr[i]));
}
}
console.log(numberIndex);
};
Works the same as finding the value in the array but instead of entering the actual value into a new array, you enter the index or the position of the value from the array.
Result:
References:
JavaScript Looping
JavaScript Array indexOf

Doesn´t recognice as equal (==)

Can someone tell me why these variables marked with red are not recognized as equal (==).
Google Apps Script is Javascript-based. In Javascript, you can not compare two arrays using ==.
One method is to loop over both arrays and to check that the values are the same. For example you can include the function:
function compareArrays(array1, array2) {
for (var i = 0; i < array1.length; i++) {
if (array1[i] instanceof Array) {
if (!(array2[i] instanceof Array) || compareArrays(array1[i], array2[i]) == false) {
return false;
}
}
else if (array2[i] != array1[i]) {
return false;
}
}
return true;
}
And then update the line in your code from if (responsables == gestPor) { to if (compareArrays(responsables, gestPor)) {
For other methods of comparing arrays in Javascript, see this answer.
It is because you are comparing arrays. If you are just getting a single cell value, use getValue() instead of getValues()
To make things work, change these:
var gestPor = hojaActivador.getRange(i,13,1,1).getValues();
var responsables = hojaConMails.getRange(1,n,1,1).getValues();
to:
var gestPor = hojaActivador.getRange(i,13).getValue();
var responsables = hojaConMails.getRange(1,n).getValue();
Do these to all getValues() where you're only extracting 1 cell/value.
See difference below:

Q. Check if 2 arrays have the same values in the same order

I have looked at other posts that supposedly have solved this issue, but this method still doesn't work for me.
I had run into an error in a larger program I was writing but I narrowed the error to this method.
I set a cell to =isMatch( {1,2,3} , {1,2,3} ) to verify my method works. The cell computes to False, and I don't know why or how to fix it.
Before I checked stackoverflow, I had originally written code identical to the answer of this post.
Here is the code I currently have.
function isMatch(arr1,arr2){//Returns True if same Array values in same location
if(arr1.length !== arr2.length)
return false;
for(var i =0; i<arr1.length; i++) {
if(arr1[i] !== arr2[i])
return false;
}
return true;
}
You're comparing a 2D array. {1,2,3} === [[1,2,3]] and not [1,2,3].
To compare a n dimensional array, you can recurse:
function isMatch(arr1, arr2) {
if (typeof arr1 !== typeof arr2)
throw new TypeError('Arrays or elements not of same type!');
if (Array.isArray(arr1))
return (
arr1.length === arr2.length && arr1.every((e, i) => isMatch(e, arr2[i]))
);
return arr1 === arr2;
}
console.info(isMatch([[1], [2]], [[1], [2]]));
console.info(isMatch([[1, 2]], [[1, 2]]));
console.info(isMatch([[1, 2]], 1));
2D array
Array#every

How to use a for loop with .createChoice in Google Apps Script to create a quiz from a sheet?

I am using Google Apps Script to generate Google Forms from a Sheet. Questions are in rows and question choices are in columns.
Here is a link to the Google sheet if needed.
It is a straightforward task when using .setChoiceValues(values)
if (questionType == 'CHOICE') {
var choicesForQuestion = [];
for (var j = 4; j < numberColumns; j++)
if (data[i][j] != "")
choicesForQuestion.push(data[i][j]);
form.addMultipleChoiceItem()
.setChoiceValues(choicesForQuestion);
}
However, when I try to use .createChoice(value, isCorrect), the parameters call for value to be a string and isCorrect to be Boolean.
An example without a loop looks like this:
var item = FormApp.getActiveForm().addCheckboxItem();
item.setTitle(data[3][1]);
// Set options and correct answers
item.setChoices([
item.createChoice("chocolate", true),
item.createChoice("vanilla", true),
item.createChoice("rum raisin", false),
item.createChoice("strawberry", true),
item.createChoice("mint", false)
]);
I can not figure out how to add the loop. After reading over other posts, I have tried the following:
if (questionType == 'CHOICE') {
var questionInfo = [];
for (var j = optionsCol; j < maxOptions + 1; j++)
if (data[i][j] != "")
questionInfo.push( form.createChoice(data[i][j], data[i][j + maxOptions]) );
form.addMultipleChoiceItem()
.setChoices(questionInfo);
}
optionsCol is the first column of questions options
maxOptions is how many options are allowed by the sheet (currently 5). The isCorrect information is 5 columns to the right.
However, this not working because the array questionsInfo is empty.
What is the best way to do this?
Probably your issue is related to the method you reference--Form#createChoice--not existing. You need to call MultipleChoiceItem#createChoice, by first creating the item:
/**
* #param {Form} formObj the Google Form Quiz being created
* #param {any[]} data a 1-D array of data for configuring a multiple-choice quiz question
* #param {number} index The index into `data` that specifies the first choice
* #param {number} numChoices The maximum possible number of choices for the new item
*/
function addMCItemToForm_(formObj, data, index, numChoices) {
if (!formObj || !data || !Array.isArray(data)
|| Array.isArray(data[0]) || data.length < (index + 2 * numChoices))
{
console.error({message: "Bad args given", hasForm: !!formObj, info: data,
optionIndex: index, numChoices: numChoices});
throw new Error("Bad arguments given to `addMCItemToForm_` (view on StackDriver)");
}
const title = data[1];
// Shallow-copy the desired half-open interval [index, index + numChoices).
const choices = data.slice(index, index + numChoices);
// Shallow-copy the associated true/false data.
const correctness = data.slice(index + numChoices, index + 2 * numChoices);
const hasAtLeastOneChoice = choices.some(function (c, i) {
return (c && typeof correctness[i] === 'boolean');
});
if (hasAtLeastOneChoice) {
const mc = formObj.addMultipleChoiceItem().setTitle(title);
// Remove empty/unspecified choices.
while (choices[choices.length - 1] === "") {
choices.pop();
}
// Convert to choices for this specific MultipleChoiceItem.
mc.setChoices(choices.map(function (choice, i) {
return mc.createChoice(choice, correctness[i]);
});
} else {
console.warn({message: "Skipped bad mc-item inputs", config: data,
choices: choices, correctness: correctness});
}
}
You would use the above function as described by its JSDoc - pass it a Google Form object instance to create the quiz item in, an array of the details for the question, and the description of the location of choice information within the details array. For example:
function foo() {
const form = FormApp.openById("some id");
const data = SpreadsheetApp.getActive().getSheetByName("Form Initializer")
.getSheetValues(/*row*/, /*col*/, /*numRows*/, /*numCols*/);
data.forEach(function (row) {
var qType = row[0];
...
if (qType === "CHOICE") {
addMCItemToForm_(form, row, optionColumn, numOptions);
} else if (qType === ...
...
}
References
Array#slice
Array#forEach
Array#map
Array#some
I am sure the above answer is very good and works but I am just a beginner and needed a more obvious (plodding) method. I am generating a form from a spreadsheet. Question types can include: short answer (text item), long answer (paragraph), drop down (list item), multiple choice, grid item, and checkbox questions, as well as sections.
I had to be able to randomize the input from the spreadsheet for multiple choice and sort the input for drop downs. I am only allowing one correct answer at this time.
The columns in the question building area of the spreadsheet are: question type, question, is it required, does it have points, hint, correct answer, and unlimited choice columns.
qShtArr: getDataRange of the entire sheet
corrAnsCol: index within the above of the column with the correct answer
begChoiceCol: index within the above of first column with choices
I hope this helps other less skilled coders.
/**
* Build array of choices. One may be identified as correct.
* I have not tried to handle multiple correct answers.
*/
function createChoices(make, qShtArr, r, action) {
// console.log('Begin createChoices - r: ', r);
let retObj = {}, choiceArr = [], corrArr = [], aChoice, numCol, hasCorr;
numCol = qShtArr[r].length - 1; // arrays start at zero
if ((qShtArr[r][corrAnsCol] != '') && (qShtArr[r][corrAnsCol] != null)) {
hasCorr = true;
choiceArr.push([qShtArr[r][corrAnsCol], true]);
for (let c = begChoiceCol ; c < numCol ; c++) {
aChoice = qShtArr[r][c];
if ((aChoice != '') && (aChoice != null)) { /* skip all blank elements */
choiceArr.push([aChoice, false]);
}
} //end for loop for multiple choice options
} else {
hasCorr = false;
for (let c = begChoiceCol ; c < numCol ; c++) {
aChoice = qShtArr[r][c];
if ((aChoice != '') && (aChoice != null)) { /* skip all blank elements */
choiceArr.push(aChoice);
}
} //end for loop for multiple choice options
}
if (action == 'random')
choiceArr = shuffleArrayOrder(choiceArr);
if (action == 'sort')
choiceArr.sort();
console.log('choiceArr: ', JSON.stringify(choiceArr) );
let choices = [], correctArr = [] ;
if (hasCorr) {
for ( let i = 0 ; i < choiceArr.length ; i++ ) {
choices.push(choiceArr[i][0]);
// console.log('choices: ', JSON.stringify(choices) );
correctArr.push(choiceArr[i][1]);
// console.log('correctArr: ', JSON.stringify(correctArr) );
}
make.setChoices(choices.map(function (choice, i) {
return make.createChoice(choice, correctArr[i]);
}));
} else { // no correct answer
if (action == 'columns' ) {
make.setColumns(choiceArr);
} else {
make.setChoices(choiceArr.map(function (choice, i) {
return make.createChoice(choice);
}));
}
}
}

As3 Sorting Alphabetically and Numerically Simultaneously

So I have a sort method designed to sort values alphabetically which works great in almost all cases:
function alphabetical(name1, name2):int {
if (name1 < name2){
return -1;
} else if (name1 > name2){
return 1;
}else {
return 0;
};
};
The problem is though, when a title contains a number in it.
For example:
['abc 8','abc 1','abc 10']
would sort to
['abc 1','abc 10','abc 8']
but what I need to happen is for it to sort alphabetically but when it encounters a number a numeric value is taken into consideration and thus the sorting would return
['abc 1','abc 8'.'abc 10']
I was hoping there was some sort of existing regex or algorithms built to do this but I am afraid I haven't the slightest clue what to search for. All my searches for sorting either does it alphabetically or numerically, not both.
thanks so much!
I found a JavaScript solution that translates to AS3: Sort mixed alpha/numeric array.
The solution would look something like:
var reA = /[^a-zA-Z]/g;
var reN = /[^0-9]/g;
function sortAlphaNum(a,b) {
var aA = a.replace(reA, "");
var bA = b.replace(reA, "");
if(aA === bA) {
var aN = parseInt(a.replace(reN, ""), 10);
var bN = parseInt(b.replace(reN, ""), 10);
return aN === bN ? 0 : aN > bN ? 1 : -1;
} else {
return aA > bA ? 1 : -1;
}
}
var arr = ['abc 8','abc 1','abc 10'];
arr.sort(sortAlphaNum);
trace(arr); // abc 1,abc 8,abc 10