How to check duplicates in an entire Google Spreadsheet - google-apps-script

I'm trying to accomplish something perhaps to ambitious but perhaps you could give me a hand.
I have a Google Spreadsheet with 4 Sheets:
Master Senpai | Chevez San | Gabbie Sama | Mario Chan
"Master Senpai" is a sheet with Consolidated data meaning everything in the other 3 sheets is within "Master Senpai"
These spreadsheet is currently being use by 3 coworkers (as you might guess Chevez Gabbie and Mario) what I need is for each of them to have the ability to confirm or check if any of the info they are inputting is duplicated.
All the Sheets have the same headers from A to L (12 Columns total) I want to check the duplicates for Column J.
So for Example if Mario Want to know if the info he just inputted Gabbie or Chevez has already put it I want that cell to get Formated with a red background.
Basically read the info from the last 3 sheets (Ignore the "Master Senpai" sheet) and check for duplicates if any found color them with a red background.
I've been reading about it and found something similar to what I'm trying to do.
/**
* Finds duplicate rows in the active sheet and colors them red,
but only pays attention to the indicated columns.
*/
function findDuplicates() {
// List the columns you want to check by number (A = 1)
var CHECK_COLUMNS = [2,3,5,6];
// Get the active sheet and info about it
var sourceSheet = SpreadsheetApp.getActiveSheet();
var numRows = sourceSheet.getLastRow();
var numCols = sourceSheet.getLastColumn();
// Create the temporary working sheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var newSheet = ss.insertSheet("FindDupes");
// Copy the desired rows to the FindDupes sheet
for (var i = 0; i < CHECK_COLUMNS.length; i++) {
var sourceRange = sourceSheet.getRange(1,CHECK_COLUMNS[i],numRows);
var nextCol = newSheet.getLastColumn() + 1;
sourceRange.copyTo(newSheet.getRange(1,nextCol,numRows));
}
// Find duplicates in the FindDupes sheet and color them in the main sheet
var dupes = false;
var data = newSheet.getDataRange().getValues();
for (i = 1; i < data.length - 1; i++) {
for (j = i+1; j < data.length; j++) {
if (data[i].join() == data[j].join()) {
dupes = true;
sourceSheet.getRange(i+1,1,1,numCols).setBackground("red");
sourceSheet.getRange(j+1,1,1,numCols).setBackground("red");
}
}
}
// Remove the FindDupes temporary sheet
ss.deleteSheet(newSheet);
// Alert the user with the results
if (dupes) {
Browser.msgBox("Possible duplicate(s) found and colored red.");
} else {
Browser.msgBox("No duplicates found.");
}
};
/**
* Adds a custom menu to the active spreadsheet
*/
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var entries = [{
name : "Find Duplicates",
functionName : "findDuplicates"
}];
sheet.addMenu("My Scripts", entries);
fillFormulas();
};
However this only looks for duplicates in the same sheet what I'm trying to accomplish is to read and compare the info from the last 3 sheets.

Related

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

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

Fetch data from source sheet based on emails provided in the Emails tab in the target spreadsheet

I have a problem where I have two sheets. one sheet is the source spreadsheet and another is a target spreadsheet. The source spreadsheet has a source sheet has which is the master database and the target spreadsheet has the target where we want to fetch data from source sheet based on emails provided in the Emails tab in the target spreadsheet.
I want the following things to happen with a script and not with IMPORTRANGE or QUERY:
The target spreadsheet will have multiple copies so I want to connect the target spreadsheet with the source spreadsheet based on the source spreadsheet's id.
I want the email matches to be case insensitive so that the users of the target spreadsheet can type emails in any case.
The Emails can go up to 50 or let's say get the last row for that column.
It will be great if the script shows a pop up saying updated after it has fetched the data.
The source sheet might have data up to 15000 rows so I am thinking about speed too.
I have shared both of the spreadsheets with hyperlinks to their names. I am not really great at scripts so it will be helpful if you can leave comments in it wherever you feel like. I would truly appreciate your help.
Thanks in advance!
Script here:
function fetch() {
//get the sheets
var source_Ssheet = SpreadsheetApp.openById('19FkL3rsh5sxdujb6x00BUPvXEEhiXfAeURTeQi3YWzo');
var target_Ssheet = SpreadsheetApp.getActiveSpreadsheet();
//get the tabs
var email_sheet = target_Ssheet.getSheetByName("Emails");
var target_sheet = target_Ssheet.getSheetByName("Target Sheet");
var source_sheet = source_Ssheet.getSheetByName("Source Sheet");
//get ranges
var email_list = email_sheet.getRange("B2:B");
var target_sheet_range = target_sheet.getRange("A1:F100");
var source_sheet_range = source_sheet.getRange("A1:F100");
//get last rows
var last_email_name = email_list.getLastRow();
var last_target_sheet_range = target_sheet_range.getLastRow();
var last_source_sheet_range = source_sheet_range.getLastRow();
//start searching for emails
for (var i=3; i < last_email_name.length+1; i++)
{
for(varj=3; j< last_source_sheet_range.length+1; j++ )
{
if(source_sheet_range[j][3].getValue() == email_list[i][3].getValue())
{
//copy matches to target sheet
target_sheet.getRange((last_target_sheet_range + 1),1,1,10).setValues(master_sheet_range[j].getValues());
}
}
}
}
Several things
last_email_name and last_source_sheet_range are numbers - they do not have any length, this is why your first forloops are not working
You are missing a space in varj=3;
email_list[i][3].getValue() does not exist because email_list only includes B - that only one column. I assume you meant email_list[i][0].getValue()
ranges cannot be addressed with the indices [][], you need to retrieve the values first to have a 2D value range.
You email values in the different sheets do not follow the same case. Apps Script is case sensitive, to suee the == comparison you need to use the toLowerCase() method.
Also mind that defining getRange("B2:B") will include many empty rows that you don't need and will make your code very slow. Replace it through getRange("B2:B" + email_sheet.getLastRow());
Have a look here at the debugged code - keep in mind that there is still much room for improvement.
function fetch() {
//get the sheets
var source_Ssheet = SpreadsheetApp.openById('19FkL3rsh5sxdujb6x00BUPvXEEhiXfAeURTeQi3YWzo');
var target_Ssheet = SpreadsheetApp.getActiveSpreadsheet();
//get the tabs
var email_sheet = target_Ssheet.getSheetByName("Emails");
var target_sheet = target_Ssheet.getSheetByName("Target Sheet");
var source_sheet = source_Ssheet.getSheetByName("Source Sheet");
//get ranges
var email_list = email_sheet.getRange("B2:B" + email_sheet.getLastRow()).getValues();
var target_sheet_range = target_sheet.getRange("A1:F100").getValues();
var source_sheet_range = source_sheet.getRange("A1:F100").getValues();
var last_target_sheet_range = target_sheet.getLastRow();
//start searching for emails
for (var i=1; i < email_list.length; i++)
{
for(var j=1; j< source_sheet_range.length; j++ )
{
if(source_sheet_range[j][0].toLowerCase() == email_list[i][0].toLowerCase())
{
target_sheet.getRange((last_target_sheet_range + 1),1,1,6).setValues([source_sheet_range[j]]);
}
}
}
}

Google script to find and update rows in other sheets by unique ID

I have about 4 separate spreadsheets that accepts user input and when the user is finished they push a button to ‘log’ the information on another sheet. All of the spreadsheet entries from user input have a unique ID assigned to them.
What I am trying to do is when the user clicks the button the script searches another sheet for the matching ID and updates Column B to the new value.
Basically like a simple lookup, but instead of returning the value, it updates.
Sheet 1 Sheet 2
A B A B
1 ID123 | Received > 1 ID123 | Ordered
2 2
So as the user changes ID123 in spreadsheet 1 to Received, the button searches for ID123 in spreadsheet 2 and if it exists (it always will), cell b1 in sheet 2 gets updated with the value in B1 in sheet 1.
I did come across another post talking about a CRUD web app, but I am thinking that is a bit more complex than what I am looking to do here!
Thank you for any assistance!
EDIT:
The button I mentioned is added in via a custom image in which this script will be assigned to.
It might help to add that the 'Starting sheet' will only ever have 1 ID on it. It is the 'destination sheet' that will have multiple ID's, even the same ID repeating multiple times, in which case all matching IDs would need to be updated.
Sample of Starting Sheet and
Sample of Destination Sheet
As you can see above, in the 'Starting Sheet', ID3 has a Status of "Received". Upon pushing that Submit button, the script would need to find "ID3" in the 'Destination Sheet' and replace whatever text is in the Value column with "Received". So in this example, ID3 is being updated from "Ordered" to "Received".
try this:
function onEdit(e) {
if(e.range.getSheet().getName() != 'Sheet1'){return;}
if(e.range.columnStart==2) {
var id=e.range.offset(0,-1).getValue();
var rg=e.source.getSheetByName('Sheet2').getDataRange()
var vA=rg.getValues();
for(var i=1;i<vA.length;i++) {
if(vA[i][0]==id) {
vA[i][1]=e.value;
break;
}
}
}
rg.setValues(vA);
}
Sheet1 and Sheet2:
You can attach this function to a button as desired:
function updateEntrees() {
var ss=SpreadsheetApp.getActive();
var sh1=ss.getSheetByName('Sheet1');
var rg1a=sh1.getRange(2,1,sh1.getLastRow()-1,1);
var vA1a=rg1a.getValues();
var rg1b=sh1.getRange(2,2,sh1.getLastRow()-1,1);
var vA1b=rg1b.getValues();
var sh2=ss.getSheetByName('Sheet2');
var rg2a=sh2.getRange(2,1,sh2.getLastRow()-1,1);
var vA2a=rg2a.getValues();
var rg2b=sh2.getRange(2,2,sh2.getLastRow()-1,1);
var vA2b=rg2b.getValues();
for(var i=0;i<vA1a.length;i++) {
for(var j=0;j<vA2a.length;j++) {
if(vA1a[i][0]==vA2a[j][0]) {
vA2b[j][0]=vA1b[i][0]
}
}
}
rg2b.setValues(vA2b);
}
You can copy/paste below code inside existing button function to copy current cell value to destination sheet cell accordingly after search by id. Check for duplicate variable names though.
var acRange = SpreadsheetApp.getActiveSheet().getActiveRange();
var val = acRange.getValue();
var row = acRange.getRow();
var col = acRange.getColumn();
var id = acRange
.getSheet()
.getRange(row, 1)
.getValue();
var rg = SpreadsheetApp.getSheetByName('destination sheet name here').getDataRange();
var vA = rg.getValues();
for (var i = 1; i < vA.length; i++) {
if (vA[i][0] == id) {
vA[i][col - 1] = val;
break;
}
}
rg.setValues(vA);

Copy only non-blank rows from one tab to another

I have a Google Sheets script that copies a range of cells from one tab to another. The problem is that I'd like it to only copy the non-blank range of cells, but instead it copies the entire range including blank rows. There are many versions of this problem already discussed, but I can't seem to find the right solution so I'm asking it again with all the specifics below.
The range I'm copying is comprised of:
Column A contains a formula that has a text output if column B is
non-blank. If column B is blank, then the formula in column A
creates a blank entry ("").
Columns B:J is an =IMPORTRANGE from a different sheet with the range set to A5:H (open ended range).
Example Source data (imported into the main sheet)
Example Main sheet (this contains the script, which can be triggered in the menu at the top under "Copy Data")
Here's the current script:
// custom menu function
function onOpen() {
var ui = SpreadsheetApp.getUi();
var menu = ui.createMenu('Copy Data');
var item = menu.addItem('Copy Data','copyData');
item.addToUi();
}
function copyData() {
// START1: get current sheet and tabs
var ss = SpreadsheetApp.getActiveSpreadsheet();
var current = ss.getSheetByName('ImportRange');
var database = ss.getSheetByName('RunningList');
// count rows to snap
var current_rows = current.getLastRow();
var database_rows = database.getLastRow() + 1;
var database_rows_new = current_rows + database_rows - 3;
var rows_new = current.getRange('A3:J' + current_rows).getValues();
var nonblank_values = rows_new.filter(String);
// snap rows, can run this on a trigger to be timed
database.getRange(database_rows, 1, nonblank_values.length, nonblank_values[0].length).setValues(nonblank_values);
}
Thank you for your time reviewing this problem.
EDIT 1
When I debug the script, it looks like the filter function is not actually filtering out blank rows. So how would I actually do that?
Debugging Info:
I believe I've found a suitable solution that currently works for my use case. I'm sure there are ways to improve it if you'd like to share your thoughts. I found a script to count nonblank rows here, and integrated it along with some adjustments to the rest of the script:
// add custom menu function "Copy Data"
function onOpen() {
var ui = SpreadsheetApp.getUi();
var menu = ui.createMenu('Copy Data');
var item = menu.addItem('Copy Data','copyData');
item.addToUi();
}
// function to identify last populated row of any tab (based on column A)
function getLastPopulatedRow(sheet) {
var data = sheet.getDataRange().getValues();
for (var i = data.length-1; i > 0; i--) {
for (var j = 0; j < data[0].length; j++) {
if (data[i][j]) return i+1;
}
}
return 0;
}
// function to copy data from one tab to another
function copyData() {
// step 1: get current sheet and tabs
var ss = SpreadsheetApp.getActiveSpreadsheet();
var current = ss.getSheetByName('ImportRange');
var database = ss.getSheetByName('RunningList');
// step 2: count number of new rows needed and grab non-blank rows from first tab
var current_lastrow = getLastPopulatedRow(current);
var database_rows = getLastPopulatedRow(database) + 1;
var database_rows_new = current_lastrow + database_rows - 3;
var rows_new = current.getRange('A3:I' + current_lastrow).getValues();
// step 3: add values to second tab
database.getRange("A" + database_rows + ":I" + database_rows_new).setValues(rows_new);
}

Using a formula across dynamically added sheets

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