Google sparesheet formula/function to get random values without recalculating(randbetween) - google-apps-script

I am trying to achive the following. In google sparesheet I have one sheet with values "AllValues", in another sheet "Randomvalues" I would like to get random values from sheet "AllValues".
I have tried two options, first I tried randbetween formula:
=INDEX(AllValues!A4:A103,RANDBETWEEN(1,COUNTA(AllValues!A4:A103)),1)
It is working, but it refresh/recalculate new values all the time column is changed. Googeled a lot and seems that there is not much to do to freeze already calculated results.
Next I tried function:
function random() {
var sss = SpreadsheetApp.getActiveSpreadsheet();
var ss = sss.getSheetByName('Values'); //the sheet that has the data
var range = ss.getRange(1,1,ss.getLastRow(), 4); //the range you need: 4 columns on all row which are available
var data = range.getValues();
for(var i = 0; i < data.length; i++)
{
var j = Math.floor(Math.random()*(data[i].length)); //method of randomization
var element = data[i][j]; // The element which is randomizely choose
ss.getRange(i+1, 6).setValue(element);
}
}
But this function is not working for me, google sparesheet gives error on line 11, that setVaue is not allowed.
Line 11: ss.getRange(i+1, 6).setValue(element);
Googled this one too, there are lot of suggestion, but I am not very familiar with functions, I did not managed to get it working.
Hope that someone can help me out.

Using a formula assumes repeated calculations usually. You cannot prevent them and only can try to return old values instead. This task is not trivial, since any formula cannot refer to the same cell where the result is to be returned (a circular reference occurs). Do not use formulas for single time calculation.
On the other hand, using a script function makes it possible to generate required data directly and only once or on demand. I think, the function below will help you to understand all the neccesary steps for sample source and target ranges.
function random() {
var source = "AllValues!A4:A103",
target = "RandomValues!F2:F22";
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourceValues = ss.getRange(source).getValues(),
targetRange = ss.getRange(target),
targetValues = [];
while (targetValues.length < targetRange.getHeight()) {
var randomIndex = Math.floor(Math.random() * sourceValues.length);
targetValues.push(sourceValues[randomIndex]);
}
targetRange.setValues(targetValues);
}
You can run it manually or choose a proper trigger.

There are multiple ways of achieving this goal.
Custom Menu
As mentioned by #Tanaike, you can avoid the recalculation and the formula dependency by using a Custom Menu:
// #OnlyCurrentDoc
// Create a function that binds the "simple trigger" for the open event:
function onOpen(e) {
// Add a menu to the UI with the function we want to be able to invoke.
const ui = SpreadsheetApp.getUi();
ui.createMenu("Randomizer")
.addItem("Sample from 'AllValues' sheet", "sampleAllValues")
.addToUi();
}
You then need a function definition matching this name sampleAllValues, and when the user selects the associated menu option, it will be invoked with the permissions of the clicking user (the user will be prompted first to provide consent for access per the script's OAuth scopes).
function sampleAllValues() {
const wb = SpreadsheetApp.getActive();
const destination = wb.getSheetByName("RandomValues");
const source = wb.getSheetByName("AllValues");
if (!source || !destination)
throw new Error("Missing required sheets 'RandomValues' and 'AllValues'");
// Create a flat array of all non-empty values in all rows and columns of the source sheet.
const data = source.getDataRange().getValues().reduce(function (compiled, row) {
var vals = row.filter(function (val) { return val !== ""; });
if (vals.length)
Array.prototype.push.apply(compiled, vals);
return compiled;
}, []);
// Sample the smaller of 50 elements or 10% of the data, without replacement.
const sample = [];
var sampleSize = Math.min(50, Math.floor(data.length * .1));
while (sampleSize-- > 0)
{
var choice = Math.floor(Math.random() * data.length);
Array.prototype.push.apply(sample, data.splice(choice, 1));
}
// If we have any samples collected, write them to the destination sheet.
if (sample.length)
{
destination.getDataRange().clearContent();
// Write a 2D column array.
destination.getRange(1, 1, sample.length, 1)
.setValues(sample.map(function (element) { return [ element ]; }));
// Write a 2D row array
// destination.getRange(1, 1, 1, sample.length)
// .setValues( [sample] );
}
}
Custom Function
If you still wanted to use a custom function from the RandomValues sheet, e.g.
RandomValues!A1: =sampleAllValues(50, AllValues!A1:A)
then you would need to return sample instead of write to a specific sheet. Note that custom functions are treated deterministically--they are computed at the time of entry and then only recalculated when the values of their arguments change. Custom functions run with very limited scope, so be sure to review their restrictions.
The above usage hints that you might find it useful to allow passing in the number of desired samples, and the values to sample from:
function sampleAllValues(sampleSize, value2Darray) {
const data = value2Darray.reduce(function (compiled, row) {
/* as above */
}, []);
/* sample as above */
return sample; // Must be 2D row or 2D column array, or a single primitive e.g. `1`
}
No matter which route you take, be sure to review your script's error logging by viewing your script's Stackdriver logs. (View -> Stackdriver Logging)
References:
Sheet#getRange
Custom functions
Custom menus
Array#reduce
Array#map
Array#splice
.push.apply

Related

Absolute reference with UDF & filtering data in google sheets script

Hi I am begineer to scripts, here is the code and googlesheet for reference
What i am getting
what i want to achieve
/**
*#param quest1 Question of the note
*#param quest1 Answer of the note
*#customfunction*/
function DMNOTE(quest1,ans1,quest2,ans2,quest3,ans3,) {
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var result = quest1+'-'+ans1+','+quest2+'-'+ans2+','+quest3+'-'+ans3;
return result;
}
I want to achieve absolute reference for "quest" parameter and i want it to loop for rest of the coulmns till column where i enter the function.Also under "Formula Required" column i have put formla for reference thats how i want my UDF to work.
Follwing up i need to filter "Non-solicit agreement" and keep only "No" under it and Copy & Paste all colums highlited in blue to update tab.
function toFilter (){
// filter and retain "no" in non-solicit agreement
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Worksheet");
ss.getRange(1,1,ss.getLastRow(),ss.getLastColumn());
var createfilter = SpreadsheetApp.newFilterCriteria().setHiddenValues("Yes").build();
ss.getFilter().setColumnFilterCriteria(8, createfilter);
}
Hope i make sense. Any help is appriciated
To work with any number of arguments, you can have a function like below in your script
function DMNOTE(...arg){
let result = ''
for(let i=0;i<arg.length;i++){
result += `${arg[i]}-${arg[i+1]},`
i++;
}
return result.substring(0, result.length - 1);
}
And then form your spreadsheet you can call as =DMNOTE(A$1,A2,B$1,B2,C$1,C2) or =DMNOTE(A$1,A2,B$1,B2,C$1,C2,D$1,D2) The function will process all the arguments being passed and returns the result.
How to filter "non-solicit agreement" and take only data which has "no"
The code you provided comes already very close to what you desire, you just need to make the following adjustments:
setHiddenValues() expect an array, no a string, so change "Yes" to ["Yes"]
getFilter() only works to modify a filter that is present in the sheet already, in case there is none, it is better to delete the old filter and create a new one:
function toFilter(){
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Worksheet");
ss.getRange(1,1,ss.getLastRow(),ss.getLastColumn());
var createfilter = SpreadsheetApp.newFilterCriteria().setHiddenValues(["Yes"]).build();
if(ss.getFilter() != null) {
ss.getFilter().remove();
}
ss.getDataRange().createFilter().setColumnFilterCriteria(8, createfilter);
}
How to Copy & Paste all columns highlited in blue to update tab
Build a function that
calls toFilter()
checks for all rows either they are hidden
copies the non- hidden rows into an array
pastes this array to the sheet Update
function copyIfNotHidden(){
toFilter();
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet1 = spreadsheet.getSheetByName("Worksheet");
var sheet2 = spreadsheet.getSheetByName("Update");
var data = sheet1.getDataRange().getValues();
var array = [];
for (var i = 0; i < data.length; i++){
if(sheet1.isRowHiddenByFilter(i+1)==false){
array.push(data[i]);
}
}
sheet2.getRange(sheet2.getLastRow()+1, 1, array.length, array[0].length).setValues(array);
}

How to Sort Sheets Cells by values applied in GAS?

Column A of my Sheet has status values [Yes,Pending,No,Withdrawn] in the form of a dropdown. I would like to sort by these values but the order they will be sorted in is not alphabetical.
I currently use a helper column with IFS formulas in the sheet that applies a numeric value to each status and sorts them in that order. My assumption is that I can use a script to accomplish this without needing a helper column in my sheet.
From other somewhat similar questions I've gathered that this may use a compare function but my knowledge of GAS is fairly introductory.
I think using the helper column is definitely the simplest option. You can "hide" the column so it isn't in the way. (Right click -> Hide column).
But you can definitely accomplish this with a script, using a compare function! Here is an example implementation. It's a little overly verbose, in hopes of being explanatory and adaptable to your exact use case.
Code.gs
// get active spreadsheet
var ss = SpreadsheetApp.getActive();
// define mapping of status to custom values
var mapping = {
Yes: 1,
No: 2,
Pending: 3,
Withdrawn: 4
};
// define range of values to sort & which one is "status"
var sortRange = "A2:B20";
var statusCol = 0;
/**
* Sort defined range by status, using defined mapping
* See: https://developers.google.com/apps-script/reference/spreadsheet/spreadsheet
*/
function sortData() {
// select sheet
var sheet = ss.getSheets()[0];
// select range
var range = sheet.getRange(sortRange);
// get values (array of arrays)
var data = range.getValues();
Logger.log("\ndata pre-sort: %s\n\n", data);
// sort using custom compare function
data.sort(sortFcn_);
Logger.log("\ndata post-sort: %s\n\n", data);
// write values back to spreadsheet
range.setValues(data);
}
/**
* Custom compare function used by sortRange
* See: https://www.w3schools.com/jsref/jsref_sort.asp
*/
function sortFcn_(rowA, rowB) {
// get "status" from row (array lookup by integer)
var aStatus = rowA[statusCol];
var bStatus = rowB[statusCol];
// convert status msg to value (object lookup by key)
var aValue = mapping[aStatus];
var bValue = mapping[bStatus];
// sort in ascending order
return aValue - bValue;
}
Now you just need to figure out how to call "sortData" at the right times. For a few options, see:
Container-bound Scripts
Simple Triggers
Custom Menus

Trying to cross-check two ranges in Google Sheets with a script

I am currently trying to write a script to compare two ranges in Google Sheets. One is a list of emails to contact in (sht1), and the other is a list of emails on a blacklist in (sht2). I want to automate a script that will compare each email in the contact list in sht1, with ALL of the emails in the blacklist of sht2. If the value in sht1 at point [i] matches any of the values in the blacklist in sht2[j], from sht2[0] to sht2[length] it will automatically cause the row in sht1 to delete at [i].
However, the script is not working. I keep getting the error "range out of bounds" I have tried changing the conditions of i from if(var i=0;i
Any help would be great appreciated.
function DeleteDupes()
{
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sht1=ss.getSheetByName('Email List All');
var dd = SpreadsheetApp.openByUrl('noneofyourbusiness')
var sht2=dd.getSheetByName('Sheet1');
var rng1=sht1.getRange('A:A');
var rng2=sht2.getRange('A:A');
var rng1A=rng1.getValues();
var rng2A=rng2.getValues();
for(var i=rng1A.length;i>=0;i--)
{
for(var j=0;j<=rng2A.length;j++)
{
if(rng1A[i]==rng2A[j]);
sht1.deleteRow(i-1);
break;
}
}
}
Thank you
function DeleteDupes(){
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sht1=ss.getSheetByName('Email List All');
var dd = SpreadsheetApp.openByUrl('noneofyourbusiness')
var sht2=dd.getSheetByName('Sheet1');
var rng1=sht1.getRange('A:A');
var rng2=sht2.getRange('A:A');
// Code changes begin here:
var rng1A=formatRangeValues(rng1);
var rng2A=formatRangeValues(rng2);
// Keep only the emails in `rng1A` that are not in `rng2A`
var filteredEmails = rng1A.filter(function(email){
return this.indexOf(email) === -1;
}, rng2A);
// Remove all emails in `rng1` in preparation of inserting filtered list
rng1.clearContent();
// Protect against an empty `filteredEmails` array
if(filteredEmails.length === 0)
return;
// Convert `filteredEmails` back to a 2D Range
// and insert back into Column A
sht1
.getRange(1, 1, filteredEmails.length)
.setValues(
filteredEmails.map(function(e){
return [e] ;
})
);
}
// Filter out blank elements.
// reduce{concat} flattens the array
function formatRangeValues(range){
return range
.getValues()
.filter(String)
.reduce(function(a,b){
return a.concat(b)
}, []);
}
This will remove any instances of an email on sht1 that are on sht2. It works by bringing the sht1 Range into memory and converting it to an array of values. It then filters the values and inserts them back into sht1.
There are two major advantages of doing the data manipulation in JavaScript instead of on the spreadsheet. First, it is going to be faster. Reads and writes to the spreadsheet document are expensive. This way we can batch them instead of doing them individually. Second, it is safer as it only manipulates the cells in question. We do not need to worry about side effects (such as deleting a row when other columns may someday be used).
Deleting Items on a BlackList
Try this:
function delItemsOnBlacklist() {
var ss=SpreadsheetApp.getActive();
var sh1=ss.getSheetByName('Contacts');
var sh2=ss.getSheetByName('BlackList')
var rg1=sh1.getRange(1,1,sh1.getLastRow(),1);
var rg2=sh2.getRange(1,1,sh2.getLastRow(),1);
var vA=rg1.getValues();
var vB=rg2.getValues();
var vBlackList=[];
var d=0;
for(var i=0;i<vB.length;i++){
vBlackList.push(vB[i][0]);
}
for(var i=0;i<vA.length;i++)
{
if(vBlackList.indexOf(vA[i][0])>-1){
sh1.deleteRow(i-d+1);
d++;
}
}
}
I changed your file names and range descriptions a bit so make sure you make the appropriate changes. I prefer to put the blacklist into an array. It just seems neater to me. If you keep track of the deleted items as in "d" and add that to the deleteRow then you can increment in the loop which I think were more use to doing.

Deleting sheets not found in lookup

I am trying to delete unneeded sheets from a template once relevant information has been copied across. To do this I am looking up the sheet name with a check list. If the lookup returns a value of 0.0 then I want to delete the sheet.
function myFunction() {
var studentsheet = SpreadsheetApp.openById('1Qj9T002nF6SbJRq-iINL2NisU7Ld0kSrQUkPEa6l31Q').;
var sheetsCount = studentsheet.getNumSheets();
var sheets = studentsheet.getSheets();
for (var i = 0; i < sheetsCount; i++){
var sheet = sheets[i];
var sheetName = sheet.getName();
Logger.log(sheetName);
var index = match(sheetName);
Logger.log(index);
if (index = "0.0"){
var ss = studentsheet.getSheetByName(sheetName).activate();
ss.deleteactivesheet();
}
else {}
}
function match(subject) {
var sourcesheet = SpreadsheetApp.openById('14o3ZG9gQt9RL0iti5xJllifzNiLuNxWDwTRyo-x9STI').getSheetByName("Sheet6").activate();
var lookupvalue = subject;
var lookuprange = sourcesheet.getRange(2, 2, 14, 1).getValues().map(function(d){ return d[0] });
var index = lookuprange.indexOf(subject)+1;
return index;
}
};
The problem is at the end when trying to delete the sheet. I have amended the code so it selects the sheet and makes it active but in the next line I am not allowed to call .deleteactivesheet(). Does anyone know how I can write this end part where I can select the sheet based on the index score being 0 and then delete it?
To delete a Sheet from a Spreadsheet, there are two applicable Spreadsheet class methods (as always, spelling and capitalization matter in JavaScript):
Spreadsheet#deleteSheet, which requires a Sheet object as its argument
Spreadsheet#deleteActiveSheet, which takes no arguments
The former is suitable for any type of script, and any type of trigger, while the latter only makes sense from a bound script working from a UI-based invocation (either an edit/change trigger, menu click, or other manual execution), because "activating" a sheet is a nonsense operation for a Spreadsheet resource that is not open in a UI with an attached Apps Script instance.
The minimum necessary modification is thus:
var index = match(sheet);
if (index === 0) { // if Array#indexOf returned -1 (not found), `match` returned -1 + 1 --> 0
studentsheet.deleteSheet(sheet);
}
A more pertinent modification would be something like:
function deleteNotFoundSheets() {
const studentWb = SpreadsheetApp.openById("some id");
const lookupSource = getLookupRange_(); // assumes the range to search doesn't depend on the sheets that may be deleted.
studentWb.getSheets().filter(function (s) {
return canDelete_(lookupSource, s.getName());
}).forEach(function (sheetToDelete) {
studentWb.deleteSheet(sheetToDelete);
});
}
function getLookupRange_() {
const source = SpreadsheetApp.openById("some other id");
const sheet = source.getSheetByName("some existing sheet name");
const r = sheet.getRange(...);
return r.getValues().map(...);
}
function canDelete_(lookupRange, subject) {
/** your code that returns true if the subject (the sheet name) should be deleted */
}
This modification uses available Array class methods to simplify the logic of your code (by removing iterators whose only purpose is to iterate, and instead expose the contained values to the anonymous callback functions). Basically, this code is very easily understood as "of all the sheets, we want these ones (the filter), and we want to do the same thing to them (the forEach)"
Additional Reading:
JavaScript comparison operators and this (among others) SO question
Array#filter
Array#forEach
If just like me you have been struggling to find a working example of #tehhowch above solution, here it is:
function deleteSheetsWithCriteria() {
let ss = SpreadsheetApp.getActiveSpreadsheet(),
sheetList = ss.getSheetByName('List'),
list = sheetList.getRange('A1:A'+sheetList.getLastRow()),
lookupRange = list.getValues().map(function(d){ return d[0] }) + ',List'; // List of sheets NOT to delete + lookuprange sheet
Logger.log(lookupRange)
//Delete all sheets except lookupRange
ss.getSheets().filter(function (sheet) {
return deleteCriteria_(lookupRange, sheet.getName());
}).forEach(function (sheetToDelete) {
ss.deleteSheet(sheetToDelete);
});
}
function deleteCriteria_(lookupRange, sheet) {
var index = lookupRange.indexOf(sheet);
Logger.log(index)
if (index > 0) {0} else {return index}; // change line 19 to 'return index;' only, if you want to delete the sheets in the lookupRange, rember to remove the lookupRange in variable LookupRage
}

Cannot find method getRange(number,(class),number)

I have read all answers about this error, but none of them work for me. So maybe someone has another idea.
I'm trying to pass parameter to function getRange but when I'm trying to invoke this function it shows me error
Cannot find method getRange(number,(class),number)
Here is my code:
function conditionalCheck(color,rangeCondition, rangeSum, criteria){
var condition = SpreadsheetApp.getActiveSheet().getRange(2,rangeCondition,15).getValues();
var val = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(2,rangeSum,15).getValues();
var bg = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(2,rangeSum,15).getBackgrounds();
var sum = 0;
for(var i=0;i<val.length;i++){
if(condition[i][0] == criteria && bg[i][0] != color){
sum += val[i][0];
}
}
return sum;
}
And I pass a custom function like:
conditionalCheck("#ffff00",1,3,A3)
This is how the sheet looks like:
I understand that JS trying to guess the type of parameters, that is why it thinks that ex. "rangeCondition" is a class, but I don't know how to give him a Number type.
Funny thing is, that this function works when I open the spreadsheet, but when I'm trying to invoke this function while I'm working it shows me this error. So to update sheet I have to reopen the whole spreadsheet.
I was able to reproduce the error by executing the function directly from the editor.
This behavior makes sense, considering custom function needs to receive a valid parameter. At runtime, your 'rangeSum' parameter is set to 'undefined', which invalidates the getRange() method as it requires a number.
It's actually quite strange that you got your 'for' loop working. In most cases, using the the '+' operator on array values obtained from the sheet will concatenate them into a string, so you'll get '5413' instead of 13 (5 + 4 + 1 + 3)
function calculateSum(column) {
column = column||1; //set the default column value to 1
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange(1, column, sheet.getLastRow());
var values = range.getValues();
var sum = 0;
for (var i=0; i < values.length; i++) {
sum += parseInt(values[i]);
}
return sum;
}
Finally, this approach is not very efficient in terms of performance. Once you get the instance of the object, you should use that instance instead of attacking the server with multiple calls like you do in the first 3 lines of your code. For example
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var range = sheet.getRange(1, 1, sheet.getLastRow(), 1);
If you plan on using this custom function in multiple places within the sheet, consider replacing it with the script that processes the entire range - this way you will only perform a single read and write operation to update all values. Otherwise, you may end up filling up your service call quotas very quickly
UPDATE
Instead of creating custom function, create the function that takes the entire range and processes all data in one call. You can test the 'calculateSum()' function above with predefined column number, e.g.
var column = 1 // the column where you get the values from
Upon getting the 'sum' value, write it into the target range
var targetRange = sheet.getRange("B1"); // the cell that you write the sum to
targetRange.setValue(sum);
Finally, you can make that function execute after you open or edit the sheet by appending the following code
function onOpen() {
var ui = SpreadsheetApp.getUi();
var menu = ui.createMenu('Menu')
.addItem('Calculate sum', 'calculateSum')
.addToUi();
calculateSum();
}
function onEdit() {
calculateSum();
}
Calling it from the custom menu would be simple - you just need to create the handler and pass function name as parameter.
I found a solution for a problem with reloading custom function after changing data.
The solution is described here:
Refresh data retrieved by a custom function in google spreadsheet
It showed that we cannot refresh custom function if we don't change inputted parameters because it is cached. So if we change inputted data, a function will reload. We can force a function if we put there timestamp