Detect formula errors in Google Sheets using Script - google-apps-script

My ultimate goal is here, but because I've gotten no replies, I'm starting to learn things from scratch (probably for the best anyway). Basically, I want a script that will identify errors and fix them
Well, the first part of that is being able to ID the errors. Is there a way using Google Script to identify if a cell has an error in it, and return a particular message as a result? Or do I just have to do an if/else that says "if the cell value is '#N/A', do this", plus "if the cell value is '#ERROR', do this", continuing for various errors?. Basically I want ISERROR(), but in the script

Use a helper function to abstract away the nastiness:
function isError_(cell) {
// Cell is a value, e.g. came from `range.getValue()` or is an element of an array from `range.getValues()`
const errorValues = ["#N/A", "#REF", .... ];
for (var i = 0; i < errorValues.length; ++i)
if (cell == errorValues[i])
return true;
return false;
}
function foo() {
const vals = SpreadsheetApp.getActive().getSheets()[0].getDataRange().getValues();
for (var row = 0; row < vals.length; ++row) {
for (var col = 0; col < vals[0].length; ++col) {
if (isError_(vals[row][col])) {
Logger.log("Array element (" + row + ", " + col + ") is an error value.");
}
}
}
}
Using Array#indexOf in the helper function:
function isError_(cell) {
// Cell is a value, e.g. came from `range.getValue()` or is an element of an array from `range.getValues()`
// Note: indexOf uses strict equality when comparing cell to elements of errorValues, so make sure everything's a primitive...
const errorValues = ["#N/A", "#REF", .... ];
return (errorValues.indexOf(cell) !== -1);
}
If/when Google Apps Script is upgraded with Array#includes, that would be a better option than Array#indexOf:
function isError_(cell) {
// cell is a value, e.g. came from `range.getValue()` or is an element of an array from `range.getValues()`
const errorValues = ["#N/A", "#REF", .... ];
return errorValues.includes(cell);
}
Now that the v8 runtime is available, there are a number of other changes one could make to the above code snippets (arrow functions, etc) but note that changing things in this manner is not required.

Update: 25 March 2020
#tehhowch remarked "If/when Google Apps Script is upgraded with Array#includes, that would be a better option than Array#indexOf".
Array.includes does now run in Apps Script and, as anticipated provides a far more simple approach when compared to indexOf.
This example varies from the previous answers by using a specific range to show that looping through each cell is not required. In fact, this answer will apply to any range length.
The two key aspects of the answer are:
map: to create an array for each column
includes: used in an IF statement to test for a true or false value.
function foo() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourcename = "source_sheet";
var source = ss.getSheetByName(sourcename);
var sourcerange = source.getRange("A2:E500");
var sourcevalues = sourcerange.getValues();
// define the error values
var errorValues = ["#NULL!", "#DIV/0!", "#VALUE!", "#REF!", "#NAME?", "#NUM!", "#N/A","#ERROR!"];
// loop though the columns
for (var c = 0;c<5;c++){
// create an array for the column
var columnoutput = sourcevalues.map(function(e){return e[c];});
// loop through errors
for (var errorNum=0; errorNum<errorValues.length;errorNum++){
// get the error
var errorvalue = errorValues[errorNum]
// Logger.log("DEBUG: column#:"+c+", error#:"+e+", error value = "+errorvalue+", does col include error = "+columnoutput.includes(errorvalue));
// if the error exists in this column then resposnse = true, if the error doesn't exist then response = false
if (columnoutput.includes(errorvalue) != true){
Logger.log("DEBUG: Column#:"+c+", error#:"+errorNum+"-"+errorvalue+" - No ERROR");
} else {
Logger.log("DEBUG: column#:"+c+", error#:"+errorNum+"-"+errorvalue+"- ERROR EXISTS");
}
}
}
return;
}

Shorter yet, use a nested forEach() on the [vals] array, then check to see if the cell value matches a value of the [errorValues] array with indexOf. This was faster than a for loop...
function foo() {
const errorValues = ["#NULL!", "#DIV/0!", "#VALUE!", "#REF!", "#NAME?", "#NUM!", "#N/A","#ERROR!"];
const vals = SpreadsheetApp.getActive().getSheets()[0].getDataRange().getValues();
vals.forEach((val,row) => { val.forEach((item, col) => {
(errorValues.indexOf(item) !== -1) ? Logger.log("Array element (" + row + ", " + col + ") is an error value.") : false ;
});
});
}

I had a similar question and resolved using getDisplayValue() instead of getValue()
Try something like:
function checkCells(inputRange) {
var inputRangeCells = SpreadsheetApp.getActiveSheet().getRange(inputRange);
var cellValue;
for(var i=0; i < inputRangeCells.length; i++) {
cellValue = inputRangeCells[i].getDisplayValue();
if (cellValue=error1.....) { ... }
}
}
Display value should give you what's displayed to the user rather than #ERROR!

Related

Google Apps Script function called from sheet - sometimes blank results (returned empty value)

My simple (as I initially thought) Google Apps script function is not working as expected as it yields different results based on the input given = it is to return generated numbers (separated by comma) from START to END - being called in [column J] as per attached picture.
When the custom function is called with two params (start number and end number) from the sheet (cell content) it yields different results if:
Scenario #1: params are static in the formula - for instance: =FillNums(5,12)
Scenario #2: params are dynamic in the formula (for instance pointing to cell A2 and B2: =FillNums(A2,B2))
In first scenario all the calculations are OK, as expected
In the SECOND scenario however it yields sometimes BLANK result.
ONLY in SCENARO #2, depending on the input parameters (in cells), I'm getting different output:
5,21 => blank output
1,21 => 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21
2,21 => 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21
3,21 => blank output
4,19 => blank output
5,19 => blank output
6,19 => blank output
7,19 => blank output
8,19 => blank output
9,19 => blank output
5,9 => 5,6,7,8,9
10,21 => 10,11,12,13,14,15,16,17,18,19,20,21
Screenshot of the mentioned issue / Google Sheet
Debugging is not working, useless, spent hours trying to resolve it and found out that it is not possible when function being called from the sheet, the same goes for Logger = unusable.
Function:
function FillNums(start,end) {
var result = ""
var a = start
var b = end
for (var i = a; i <= b; i++) {
if (i == b) {
result = result + i
} else {
result = result + i + ","
}
}
return result
}
Any solutions are mostly welcome to resolve this issue. That function should yield ALL THE TIME row of numbers from start to end.
I do script from time to time, but I'm just a beginner here and cant understand why such simple thing is yielding empty result sometimes :(
Link to a example sheet containing the script function: Google Sheet
When I saw your script and your sample Spreadsheet, I thought that in your situation, the values of start,end of FillNums(start,end) might be the string values. I thought that this might be the reason of your issue. When this is reflected in your script, it becomes as follows.
Modified script 1:
function FillNums(start, konec) {
var result = ""
var a = Number(start); // Modified
var b = Number(konec); // Modified
var i = 0
for (i = a; i <= b; i++) {
if (i == b) {
result = result + i
} else {
result = result + i + ","
}
}
return result
}
Modified script 2:
As other sample, when an array is used, it becomes as follows.
function FillNums(start, konec) {
var a = Number(start);
var b = Number(konec);
var result = [];
for (var i = a; i <= b; i++) {
result.push(i);
}
return result.join(",");
}
Modified script 3:
As other sample, when an array is used, it becomes as follows. In this case, you can use this script like =FillNums2(A2:B10).
function FillNums2(values) {
return values.map(([start, konec]) => {
if (!start && !konec) return ["no data"];
var a = Number(start);
var b = Number(konec);
var temp = [];
for (var i = a; i <= b; i++) {
temp.push(i);
}
return [temp.join(",")];
});
}

App Scripts If Statement always returning Else Condition

I am working on a script to perform an Index/Match task on two seperate workbooks. The code seems to be working but my If statement is always returning its Else condition. I have logged the compared vairables find and searchref and found that they do match at some point durring the loop but the If statement still returns its Else condition.
I suspect this has something to do with how I am comparing these arrays but I have not been able to figure it out.
Here is a snip of the first few columns and rows for the source spreadsheet for searchData I am trying to access the information in column B.
Source data for searchData
Here is the output from Logger.log for findData and searchData
Logger with labels
Logger arrays
Source data for findData
function generateBillOfMaterials() {
// --------------------------------------------------------------------
// Declare variables
var i, j
var find
var searchref
var found = []
// --------------------------------------------------------------------
var search_spreadsheet = SpreadsheetApp.openById("Searched-Spreadsheet-ID");
var find_spreadsheet = SpreadsheetApp.openById("1xg2yVimBwE5rGvSFMtID9O9CB7RauID34wqIH5LLTeE");
var ssheet = search_spreadsheet.getSheetByName("METAL PARTS");
var fsheet = find_spreadsheet.getSheetByName("Bill of Materials");
var FMaxR = fsheet.getMaxRows();
fsheet.getRange(2, 3, FMaxR, 1).clear({contentsOnly: true});
var findData = fsheet.getDataRange().getValues();
var searchData = ssheet.getDataRange().getValues();
for (i = 0; i < findData.length; i++) {
for (j = 0; j < searchData.length; j++) {
find = findData[i][1];
//Logger.log(find)
searchref = searchData[j][0];
//Logger.log(searchref)
if (find == searchref && find != "")
{
found[i] = searchData[j][1]
}
else
{
found[i] = ['n/a']
}
// found = ssheet.getRange(j+1,2,1,1).getDisplayValue();
// fsheet.getRange(i+1,16,1,1).setValue(found);
}
}
Logger.log(found)
fsheet.getRange(2, 3, found.length, 1).setValues(found)
}
The main problem in the sample code is the else statement containing this:
found[i] = ['n/a']
This will overwrite whatever is found earlier in the loop, because even after a match has been found (and assigned to the found array), the loop continues comparing the remaining values in the inner loop.
The following approach shows how to correct this, making as few changes as possible to your existing code:
function generateBillOfMaterials() {
// --------------------------------------------------------------------
// Declare variables
var i, j
var find
var searchref
// --------------------------------------------------------------------
var search_spreadsheet = ... ;
var find_spreadsheet = ... ;
var ssheet = search_spreadsheet.getSheetByName("METAL PARTS");
var fsheet = find_spreadsheet.getSheetByName("Bill of Materials");
var FMaxR = fsheet.getMaxRows();
fsheet.getRange(2, 3, FMaxR, 1).clear({contentsOnly: true});
var findData = fsheet.getDataRange().getValues();
var found = new Array(findData.length).fill('n/a');
var searchData = ssheet.getDataRange().getValues();
for (i = 0; i < findData.length; i++) {
for (j = 0; j < searchData.length; j++) {
find = findData[i][1];
searchref = searchData[j][0];
if (find === searchref && find !== "") {
found[i] = searchData[j][1];
break;
}
}
}
const found2 = found.slice(1).map(x => [x]);
fsheet.getRange(2, 3, found.length-1, 1).setValues(found2);
}
Notes:
We pre-fill the array of "found" values with "n/a":
var found = new Array(findData.length).fill('n/a');
This allows us to overwrite "n/a" when we find a value - otherwise we leave the "n/a" untouched.
When a match is found, we break out of the inner loop using break.
Then we can remove the else condition - as we no longer need it.
The remaining changes are to ensure the final shape of the found data is a two-dimensional array which can be written to the spreadsheet.
The above approach involves repeatedly looping over the data in the inner loop.
In reality, we only need to visit each list once, in order to perform the lookups we need.
Implementing this alternative approach would basically be a rewrite of what you have - and I would imagine that what you have, even if it is somewhat inefficient, is perfectly OK for your needs. But I did want to mention this.
The other note which may be of interest is that my alternative approach is more-or-less the equivalent of using a Google Sheets vlookup formula. Apologies if you are already aware of that. And I have never tried using that formula across 2 separate files, anyway. But again, just wanted to mention it, for completeness.
Update 2
"Is there a lookup command that could be used in place of the for loops?"
It's more a question of avoiding the nested loops, and using a JavaScript data structure that supports lookups (a Map).
Here is a sketch:
// assume we have already populated searchData and findData, as usual.
// first iterate searchData to build a lookup map:
let partsLookup = new Map();
for (i = 1; i < searchData.length; i++) {
partsLookup.set( searchData[i][0], searchData[i][1] );
}
// now iterate the BOM data and use the lookup map:
for (i = 1; i < findData.length; i++) {
var foundValue = partsLookup.get( findData[i][1] );
console.log( foundValue ); // add this to the "found" array
}
This is obviously not "finished" code - it just shows the approach. But no nested iterations are needed.
The number of loops performed is searchData.length + findData.length, instead of up to searchData.length * findData.length

Improve loading efficiency of App Script code in Google Sheets

ISSUE
I have a spreadsheet whereby I generate the end column based on the other columns present. I do this using the app script code below. There are now 1147 rows in this spreadsheet and I often notice a long period of loading to retrieve all of the rows.
Are there any suggestions on how I can improve the efficiency and responsiveness?
EXAMPLE
ARRAY FORMULA ON END COLUMN
=ARRAYFORMULA(
IF(A2:A="Spec", "# SPEC "&B2:B,
IF(A2:A="Scenario", "## "&B2:B,
IF(A2:A="Step", "* "&TAGS(C2:C,D2:D),
IF(A2:A="Tag", "Tags: "&REGEXREPLACE(B2:B,"\s",""),
IF(A2A="", ""))))))
APP SCRIPT CODE
Utilities.sleep(3000)
/** #OnlyCurrentDoc */
function TAGS(input,textreplacement) {
if (input.length > 0) {
var lst = input.split(",")
var rep = textreplacement.match(/<[^>]*>/g)
for (i in rep){
textreplacement = textreplacement.replace(rep[i],'"'+lst[i]+'"')
}
return textreplacement
}
else{
return textreplacement
}
}
EDIT
From the image below I would like to replace everything with triangle brackets < > in column D, with the values in column C, separated by comma.
I use the Array Formula in column E to do an initial conversion and then use the TAGS function to add in the values.
Ideally I would use the Array Formula in one cell at the top of column E to do all the replacements.
Custom functions in Google Apps Script tend to take long time to process and I wouldn't recommend to use it in several cells. I would like to understand better what you trying to do with this data in order to answer properly, but anyway, I would try one of these two solutions:
1 - Inline formula:
Using only native functions has a better performance. Not sure how you could achieve this, since you are iterating inside that TAGS function.
2- Calculate values interely with Script and replace values in column E:
You could create a function that may run from onEdit event or get activated by a custom menu. Generally it would be like this:
function calculateColumnE() {
var sheet = SpreadsheetApp.openById('some-id').getSheetByName('some-name');
var row_count = sheet.getLastRow();
var input_data = sheet.getRange(1, 1, row_count, 4).getValues();
var data = [];
for (var i = 0; i < row_count; i++) {
var row_data; // this variable will receive value for column E in this row
/*
...
manage input_data here
...
*/
data.push([row_data]); // data array MUST be a 2 dimensional array
}
sheet.getRange(1, 5, data.length, 1).setValues(data);
}
EDIT
Here is the full code for solution 2:
function TAGS(input,textreplacement) { //keeping your original function
if (input.length > 0) {
var lst = input.split(",")
var rep = textreplacement.match(/<[^>]*>/g)
for (i in rep){
textreplacement = textreplacement.replace(rep[i],'"'+lst[i]+'"')
}
return textreplacement
}
else{
return textreplacement
}
}
function calculateColumnE() {
var sheet = SpreadsheetApp.openById('some-id').getSheetByName('some-name');
var row_count = sheet.getLastRow();
var input_data = sheet.getRange(1, 1, row_count, 4).getValues();
var data = [];
for (var i = 0; i < row_count; i++) {
var row_data; // this variable will receive value for column E in this row
if (input_data[i][0] == "Spec") {
row_data = "# SPEC " + input_data[i][1];
} else if (input_data[i][0] == "Scenario") {
row_data = "## " + input_data[i][1];
} else if (input_data[i][0] == "Step") {
row_data = "* " + TAGS(input_data[i][2], input_data[i][3]);
} else if (input_data[i][0] == "Tag") {
row_data = "Tags: " + input_data[i][1].replace(/\s/, ''); // not sure what this is doing
} else if (input_data[i][0] == "") {
row_data = "";
}
data.push([row_data]); // data array MUST be a 2 dimensional array
}
sheet.getRange(1, 5, data.length, 1).setValues(data);
}
I also created a working example, which you can check here: https://docs.google.com/spreadsheets/d/1q2SYD7nYubSuvkMOKQAFuGsrGzrMElzZNIFb8PjM7Yk/edit#gid=0 (send me request if you need it).
It works like a charm using onEdit event to trigger calculateColumnE() with few lines, I'm curious to know about the result in your 1000+ rows sheet. If it get slow, you may need to run this function manually.
Not sure if this will be faster:
function TAGS(input,tr) {
if (input.length > 0) {
var lst = input.split(",");
var i=0;
tr=tr.replace(/<[^>]*>/g,function(){return '"' + lst[i++] + '"';});
}
return tr;
}

Importing data from different Spreadsheets - cache issue google app script

*,
Specs:
import data from a target sheet (A) to another (B) in a different Google Spreadsheet;
data on B sheet need to be filtered/sorted by user without affecting A sheet
when A data change, B data should update too (live or at least on refresh/push a button)
(optional) import B sheet notes into A sheet
Structure of A sheet (and then B sheet which is a mirror basically) is a list of items where every item has a column "ID".
Originally I tried IMPORTRANGE which works great with live updates, but unfortunately on B sheet user cannot use native filters to sort/filter data.
I wrote this custom function:
function importSingleItemData(idItem) {
//vars for debugging
//var idItem = 1;
// Id of spreadsheet where data are contained
var inKey = "xxxxx";
// Actual code
var outData;
var idItemColumn;
var ss = SpreadsheetApp.openById(inKey); // target sheet
// 1. Import idItemColumn
if (ss) {
idItemColumn = ss.getRange("sheet1!A1:A500").getValues();
// 2. find id_property row
for (var i = 0; i < idItemColumn.length; i++){
if(idItemColumn[i][0] == idItem){
var idFound = idItemColumn[i][0];
// 3. import property availability range
var row = i+1;
var RangeString = "sheet1!B"+row + ":AM"+row;
var range = ss.getRange(RangeString);
// copy formatting
// range.copyFormatToRange(range.getGridId(), 3, 4,5,7); !not working
outData = range.getValues();
break;
}
}
return outData;
}
}
Where I try to locate the Id of the item and import the interested data of that row. Then I apply it on B sheet using =importSingleItemData(A1) where A1 contains the id of item =1; A2 = 2, etc like
ID ItemData
1 =importSingleItemData(A1)
2 =importSingleItemData(A2)
...
This works great, the problem is that it does not update data on B sheet when A changes. I read a few posts on stackoverflow about this caching beaviour and tried a few things with no luck (like adding time to import, which is no longer supported), also tried setValue method which does not work with custom function.
I was now thing some combination of VLookup/Hlookup with IMPORTRANGE, not sure whether this will work.
Any tips how to sort this out guys?
Thanks in advance!!
If your working with alot of data between two different areas and matching up alot of info. I made script based vlookup. It maybe helpful in the future.
//-------------------------------------------------(Script Vlookup)------------------------------------------------//
/*
Benefit of this script is:
-That google sheets will not continually do lookups on data that is not changing with using this function
-Unlike Vlookup you can have it look at for reference data at any point in the row. Does not have to be in the first column for it to work like Vlookup.
Useage:
var LocNum = SpreadsheetApp.openById(SheetID).getSheetByName('Sheet1').getRange('J2:J').getValues();
FinderLookUpReturnArrayRange_(LocNum,0,'Data','A:G',[3],'test',1,1,'No');
-Loads all Locations numbers from J2:J into a variable
--looks for Location Numbers in Column 0 of Referance sheet and range eg "Data!A:G"
---Returns results to Column 3 of Target Sheet and range eg "test!A1" or "1,1"
*/
function FinderLookUpReturnArrayRange_(Search_Key,SearchKey_Ref_IndexOffSet,Ref_Sheet,Ref_Range,IndexOffSetForReturn,Set_Sheet,Set_PosRow,Set_PosCol,ReturnMultiResults)
{
var twoDimensionalArray = [];
var data = SpreadsheetApp.getActive().getSheetByName(Ref_Sheet).getRange(Ref_Range).getValues(); //Syncs sheet by name and range into var
for (var i = 0, Il=Search_Key.length; i<Il; i++) // i = number of rows to index and search
{
var Sending = []; //Making a Blank Array
var newArray = []; //Making a Blank Array
var Found ="";
for (nn=0,NNL=data.length;nn<NNL;nn++) //nn = will be the number of row that the data is found at
{
if(Found==1 && ReturnMultiResults=='No') //if statement for found if found = 1 it will to stop all other logic in nn loop from running
{
break; //Breaking nn loop once found
}
if (data[nn][SearchKey_Ref_IndexOffSet]==Search_Key[i]) //if statement is triggered when the search_key is found.
{
var newArray = [];
for (var cc=0,CCL=IndexOffSetForReturn.length;cc<CCL;cc++) //cc = numbers of columns to referance
{
var iosr = IndexOffSetForReturn[cc]; //Loading the value of current cc
var Sending = data[nn][iosr]; //Loading data of Level nn offset by value of cc
if(isEmpty_(Sending)==true) //if statement for if one of the returned Column level cells are blank
{
var Sending = "#N/A"; //Sets #N/A on all column levels that are blank
}
if (CCL>1) //if statement for multi-Column returns
{
newArray.push(Sending);
if(CCL-1 == cc) //if statement for pulling all columns into larger array
{
twoDimensionalArray.push(newArray);
Logger.log(twoDimensionalArray);
var Found = 1; //Modifying found to 1 if found to stop all other logic in nn loop
break; //Breaking cc loop once found
}
}
else if (CCL<=1) //if statement for single-Column returns
{
twoDimensionalArray.push(Sending);
var Found = 1; //Modifying found to 1 if found to stop all other logic in nn loop
break; //Breaking cc loop once found
}
}
}
if(NNL-1==nn && isEmpty_(Sending)==true) //following if statement is for if the current item in lookup array is not found. Nessessary for data structure.
{
for(var na=0,NAL=IndexOffSetForReturn.length;na<NAL;na++) //looping for the number of columns to place "#N/A" in to preserve data structure
{
if (NAL<=1) //checks to see if it's a single column return
{
var Sending = "#N/A";
twoDimensionalArray.push(Sending);
}
else if (NAL>1) //checks to see if it's a Multi column return
{
var Sending = "#N/A";
newArray.push(Sending);
}
}
if (NAL>1) //checks to see if it's a Multi column return
{
twoDimensionalArray.push(newArray);
}
}
}
}
if(typeof Set_PosRow != "number") //checks to see if what kinda of variable Set_PosRow is. if its anything other than a number it will goto next avaible row
{
var Set_PosRow = getFirstEmptyRowUsingArray_(Set_Sheet); //for usage in a database like entry without having to manually look for the next level.
}
for (var l = 0,lL=Search_Key.length; l<lL; l++) //Builds 2d Looping-Array to allow choosing of columns at a future point
{
if (CCL<=1) //checks to see if it's a single column return for running setValue
{
SpreadsheetApp.getActive().getSheetByName(Set_Sheet).getRange(Set_PosRow + l,Set_PosCol).setValue(twoDimensionalArray[l]);
}
}
if (CCL>1) //checks to see if it's a multi column return for running setValues
{
SpreadsheetApp.getActive().getSheetByName(Set_Sheet).getRange(Set_PosRow,Set_PosCol,twoDimensionalArray.length,twoDimensionalArray[0].length).setValues(twoDimensionalArray);
}
SpreadsheetApp.flush();
}
//*************************************************(Script Vlookup)*************************************************//
And some helper Functions
//;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
//Copy this block of fucnctions as they are used in the Vlookup Script
//;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
//-------------------------------------------------(Find Last Row on Database)------------------------------------------------//
function getFirstEmptyRowUsingArray_(sheetname)
{
var data = SpreadsheetApp.getActive().getSheetByName(sheetname).getDataRange().getValues();
for(var n = data.length ; n<0 ; n--)
{
if(isEmpty_(data[n][0])=false)
{
n++;
break;
}
}
n++
return (n);
}
//*************************************************(Find Last Row on Database)*************************************************//
//-------------------------------------------------(Blank Array Extractor/Rebuilder)------------------------------------------------//
function cleanArray_(actual)
{
var newArray = new Array();
for(var i = 0; i<actual.length; i++)
{
if (isEmpty_(actual[i]) == false)
{
newArray.push(actual[i]);
}
}
return newArray;
}
//*************************************************(Blank Array Extractor/Rebuilder)*************************************************//
//-------------------------------------------------(Even/Odd)------------------------------------------------//
function isEven_(value) {
if (value%2 == 0)
return true;
else
return false;
}
//*************************************************(Even/Odd)*************************************************//
//-------------------------------------------------(Array Col Sum Agent)------------------------------------------------//
function SumColArray_(sumagent)
{
var newArray = new Array();
for(var i = 0; i<sumagent.length; i++)
{
var totalsum = 0
var CleanForSum = cleanArray_(sumagent[i]);
for(var d = 0; d<CleanForSum.length; d++)
{
totalsum += CleanForSum[d];
}
newArray.push(Math.round(totalsum));
}
return newArray;
}
//*************************************************(Array Col Sum Agent)*************************************************//
//-------------------------------------------------(Empty String Check)------------------------------------------------//
function isEmpty_(string)
{
if(!string) return true;
if(string == '') return true;
if(string === false) return true;
if(string === null) return true;
if(string == undefined) return true;
string = string+' '; // check for a bunch of whitespace
if('' == (string.replace(/^\s\s*/, '').replace(/\s\s*$/, ''))) return true;
return false;
}
//*************************************************(Empty String Check)*************************************************//
Eventually I sorted out with native functions filtering on a single row by id
=IFERROR(FILTER(IMPORTRANGE("key";"sheet1!B1:AN300");IMPORTRANGE("key";"sheet1!A1:A300") = id))

Remove duplicates values in array Google Apps Script

I would like to know how to remove duplicates values from 2 arrays, combined into one main array.
This values must NOT be removed from the sheets or document, just in the array, thats why I didnt use clear() or clearContents() built in functions;
Ive also tried to modify the removeDuplicates() function from the GAS tutorials, but it throws me rows inside columns from A to Z, instead filtered rows...a total mess.
Notes:
Parameters from getClients() are from others functions, and works ok.
newClients list clients from the sheet 'Users' and newUsers list users from another sheet called 'Data'.
Boths sheets belongs to the same spreadsheet.
newClients and newUsers: both arrays only contains strings (usernames), and in both there are duplicated values.
So the goal is identified and remove those values, the original and the duplicate.
Should be easier I think, but Im new in JS, so everything Ive been tried, didnt worked.
Thanks
The Code
function getAllClientsFromData(body,project){
var ss = SpreadsheetApp.getActiveSpreadsheet();
// Active the Sheets
var sheet= ss.getSheets()[1,3];
// Access data in Sheet "Data"
var rangeC = ss.getRangeByName("clients"); //A:2:C273
var rangeU = ss.getRangeByName("names"); //A5:A
var clients = rangeC.getValues();
var users = rangeU.getValues();
var lastRow = sheet.getLastRow()-2;
body += "<h2>Total of " + lastRow + " clients in " + project + "</h2>"
body += "<table style=" + STYLE.TABLE + ">";
body += getClients(ss,clients,users,project);
body += "</table>";
return body;
}
function getClients(ss,clients,users,project){
var body = "";
var newClients = [];
for( var g = 0; g < clients.length; g++ ){
var currentProject = clients[g][2];
if( clients[g]!= "" ){
newClients.push(clients[g][0]);
}
} // end for
var newUsers = [];
for( var u = 0; u < users.length; u++ ){
if( users[u] != "" ){
newUsers.push(users[u][0]);
}
} // end for
var allData = newUsers.concat(newClients);
var uniqueData = allData.sort();
body += "<tr><td style=" + STYLE.TD + ">" + uniqueData.join("</td><tr><td style=" + STYLE.TD + ">") + "</td></tr></tr>";
return body;
}
UPDATES!
The answers works great filtering, but Im getting the same result as on my previous tries: displaying the filtered results. I need to remove them from the array. example:
var array = ['aa','bb','aa','ff','pp', 'pp'];
filtering code...
var array = ['bb','ff'];
I try to add splice() js method but the params I pass, does not working ok.
The array you are working on is not a 2D array anymore since you extracted the fields before sorting... so you can use a very simple duplicate removal function as shown below with an example and some added Logger.log to see how it works.
function test(){
var array = ['aa','bb','cc','aa','dd','cc']
Logger.log(removeDups(array));
}
function removeDups(array) {
var outArray = [];
array.sort():
outArray.push(array[0]);
for(var n in array){
Logger.log(outArray[outArray.length-1]+' = '+array[n]+' ?');
if(outArray[outArray.length-1]!=array[n]){
outArray.push(array[n]);
}
}
return outArray;
}
in your code this would replace the line
var uniqueData = allData.sort();
that would become :
var uniqueData = removeDups(allData);
EDIT :
If letter case is an issue, you can modify this code to ignore it. You should change the condition and the sort function so that they both ignore the case in your names but preferably keep the original letter case.
This could be achieved with the code below :
function test(){
var array = ['aa','bb','Cc','AA','dd','CC'];// an example with Upper and Lower case
Logger.log(removeDups(array));
}
function removeDups(array) {
var outArray = [];
array.sort(lowerCase);
function lowerCase(a,b){
return a.toLowerCase()>b.toLowerCase() ? 1 : -1;// sort function that does not "see" letter case
}
outArray.push(array[0]);
for(var n in array){
Logger.log(outArray[outArray.length-1]+' = '+array[n]+' ?');
if(outArray[outArray.length-1].toLowerCase()!=array[n].toLowerCase()){
outArray.push(array[n]);
}
}
return outArray;
}
Logger result :
EDIT 2 :
Here is another version that keeps only unique values (I didn't understand correctly your request in the first version as it kept one element from the duplicates...)
I simply added an else if condition to remove the elements that were part of a group of duplicates.(I kept the case insensitive version but you can remove it easily)
function test(){
var array = ['aa','dd','hh','aa','bb','Cc','cc','cc','bb','nn','bb','AA','dd','CC'];// an example with Upper and Lower case
Logger.log('original array = '+array);
Logger.log('unique result = '+removeDups2(array));
}
function removeDups2(array) {
var uniqueArray = []
array.sort(lowerCase);
function lowerCase(a,b){
return a.toLowerCase()>b.toLowerCase() ? 1 : -1;// sort function that does not "see" letter case
}
var temp = array[0];
for(var n=1 ;n<array.length ; n++){
Logger.log(temp+' = '+array[n]+' ?');
if(temp.toLowerCase()!=array[n].toLowerCase()){
uniqueArray.push(array[n]);
temp = array[n];
}else if(uniqueArray[uniqueArray.length-1]==temp){
uniqueArray.pop();// remove it from result if one of the duplicate values
}
}
return uniqueArray;
}
Logger result new code :
In your code you are not doing anything to filter the duplicate values.
This line will just sort the data and won't give you unique data.
var uniqueData = allData.sort();
You can do something like this on your merged array, after you 'installing' 2DArray lib: https://sites.google.com/site/scriptsexamples/custom-methods/2d-arrays-library
var uniqueData = unique(allData);
Another option is to create a loop and check for duplicate values, but you should remember to transform all the values of the string to lowercase before you do these matches.
I created this function and it worked.
function removeDups(data) {
var newData = [];
data.forEach(function(value) {
if (newData.indexOf(value) == -1) {
newData.push(value);
}
});
return newData;
}
Yet another solution:
function removeDups(array) {
array.sort()
var lastValue = !array[0]
var outArray = array.filter(function(value) {
if (value == lastValue)
return false
lastValue = value
return true
})
return outArray
}
This also works correctly for empty arrays whereas some earlier solutions yield [null] in this special case.