Updating a field based on contents on another tab - google-apps-script

I've built a spreadsheet with multiple tabs.
My 'Source' Tab 1 contains columns 'Id' and 'Name'
My 'Target' Tab 2 contains 'Id'
I'm attempting to iterate through the rows in my Target Tab and read the value of the Id and then search my Source Tab for the Id. When I find the Id, I want to grab the value in the 'Name' field and add it to a cell on the same line of my Target Tab's Id.
I'm having a hard time working through the logic of iterating through one array and ensuring I find the value in either the Target tab or an array of the Target Tabs contents. In the Target tab, there may be more than one occurrence of an Id, and I would need to update them all.
Any suggestions would be welcome!

here is a suggestion :
/* let us say source array with name(columnA) & ID(columnB) is array 'source'
and target array with only IDs is array 'target', you get these with something like*/
var source = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0].getDataRange().getValues();
// and
var target = SpreadsheetApp.getActiveSpreadsheet().getSheets()[1].getDataRange().getValues();// if other columns, change index values in the arrays : 0=A, 1=B ...
// then let's create a 3 rd array that will be the new target with ID + names, and call it 'newtarget'
var newtarget=new Array()
// the iteration could be like this :
for(i=0;i<target.length;++i){ // don't miss any ID
for(j=0;j<source.length;++j){ // iterate through source to find the name
if(target[i][0].toString().match(source[j][1].toString()) == source[j][1].toString()){
var newtargetrow=[source[j][0],target[i][0]] // if match found, store it with name (idx0) and ID (idx 1)
}else{
var newtargetrow=['no name found',target[i][0]] // if no match, show it in name column
}
newtarget.push(newtargetrow);// store result in new array with 2 columns
} //loop source
} // loop target
/* now you have a newtarget array that can directly overwrite the old target
using setValues() */
var sh = SpreadsheetApp.getActiveSpreadsheet().getSheets()[1];// assuming the target sheet is sheet nr2
sh.getRange(1,1,newtarget.length,newtarget[0].length).setValues(newtarget);
//
note I didn't test this code but it should give you a starting point. I made comparison on strings with a .match but you could use other comparisons (direct equality between array elements)... you could also remove spaces in IDs if there is a risk of undesired spaces in the sheet data... I don't know your data so it's up to you.

Related

Setting the row Choices in a Google Form Multi Choice Grid Item?

I have used the Setting the Column Choices in a Google Form Multi Choice Grid Item? topic to produce a Apps Script which produces a dynamic Google Form Grid Item based on an underlying Google sheet.
A trigger runs on the google sheet, so that when a new record is entered, this should be replicated as a new row in the form item.
The problem I am having is if I use an array formula to populate the setRows attribute at line 20 (namesList.setRows([studentNames])) of the code, the form item is produced but each of the names is produced as a single concatenated line (i.e. Person 1, Person2, Person3 etc).
If I hard code the person elements in the setRows element at line 21 (namesList.setRows(['Person1','Person2','Person3','Person4','Person5','Person6','Person7'])) of the code, the form item is produced, with (in this instance) 7 lines, with each record having its own line. i.e.
Person1
Person2
Person3
I don't want to use a hard coded list as the list of person names could increase on a regular basis, so need to use an array to produce the list but then use the array in the setRows element.
function updateForm(){
// call your form and connect to the drop-down item
var form = FormApp.openById("FakeFormID");
var namesList = form.getItemById("FakeFormItemID").asGridItem();
// identify the sheet where the data resides needed to populate the drop-down
var ss = SpreadsheetApp.getActive();
var names = ss.getSheetByName("FakeSheetName");
// grab the values in the first column of the sheet - use 2 to skip header row
var namesValues = names.getRange(2, 1, names.getMaxRows() - 1).getValues();
var studentNames = [];
// convert the array ignoring empty cells
for(var i = 0; i < namesValues.length; i++)
if(namesValues[i][0] != "")
studentNames[i] = namesValues[i][0];
// populate the drop-down with the array data
namesList.setRows([studentNames]) //creates a concatenated list
namesList.setRows(['Person1','Person2','Person3','Person4','Person5','Person6','Person7']) //creates individual lines but is hard coded
namesList.setColumns(['1','2','3','4','5']);
}
Replace
studentNames[i] = namesValues[i][0];
by
studentNames.push(namesValues[i][0]);
the above is to avoid that studenNames includes empty elements.
Also instead of getMaxRows() use getLastRow() as the last it's very likely that will make your script more efficient as usually a sheet includes empty rows at the bottom that under certain circumstances might be too many.

How to delete Named Ranges with Tab names via Apps Script

I'm trying to clean a spreadsheet of a slew of Named Ranges I no longer need, and leave behind the few I'm still using. All of these Named Ranges include the Tab name, because they originate on a template Tab (named tmp), from which other Tabs are duplicated. Even after I delete all the spinoff Tabs from the sheet and leave behind only tmp, the 'tmp'! appears in the names of the Ranges, both as displayed in the Named Ranges sidebar and as they come in on getNamedRanges().
When I try to selectively delete obviated Named Ranges, no matter how I spec the name of the Ranges, I get errors saying no such Named Ranges exist. Basically, I'm feeding back the same information getNamedRanges() and getRange().getSheet().getSheetName() give me, only to have it garbled along the way.
The problem is isolated in the following test snippet, and involves rendering the single quotes around the Tab name. I have tried several approaches, including escaping the single quotes with slashes, and have added to the code the Comments of the errors I got on the line targetDoc.removeNamedRange(namedRange).
const analyzerDoc = '1pYgcX2dxzHd4cCofy0RFZTzEl36QesiakMGIqCC2QlY'
const openAnalyzerDoc = SpreadsheetApp.openById(analyzerDoc)
function testDeleteNamedRange (){
var docUrl = openAnalyzerDoc.getRangeByName('docUrl').getValue();
var targetDoc = SpreadsheetApp.openByUrl(docUrl);
// var namedRange = 'dyCl_MoodEntries' // The named range "dyCl_MoodEntries" does not exist.
// var namedRange = 'tmp!dyCl_MoodEntries' // The named range "tmp!dyCl_MoodEntries" does not exist.
// var namedRange = "'tmp'!dyCl_MoodEntries" // The named range "'tmp'!dyCl_MoodEntries" does not exist.
// var namedRange = "\'tmp\'!dyCl_MoodEntries" // The named range "'tmp'!dyCl_MoodEntries" does not exist.
targetDoc.removeNamedRange(namedRange);
}
This bug is in the way of a longer function, which is working fine but for the part isolated in this test function.
The longer function gets the names and Tabs of Ranges to delete from this sheet:
What is the right way to do this? Thank you!
This function will remove all of the named range that have their sheet name within the range name.
function deleteAllNamedRange() {
const ss = SpreadsheetApp.getActive();
ss.getNamedRanges().filter(r => ~r.getName().indexOf(r.getRange().getSheet().getName())).forEach(r => r.remove());
}
I have an answer to my own question. There is probably more than one solution, but I have chosen to sidestep the challenge I am facing, and instead of specifying the Named Ranges by name, I am going to spec them by their position in the document's Named Ranges, and simply use remove() instead of removeNamedRange(namedRange). I had gotten so caught up in the recommended method involving forEach, that I had forgotten that the outcome of getNamedRanges() is not an object, but an array.
The solution then lies in amending my process of collecting the Names and other information from the result of getNamedRanges(). Instead of using forEach, I loop over the results of getNamedRanges(), and while I get the information I desire concerning each Named Range, I also log the loop iteration and thereby get the Index Number of each Named Range.
I proceed as before, pasting this information in a Tab where I can select which Ranges to delete.
My delete function then loops over the Named Ranges directly, in reverse order, and checks the loop iteration against the Ranges I have ticked off in that analysis Tab.
I have tested this in a sample document; you may view it here.
In this demo, all functions are within the same document, so I'm using getActive() instead of openByUrl.
This document has 3 Tabs named Sheet1, Sheet2 and Sheet3. Each Tab has 3 Named Ranges named Moe, Larry and Curly. There is also a Tab NamedRanges which the following function collects Named Range into:
function getnamedRanges() {
var namedRanges = SpreadsheetApp.getActive().getNamedRanges();
var namedRangeData = [];
for (i=0; i<namedRanges.length; i++) {
var namedRange = namedRanges[i];
var nrName = namedRange.getName();
var nrRange = namedRange.getRange().getA1Notation();
namedRangeData.push([nrName,nrRange,i])
}
SpreadsheetApp.getActive().getSheetByName('NamedRanges').getRange(2,1,namedRangeData.length,3).setValues(namedRangeData)
}
Here's the Named Range Tab after running that function, and choosing 3 Named Ranges to delete:
Next, here is the function that removes the selected Named Ranges:
function deleteSelectedNamedRanges () {
var namedRangeData = SpreadsheetApp.getActive().getSheetByName('NamedRanges').getDataRange().getValues();
namedRangeData.shift(); // Remove header row data.
var rangesToDelete = namedRangeData.filter(function(nrDatum) {if (nrDatum[3]==true) return nrDatum});
// [3] equivalates to Column D, the checkboxes where I select which Named Ranges to delete.
console.log (rangesToDelete.map(value => value[0])); // [ 'Sheet3\'!Moe', 'Sheet2\'!Curly', 'Sheet1\'!Moe' ]
console.log (rangesToDelete.map(value => value[2])); // [ 0, 1, 5 ] // [2] is the index number of the Named Ranges.
/* The order here derives from how values in Tab Named Ranges happen to be sorted.
In this instance, I have not changed that order, so the Named Ranges To Delete are in ascending order.
For one thing, this is the opposite of what we want;
for another, I want to be able to sort the Named Range Tab freely before making my selections.
So, we must sort this data in DESCENDING order. */
rangesToDelete.sort(function(value1,value2){if (value1[2]<value2[2]) return 1; if (value1[2]>value2[2]) return -1; return 0});
console.log (rangesToDelete.map(value => value[0])); // [ 'Sheet1\'!Moe', 'Sheet2\'!Curly', 'Sheet3\'!Moe' ]
var rangesToDeleteIndexNumbers = rangesToDelete.map(value => value[2])
console.log (rangesToDeleteIndexNumbers); // [ 5, 1, 0 ]
var namedRanges = SpreadsheetApp.getActive().getNamedRanges();
for (i=namedRanges.length-1; i>=0; i--) {
/* We must loop in descending order because deleting Named Ranges will change the index numbers
of all Named Ranges that come after each we delete. */
if (rangesToDeleteIndexNumbers.indexOf(i) !== -1) {namedRanges[i].remove(); console.log ('Removed NR # '+i)}
// Delete Named Range if this iteration number can be found in rangesToDeleteIndexNumbers.
}
}
After running this function, you can see that the 3 Names Ranges have been removed, leaving 6 behind:

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')

Make a new array with different columns

I want a report with a few selected columns from a sheet.
The selection will be as per the user input like "1,4,7,12" for including column 1,4,7 and 12 in the report.
Or the input can be "2,3" to include column 2 and 3 in the report.
//user input is saved in variable input "1,4,7,8" or "2,4" or "3,2,5"
//
var jobvals = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getDataRange().getValues();
var flds=input.split(",");
var tstr="";
for (i=1;i<jobvals.length;i++){
for (s=0;s<flds.length;s++){
//tstr+=jobvals[i][flds[s]]+","; //I can make a comma separated string for each row. But, I want an array for further processing
//getting stuck here - how to make an array
}
tstr+="\n";
}
I want to make a new array with the selected columns. The number of columns in the result array will vary. And, the column selection will also vary.
Appreciate some ideas how to achieve this.
I got help from
Best method to extract selected columns from 2d array in apps script
Google Script GetRange with Filter based on values in a column AND select only certain columns
Solution
function extract_some_columns{
var jobvals = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getDataRange().getValues();
var new_array = getCols(jobvals,'['+input+']');
//new_array has only column 1,3,5 (as specified in the variable input = "1,3,5")
}
function getCols(arr,cols) {
return arr.map(row =>
row.filter((_,i) => cols.includes(++i)))
}
You can declare an empty array and add each element one by one by using push() function on an array. Your code should look like this:
var jobvals = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getDataRange().getValues();
var flds=input.split(",");
var tstr=[];
for (i=1;i<jobvals.length;i++){
for (s=0;s<flds.length;s++){
tstr.push(jobvals[i][flds[s]]);
}
}

AppsScript to match ID's and write value to cell when match is found

I have a project in which I have two sheets, 'staging' and 'export' with cells that contain a unique ID. When I set the value in Column A of 'staging' to "export", that row of data is included on the sheet 'export'. I have a script on the export sheet that when run, exports the data to a csv file and sends it to a google drive folder. It also copies the set of exported data to another sheet named 'exported' as an archive of exported records. What I want the script to also do is loop through the id's of both the 'staging' and 'export' rows, find the id's that match, and when the match is found, change the value in column A of 'staging' to "Complete", which removes it from the export tab.
I am unsure how to write the loop and if statement to toggle the status column on the 'staging' sheet. Here is my code so far:
//this script will toggle the export? column on staging to 'complete'
//after the export script completes
function toggleComplete(){
//get the ID's on the 'staging' tab
const ss = SpreadsheetApp.getActiveSpreadsheet();
const staging = ss.getSheetByName('staging');
const stagingIDRange = staging.getRange(2,4,staging.getLastRow(),1).getValues();
const statusRange = staging.getRange("A2:A");
console.log(stagingIDRange);
//get the ID's on the 'toexport' tab
const exportsheet = ss.getSheetByName('toexport');
const toExportIDRange = exportsheet.getRange(2,8,exportsheet.getLastRow(),1).getValues();
console.log(toExportIDRange);
//match to the id in column H of the toexport tab
//if (stagingIDRange = toExportIDRange)
//get row number of matching ID's
//use row number to toggle the matching rows
//if the ids's match then toggle column A on staging for that row to complete
//statusRange.setValue('Complete');
}
Any help would be appreciated. Here is a link to make a copy of the sheet that I am working on. https://docs.google.com/spreadsheets/d/1XtdPk93z_gSBWdJpv8x3cAf6-QCAQ69a2x7j8Net0aQ/copy
An approach you can try is to get the 1D array equivalent of both ID columns for easier comparison.
Once you got them, traverse the staging IDs and then by using includes, you can check if a staging ID is present in the toExport IDs array.
Code:
function toggleComplete(){
const ss = SpreadsheetApp.getActiveSpreadsheet();
//get the ID's on the 'staging' tab
const staging = ss.getSheetByName('staging');
const stagingIDRange = staging.getRange(2,4,staging.getLastRow() - 1,1).getValues();
const stagingIDs = stagingIDRange.flat().filter(String);
//get the ID's on the 'toexport' tab
const exportsheet = ss.getSheetByName('toexport');
const toExportIDRange = exportsheet.getRange(2,8,exportsheet.getLastRow() - 1,1).getValues();
const toExportIDs = toExportIDRange.flat().filter(String);
stagingIDs.forEach(function (stagingID, index){
if(toExportIDs.includes(stagingID))
// properly offset the row parameter
staging.getRange(index + 2, 1).setValue("COMPLETE");
});
}
Note:
flat transforms your multilevel array into a 1D array
filter(String) filters your data and remove non-string elements (thus removes blank elements in your array)
includes checks if a value is an element of an array
Output: