Locating all cell's positions in google sheets - javascript

The problem is: I have big spreadsheet (more than 4500 rows) with a lot of data in the first column - for ex. with types of fruits, which are not unique, like this:
APPLE
BANANA
APRICOTS
APPLE
BLACKCURRANT
APPLE
BANANA
APRICOTS
etc.
What I need - locate each BANANA, to be able to put in cell beside some info, for ex. YES. I tried to loop solution from Locating a cell's position in google sheets using a string or an integer but for sure my code is wrong. I already spent a lot of hours to invent something, but still don't understand what I'm missing.
function test(){
var dispatch = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("FRUITS");
var find = dispatch.getRange("A:A").getValues();
var name = "BANANA";
var lastRow = dispatch.getLastRow();
var n = 1;
var temp = dispatch.getRange(n, 2).getValue();
var i = 0;
while (temp != ""){
for(var n in find){
if(find[n][0] === name){break}
}
n++;
var n = n + i;
dispatch.getRange(n, 2).setValue("YES");
var temp = dispatch.getRange(n, 2).getValues();
var find = dispatch.getRange(n, 2, lastRow).getValues();
var i = n;
}
}
I will be very grateful for the help.

The code example is below:
function test(){
var dispatch = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("FRUITS");
var range = dispatch.getRange(1, 1, dispatch.getLastRow(), 2);
var values = range.getValues();
values.map(function(row) {
if (row[0] == "BANANA")
row[1] = "YES";
});
range.setValues(values);
}
JS array map() method does the most part of work. We convert range values to JS array and back after mapping completes.

Related

How do i shorten time this GAS code(search correct column and data paste)

https://i.stack.imgur.com/7VAJk.png
i want to copy data from "dB" sheet A5:A29 and paste to correct column.
so i use the script to find the correct column.
there range B2:CX2 have 0(not-correct) or 1(correct) value, so i use 'for' & 'if'
BUT!! It's too delay!!
i use console.time() and i get 25909ms(timecheck2 value) !!!
please help me.....
here is my code
function save(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('dB');
console.time("timecheck1");
//find last row
var copyrangeO = sheet.getRange(5,1,25,1).getValues();
var lastrowO = copyrangeO.filter(String).length;
var copyrange = sheet.getRange(5,1,lastrowO,1);
console.timeEnd("timecheck1");
//my dB data start "B2".
var cv = 1;
//find correct value(1). B2 ~ CX2 (#100)
console.time("timecheck2");
for (var i=2; i<101;i++){
if(sheet.getRange(2,i).getValue()===1){
cv = i;
}
}
console.timeEnd("timecheck2");
//if data isn't correct, cv===1. so error msg print.
console.time("timecheck3");
if(cv ===1){
Browser.msgBox("ERROR")
}else {
//data copy and paste.
var columnToCheck = sheet.getRange(4,cv,1000).getValues();
var lastrow = getLastRowSpecial(columnToCheck);
var pasterange = sheet.getRange(lastrow+4,cv);
copyrange.copyTo(pasterange, SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
Browser.msgBox(lastrowO + " saved!");
}
console.timeEnd("timecheck3");
}
Issue:
If I understand your situation correctly, you want to find the cell in B2:CX2 in which the value is 1, but the script is taking too much time for this.
The problem here is that you are using getRange and getValue in a loop (sheet.getRange(2,i).getValue()===1). This greatly increases the amount of calls to the Sheets service, which slows down your script, as you can see at Minimize calls to other services.
Solution:
In that case, I'd suggest doing the following:
Get the values from all columns at once using getValues().
Use findIndex to get the column index for which value is 1.
In order to do that, replace this:
var cv = 1;
//find correct value(1). B2 ~ CX2 (#100)
console.time("timecheck2");
for (var i=2; i<101;i++){
if(sheet.getRange(2,i).getValue()===1){
cv = i;
}
}
With this:
var ROW_INDEX = 2;
var FIRST_COLUMN = 2; // Column B
var LAST_COLUMN = 102; // Column CX
var columnValues = sheet.getRange(ROW_INDEX, FIRST_COLUMN, 1, LAST_COLUMN-FIRST_COLUMN+1).getValues()[0];
var cv = columnValues.findIndex(columnValue => columnValue === 1) + FIRST_COLUMN;
Note:
If there's no cell in the range with value 1, findIndex returns -1 which, added to FIRST_COLUMN, results in 1. That's appropriate for your current script, but won't work if the FIRST_COLUMN stops being 2, so be careful with this (either change the condition if(cv ===1){ to something less strict, or don't assign the resulting value to cv if findIndex returns -1).
The function will spend most of its time in the for loop because it repeats the Range.getValue() call many times. You can speed things up quite a bit by getting all values with one Range.getValues() call, like this:
let cv = 1;
console.time("timecheck2");
sheet.getRange('B2:B100').getValues().flat()
.some((value, index) => (cv = 2 + index) && value === 1);
console.timeEnd("timecheck2");
Note that this is not a cleanest way of finding cv, but it should help illustrate why you have a performance issue. You may want to do a complete rewrite of the code, using declarative instead of imperative style.
Try this:
I don't know what you're doing in the save because to did not supply the helper function code.
function save(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('dB');
var vs0 = sh.getRange(5,1,25,1).getValues();
var lr0 = vs0.filter(String).length;
var crg = sh.getRange(5,1,lr0,1);
var cv = 1;
const vs1 = sh.getRange(2,2,1,99).getValues().forEach((c,i) => {
if(c == 1)cv = i + 2
})
if(cv == 1){
Browser.msgBox("ERROR")
}else {
var vs2 = sh.getRange(4,cv,1000).getValues();
var lastrow = getLastRowSpecial(vs2);
var drg = sh.getRange(lastrow+4,cv);
crg.copyTo(drg, SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
Browser.msgBox(lr0 + " saved!");
}
}

Script based DataValidation not getting all the values (Sheets)

This code was working and although I don't recall having changed anything structure wise, it stopped working partially.
Once I select an item on cell W4, it is apparently only giving me the first option from another sheet on column C related to that item, while that the dropdown list should have about 04 items. I can't find where the flaw is:
function onEdit(){
var tabLists = "ArquivoItens";
var tabValidation = "EditarItem";
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var datass = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(tabLists);
var activeCell = ss.getActiveCell();
//It checks if the active cell is where the product name is
if(activeCell.getColumn() == 23 && activeCell.getRow() == 4 && ss.getSheetName() == tabValidation){
//if it is, it clears the Versão cell, so you can choose the ones related to the product of choice.
activeCell.offset(0, 6).clearContent().clearDataValidations();
//specifies the data range where the Product and the related field you want filtered is on
var makes = datass.getRange(2, 1, datass.getLastRow(), 3).getValues();
Logger.log(makes);
//this is what I need to study - map function
var searchArray = makes.map(function(e){return e[1];});
var makeIndex = searchArray.indexOf(activeCell.getValue());
Logger.log(searchArray);
//if anything on B (where desired column data is) is found related to A, where the product sits
if(makeIndex != -1){
//push found items to the datavalidation list
var validationRange = datass.getRange("C" + (2+makeIndex));
var validationRule = SpreadsheetApp.newDataValidation().requireValueInRange(validationRange).build();
activeCell.offset(0, 6).setDataValidation(validationRule);
}
}
}
Thanks a lot for helping me build the solutions I need, while learning tons on js.
Cheers,
indexOf() finds only the first instance of a search key
If you expect activeCell.getValue() to be present more than once, you either need to implement a loop where you call indexOf() multiple times, or - easier - use the Apps Script method createTextFinder().findAll().
Also, if your values of interest are not contained in an adjacent range, you cannot use requireValueInRange(). Use instead requireValueInList().
Sample:
...
if(activeCell.getColumn() == 23 && activeCell.getRow() == 4 && ss.getSheetName() == tabValidation){
var makes = datass.getRange(2, 1, datass.getLastRow(), 3).getValues();
var validationList = [];
var searchArray = datass.getRange(2, 2, datass.getLastRow(), 1).createTextFinder(activeCell.getValue()).findAll();
for(var i = 0; i < searchArray.length; i++){
var row = searchArray[i].getRow();
var Cvalue = makes[row-1][2];
validationList.push(Cvalue);
}
if(validationList.length > 0){
var validationRule = SpreadsheetApp.newDataValidation().requireValueInList(validationList).build();
activeCell.offset(0, 6).setDataValidation(validationRule);
}
}
...
My limited knowledge doesn't allow me to play around with advanced scripting, so I looked ffor another approach and here's how I ended up getting this to work:
function onEdit() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.getActiveSheet();
var dataSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("ArquivoItens");
var versionList = new Array();
var activeCell = ss.getActiveCell();
if(activeCell.getColumn() == 23 && activeCell.getRow() == 4 && ss.getSheetName() == "EditarItem"){
activeCell.offset(0, 6).clearContent().clearDataValidations();
var dataRng = dataSheet.getSheetValues(2, 2, dataSheet.getLastRow(), 2);
for (var i = 0; i < dataRng.length; i++){
if(dataRng[i].indexOf(activeCell.getValue())!=-1){
versionList.push(dataRng[i][1])
}
}
var validation = SpreadsheetApp.newDataValidation();
validation.setAllowInvalid(false);
validation.requireValueInList(versionList, true);
activeCell.offset(0, 6).setDataValidation(validation.build());
}
}
Thanks a lot for your help!

How do I merge duplicate cells together with google app script?

I currently have a list with two columns. The first column is student name, and the second column is the number of points they have.
I imported this list from multiple spreadsheets so there were many duplicates on the names of the students. I am able to remove the duplicates, but I want to keep a tally on the total points they have. For example:
Amy 10
Bob 9
Carol 15
Amy 12
would turn into:
Amy 22
Bob 9
Carol 15
This is what I have so far:
var target = SpreadsheetApp.getActiveSpreadsheet();
var sheet = target.getSheetByName("Sheet2");
var data = sheet.getRange("A2:B1000").getValues();
var newData = new Array();
var k = 0
var finallist = []
for(i in data){
k++;
var row = data[i];
var duplicate = false;
for(j in newData){
if(row[0] == newData[j][0]){
duplicate = true;
var storedHour = sheet.getRange("B"+k).getValue();
var position = finallist.indexOf(row[0]);
var originalCell = sheet.getRange("B"+(position+1));
var originalHour = originalCell.getValue();
originalCell.setValue(originalHour + storedHour);
sheet.getRange(k,2).setValue("")
sheet.getRange(k,1).setValue("")
}
}
if(!duplicate){
newData.push(row);
finallist.push(row[0])
}
}
}
The problem I'm having is that we have a really large data sample and I'm afraid it may run over Google's 5 minute maximum execution time. Is there another more efficient way to achieve my goal?
Your code is running slow because Spreadsheets API methods (like getRange) are time consuming and much slower then other JavaScript code.
Here is optimized function with reduced number of such Spreadsheets API calls:
function calcNumbers()
{
var target = SpreadsheetApp.getActiveSpreadsheet();
var sheet = target.getSheetByName("Sheet2");
var lastRow = sheet.getLastRow();
var dataRange = sheet.getRange(2, 1, lastRow-1, 2);
var data = dataRange.getValues();
var pointsByName = {};
for (var i = 0; i < data.length; i++)
{
var row = data[i];
var curName = row[0];
var curNumber = row[1];
// empty name
if (!curName.trim())
{
continue;
}
// if name found first time, save it to object
if (!pointsByName[curName])
{
pointsByName[curName] = Number(curNumber);
}
// if duplicate, sum numbers
else
{
pointsByName[curName] += curNumber;
}
}
// prepare data for output
var outputData = Object.keys(pointsByName).map(function(name){
return [name, pointsByName[name]];
});
// clear old data
dataRange.clearContent();
// write calculated data
var newDataRange = sheet.getRange(2, 1, outputData.length, 2);
newDataRange.setValues(outputData);
}
Sorting before comparing allows looking at the next item only instead of all items for each iteration. A spillover benefit is finallist result is alphabatized. Execution time reduction significant.
function sumDups() {
var target = SpreadsheetApp.getActiveSpreadsheet();
var sheet = target.getSheetByName("Sheet2");
var data = sheet.getRange("A2:B" + sheet.getLastRow()).getValues().sort();
var finallist = [];
for(var i = 0; i<= data.length - 1; i++){
var hours = data[i][1];
while((i < data.length - 1) && (data[i][0] == data[i+1][0])) {
hours += data[i+1][1];
i++;
};
finallist.push([data[i][0], hours]);
};
Logger.log(finallist);
}
Edit: the simple data structure with the name being in the first column allows this to work. For anything more complex understanding and applying the methods shown in #Kos's answer is preferable

Ignore empty rows from a loop in Google Apps Script

I'm new to Google Apps Script and I'm trying to ignore the empty rows from a for loop, but I'm still getting the empty rows in my log. Here are my codes,
function getNonEmptyRows() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet1 = ss.getSheetByName("Bldng4");
var lr = sheet1.getLastRow() - 17;
for (var i = 1; i < lr; i++) {
var singleRow = sheet1.getRange(i, 1, 1, sheet1.getLastColumn()).getValues();
if (singleRow.length > 0) {
Logger.log(singleRow);
}
}
}
How can I get the only non empty rows from the loop? Need this help badly. Thanks.
var range_data =
sheet1.getRange("A2:A") //Column Range
.getValues() //Get array from range values
.filter(array=>array != '') //Filter non-empty values
I was looking for a solution to a similar problem and here is what I did:
first, I found this tutorial on how to create google apps script to eliminate duplicate rows.
next, i modified it like this to eliminate empty rows:
function removeEmptyRows() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("settings");
var data = sheet.getRange("A2:D").getValues();
var newData = new Array();
for(i in data){
var row = data[i];
var empty = false;
for(i in data){
if(row.toString() == ",,,"){
empty = true;
}
}
if(!empty){
newData.push(row);
}
sheet.getRange(2, 6, newData.length, newData[0].length).setValues(newData);
};
As you can see, it takes A2:D range, removes empty rows and then pastes filtered data without empty rows into range F2:I.
You can try to use this script, but you may need to adjust it to "width" of your array. To do so change the number of commas in the following string:
if(row.toString() == ",,,"){
edit:
I modified script a bit to automatically adjust to width of your array:
function removeEmptyRows() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("settings");
var range = sheet.getRange("A2:C");
var data = range.getValues();
var dataWidth = range.getWidth();
var newData = new Array();
if (dataWidth<=1) {
var stringToCompare = '';
}else{
var stringToCompare = ',';
for (var i=0;i<dataWidth-2;i++) stringToCompare+=","
};
for(i in data){
var row = data[i];
var empty = false;
for(i in data){
if(row.toString() == stringToCompare){
empty = true;
}
}
if(!empty){
newData.push(row);
}
}
sheet.getRange(2, 6, newData.length, newData[0].length).setValues(newData);
};
The getValues() method you are using always return a 2 dimensions array, whatever the contents of the cells might be. There are several ways to get the cells content, one of them is to stringify the range content (convert matrix to single string) , remove the commas (and eventually "invisible" spaces) and check the length of the resulting string.
replacement code could go like this :
var singleRow = sheet1.getRange(i, 1, 1, sheet1.getLastColumn()).getValues().toString().replace(',','').replace(' ','');
That said, this code is very inefficient because it uses SpreadSheetApp service in each loop iteration which is particularly slow.
You'll find better approches in the documentation about best practices.

Pull data from one Google spreadsheet to another using Google script

I have 2 spreadsheet in my Drive. I want to pull data from a cell in 1 spreadsheet and copy it in another.
The spreadsheet "TestUsage" will sometimes have data in column A, but none is column B.
I would like so that when I open the spreadsheet, it would populate that empty cell in sheet "TestUsage" from sheet "TestParts".
Here is my code:
var ssTestUsage = SpreadsheetApp.getActiveSpreadsheet();
var sTestUsage = ssTestUsage.getActiveSheet();
var lastRowTestUsage = sTestUsage.getLastRow();
var rangeTestUsage = sTestUsage.getSheetValues(1, 1, lastRowTestUsage, 4);
var TESTPARTS_ID = "1NjaFo0Y_MR2uvwit1WuNeRfc7JCOyukaKZhuraWNmKo";
var ssTestParts = SpreadsheetApp.openById(TESTPARTS_ID);
var sTestParts = ssTestParts.getSheets()[0];
var lastRowTestParts = sTestParts.getLastRow();
var rangeTestParts = sTestParts.getSheetValues(1, 1, lastRowTestParts, 3);
function onOpen() {
for (i = 2; i < lastRowTestUsage; i++) {
if (rangeTestUsage[i][0] !== "" && rangeTestUsage[i][1] == "") {
for (j = 1; j < lastRowTestParts; j++) {
if (rangeTestUsage[i][0] == rangeTestParts[j][0]) {
Logger.log(rangeTestUsage[i][1]);
Logger.log(rangeTestParts[j][1]);
rangeTestUsage[i][1] = rangeTestParts[j][1];
break;
}
}
}
}
}
The problem with this is this doesn't do anything:
rangeTestUsage[i][1] = rangeTestParts[j][1];
I know there must be a method that can get data from one range to another.
Please let me know if I am totally incorrect or I am on the right path.
the statement
"this doesn't do anything:"
rangeTestUsage[i][1] = rangeTestParts[j][2];
is not really true... and not really false neither..., actually it does assign the value to rangeTestUsagei but you dont see it because it is not reflected in the spreadsheet.
Both values are taken from the Sheet using getValues so at that time they are both array elements.
What is missing is just writing back the array to the sheet using the symetrical statement setValues()
Give it a try and don't hesitate to come back if something goes wrong.
EDIT :
I didn't notice at first that you were using getSheetValues instead of getValues (simply because I never use this one)... the only difference is that getValues is a method of the range class while yours belongs to the sheet class; the syntax is similar in a way, just use
Sheet.getRange(row,col,width,height).getValues()
it takes one word more but has the advantage to have a direct equivalent to set values
Sheet.getRange(row,col,width,height).setValues()
Serge insas has a good explanation of why your code doesn't work and hints at the solution below.
I recommend you use an array to store the updated values of column B that you want then set the entire column at the end.
Modifying your code...
var ssTestUsage = SpreadsheetApp.getActiveSpreadsheet();
var sTestUsage = ssTestUsage.getActiveSheet();
var lastRowTestUsage = sTestUsage.getLastRow();
var rangeTestUsage = sTestUsage.getSheetValues(1, 1, lastRowTestUsage, 2);
var TESTPARTS_ID = "1NjaFo0Y_MR2uvwit1WuNeRfc7JCOyukaKZhuraWNmKo";
var ssTestParts = SpreadsheetApp.openById(TESTPARTS_ID);
var sTestParts = ssTestParts.getSheets()[0];
var lastRowTestParts = sTestParts.getLastRow();
var rangeTestParts = sTestParts.getSheetValues(1, 1, lastRowTestParts, 2);
var colB = [];
function onOpen() {
for (i = 2; i < lastRowTestUsage; i++) {
if (rangeTestUsage[i][0] !== "" && rangeTestUsage[i][1] == "") {
var matched = false;
for (j = 1; j < lastRowTestParts; j++) {
if (rangeTestUsage[i][0] == rangeTestParts[j][0]) {
//Logger.log(rangeTestUsage[i][1]);
//Logger.log(rangeTestParts[j][1]);
colB.push([rangeTestParts[j][1]]); // push the value we want into colB array
matched = true;
break;
}
}
if(!matched) // this is in case you don't have a match
colB.push([""]); // incase we don't have a matching part
} else {
colB.push([rangeTestUsage[i][0]]); // we already have a value we want so just add that to colB array
}
}
sTestUsage.getRange(2,2,lastRowTestUsage).setValues(colB); // update entire column b with values in colB array
}

Categories