Dynamically Validating Multiple Google Sheet Tabs - javascript

I am writing a script for google sheet validation on localization tests. I've gotten stuck on some of the logic. The purpose of the script is to 1) Iterate through all tabs. 2) Find the column on row 2 that has the text "Pass/Fail". Lastly, 3) Iterate down that column and return the rows that say Fail.
The correct script to look at is called combined(). Step 1 is close to being correct, I think. Step 2 has been hard coded for the moment and is not dynamic searching the row for the text. Step 3 is done.
Any help would be great :)!!! Thanks in advance.
https://docs.google.com/spreadsheets/d/1mJfDtAi0hHqhqNB2367OPyNFgSPa_tW9l1akByaTSEk/edit?usp=sharing
/*This function is to cycle through all spreadsheets.
On each spreadsheet, it will search the second row for the column that says "Pass/Fail".
Lastly, it will take that column and look for all the fails and return that row*/
function combined() {
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
var r =[];
for (var i=0 ; i<sheets.length ; i++){//iterate through all the sheets
var sh = SpreadsheetApp.getActiveSheet();
var data = sh.getDataRange().getValues(); // read all data in the sheet
//r.push("test1"); //Testing to make sure all sheets get cycled through
/*I need something here to find which column on row two says "Pass/Fail"*/
for(i=3;i<data.length;++i){ // iterate row by row and examine data in column A
//r.push("test2"); //Testing to make sure the all
if(data[i][7]=='Fail'){ r.push(data[i])}; // if column 7 contains 'fail' then add it to the list
}
}
return r; //Return row of failed results on all tabs
}

At first, it retrieves data at column g. It retrieves a result from the data. The result is 2 dimensional array. The index of each element of the 2D array means the sheet index. If the sheet doesn't include values in column g, the element length is 0.
For example, in the case of following situation,
Sheet 0 doesn't include values in column g.
Sheet 1 includes values in column g. There are "Fail" value at the row number of 3, 4, 5.
Sheet 2 includes values in column g. There are "Fail" value at the row number of 6, 7, 8.
The result (return r) becomes below.
[[], [3, 4, 5], [6, 7, 8]]
Sample script 1:
function combined() {
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
var data =[];
sheets.forEach(function(ss){
try { // In the case of check all sheets, if new sheet is included in the spreadsheet, an error occurs. This ``try...catch`` is used to avoid the error.
data.push(ss.getRange(3, 7, ss.getLastRow(), 1).getValues());
} catch(e) {
data.push([]);
}
});
var r = [];
data.forEach(function(e1, i1){
var temp = [];
e1.forEach(function(e2, i2){
if (e2[0] == "Fail") temp.push(i2 + 3);
});
r.push(temp);
});
return r;
}
If I misunderstand your question, I'm sorry.

Related

Google Apps Script - Compare two sheets for changes by column names instead of hardcoded ranges

I have two sheets, "IMPORT" and "CASES".
In the "IMPORT" sheet, I am importing data from an external source that sometimes have more columns or existing columns are arranged each time differently.
In the "CASES" sheet, this is where I store a weekly snapshot of all last week's imported data, and I add my additional columns with more pieces of information such as comments, next steps etc.
I am looking for a way to compare both sheets without hardcoding any column ranges. I thought the most efficient way to do it is by looking up column header names in both sheets and then checking for changes in reference to the "Case Number" row. Please let me know if you can think of a better way.
I have already managed to write a code to look through headers and identify the Index number for a specific column name, "Case Number".
This column will always be present in both sheets, and it could serve as a reference point to the row that should be validated, but it could be a different row for each sheet at a time.
I will need the same time loop through all the column headers from the CASES sheet and check for updates from the IMPORT sheet.
I only need to check/loop for changes for few specific columns from the CASES sheet. Columns names such: Contact Name, Title, Priority, Status.
I am aiming to achieve 3 possible outcomes:
[ COMPLETED ] "Case Number" from the CASES sheet was NOT FOUND in the IMPORT sheet - that means the case was closed since last week.
Action: Highlight an entire row in the CASES sheet as grey (this will indicate the case is no longer open and should be removed from the list after confirmation).
"Case Number" from the IMPORT sheet was NOT FOUND in the CASES sheet - this means the case is new and needs to be added to the CASES sheet at the bottom.
Action: Copy the data from the IMPORT sheet to the CASES sheet and paste it in the correct columns at the bottom and highlight the entire row as green to indicate a new data entry.
For all non-existing columns in the CASES sheet that are in the IMPORT sheet, those should be skipped.
"Case Number" from the IMPORT sheet WAS FOUND in the CASES sheet - for the matching Case Number records, I need to validate if there were any changes in any CASES sheet columns since last week.
Action: If a change was found in any of the cells, update the cell with new data in CASES sheet and change the cell background colour to yellow to highlight the cell was updated. For cells without changes, skip.
I apologise for the lengthy problem statement.
I am new to JS and GAS, and I wrote it hoping that some JavaScript expert will understand my idea and advise maybe the easier way to complete my project.
Currently, I am stuck with finding a proper way to loop through Header Names then check cell value from the IMPORT sheet and comparing it with the CASES sheet based on the Case Name value/row.
OUTCOME 1 - Completed
OUTCOME 2 - In Progress
OUTCOME 3 - tbd...
I will continue to update this topic to show the latest progress on this project.
All the examples I found so far on the Internet were based on hardcoded ranges of cells and columns. I think my approach is interesting as it gives future-proof flexibility to the datasets.
Please let me know your thoughts or ideas for a more straightforward approach :)
Link to live sheet
UPDATED code:
// Create Top Menu
function onOpen() {
let ui = SpreadsheetApp.getUi();
ui.createMenu('>> REPORTS <<').
addItem('Highlight Closed Cases', 'closedCases').
addItem('Check for new Cases', 'addCases').addToUi();
}
// IN PROGRESS (Outcome 2) - Add and highlight new cases in CASES sheet
function addCases() {
let ss = SpreadsheetApp.getActiveSpreadsheet();
// Get column index number for Case Number
let activeImportCol = getColumnIndex("Case Number", "IMPORT");
let activeCasesCol = getColumnIndex("Case Number", "CASES");
let importHeaders = loadHeaderNames("IMPORT");
let casesHeaders = loadHeaderNames("CASES");
// Load Case Number columns values into array
let loadImportValues = getColumnValues("Case Number", "IMPORT");
let loadCasesValues = getColumnValues("Case Number", "CASES");
// Convert to 1D array
let newImportValues = loadImportValues.map(function (row) { return row[0]; });
let newCasesValues = loadCasesValues.map(function (row) { return row[0]; });
// Get number of columns
var numImportCol = ss.getSheetByName("IMPORT").getLastColumn();
// Loop through IMPORT sheet "Case Number" column to find new Case Numbers - execute OUTCOME 3 or 2
for (var line in newImportValues) {
var isMatched = newCasesValues.indexOf(newImportValues[line]);
if (isMatched !== -1) {
// "Case Number" from the IMPORT sheet WAS FOUND in the CASES sheet - EXECUTE OUTCOME 3
// ****************************************************************************************
// For the matching Case Number records, I need to validate if there were any changes in any CASES sheet columns since last week
// Action: If a change was found in any of the cells, update the cell with new data in CASES sheet
// and change the cell background colour to yellow to highlight the cell was updated. For cells without changes, skip.
} else {
// "Case Number" from the IMPORT sheet was NOT FOUND in the CASES sheet - EXECUTE OUTCOME 2
// ****************************************************************************************
// Copy the new data row from the IMPORT sheet to the CASES sheet and paste it in the correct columns
// at the bottom and highlight the entire row as green to indicate a new data entry.
// For all non-existing/not matching column names in the CASES sheet that are not in IMPORT sheet, those should be skipped.
}
}
}
// COMPLETED (Outcome 1) - Highlight entire row grey for missing values in CASES sheet
function closedCases() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// Load all Casen Number columns values into array
var importValues = getColumnValues("Case Number", "IMPORT");
var casesValues = getColumnValues("Case Number", "CASES");
// Convert to 1D array
var newImportValues = importValues.map(function (row) { return row[0]; });
var newCasesValues = casesValues.map(function (row) { return row[0]; });
// Get column index number for Case Number
var activeCol = getColumnIndex("Case Number", "CASES");
// Get number of columns
var numCol = ss.getSheetByName("CASES").getLastColumn();
// Loop though CASES "Case Number" column and highlight closed cases (not found in IMPORT tab)
for (var line in newCasesValues) {
var isMatched = newImportValues.indexOf(newCasesValues[line]);
if (isMatched !== -1) {
// If found then...
ss.getSheetByName("CASES").getRange(+line + 2, 1, 1, numCol).setBackground(null);
} else {
// Higlight row with missing cases - grey
ss.getSheetByName("CASES").getRange(+line + 2, 1, 1, numCol).setBackground("#d9d9d9");
};
}
}
// Load column values
function getColumnValues(label, sheetName) {
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
// Get column number for Case Number
var colIndex = getColumnIndex(label, sheetName);
// Get number of rows in Case Number
var numRows = ss.getLastRow() - 1;
// Load Case Number values into array
var colValues = ss.getRange(2, colIndex, numRows, 1).getValues();
return colValues;
}
// Load column header names
function loadHeaderNames(sheetName) {
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
let HeaderArray = ss.getRange(1, 1, 1, ss.getLastColumn()).getValues()[0];
let colidx = {};
HeaderArray.forEach((h, i) => colidx[h] = i);
return HeaderArray;
}
// Get column name index value
function getColumnIndex(label, sheetName) {
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
// Find last column
var lc = ss.getLastColumn();
// Load headers into array
var lookupRangeValues = ss.getRange(1, 1, 1, lc).getValues()[0];
// Search for label and return the column number
var index = lookupRangeValues.indexOf(label) + 1;
return index;
}
One way to make all this processing much easier is to reorder the columns so that they always fall in the same place, like this:
=arrayformula(
iferror(
vlookup(
hlookup("Case Number"; IMPORT!A1:G; row(IMPORT!A2:G); false);
{
hlookup("Case Number"; IMPORT!A1:G; row(IMPORT!A1:G); false) \
IMPORT!A1:G
};
match(IMPORT!A1:G1; CASES!A1:G1; 0) + 1;
false
)
)
)
The formula will reorder the columns in IMPORT so that the columns are in the same order as they are listed in CASES!A1:G1.
You can then use further formulas or script functions to work on the data, confident that a particular kind of data will always be in the same column. For instance, you can list closed cases with something like this:
=filter( 'CASES normalized'!A2:G; isna(match('CASES normalized'!C2:C; 'IMPORT normalized'!C2:C; 0)) )
...and open cases like this:
=filter( 'CASES normalized'!A2:G; match('CASES normalized'!C2:C; 'IMPORT normalized'!C2:C; 0) )
See your sample spreadsheet.

Move Specific Rows depending on Filtering Keywords within unknown amount of rows using Google Sheets Apps Scripts

I do SEO, and therefore I have a lot of keywords flowing around in different spreadsheets. I'd like a way to filter these into seperate sheets based on specific filters, but I can't for the life of me, figure out how to do this in Google Apps Script.
Criteria I set myself for this to work out:
A list of strings and their corresponding volumes are entered in column 1+2.
A list of filter-words are written in column 3.
The script has to create a new sheet for each of the filter words and move the strings + volumes into these different sheets if the string contains a filter word.
Example:
Filter words: Apple, Banana, Pineapple
String: "The Apple Was Big", Volume: "100"
The script would move the string and volume into the sheet called "Apple" on row 1
(Beware, I'm in no means experienced in coding)
I believe you can use the following structure:
for(let i = 0; i <= column3RowAmount; i++){ //Run as long as there are more filter words
create(column3Row[i]); //create a new sheet with the name of the filter word
for(let j = 0; j <= column1RowAmount; j++){ //Run as long as there are more keywords
if(column1Row[j].indexOf(column3Row[i]) >= 0){ //If the Row in column 1 contains the filter word
column1Row[j].moveToSheet(column3Row[i]); // Make sure not to move Column 3, but only 1+2
}
}
}
Example sheet: https://docs.google.com/spreadsheets/d/15YIMyGmmfZdy094gwuJNxFmTd8h7NOLnA8KevZrGtdU/edit?usp=sharing
Explanation:
Your goal is to create a sheet for every filter-word in column C. Then copy the data in columns A, B but only the rows that include the filter-word to the corresponding sheet.
For starters, you need to get the filter-word list. You can get the full range of column C and filter out the empty cells:
const sh_names = sh.getRange('C1:C').getValues().flat().filter(r=>r!='');
Similarly, you need to get the data in columns A and B:
const data = sh.getRange('A1:B'+sh.getLastRow()).getValues();
The next step is to iterate over sh_names and for every element / filter-word, check if a sheet with that name exists. If it does not exist, then create a sheet with that name, if it exists then skip the creation part:
if(!ss.getSheetByName(s)){
ss.insertSheet().setName(s);}
The next step is to filter data on the rows that include the filter-word:
let f_data = data.filter(r=>r[0].includes(s));
Finally, check if the length of the data is bigger than 0, otherwise there is not data to use and set the values of data to the corresponding sheet:
sheet.getRange(sheet.getLastRow()+1,1,f_data.length,f_data[0].length).setValues(f_data)
Solution
function myFunction() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Ark1');
const filter_sh = ss.getSheetByName('Filter');
const data = sh.getRange('A1:B'+sh.getLastRow()).getValues();
const sh_names = filter_sh.getRange('A1:A'+filter_sh.getLastRow()).getValues().flat();
sh_names.forEach(s=>{
if(!ss.getSheetByName(s)){
ss.insertSheet().setName(s);}
let sheet = ss.getSheetByName(s);
let f_data = data.filter(r=>r[0].includes(s));
if(f_data.length>0){
sheet.getRange(sheet.getLastRow()+1,1,f_data.length,f_data[0].length).setValues(f_data);}
});
}
This function will place all of your results into column 4 next to the appropriate word rather than creating a page for each word. So it runs much faster.
function stringswords() {
const ss=SpreadsheetApp.getActive();
const sh=ss.getSheetByName('Sheet1');
const sr=2;
const rgd=sh.getRange(sr,1,sh.getLastRow()-sr+1,2);
const data=rgd.getDisplayValues();
const rgw=sh.getRange(sr,3,sh.getLastRow()-sr+1,1);
const words=rgw.getDisplayValues().flat();
const wiObj={};
words.forEach(function(w,i){wiObj[w]=i});
const rgr=sh.getRange(sr,4,sh.getLastRow()-sr+1,1);
rgr.clearContent();
var results=rgr.getValues();
words.forEach(function(w,i,A){
data.forEach(function(r,j,D) {
if(data[j][0] && data[j][0].indexOf(w)!=-1) {
results[wiObj[w]][0]+=Utilities.formatString('String:%s Vol:%s\n',data[j][0],data[j][1]);
}
});
});
rgr.setValues(results);
}
Image of Data and output:

Google script loop check for duplicate before writing data

I have a script which reads data from a site, stores the data in an array variable and then writes the data to a google sheet.
Per item id (JSON format), the data which is read is of the form:
[timestamp number text1 text2]
and these details are duplicated across different ids in a for loop.
What i'm left with on the sheet is per row (one item in each cell):
timestamp(id1) number1(id1) text1(id1) text2(id1) timestamp(id2) number1(id2) text1(id2) text2(id2) timestamp(id3) number1(id3)...etc
each row will contain only a single value for timestamp, however the timestamp variable is written multiple times. Is it possible to adapt my script to check column A of the bottom row on my sheet and only write the new row if the timestamp in the current bottom row is different to the timestamp in the new row about to be written.
for loop iterates through json file and stores data in "values" variable.
{
{.....
let values = [];
values.push(timestamp, number1, text1, text2); //final line of for loop
}
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("test");
var range = ss.getRange(3, 1, 1, values.length);
if (range.isBlank()) {
range.setValues([values]);
} else {
ss.appendRow(values);
}
}
2 Requests:
a) I would like the timestamp variable to only be written once, in column A.
b) I would like the script to check the last written row to ensure that the timestamp value printed in column A is different to the value about to be written in the new row. If it is the same, do not write to the row.
Thanks
So for Request A: You need to change the array you are passing to the setValues()method. If you already know which columns these are, then you can modify the array by replacing the existing value with an empty string.
const outputRow = [ … ]; // This is where your current output is set
const emptyTheseColumns = [3, 6, 9]; // columns C, F, I
const cleanedOutputRow = outputRow.map( (item, index) => {
const column = index + 1; // columns start at 1, index at 0
// return empty string for stated columns
if( emptyTheseColumns.indexOf( column ) != -1){
return ""
}
// otherwise return the current value
return item
});
// then use the new array in setValues( cleanedOutputRow )

Select specific rows to be appended into google sheets via array

Basically I have an array of information that I can currently append into google sheets, the thing is that a lot of the information is not necessary for my need so I wanted to find a way to just append the columns I need.
The picture above shows how everything looks,
basically the idea is to make it looks like in the following picture.
so basically I only need to append columns 4,5,7
currently why I do is this!
if (tozip.getContentType() == "application/zip"){ //for ZIP files
var unZip = Utilities.unzip(tozip); //assigns the unzipped file to a new variable
var table = Utilities.parseCsv(unZip[0].getDataAsString());// assigns the data to variable
for (var i = 0; i < table.length; i++) {//loops trought the array an appends the data as it goes.
sheet.appendRow(table[i]);
}
the data comes from a csv file and looks like this.
[[isApplication, applicationDate, isQualified, Funded_Date, isFunded, requested_loan_amount, amountFunded], [1, 2020-02-03, 1, 2020-02-03, 1, , 1300.0000], [1, 2019-12-29, 1, 2019-12-30, 1, 3000.0000, 2000.0000], [1, 2020-01-27, 1, 2020-01-28, 1, , 800.0000], [1, 2020-01-08, 1, 2020-01-10, 1, 2500.0000, 2500.0000], [1, 2020-02-04, 1, 2020-02-10, 1, , 1400.0000], [1, 2020-01-21, 1, 2020-01-21, 1, 5000.0000, 2000.0000], [1, 2020-02-06, 1, 2020-02-06, 1, 1100.0000, 1400.0000], [1, 2020-02-01, 1, 2020-02-04, 1, 1500.0000, 601.0000], [1, 2020-02-11, 1, 2020-02-11, 1, 500.0000, 800.0000]]
so yeah a lot of messy csv data.
I tried adding this to the code and a few variations of it so It can select the inside data
for (var i = 0; i < table.length; i++) {//loops trought the array an appends the data as it goes.
var columns = [];
columns.push(3);
columns.push(4);
columns.push(6);
sheet.appendRow(table[i][columns]);
}
but it does not work I'm super new to this type of stuff, so I'm pretty sure that's not the correct way to try and select the information I want from the array.
let me know if I need to elaborate more on this, I'm not super good at explaining this stuff.
Thank you in advance for the answers I really appreciate the help on this.
You want to retrieve the columns "D", "E" and "G" from the data retrieved by parsing the CSV data.
In your script, table of var table = Utilities.parseCsv(unZip[0].getDataAsString()); is the 2 dimensional data shown in your question.
You want to put the retrieved values to the Spreadsheet.
You want to achieve this using Google Apps Script.
If my understanding is correct, how about this answer? Please think of this as just one of several possible answers.
Modification points:
table of var table = Utilities.parseCsv(unZip[0].getDataAsString()); is 2 dimensional array.
When for (var i = 0; i < table.length; i++) {} is used, each row can be retrieved by table[i]. And the values from the columns "D", "E" and "G" can be retrieved by table[i][3], table[i][4], table[i][6].
In this modification, var values = [] is prepared, and each row is put with values.push([table[i][3], table[i][4], table[i][6]]).
When the method of appendRow() is used in the for loop, the process cost becomes high. So in this case, an array is created in the for loop. And the array is put to the Spreadsheet using setValues(). By this, the cost can be reduced.
When above points are reflected to your script, it becomes as follows.
Modified script:
var table = Utilities.parseCsv(unZip[0].getDataAsString());
// I modified below script.
var values = [];
for (var i = 0; i < table.length; i++) {
values.push([table[i][3], table[i][4], table[i][6]]);
}
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
sheet.getRange(sheet.getLastRow() + 1, 1, values.length, values[0].length).setValues(values);
Above script, the values are put to "Sheet1". If you want to change this, please modify getSheetByName("Sheet1").
In this case, table is var table = Utilities.parseCsv(unZip[0].getDataAsString()).
Note:
When var table = Utilities.parseCsv(unZip[0].getDataAsString()) doesn't return the array of CSV data, above modified script cannot be used. Please be careful this.
References:
parseCsv(csv)
getRange(row, column, numRows, numColumns)
setValues(values)
If I misunderstood your question and this was not the direction you want, I apologize.

How to compare two sheets and delete/add any column with a distinct value in row 1? Google Script

I want to compare two sheets (based on header values in row 1) and delete any column with a unique value (without a match). For example, Assuming Sheet1, Row 1 data and Sheet 2, Row 1 are uniform, if a user adds/deletes a column within any sheet, I want to always match the number of columns in both sheets with their values
Screenshots of sheets headings.
IF both sheets looks like this
And a user adds a new Column N
Or delete column N
How can I ensure that both sheet matches by deleting the odd/distinct column in Sheet 1?
I have tried modifying this code below but I can't just get the unique one out. This code only look for headers with a defined value.
function deleteAloneColumns(){
var sheet = SpreadsheetApp.getActiveSheet();
var lastColumnPos = sheet.getLastColumn();
var headers = sheet.getRange( 1 ,1, 1, lastColumnPos ).getValues()[0];
for( var i = lastColumnPos ; i < 1; i--){
if( headers[i] === "alone" ) sheet.deleteColumn(i);
}
SpreadsheetApp.getUi().alert( 'Job done!' );
}
Any help to compare and delete the column with the unique value will be appreciated.
Problem
Balancing sheets based on header row values mismatch.
Solution
If I understood you correctly, you have a source sheet against which validation is run and two primary use cases: user adds a new column named differently than any other column (if you want to check that the column strictly matches the one in sheet1, it is easy to modify) in source sheet or deletes one that should be there.
const balanceSheets = (sourceShName = 'Sheet1',targetShName = 'Sheet2') => {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const s1 = ss.getSheetByName(sourceShName);
const s2 = ss.getSheetByName(targetShName);
const s2lcol = s2.getLastColumn();
//keep all vals from source to reduce I/O
const s1DataVals = s1.getDataRange().getValues();
const s2Vals = s2.getRange(1, 1, 1, s2lcol).getValues();
const h1Vals = s1DataVals[0];
const h2Vals = s2Vals[0];
//assume s1 is source (validation) sheet
//assume s2 is target sheet that a user can edit
//case 1: target has value not present in source -> delete column in target
let colIdx = 0;
h2Vals.forEach(value => {
const isOK = h1Vals.some(val => val===value);
isOK ? colIdx++ : s2.deleteColumn(colIdx+1);
});
//case 2: target does not have values present in source -> append column from source
h1Vals.forEach((value,index) => {
const isOK = h2Vals.some(val => val===value);
!isOK && s2.insertColumnAfter(index);
const valuesToInsert = s1DataVals.map(row => [row[index]]);
const numRowsToInsert = valuesToInsert.length;
s2.getRange(1,index+1, numRowsToInsert,1).setValues(valuesToInsert);
});
};
Showcase
Here is a small demo of how it works as a macros:
Notes
Solving your problem with two forEach is suboptimal, but I kept number of I/O low (it can be lowered further by, for example, moving deleteColum out of the loop while only keeping track of column indices).
The script uses ES6 capabilities provided by V8, so please, be careful (although I would recommend migrating as soon as possible - even if you encounter bugs / inconsistencies , it is worth more than it costs.
UPD made script more flexible by moving sheet names to parameter list.
UPD2 after discussing the issue with deleteColumn() behaviour, the answer is updated to keep column pointer in bounds (for those curious about it - forEach kept incrementing the index, while deleteColumn reduced bounds for any given index).
Reference
insertColumnAfter() method reference

Categories

Resources