Find rows with particular value from a Google Spreadsheet without using loop? - google-apps-script

I am new to spreadsheet scripting. I am generating a report (new sheet) based on another sheet where I enter values daily. In Apps Script I first generate the sheet then loop through the data range retrieved from that input sheet.
After that I have to merge values based on dates and categories.
Now my report format is such that rows are categories and dates are columns.
So if in input if there is another value with same date and same category I have to add the value.
My problem is how to check if the value with same date and category exists in the report and I DO NOT want to use loops as I am already in loops so that will make the process run very very slow.

I don't think it is possible to do it without some looping. Since this operation is carried out server side without the need to make calls to the spreadsheet it would take a very small amount of time even with a very large dataset.
If your script is already slow it more than likely because of inefficiencies/ delays in some other part of the script. I have a script which duplicates a spreadsheet and renames it, just those to operations take between 5 & 8 seconds.
As an example:
function test(){
var ss = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
var value = "agdsdfgsdfg" // This value is in cell BD1000
for(var i = 0; i < ss.length; i ++){
if(ss[i].indexOf(value)>=0){
var x = ss[i].indexOf(value) + 1;
break;
}
}
var y = i + 1
// var x & y are the row, cell coordinates of value in the data range
}
This operation carried out on a dataset 56 columns x 1000 rows completes in 0.88 seconds with the search value in the last cell of the range.

Your report sounds a fair bit like a Pivot Table with categories in rows, dates in columns, and SUM(Value) as the data field. To reproduce this with a script report, you can use an Object variable that maps between a key and a "value" Object:
This probably isn't your exact use case (it assumes you need to generate a new report from a possibly-large stack of feeder data, but it should demonstrate how you can use nested Objects to simplify / internalize the lookup process, including testing for undefined values and enforcing a rectangular output.
// Make an array of the data, to limit use of the slow spreadsheet interface.
var inputs = SpreadsheetApp.openById(<id>).getSheetByName(<dataSheetName>)
.getDataRange().getValues();
// The first row is probably column headers from the input sheet, and
// doesn't likely contain useful data that you want in your report.
var headers = inputs.splice(0, 1);
var report = {};
for(var row = 0; row < inputs.length; ++row) {
// Change these indexes (0, 1, 2) to the proper values.
// Also do any necessary formatting / validation, etc. for "category" and "date".
var category = inputs[row][0];
var date = inputs[row][1];
var value = inputs[row][2];
// If this category doesn't exist, default construct its report object.
// For each category, a nested object is used to store the date-value pair.
if(!report[category]) {
report[category] = {};
}
// Otherwise, if the date is not yet seen for the category, set
// the value. If it is seen, increment the stored value by the new value.
if(!report[category][date]) {
report[category][date] = value;
} else {
// Treat this as numeric addition, not string concatenation.
report[category][date] += value - 0;
}
}
// To print your report, you need a header you can index against.
var outputHeader = [];
for(var category in report) {
for(var date in category) {
outputHeader.push(date);
}
}
// Sort this header row. If the dates are strings that don't simply
// coerce to proper Date objects, you'll need to write your own sort() method.
// (You don't technically need to sort, but if you don't then the dates
// won't be "in order" when the report prints.)
outputHeader.sort();
// After sorting, add a row label for the header of sorted dates.
outputHeader.splice(0, 0, "Category / Date");
// Serialize the report object into an array[][];
var output = [outputHeader];
var totalColumns = outputHeader.length;
for(var category in report) {
// Initialize each row with the row label in the 0 index position.
var row = [category];
for(var date in category) {
var index = outputHeader.indexOf(date);
row[index] = category[date];
}
// Unless you are guaranteed that every category has a value for every date
// in the report, you need to ensure that the row has a value at each index.
// (This is a good idea anyway, to ensure that you have a rectangular array.)
var filled = Object.keys(row);
// We can start at 1 since we know that every row starts with its category.
for(var col = 1; col < totalColumns; ++col) {
if(filled.indexOf(String(col)) < 0) {
row[col] = "";
}
}
output.push(row);
}
SpreadsheetApp.openById(<id>).getSheetByName(<reportSheetName>)
.getRange(1, 1, output.length, output[0].length).setValues(output);

Related

Chekcing cell background of a merged cell

I have a range which I am trying to iterate and store the arrays present in an array until a specific case is encountered.
The data is as follows:
I am trying to iterate from D5:D and store in an object with their A1 notation.
This is what I tried,
var gg = Sheet_1.getRange("D5").getValue();
Concat_rows = {};
const COLUMN = 'D';
const START_LINE = 5;
const Filled_LAST_ROW = Sheet_1.getLastRow();
var LAST_ROW ;
//To get the last row to iterate upto
for (var i = START_LINE; i <= Filled_LAST_ROW; i++){
let cellcolor = Sheet_1.getRange(COLUMN+i).getBackground();
// Trying to check with the cell color to identify upto where to get the values from
if(cellcolor == "#c6d9f0"){
LAST_ROW = i;
break;
} else {continue;}
}
console.log(LAST_ROW);
Once I get that in D5:D I need to just iterate over D5:D26 then I need to pull the values and their A1 notations to store in an objet for further use
I have tried this for the above,
for(let h = START_LINE; h <= LAST_ROW; h++) {
let cellValue = Sheet_1.getRange(COLUMN+h).getValue()
if (cellValue.length >= 1) {
Concat_rows[cellValue] = COLUMN+h;
The problem is that the cell D27 is a merged cell and I am not able to figure out how to deal with it.
Only the top left cell of a merged range holds the value displayed in the user interface, so if you want the value of C27:D27 you could do SpredsheetAppg.getRange('C27:D27').getValue(). If the location of the merged range is static the solution is straight fortwarth, just add a condition and when the loop reach get the value from the merged range instead of the cell, in other words, instead of
let cellValue = Sheet_1.getRange(COLUMN+h).getValue()
use
let cellValue = h === 27
? Sheet_1.getRange('C27:D27').getValue()
: Sheet_1.getRange(COLUMN+h).getValue();
If the location isn't known your script needs to check the location of the merged cells but doing this on a loop it's very likely that will cause that the scripts exceeds the maximum execution time. Unfortunatelly the question doesn't include enough details to provide a specific simple way to handle this. The general advice is to use batch operations and in order of get the best possible performance use the Advanced Sheets Service instead of the Spreadsheet Service.

How to get data from one column based on a variable in another column

I'm trying to get data from an input sheet and use it to update a database in another sheet without hardcoding column letters and row numbers. Here is a picture of my input sheet:
CONTEXT:
Users input data into E. Columns G and H contain the field names (G) and the values input by the user (H). I want to get the values from Column H and then update the right record in the database (a different sheet) based on the ContractorID field (which is in Col A of the database sheet) and the field names in Column G (which are the column headers in my database sheet).
MY ASK
I'd like code that dynamically gets the values in Column H (without referencing "H") based on the field names in Column G (without referencing "G").
I don't know Google App script well enough, but I think the logic would look something like this:
Iterating for each field name in Column G:
Set variable for the field name in column G
Get the column with "FIELDNAMES" in row 1
Get the row # of the field name variable in the FIELDNAMES column
Get the column with "FIELDVALUES" in row 1
Get the cell value in the row # in the FIELDVALUES column
Then I can use that value to populate the right field in the database sheet.
This script accomplishes what you're looking to do: it finds the data without explicitly calling out columns, then matches the contractor ID in the database sheet, finds the corresponding columns for each item and inserts the data.
EDIT
Added functions at the end to compare the input fields with the columns and use a toast notification to alert the user around what columns are missing.
A couple of notes:
Check the sheet names of course. this should work even if the database is in a separate worksheet. Just need to update the variable accordingly.
I borrowed from this script to merge the input and database arrays.
See it in action (I added a button to run the script) here
const ss = SpreadsheetApp.getActiveSpreadsheet()
const input = ss.getSheetByName("input")
const db = ss.getSheetByName("database")
function addRecord() {
// Find the columns you want, then get all the data in the columns to an array called inputData
var fieldRangeStart = input.createTextFinder("FIELDNAMES").findNext().getColumn()
var inputData = input.getRange(1,fieldRangeStart,input.getLastRow(),2).getDataRegion().getValues()
// In the database sheet, get the column headers and create an array called dataColumns that gives us those positions in the database sheet.
var headers = db.getRange(1,1,1,db.getLastColumn()).getValues().toString().split(",")
var dataColumns = []
for (var i=0; i<headers.length; i++){
dataColumns.push([headers[i],i+1])
}
// Merge the arrays, matching the fieldName values in each.
const data = inputData.map(x => dataColumns.filter(y => y[0] === x[0]).map(y => [x[0], y[1], x[1]])).flatMap(x => x)
// Filter out the contractorID into its own variable
const contractorID = data.filter(x => x[0] === "ContractorID").map(x => x[2])
console.log(contractorID)
//Now we have the contractorID to search for, and a data array that contains the field names, database column index, and input values for the data. Just need to find the row with the matching Contractor ID and then write the values to their respective columns.
//Start by getting all the values in the first column, except the header (start the range at Row 2).
var contractors = db.getRange(2,1,db.getLastRow(),1).getValues()
//Then find the contractor ID in the list. Add 2 to the row index because we need to account for the header (1 row) and the fact that the index is 0-based but rows start at 1 when doing A1 Notation.
var contractorRow = contractors.flat(1).indexOf(contractorID.toString())+2
//Finally, confirm the contractor is in the database, and then loop through the data array and write each item to its cell, using the column reference we already have and the new contractorRow reference we just acquired, or setting a new row if the contractor doesn't exist in the database. Issue toast notifications if input fields are missing from the db
if(contractorRow === 1){
var newRow = db.getLastRow()+1
for (var i=0; i<data.length; i++){
db.getRange(newRow,data[i][1]).setValue(data[i][2])
}
} else {
for (var i=0; i<data.length; i++){
db.getRange(contractorRow,data[i][1]).setValue(data[i][2])
}
}
// Gather just the FIELDNAMES data from the input sheet into a new variable called inputFields, ignoring the first row and flattening the result so it's not a 2-dimensional array.
var inputFields = input.getRange(2,fieldRangeStart,input.getLastRow(),1).getValues().flat(1)
// We already have the column headers in a variable called headers. Filter inputFields for NOT being included in the headers variable and also remove blank entries.
let missing = inputFields.filter(x => !headers.includes(x) && x != "")
// Now trigger the toast notification
SpreadsheetApp.getActiveSpreadsheet().toast('Oops! These fields are missing from the database: ' + missing, 'Missing Columns')

Copy data range and paste to new page, and repeat

I am trying to copy a range from sheet 'Full' and paste the values only to a new sheet, 'Dump'. While the macro below does its action once, I am regenerating the original data range (Full), so I want to copy that new set and append to the same output page, indexed down to a blank row and keeping the first pasted data. Also then to do this 100 times.
The recoded macro is below, and I need to understand the script to add in to;
repeat the copy/paste function 100 times, and also
offset the paste range by a set number of rows.
Sorry, genuine newbie at editing google sheet macros. The Excel macro I use doesn't translate over.
Appreciate any answers you have.
function xmacro() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getRange('A1').activate();
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Full'), true);
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Dump'), true);
spreadsheet.getRange('Full!BK3:BT34').copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);```
};
Your macro is just an automatically generated app script. You can extend its functionality by building off that with some more code. First I'll explain some of the basic concepts, if you know this, then just skip down to the code.
Sheets Concepts
Here are some basic concepts that took me forever to figure out because most of the documentation assumes you are already proficient at Javascript.
A range is a 2 dimensional array that has one array for each row, and the contents of that array are the columns:
someRange = [
[row1Col1, row1Col2, row1Col3, row1Col4],
[row2Col1, row2Col2, row2Col3, row2Col4],
[row3Col1, row3Col2, row3Col3, row3Col4]
]
To access a specific value you need to reference the row array, and then the index of the column you want.
Think about it like hotel room numbers. The first part of the number is the floor,
and the second part is the specific room on that floor.
You access arrays by calling the array name, then square brackets with the index number of the element you want.
Arrays are indexed starting at 0, so to get row 1 you would use:
someRange[0] would return the inner array [row1Col1, row1Col2, row1Col3].
But that doesn't give you a specific cell values - so you would use a second set of brackets to access the column in that row:
someRange[0][1] = 'row1Col2'
Arrays also have built in information, so you can find the length of an array by using Array.length no parenthesis.
Since the rows are in the outer array, you can get the number of rows by seeing how many inner arrays there are.
someRange.length = 3 There are 3 row arrays in the someRange array.
You can do the same with columns, since the number of columns is equal to the number of elements in an array. To get the number of elements in the first row you would use:
someRange[0].length - which would be 4
And since a range has the same number of columns for each row, you can pick any row
to get the number of columns (generally, there are always exceptions)
The Code
The first function will create a custom menu item to run the code.
// create a new menu item for your custom function
function onOpen(){
SpreadsheetApp.getUi().createMenu()
.addItem('100 Copies', 'lotsOfCopies')
.addToUi();
}
function lotsOfCopies() {
var ss = SpreadsheetApp.getActive();
var copySheet = ss.getSheetByName('yourCopySheetName');
var pasteSheet = ss.getSheetByName('yourPasteSheetName');
// the range you wish to copy, change to fit your needs
var copyRange = copySheet.getRange('A1:B7');
var copyValues = copyRange.getValues();
var copyRows = copyValues.length;
var copyCols = copyValues[0].length;
// define the first row to be pasted into
var pasteRow = 1;
// define the left side column of the range to be pasted into
var pasteCol = 1
// build a loop that does the same thing 100 times,
// and each time offsets the paste range by the number of rows in the copy range
for (var i = 0; i < 100; i++) {
// for every iteration after the first,
// add the number of rows in the copy range to the variable 'row'
// example if there are 10 rows in the copy range then
// iteration 1 row = 1 Iterartion 2 row = 11, Iteration 3 row = 21
if (i > 0) {
pasteRow = +pasteRow + +copyRows
}
// build the range to paste into - it starts on pasteRow and paste col,
// and is as many rows as the copied range, and as many columns as the copied range
let pasteRange = pasteSheet.getRange(pasteRow, pasteCol, copyRows, copyCols);
// put the values from copyValues into the pasteRange
pasteRange.setValues(copyValues);
}
}
function xmacro() {
const ss = SpreadsheetApp.getActive();
const ssh = ss.getSheetByName('Full')
const dsh = ss.getSheetByName('Dump')
ssh.getRange('BK3:BT34').copyTo(dsh.getRange('A1'), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
}

Scan cells in one column for specific text value, then input different text value in same row in another column

We have a spreadsheet we are using where personnel input text values in one column, within these text values are certain words we want to hunt for.
If they appear, we want to populate another column with another word. E.g.:
Col1: 'today i went to the beach'
function finds 'beach'
function enters 'sand' in same row of Col5.
Any help greatly appreciated.
Here's a simple approach. It doesn't deal with a cell containing multiple terms or with a term being embedded in another word. eg. If a term were see then seething would also match.
function findTerms() {
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getDataRange().getValues();
// Key-value pairs of a term to look up and a value to display
var pairs = [
["from", "to"],
["this", "that"],
["...", "..."]
];
var inspectCol = 1; // The column you want to look in
var resultsCol = 2; // The column to write results to
// loop over the rows of data
for(var i=0; i < data.length; i++){
// loop over the terms and values
for(var n in pairs){
var term = pairs[n][0];
var val = pairs[n][1];
// if the specified column in the current row contains the current term
if(data[i][inspectCol-1] && data[i][inspectCol-1].indexOf(term) != -1){
// write val to the results column
sheet.getRange(i+1, resultsCol).setValue(val);
}
}
}
}
To get fancier and do things like only find complete words, use match() and regular expressions, instead of indexOf().

Autofill google forms based on user input

Alright stack friends,
I'm working on my first projects using google scripts and it's been pretty fun so far. My project is to create a form for data entry that can either accept an ID number and fill in the rest of the fields, or let the user fill out the entire form. Basically my method to fill in the other fields is just to have a lookup table on the second sheet. When the user submits a form, the script runs, looks for the ID of the last row, scans the reference table for the ID, and then fills in the details.
I think the problem I'm having is the assumption that the data from the form is already in the sheet when the script runs. The problem I noticed is that the script sometimes fails to fill in the gaps. I tried creating form submissions in a loop with the same ID and they function somewhat erratically but it seems like the last sumbission always works which would make sense if the script executions are not matching up with the form submissions. Here's the script for reference:
function fillGaps() {
// First take in the appropriate spreadsheet objects and get the sheets from it
var ss = SpreadsheetApp.openById(id);
var sheet = ss.getSheets()[0];
var refSheet = ss.getSheets()[1];
// Here's the last rows' index
var lastRow = sheet.getLastRow();
var lastRowRef = refSheet.getLastRow();
// now this is an array of values for the last row and the student ID entered
var response = sheet.getRange(lastRow, 1, 1, 7).getValues();
var enteredID = response[0][1];
// Next we're going to try to load up the lookup table and scan for the ID
var stuIDs = refSheet.getRange(2, 4, refSheet.getLastRow()).getValues();
var row = 0;
while(enteredID != stuIDs[row] && row <= lastRowRef){
row++;
}
// Okay at this point the row variable is actually -2 from what the sheet index
// is that I'm thinking of. This is because we didn't load the first row (names)
// and the way arrays are indexed starts with 0.
row++;
row++;
// now assuming that it found a match we'll fill in the values
if(row < refSheet.getLastRow()){
// Alright now we need to wrangle that row and format the data
var matchedRow = refSheet.getRange(row, 1, 1, 6).getValues();
// modify the response
var replacement = [response[0][0],enteredID, matchedRow[0][1],matchedRow[0][0],matchedRow[0][2],matchedRow[0][4],matchedRow[0][5]];
sheet.getRange(lastRow, 1, 1, 7).setValues([replacement]) ;
}
}
So I'm wondering:
Does this seem like the right diagnosis?
If so, what would be the best way to remedy? I thought of adding a little delay into the script as well as trying to capture the submissions timestamp (not sure how to do that)
Thank you much!
The following code gives a 2D array:
var stuIDs = refSheet.getRange(2, 4, refSheet.getLastRow()).getValues();
Also,refSheet.getLastRow gives the last row, lets say it is 10 in this case. The syntax for getRange is getRange(row, column, numRows) and the last argument is the number of rows, not the last column. So in the above code the selected range would be row 2 - 11 rather than 2- 10. Unless that is what you intended, modify the code like so:
var stuIDs = refSheet.getRange(2, 4, refSheet.getLastRow()-1).getValues();
To access the values in stuIDs you should use stuIDs[row][0] (2D array) to check for matching ID. Assuming your ID was to be matched was in column 1.
Secondly, in the loop you are using the following to check for the last index in array row <= lastRowRef which will cause it go out of range(because array starts at 0 and sheet row at 1) instead use this row < stuIDs.length
Finally, in case you don't find a match you will end up with the last row and your code will end you taking the last row as the matched index. This can be prevented by using a boolean variable to check for a match.
var foundId = false
var row = 0;
var i = 0;
for (i in stuIDs){
if(stuIDs[i][0] == enteredID)
foundID = true
break
}
}
row = i + 2
if (foundID){
var matchedRow = refSheet.getRange(row, 1, 1, 6).getValues();
// modify the response
var replacement = [response[0][0],enteredID, matchedRow[0][1],matchedRow[0][0],matchedRow[0][2],matchedRow[0][4],matchedRow[0][5]];
sheet.getRange(lastRow, 1, 1, 7).setValues([replacement]) ;
}
PS: You can also use event objects to get the values of response (eventObj.values). As mentioned here: https://developers.google.com/apps-script/guides/triggers/events
Hope that helps!