How to edit an imported Google sheet using ImportRange? - google-apps-script

I used =QUERY(IMPORTRANGE..) to import data from Sheet 1 to Sheet 2 for Column A, B & C. I have to take note in Column D of Sheet 2 for each entry imported. However, for any new one added, the note (for the previous ones) in Column D stays in the same cells. For example, the formula is in A2, so the new data will be added to A2,B2 & C2. The note is in D2. When a new one is imported, the previous one moved to A3, B3 & C3. However, the note is still in D2.
Is there any way to make those notes to move to the next row automatically when a new entry is added?
Here are the files the data has to be imported to and from: https://drive.google.com/drive/folders/1wbOfW9PbSfJbTBv_CwXOTiyyN_LBTiFq?usp=sharing

If my understanding is correct, you want to accomplish the following:
Import data from one spreadsheet to another using IMPORTRANGE.
Add notes manually to a column in your destination spreadsheet.
When a new row is imported to the destination spreadsheet and make previously imported data, the notes should move too.
To achieve that, you would need to keep track of which note belongs to which row of imported data. Both sets of data should be somehow attached. Considering that you have a timestamp in column A, and that this timestamp is probably unique for each row, this timestamp could be used to attach both (if that's not possible, I'd propose adding another column that will be used to identify each row without ambiguity, via some kind of id).
At this point, I would consider using Google Apps Script to accomplish your needs. With this tool, you could develop the functionality that =QUERY(IMPORTRANGE(...)) is providing right now, and you could use other Apps Script tools to reach the desired outcome. Two tools could be specially necessary to accomplish this:
onEdit triggers, to keep track of when the different spreadsheets are edited and make the appropriate changes if that's the case (basically, copying data from one spreadsheet to another).
Properties Service, to store the information about which note is attached to which row of data.
You could do something on the following lines:
Install two edit triggers, (1) one that will fire a function when the source spreadsheet is edited, and (2) another one that will fire when the destination spreadsheet is edited (a simple trigger cannot be used because you have to reference files to which your spreadsheet might not be bound). You can do this manually or programmatically.
Create a function that, for each note that is added to the destination sheet (in this code sample, that's in column D, please change according to your preferences), stores a key-value pair where the key is the value in column A (which should uniquely identify a row of data) and value is the note. This will be used later for the script to know where each note belongs to:
function storeNotes(e) {
var scriptProperties = PropertiesService.getScriptProperties();
var cell = e.range;
var sheet = cell.getSheet();
var rowIndex = cell.getRow();
var column = cell.getColumn();
var noteColumn = 4; // The column where notes are written, change accordingly
// Check whether correct sheet, column and row is edited:
if (column == noteColumn && rowIndex > 1 && sheet.getName() == "Destination") {
var row = sheet.getRange(rowIndex, 1, 1, sheet.getLastColumn()).getValues()[0];
scriptProperties.setProperty(row[0], row[noteColumn - 1]); // Store property to script properties
}
}
Create a function that, every time the source spreadsheet is edited, will delete all content in the destination spreadsheet and copy the data from the source. Then, it will look at the script properties that were store and, using this information, it will write the notes to the appropriate rows (because I see you only want to copy/paste some of the columns, in this sample some of the columns - the ones whose index is in columnsToDelete - are not copied/pasted, you can change this easily to your preferences):
function copyData(e) {
var range = e.range;
var origin = range.getSheet();
var row = range.getRow();
if (origin.getName() == "Origin" && row > 1) { // Check if edited sheet is called "Origin" and edited row is not a header.
var dest = SpreadsheetApp.openById("your-destination-spreadsheet-id").getSheetByName("Destination");
var firstRow = 2;
var firstCol = 1;
var numRows = origin.getLastRow() - 1;
var numCols = origin.getLastColumn();
var values = origin.getRange(firstRow, firstCol, numRows, numCols).getValues();
// Removing some of the columns to get copied/pasted (in this case B and D):
var columnsToDelete = [1, 3];
values = values.map(function(row) {
for (var i = row.length; i > 0; i--) {
for (var j = 0; j < columnsToDelete.length; j++) {
if (i == columnsToDelete[j]) {
row.splice(i, 1);
}
}
}
return row;
})
// Copying content from source to destination:
var firstRowDest = 2;
var firstColDest = 1;
var numRowsDest = values.length;
var numColsDest = values[0].length;
var noteColumn = 4;
var currentValues = dest.getDataRange().getValues();
if (currentValues.length > 1) dest.deleteRows(2, dest.getLastRow() - 1);
var importedRange = dest.getRange(firstRowDest, firstColDest, numRowsDest, numColsDest);
importedRange.setValues(values);
// Writing notes stored in Properties in the appropriate rows:
var properties = PropertiesService.getScriptProperties().getProperties();
for (var i = 0; i < values.length; i++) {
for (var key in properties) {
if (key == values[i][0]) {
dest.getRange(i + 2, noteColumn).setValue(properties[key])
}
}
}
}
}
Notes:
All these functions should be in the same script if you want all both functions to use Properties.
In this sample, the sheet with source data is called Origin and the sheet where it is copied is called Destination (from what I understood, they are in different spreadsheets).
In this simplified example, columns A, B, E from source sheet get copied to columns A, B, C of the destination sheet, and notes are added to column D. Please change this to fit your case by modifying the corresponding indexes.
I hope this is of any help.

Thank you everyone for helping me, especially Lamblichus & user11982798. I recently noticed that importrange will import data to the destination in the same order as that of the source file. Before I sorted the data based on the timestamp in descending order so the new entry was always on the first row. If I changed it to ascending order, the new one is added to the last row, so the note/comment order will not be affected.
Is it possible to update the note/comment in the destination file back to the source one?

If the note is string please try to put in D2 like this:
=ARRAYFORMULA(if(row(A2:A) = max(arrayformula(if(ISBLANK(A2:A),0,row(A2:A)))),"Your Note", ""))
This will automatically place your note to last row of data

Related

Update or create new row based on Unique ID Google Sheets

I have found a few options that come close to what I want to do, but nothing matching it exactly.
The request is simple enough.
"Sheet A" - Master Sheet (has 1 header row)
"Sheet B" - Input Sheet (has 1 header row)
"Column C" - Unique ID (same column on both sheets)
Trigger
Sheet B is edited
Actions
Script finds the Unique ID from Sheet B in Column C and looks for it in Sheet A in Column C.
If it finds it, the entire respective row on Sheet A is replaced with the entire respective row from Sheet B.
If it does not find it, a new row is added at the bottom of Sheet A and the entire respective row from Sheet B is added as a new record into the new row at the bottom of Sheet A.
The entire respective row on Sheet B is Deleted.
Actions repeat until there are no populated rows in Sheet B from row 2 on (i.e. excluding the row 1 header).
Thanks
Edits
For clarification on why I am looking to do this. I have a Form that is being submitted and sending the data through to Google Sheets (Cognito -> Zapier -> Google Sheets). Part of this form involves repeating sections (line items). The current method that is importing the responses has no issue with adding new responses correctly, however when a response is updated, it cannot find/update the existing row(s) correctly for the repeating sections. So I had the intention of using Sheet A as my master sheet and then using Sheet B to simply be a receiving sheet. This way I can just submit every entry (including updated ones) as a "new" entry on Sheet B, and then have my script do the updating.
Sheet B will be edited automatically every time a new form entry is submitted or updated. The "edit" is basically a new row being added and data being populated into that row. It may be a good idea to add a 1 minute timer to the trigger so that if there is lots of data being added that it gives time for that to happen.
I am not even remotely close to a script expert. I just browse around different scripts other people have made and try to combine them to get them to work for what I need. I have found scripts that will move a row over and then delete it, but it does not check for matching values to update. I have found other scripts that check for unique values and copy over, but they do not delete the original row on the other sheet. I have tried to combine them, but since I don't have the base knowledge, I can't seem to get it to work.
As a workaround I'd use the onEdit simple trigger and a O(n) search
Here's my approach:
function onEdit(e) {
// If it's not the Sheet B it won't make changes
if (e.range.getSheet().getName() !== "Sheet B") {
return;
}
var range = e.range;
var numberRow = range.getA1Notation().slice(1);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetA = ss.getSheetByName("Sheet A");
var sheetB = ss.getSheetByName("Sheet B");
var currentRowB = sheetB.getRange(`A${numberRow}:D${numberRow}`);
var id = currentRowB.getValues()[0][2];
// There's to be 4 values in the row (no empty values)
if(currentRowB.getValues()[0].filter(value => value !== '').length === 4) {
// Get all the values in Sheet A
var rows = sheetA.getDataRange().getValues();
for (row=1; row < rows.length; row++) {
// If column C matches the ID replace the row
if(rows[row][2] === id) {
var currentRowA = sheetA.getRange(`A${row+1}:D${row+1}`);
currentRowA.setValues(currentRowB.getValues());
currentRowB.deleteCells(SpreadsheetApp.Dimension.COLUMNS);
return;
}
}
// If the ID doesn't match then insert a new row
var newRow = sheetA.getRange(`A${rows.length+1}:D${rows.length+1}`);
newRow.setValues(currentRowB.getValues());
currentRowB.deleteCells(SpreadsheetApp.Dimension.COLUMNS);
}
}
Which meets the requirements you asked:
Script finds the Unique ID from Sheet B in Column C and looks for it in Sheet A in Column C. (line 19-28)
If it finds it, the entire respective row on Sheet A is replaced with the entire respective row from Sheet B. (line 22-24)
If it does not find it, a new row is added at the bottom of Sheet A and the entire respective row from Sheet B is added as a new record into the new row at the bottom of Sheet A. (line 31-33)
The entire respective row on Sheet B is Deleted. (line 22 and 33)
I used this Sheet format as example:
Both Sheets have the same format. Keep in mind that this script checks if there's a valid row (in this specific case 4 columns which compose a row) before replacing it.
As a different approach (handling blank data)
In a summary this script should run every X minutes or the time you want it doesn't matter if there's new data incoming because this code will handle all the data given a certain time.
I edited the code in order to use the Z1 cell as a blocker cell and a time based trigger:
Trigger:
Code
function processCells() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetA = ss.getSheetByName("Sheet A");
var sheetB = ss.getSheetByName("Sheet B");
// If it's not the Sheet B or if there's a process running it won't make changes
if (sheetB.getName() !== "Sheet B" || sheetB.getRange("Z1") === "Running") {
return;
}
// Use the Z1 cell in order to block or unblock this sheet
sheetB.getRange("Z1").setValue('Running');
// Process all the rows
var numCells = sheetB.getDataRange().getValues().length + 1;
for (numberRow = 2; numberRow <= numCells; numberRow++) {
var currentRowB = sheetB.getRange(`A${numberRow}:D${numberRow}`);
var id = currentRowB.getValues()[0][2];
// Get all the values in Sheet A
var rows = sheetA.getDataRange().getValues();
var match = false;
for (row=1; row < rows.length; row++) {
// If column C matches the ID replace the row
if(rows[row][2] === id) {
var currentRowA = sheetA.getRange(`A${row+1}:D${row+1}`);
currentRowA.setValues(currentRowB.getValues());
currentRowB.deleteCells(SpreadsheetApp.Dimension.COLUMNS);
match = true;
break;
}
}
if(!match) {
// If the ID doesn't match then insert a new row
var newRow = sheetA.getRange(`A${rows.length+1}:D${rows.length+1}`);
newRow.setValues(currentRowB.getValues());
currentRowB.deleteCells(SpreadsheetApp.Dimension.COLUMNS);
}
}
sheetB.getRange("Z1").setValue('');
}
Note that every time the script runs it'll check if there's another one processing the rows by using Z1.
References
onEdit
Event Object

How to use a cell value as a sheet name in Google Apps Script

I'm trying to create a script that copies rows from one sheet to another. The core of this is something I've done before using GetSheetByName() to name the source and target sheets, but for this particular project there are a lot of target sheets, so I need the target sheet names to be variable and based on a String that will be in the final cell of each row.
To complicate matters, that final cell will be using a VLOOKUP formula to get the relevant sheet name based on the data in the row.
I know I've been able to use cell references as a sheet reference in a formula natively in Google Sheets using the INDIRECT function, but I haven't found an equivalent for Google Apps Script.
Any help would be greatly appreciated.
Thanks.
EDIT
Assuming that the last cell of each row is in the same column and you want to get the last cell of every row, you could use getRange(rage).getValue() to get all the data within the specified range or getDataRange().getValue() to get all the data within a Sheet. Both will return an array which you can loop through to access the data.
Sample Code:
function getRowLastCell(){
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spreadsheet.getSheetByName("Sheet1");
var data=sheet.getDataRange().getValues(); //get all data from Sheet1
var array = new Array();
for(var i = 0; i < data.length; i++) {
for(var j = 0; j < data[i].length; j++) {
if(j == data[i].length-1){ //check the last cell for each row.
array.push(data[i][j]); //save to array
}
}
}
}
In my example, I looped through the array and added condition that will check the last element of each row then save the content to an array.
From there, you can loop through the newly populated array and access each element to do operations.
Reference
GetDataRange()
GetRange()
If I understand correctly, what you want is to copy the values of each rows of a sheet into their respective destination sheet which can be determined by the last column of each row.
I tried replicating the scenario on my end and came up with this code:
function myFunction(){
let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("test");
let range = sheet.getRange(1,1,sheet.getLastRow(),sheet.getLastColumn())
for(var i=1;i<=range.getLastRow();i++){
let sourceval = sheet.getRange(i,1,1,range.getLastColumn());
let destname = sheet.getRange(i,range.getLastColumn()).getValue();
let destsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(destname);
sourceval.copyTo(destsheet.getRange(destsheet.getLastRow()+1, 1));
}
}
Snippet of sample spreadsheet
This may not be the exact code that you are looking for. If you have questions, feel free to comment.

retrieve google forms edit urls and maintain existing data structure

I am interested in making some alterations to a snippet of the code that allows you to record URLs to edit Google Form's responses.
What I wish to achieve is a repeatable automated process of receiving those URLs upon form submit. However, one condition that I want to follow is to have those URLs pasted in the first column of the spreadsheet, maintaining the integrity of existing data in the following columns.
I think it is the only viable option for me, as the form is still in the development stage, and may receive more variables as the time progresses.
I have attempted:
Manually inserting a column in the Responses' Google Sheet and then try to setup the assignEditUrls{} function, but it yields no results.
Using a one time mini-function to insert the left most column, and then run the code. Also produced no results, unless it was included in the main function, which then inserts the leftmost column every time there is a submission.
Here's that:
function insertLeftMostColumn(){
//insert a column before the first one
sheet.insertColumnBefore(1);
//rename the header for the new column
var cell = sheet.getRange("A1");
cell.setValue("columnName");
}
This is the function.
function assignEditUrls() {
var form = FormApp.openById('1cg7bGRQjsv91sSCjYCwNJyoB3wN_MZ_9raV3tP3v1MA');
//enter form ID here
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Form Responses 1');
var lastColumn = sheet.getLastColumn(); // This logs the value of the very last column of this sheet (without the values)
//Change the sheet name as appropriate
var data = sheet.getDataRange().getValues();
var urlCol = 5;
var responses = form.getResponses();
var timestamps = [], urls = [], resultUrls = [];
for (var i = 0; i < responses.length; i++) {
timestamps.push(responses[i].getTimestamp().setMilliseconds(0));
urls.push(responses[i].getEditResponseUrl());
}
for (var j = 1; j < data.length; j++) {
resultUrls.push([data[j][0]?urls[timestamps.indexOf(data[j][0].setMilliseconds(0))]:'']);
}
sheet.getRange(2, urlCol, resultUrls.length).setValues(resultUrls);
}
current result
expected result
If my understanding is right, you want to know how to insert the edit URLs in the first column instead of the last one.
If so, you need to implement some small modifications in your code:
Insert an empty column into the sheet, e.g. with the function insertLeftMostColumn(), as you intended above. Keep in mind that if you want to run the function only once, it needs to be called separately from assignEditUrls(), so you have to define sheet one more time within the function
urlCol is the column number, where the URLs shall be pasted. To change the column to the first one, modify var urlCol = 5; to var urlCol = 1;
data[j][0]?urls[timestamps.indexOf(data[j][0] tries to find timestamps in the first column.
Given that you inserted an empty column and the timestamps have been shifted to the second column - you need to change the code to data[j][1]?urls[timestamps.indexOf(data[j][1];
If you want the new URLs to be inserted every time upon form submit, you need to incorporate into your assignEditUrls() function a Form Submit trigger. Here you can find information about how to incorporate installable triggers.

Script to copy from one sheet to another, needs edit

I have this script which is working well, but i need to edit it to
a) only return new rows since last run
b) only return certain cells instead of whole row
any guidance would be greatly appreciated
function Copy() {
var sourceSheet = SpreadsheetApp.openById('1WAtRDYhfVXcBKQoUxfTJORXwAqYvVG2Khl4GuJEYSIs')
.getSheetByName('Jobs Log');
var range = sourceSheet.getRange(1, 1, sourceSheet.getLastRow(), sourceSheet.getLastColumn());
var arr = [];
var rangeval = range.getValues()
.forEach(function (r, i, v) {
if (r[1] == 'Amber') arr.push(v[i]);
});
var destinationSheet = SpreadsheetApp.openById('137xdyV8LEh6GAhAwSx4GmRGusnjsHQ0VGlWbsDLXf2c')
.getSheetByName('Sheet1');
destinationSheet.getRange(destinationSheet.getLastRow() + 1, 1, arr.length, arr[0].length)
.setValues(arr);
}
In order to only check new data added after last runtime we have to store .getLastRow() value in properties and retrieve it every runtime. We would also have to work under a few assumptions:
In the input data new values are only appended at the bottom and never inserted between other data
Data is never deleted from the input sheet (if you ignore this, then you must also have an update script for the last row that runs after deleting data)
The sheet is not sorted after new data is added but before this script is run.
So you would need something along the lines of
var sourceSheet = SpreadsheetApp.openById('1WAtRDYhfVXcBKQoUxfTJORXwAqYvVG2Khl4GuJEYSIs')
.getSheetByName('Jobs Log');
var lastRow = sourceSheet.getLastRow();
// note that you need to hav the script property initialized and stored
// or adjust the if to also check if prevLastRow gets a value
var prevLastRow = PropertiesService.getScriptProperties().getProperty('lastRow')
if (lastRow <= prevLastRow) {
return; // we simply stop the execution right here if we don't have more data
}
// then we simply start the range from the previous last row
// and take the amount of rows added afterwards
var range = sourceSheet.getRange(prevLastRow,
1,
lastRow - prevLastRow,
sourceSheet.getLastColumn()
);
As for the second question, inside the forEach you need to simply push an array into arr that will contain only the columns you want. So for example
if (r[1] == 'Amber') arr.push(v[i]);
changes into
if (r[1] == 'Amber') arr.push([v[i][0], v[i][3], v[i][2]]);
which will output A D C columns (in that order) for each row.
Finally, the last thing you need to run before the script ends is
PropertiesService.getScriptProperties().setProperty('lastRow', lastRow)
which will let us know where we stopped the next time we run the script. Again, keep in mind that this works only if new data will always be in new rows. Otherwise, you need to do a different method and retrieve 2 arrays of data. 1 for the entire input sheet and 1 for the output sheet. Then you would have to perform 2 if checks. First one to see if your criteria are met and a second one to see if it already exists in the output data.

Google App Scripts(spreadsheet) - consolidate data into one sheet

Here is the set up
We have a contest with all employees based on project scores. Each project has two categories of employees(4 employees per category) and two scores(one for each category of employee).
I need to grab all the scores for the employees and output it into a spreadsheet. The following spreadsheet has misc. columns removed
Sheet Explanation
The sheet labeled "Example data" is the source we will be pulling data from
We need to match Editor and Editor Score
We need to match Webmaster and webmaster score
The sheet labeled "Example output" is what I want to be generated in another spreadsheet named "Contest Result" with the sheet name from the source sheet(They are named by date ranges).
We need to compile each employee by the categories
We need to compile all scores to the row for a singular employee
I had found this Removing Duplicates Article that seemed to at least process the information and compare it in a manner that I think this can be done, but am failing to make it work due to being inexperienced.
Did not know what Transpose was till someone commented :)
Here is the solution in another article for how to pull it off with Google Apps Script and with using the spreadsheet option.
How to split and transpose results over 2 columns
Here is the actual code I used to make it work(it is a little horrible but I tried) suggestions on how to improve this?:
function createScoreSheet() {
// Get Source spreadsheet
var source = SpreadsheetApp.getActive();
var sourceSheet = source.getActiveSheet();
var SourceActivate = sourceSheet.activate();
// Set Sheet Name
var sheetName = sourceSheet.getSheetName();
// Set Values to transpose and combine
var sourceEditor = sourceSheet.getRange("C1:C51");
var sourceWeb = sourceSheet.getRange("D1:D51");
var editorScores = sourceSheet.getRange("L1:L51");
var webScores = sourceSheet.getRange("K1:K51");
// Used to create a new spreadsheet
var sheetNameNew = sheetName + " Scores";
var createSheet = SpreadsheetApp.getActive().insertSheet(sheetNameNew,0);
var targetSheet = source.getSheetByName(sheetNameNew);
var totalScore = 1;
// s is the the counter we use to stick values into the rows
var s = 3;
// n is the the counter we use to stick values into the columns
var n = 1;
// loops through twice, once for the editor values, once for the webmaster
for (var j = 1; j<3; j++) {
if (j == 1) {
// grab values for the editors and copy to new sheet
sourceEditor.copyTo(targetSheet.getRange("A1"));
editorScores.copyTo(targetSheet.getRange("B1"));
// delete the header row then sort the column ASC by default
targetSheet.deleteRow(n);
targetSheet.sort(1);
// Find the last value to see how many scores we have
var lastRow = targetSheet.getLastRow();
}
if (j == 2) {
// grab values for the webmasters and copy to new sheet
sourceWeb.copyTo(targetSheet.getRange(n,1));
webScores.copyTo(targetSheet.getRange(n,2));
// delete the header row then sort the column ASC by default
targetSheet.deleteRow(n);
lastRow = targetSheet.getLastRow();
targetSheet.getRange(n,1,lastRow,2).sort(1);
lastRow = targetSheet.getLastRow();
}
// this loop will check to see if the value of the cell is equal to the next on the list and move the score
for (var i = 1; i<lastRow+1; i++) {
// Grab the name of the current row and the next
var firstName = targetSheet.getRange(n,1).getValue();
var nextName = targetSheet.getRange(n+1,1).getValue();
// Grab the scores
var oldScore = targetSheet.getRange(n+1,2);
var newScore = targetSheet.getRange(n,s);
// Loop to check to see if the firstname is blank and break to find the next value
if (firstName === "") {
break;
}
// checks to see if name is equal to the next then shifts then copies the score and adjust the horizontal position
if (firstName == nextName) {
totalScore = oldScore + newScore;
oldScore.copyTo(newScore);
s = s+1;
targetSheet.deleteRow(n+1);
}
// resets horizontal position for the score and increases the row
else {
s=3;
n=n+1;
}
}
// kills remaining rows
targetSheet.deleteRows(n,37);
}
}
I would do it like this:
If you want to generate the names automatically as well, then write this to the output sheet A1:
=unique('Example Data'!B2:B) - This function simply generate the editor names to the A2-A5 cells.
Now write this to the B2 cell:
=transpose(filter('Example Data'!E:E,'Example Data'!B:B=A2)) - This function filters the editor points according to the given name in the beginning of the row (in this case its A2). Then transposes the result in a horizontal form. To get the result for the other rows, simply populate this formula down.
I think you can find out the rest. :)
Hope it helps.