Quickly looping through multiple sheets to find data - google-apps-script

Hi i'm trying to write a custom function that takes a pair of cells, loops through all the worksheets in the spreadsheet to find an identical matching pair of cells, and returns another value from that same row.
Background; sheet 0 is a master sheet of all LOA and ID combinations (essentially location and serial #) which need to have there inspection dates filled in. The people who do these inspections update their personal worksheets with the LOA-ID combination + inspection data on google drive. Im trying to get the master sheet to update automatically whenever this data is added.
The sheets all follow the same format (LOA, ID in 1st & 2nd columns, inspection date in the 14th). This is a custom function im using which does what i intend, but works painfully slow. How do i make this run faster? It takes several seconds PER CELL; i need to run this over 10k+ cells.
function findMatch(LOA,GRID) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var returnDate = "not found"
for (var sheetNum = 1; sheetNum < sheets.length; sheetNum++){
var ws = ss.getSheets()[sheetNum]
for (var count = 1; count<ws.getLastRow(); count++){
if (ws.getRange(count,1,1,1).getValues()==LOA && ws.getRange(count,2,1,1).getValues()==GRID)
{
returnDate = ws.getRange(count,14,1,1).getValue()
break;
}
else
{
}
}
}
Logger.log(returnDate)
return returnDate

It is best practice to perform as few Spreadsheet Service calls as possible, and particularly avoid making them inside loops. Instead, retrieve all the data in a batch with getValues() and use Javascript to iterate over that data.
function findMatch(LOA,GRID)
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
var returnDate = "not found", data;
for (var sheetNum = 1; sheetNum < sheets.length; sheetNum++)
{
data = sheets[sheetNum].getDataRange().getValues();
for (var count = 1; count < data.length; count++)
{
if (data[count][0] == LOA && data[count][1] == GRID)
{
returnDate = data[count][13];
break;
}
}
}
Logger.log(returnDate);
return returnDate;
}

Related

Google Sheets Script: That Searches For Previous Match in Memo and If Found Automaticly Fills In A Category

I am trying to edit a google sheets budgeting template. I need a script that looks at previous memos that have been assigned a category and will match newly entered memos with a category if it has already been matched above.
Memos consist of multiple words and a match should only happen if the exact words are present.
What Spreadsheet Looks Like
Spreadsheet Link
I don't know if this is relevant but the template consists of multiple sheets.
I found someone else's code (Source) trying to do what I do but I cant get it to work. This Is what they did...
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var ui = SpreadsheetApp.getUi();
ui.createMenu('Aspire Budgeting')
.addItem('Auto Fill Category. Click on first empty Category in Transaction first', 'autoFillCategory')
.addToUi();
}
function autoFillCategory() {
// to use this script import new Transactions then click on the first empty category in Transactions.
var sheet = SpreadsheetApp.getActiveSheet();
// startRow is off set by 1 in getDataRange
var startRow = sheet.getActiveCell().getRow() - 1;
// categoryColumn is off set by 1 in getDataRange
var categoryColumn = sheet.getActiveCell().getColumn() -1;
var memoColumn = categoryColumn + 2
var data = sheet.getDataRange().getValues();
// Check to make sure the current cell is set to Transactions -> Categories
if (sheet.getName() == "Transactions" && data[6][categoryColumn] == "Category" && startRow > 8) {
for (var currRow = startRow; currRow < data.length; currRow++) {
// memoValue to search for
var memoValue = data[currRow][memoColumn];
// SpreadsheetApp.getUi().alert(currRow);
var previousCategory = "";
//Search for the previous instance of memoValue.
for (var i = 0; i < currRow; i++) {
var row = data[i];
if (row[memoColumn] == memoValue) {
previousCategory=row[categoryColumn];
sheet.getRange(currRow + 1,categoryColumn + 1).setValue(previousCategory);
break;
}
}
}
} else {
SpreadsheetApp.getUi().alert("Before running this script import new transactions then click on the first empty category in transactions you want to search for.");
return;
}
}
I dont really have much coding experience so I don't really where Im going wrong

Using a formula across dynamically added sheets

I'm trying to work out a formula to sum up values across different sheets for particular rows as in the following:
Sheet 1
A | B
---------
bob | 3
foo | 14
bar | 7
Sheet 2
A | B
---------
bob | 2
foo | 1
bar | 9
But with the caveat that a new 'Sheet 3' can be added with relevant data and will automatically be picked up.
How do I go about getting the summed values for each row in each sheet while handling new (properly named) sheets?
Results
-------
bob | 5
foo | 15
bar | 16
We can assume that the row values are all the same, such as in a named range, People = {bob, foo, bar}.
My attempt has been something like:
={People,ARRAYFORMULA('Sheet 1'!B1:B3+'Sheet 2'!B1:B3)}
but for new sheets, I would have to manually update the formula by adding in
+'Sheet 3'!B1:B3
I've tried using INDIRECT to dynamically generate the sheet names but it doesn't take an array. I'm guessing that I might have to use
SpreadsheetApp.getActiveSpreadsheet().getSheets()
in a script, but if I could do it just as a formula, it would be easier to explain to people inheriting the spreadsheet.
Thanks
I have alternate solution to your problem, using a slightly different approach.
I would suggest pulling all of the data into one results page and summing it there. Then you don't need a massive 'sum' function or a script.
Using indirect you can pull information from each sheet, provided the data is in the same cell location on each sheet. If that's not possible you could also use vlookup to pull the data. I've shared an example of how I would do this using your numbers.
Syntax for 'bob' value on Sheet1 =iferror(indirect("'"&C$2&"'!"&"b3"),"")
where C$2 is the Sheet name (in this case Sheet1) and B3 is the value for bob on Sheet1. Then you can copy this formula across the columns and update your sheet names at the top of the table.
https://docs.google.com/spreadsheets/d/15pB5CclseUetl5zSRPDOR9YA4u6_7TK8RB8CpSxqhnk/edit?usp=sharing
Sample File
The workaround is to use a custom function:
Warning! The function won't refresh automatically. It will refresh if you add and then delete row above it.
Syntax:
=sumBySheets("Sheet1", "Sheet2", "B1:B3")
"Sheet1" — sheet from
"Sheet2" — sheet to
"B1:B3" — range address for a sum.
There may be more sheets between Sheet1 and Sheet2:
The code:
function test_sumBySheets()
{
var sheet1 = 'Sheet1';
var sheet2 = 'Sheet2';
var range = 'b1:b3';
Logger.log(sumBySheets(sheet1, sheet2, range));
}
function sumBySheets(sheet1, sheet2, address)
{
var file = SpreadsheetApp.getActive();
var s1 = file.getSheetByName(sheet1);
var s2 = file.getSheetByName(sheet2);
var i1 = s1.getIndex() - 1; //get sheet indexes
var i2 = s2.getIndex() - 1;
var sheets = file.getSheets();
var result = [];
var arrays = [];
// remember all the data
for (var i = i1; i <=i2; i++)
{
var s = sheets[i];
var range = s.getRange(address);
arrays.push(range.getValues());
}
return sumByArrays_(arrays);
}
function sumByArrays_(arrays)
{
// take array #1
var arr = arrays[0];
l = arr.length;
ll = arr[0].length
// plus all arrays
for (var x = 1, xx = arrays.length; x < xx; x++) // loop arrays
{
for (var i = 0; i < l; i++) { // loop rows
for(var ii = 0; ii < ll; ii++) { // loop cells
arr[i][ii] += arrays[x][i][ii];
}
}
}
return arr;
}
Note:
please run the function test_sumBySheets first and get the permissions.
Been a while, but here's what I ultimately ended up with for my solution.
The following code dynamically loads all sheets associated with the spreadsheet (note that I edited it a bit for readability on SO, might be some typos):
// These vars indicate what sheet, column and rows to start
// the list of sheets.
var main_sheet = 'Sheet1';
var sheet_col = 'A';
var sheet_row_start = 1;
function onEdit(e) {
// The onEdit trigger is the most useful as it fires most often.
// Therefore it is more likely to update the list of sheets relatively
// quickly.
_set_sheet_cells();
}
function _clear_sheet_cells(num_sheets, sheets_length) {
// Clear sheet cells, in case number of sheets has dropped.
var sheet_ctr = Number(num_sheets);
var stats = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(main_sheet);
if (sheet_ctr) {
if (sheet_ctr >= sheets_length) {
for (var i=0 ; i<sheet_ctr ; i++) {
stats_sheet.getRange(sheet_col+(sheet_row_start+i)).clearContent();
}
}
}
}
function _get_sheets() {
// Gather our sheets, can be combined with a regex to prune sheets.
var out = new Array();
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i=0 ; i<sheets.length ; i++) {
out.push( sheets[i].getName() );
}
}
return out
}
function _set_sheet_cells() {
var userProperties = PropertiesService.getUserProperties();
var num_sheets = Number(userProperties.getProperty('sheet_names'));
var sheets = _get_sheets();
if (num_sheets == sheets.length) {
// Do nothing, no changes, remove if concerned about renaming sheets.
return;
}
_clear_sheet_cells(num_sheets, sheets.length);
var stats = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(main_sheet);
for (var j=0 ; j<sheets.length ; j++) {
// Put one sheet name per row.
stats_sheet.getRange(sheet_col+(sheet_row_start+j)).setValue(sheets[j]);
}
userProperties.setProperty('num_sheets', sheets.length)
}
Once I had all the sheets loading dynamically, I just used #cgm990's answer, with the benefit that if I added another sheet to the spreadsheet, it automatically updated all my sums.

Google Script Optimization

I have a sheet in which IDs are saved. Now occasionally I need to read these IDs and check if other values(Name) in the sheet still fits the ID. The code I have is:
var name = sheet.getRange(line,row).getValue();
var id = sheet.getRange(line,10).getValue();
if(name!== "" && id === "")
{
try
{ get an ID}
Now from what I read I know that calling each cell value seperately takes way more time. However I am not sure how to apply getValues to have these cases fixed.
Basically the same problem in a different dress I have with the following code:
var id = sheet.getRange(line,row).getValue();
if(id!== "" && id!="Not Found" && id!="Not Found old")
{
var url = "some api url "+id+"api key";
try
{
var str = UrlFetchApp.fetch(url).getContentText();
So how do I use get values to check every ID I get. I guess I would have to use some sort of
foreach(id)
or a
for(i= 0; i <= id.range; i++)
{
use id[i] to blablabal
}
But I have no idea how to implement it, any ideas?
Is there maybe even a different, more efficient way?
After calling getValues you get a double array, like [[valueA1, valueB1], [value A2, value B2]]. Process it in a for loop, which may be a double loop if all row/column combinations are involved. Keep in mind that spreadsheet uses 1-bases indexing but in JavaScript they are 0-based. Often, the top row is headers, so it's omitted from the loop below.
function processData() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getDataRange(); // all of data. could be some part of it
var values = range.getValues();
for (var i = 1; i < values.length; i++) { // skipping with header row
for (var j = 0; j < values[0].length; j++) {
if (condition) {
// do something to values[i][j];
}
}
}
range.setValues(values); // put updated values back
}
If the data of interest is in, say, column D, then you can decide to fetch only that column. For example:
function fetchStuff() {
var sheet = SpreadsheetApp.getActiveSheet();
var lastRow = sheet.getLastRow();
var range = sheet.getRange(1, 4, lastRow, 1); // all of column D, note 1-based indexing
var values = range.getValues();
for (var i = 1; i < values.length; i++) { // skipping header row
if (condition) {
var str = UrlFetchApp.fetch(values[i][0]).getContentText();
// do something
}
}
}
Keep in mind that fetch is by far the slowest operation here, and that it is subject to quotas. Use Utilities.sleep(ms) to avoid invoking services too often, and keep track of how much data you are asking the script to fetch.

Search a spreadsheet column for duplicate entries

I have some customer data in a Google Spreadsheet as shown below. New entries are added regularly, and I would like to use a script to search the contact number column to check if the contact number for a new entry already exists.
Also, if possible, I'd like the script to give some kind of notification that indicates the row number of the existing entry. For example, the notification could be a new column where it would just write the "row number" or "no entries found".
Data Available:
Contact Number Email ID Venue Address Service Date
1234567890 Test1#gmail.com cypress 21/04/2016
0123456789 Test2#gmail.com river run drive 22/04/2016
What I am looking for:
Contact Number Email ID Venue Address Service Date Duplicate entry in
1234567890 Test1#gmail.com cypress 21/04/2016 row1
0123456789 Test2#gmail.com river run drive 22/04/2016 no entries found
So, your description implies that you want to search for entry before it's input. Rather than searching for duplicates, which means there is already duplicate rows in your data. I have constructed a method for both for you.
This shows an example of both, this will either find the duplicates or it will find an entry by phone number. The remainder of the logic and customization is up to you.
Here is the example sheet for this: Example Spreadsheet
Edit: Added column finding logic, and added a button you can press to see the duplicates in the sheet. NOTE: You must be signed into a google account for the script to run
//Entry Point
function myFunction() {
var sheet = SpreadsheetApp.getActiveSheet(); //Get the current sheet
var dataRange = sheet.getDataRange();
var valuesRange = dataRange.getValues(); //The array of data on the sheet
var columns = GetColumns(valuesRange, dataRange.getNumColumns(), 0);
var duplicates = SearchForDuplicates(valuesRange, columns.columns['Contact Number'].index); //Search for existing duplicates
var elementIndex = SearchForValue(valuesRange, columns.columns['Contact Number'].index, 123456789); //Search for a phone number of 123456789
if(duplicates.length > 0){ //If there are duplicates
var isDuplicateColumn = columns.columns['Is Duplicate'].index;
for(var i = 0; i < duplicates.length; i++){
valuesRange[duplicates[i].index][isDuplicateColumn] = 'Yes'; //Assign Yes to the appropriate row
}
dataRange.setValues(valuesRange); //Set the spreadsheets values to that of the modified valuesRange
}
Logger.log(duplicates);
Logger.log(elementIndex);
}
//Searches an array for duplicate entries
function SearchForDuplicates(array, columnIndex){
var uniqueElements = {};
var duplicates = [];
for(var i = 0; i < array.length; i++){
if(typeof uniqueElements[array[i][columnIndex]] === 'undefined'){
uniqueElements[array[i][columnIndex]] = 0; //If the element does not yet exist in this object, add it
} else {
duplicates.push({row: array[i], index: i}); //If the element does exist, it's a duplicate
}
}
return duplicates;
}
//Searches an array for a value
function SearchForValue(array, columnIndex, value){
for(var i = 0; i < array.length; i++){
if(array[i][columnIndex] == value){
return i; //Element found, return index
}
}
return -1; //No element found, return -1
}
//Gets a columns object for the sheet for easy indexing
function GetColumns(valuesRange, columnCount, rowIndex)
{
var columns = {
columns: {},
length: 0
}
Logger.log("Populating columns...");
for(var i = 0; i < columnCount; i++)
{
if(valuesRange[0][i] !== ''){
columns.columns[valuesRange[0][i]] = {index: i ,value: valuesRange[0][i]};
columns.length++;
}
}
return columns;
}

Google App Script - loops not executing

I want to create a function that will iterate over every sheet until a given sheet. The function receives the name of that last sheet as an argument.
function getUntilMonthSavings(month) {
var spreadsheet = SpreadsheetApp.getActive();
var monthSheet = spreadsheet.getSheetByName(month);
var allSheets = spreadsheet.getSheets();
var sheetNumber = monthSheet.getIndex();
var totalSavings=0;
for (var i = 1; i < monthSheet; i++){
totalSavings = totalSavings + allSheets[i].getRange("I20").getValue();
}
return totalSavings;
}
My problem is that what is returned is always 0. I've also returned i to check if it is being iterated, but it returns 1 even when the sheet index is greater than 1.
I'm sure to be doing some kind of basic blunder, but I'm quite at a loss as why this code is not working.
MonthSheet is an object and not something you can compare i to in your loop. You need the actual index of the sheet referred to.
Try:
for (var i = 0; i < monthSheet.getIndex(); i += 1) {