I have defined some named ranges in a sheet that I later delete. Afterwards, the ranges remain in the sidebar "Data->Named ranges...", with the range "#REF". I would like to delete them because I don't want them to accumulate.
They are not listed in SpreadsheetApp.GetActiveSpreadsheet.getNamedRanges().
How can I delete them programatically?
An alternative solution would be how to define a named range that is removed when a sheet is deleted. This happens if you have a named range in a sheet that is duplicated - the named range has a name like "'Sheet1Copy'!RangeName", but it's not possible to define a name like this.
Use removeNamedRange(name) to remove a Named Range. It will work even with Named Ranges that has #REF! as range and are not returned by SpreadsheetApp.getActiveSpreadsheet().getNamedRanges().
In order to make easier to maintain your spreadsheets free of Named Ranges with #REF! as range, keep a list of your Named Ranges. You could use an auxiliary spreadsheet for that.
UPDATED BELOW
The accepted answer works fine if you are removing a unique Named Range, but not if you have more than one Range sharing the same Name (i.e. Sheet-scoped Named Ranges vs Spreadsheet-scoped Named Ranges).
The first instance of the Named Range will probably be scoped to the entire Spreadsheet, whereas any copies of the Sheet (that contain the original Named Range) will feature Named Ranges that are scoped to the copied Sheet (e.g. 'SheetCopy'!NamedRange instead of NamedRange).
If you want to remove Sheet-scoped Named Ranges whose references are no longer valid, try running the following script from the Script Editor:
function removeDeadReferences()
{
var activeSS = SpreadsheetApp.getActiveSpreadsheet();
var sheets = activeSS.getSheets();
var sheetNamedRanges, loopRangeA1Notation;
var x, i;
// minimum sheet count is 1, no need to check for empty array, but why not
if (sheets.length)
{
for (x in sheets)
{
sheetNamedRanges = sheets[x].getNamedRanges();
// check for empty array
if (sheetNamedRanges.length)
{
for (i = 0; i < sheetNamedRanges.length; i++)
{ // get A1 notation of referenced cells for testing purposes
loopRangeA1Notation = sheetNamedRanges[i].getRange().getA1Notation();
// check for length to prevent throwing errors during tests
if (loopRangeA1Notation.length)
{ // check for bad reference
// note: not sure why the trailing "!" mark is currently omitted
// ....: so there are added tests to ensure future compatibility
if (
loopRangeA1Notation.slice(0,1) === "#"
|| loopRangeA1Notation.slice(-1) === "!"
|| loopRangeA1Notation.indexOf("REF") > -1
)
{
sheetNamedRanges[i].remove();
}
}
}
}
}
}
}
Edit / Update
I recently needed to run this script again, and Google must've made changes to the way the .getRange() function works. The previous script will not work if the Named Range contains an invalid reference.
Also, after re-reading the initial question, I feel like I was too focused on the Sheet-scoped aspect of the answer.
The OP seemed to be after more of an automated experience, which the Accepted Answer does not provide. In addition to not requiring an externally maintained list of Named Ranges, the following script will remove both Sheet-scoped and Spreadsheet-scoped Named Ranges that contain invalid references.
Here's the updated script, tested to work with the V8 engine:
function removeDeadReferences()
{
var activeSS = SpreadsheetApp.getActiveSpreadsheet();
var sheets = activeSS.getSheets();
var sheet;
var sheetName;
var sheetNamedRanges, sheetNamedRange, sheetNamedRangeName;
var loopRange, loopRangeA1Notation;
var x, i;
// minimum sheet count is 1, no need to check for empty array
for (x in sheets)
{
sheet = sheets[x];
// for logging
sheetName = sheet.getName();
sheetNamedRanges = sheet.getNamedRanges();
// check for empty array
if (sheetNamedRanges.length)
{
for (i = 0; i < sheetNamedRanges.length; i++)
{
sheetNamedRange = sheetNamedRanges[i];
// for logging
sheetNamedRangeName = sheetNamedRange.getName();
// v8 engine won't allow you to get range if it is invalid
try {
loopRange = sheetNamedRange.getRange();
}
catch (error)
{
Logger.log(error);
loopRange = null;
}
// get A1 notation of referenced cells for testing purposes
loopRangeA1Notation = (
loopRange != null
? loopRange.getA1Notation()
: false
);
// check for bad reference
// added tests to ensure future compatibility
// but any of these should suffice
// comment out ones you don't want to test for
if (
loopRangeA1Notation == false
|| loopRangeA1Notation.slice(0,1) === "#"
|| loopRangeA1Notation.slice(-1) === "!"
|| loopRangeA1Notation.indexOf("REF") > -1
)
{
Logger.log("The named range, '" + sheetNamedRangeName + "', within the Sheet named, '" + sheetName + "', was removed.");
sheetNamedRange.remove();
}
}
}
}
}
The function below loops sheets and deletes "bad" named ranges.
You may see it live here. Make your own copy and go to menu:
⚡ Test Automation > 🍏 Sheets > 🦶 Delete Bad Named Ranges
please try
function test_deleteBadNamedRanges() {
var sets = {
delimiter: '|',
file: '', // leave blank to use active spreadsheet
sheet_names: 'nonono|Sheet1|Sheet2', // note put empty string or null to use all sheets
remove_copies: true, // set true to delete 'copy of sheet'!named_ranges
success_message: '🦶Removed "bad" named ranges!'
}
var result = deleteBadNamedRanges_(sets);
console.log(result);
console.log(sets);
}
function deleteBadNamedRanges_(sets) {
var book, msg = '';
try {
if (!sets.file || set.file === '') {
book = SpreadsheetApp.getActive();
} else {
book = SpreadsheetApp.openById(sets.file);
}
} catch (err) {
msg = '🤪Could not get the book for some reason.'
msg += ' The error is: ' + err;
return msg;
}
if (!book) {
msg = '🤪The book object could not be retrieved.'
return msg;
}
/** function to get sheets by names */
var sheets = [];
var errors_log = [];
var getSheetsByNames_ = function(book, names) {
var sheets = [], sht, msg;
for (var i = 0; i < names.length; i++) {
mag = 'ok';
try {
sht = book.getSheetByName(names[i]);
} catch (err) {
msg = '❌no sheet ' + names[i] + '. ';
msg += 'Err: ' + err;
sht = false;
}
if (!sht) {
msg = '❌not found sheet ' + names[i]
}
sheets.push(sht);
errors_log.push(msg);
}
return sheets;
}
/** get sheets */
if (!sets.sheet_names || sets.sheet_names === '') {
sheets = book.getSheets();
} else {
sheets = getSheetsByNames_(
book,
sets.sheet_names.split(sets.delimiter));
}
/** delete bad named ranges for each sheet */
var res = [];
for (var i = 0; i < sheets.length; i++) {
sets.sheet = sheets[i];
if (sets.sheet) {
res.push(deleteBadNamedRangesSheet_(sets))
} else {
res.push(errors_log[i]);
}
}
sets.sheet = '_end_of_loop_';
var result = sets.success_message + '\\n\\n' +
res.join('\\n');
return result;
}
/**
*
* delete bad ranges for 1 sheet
*/
function deleteBadNamedRangesSheet_(sets) {
var sheet = sets.sheet;
var sheet_name = sets.sheet.getName();
// for logs
if (!sets.removed_ramed_ranges) {
sets.removed_ramed_ranges = [];
}
/** function to remove named range
* and log it to memory
*/
var count_deleted = 0;
var add2removed_ = function(namedrange, a1, name) {
namedrange.remove();
sets.removed_ramed_ranges.push({
sheet_name: sheet_name,
name: name,
a1: a1
});
count_deleted++;
return 0;
}
/** loop and check named ranges */
var ranges = sheet.getNamedRanges();
var a1 = '', nr, name = '', check_copy;
for (var i = 0; i < ranges.length; i++) {
nr = ranges[i];
a1 = nr.getRange().getA1Notation();
name = nr.getName();
// remove copy of named range
if (sets.remove_copies) {
check_copy = name
.replace(sheet_name, '')
.substring(0,3);
if (check_copy === "''!") {
add2removed_(nr, a1, name);
}
}
// remove named range
if (a1 == '#REF!') {
add2removed_(nr, a1, name);
}
}
return '✔️Sheet ' + sheet_name + ', deleted: ' + count_deleted;
}
Related
I am trying to get results of following function in cells in google sheet.
I can get the results in script log but i need it in cells .. can someone add to this syntax so that its results can be placed in cells.
function scrapeDemo(url) {
var html = UrlFetchApp.fetch(url).getContentText();
var res = html.match(/<div class="cr_data rr_stockprice module">.+?(<table .+?<\/table>)/);
var document = XmlService.parse(res[1]);
var root = document.getRootElement();
var trNodes = root.getChild('tbody').getChildren();
trNodes.forEach((trNode) => {
var tdNodes = trNode.getChildren();
var fieldName;
var fieldValue;
tdNodes.forEach((tdNode, idx) => {
if (idx % 2 === 0) {
fieldName = tdNode.getValue().trim();
} else {
fieldValue = tdNode.getValue().trim();
console.log( fieldName + " : " + fieldValue );
}
} );
} );
}
I am trying to get the result that are currently available in script log in cells. When I put scrapeDemo(url) in a cell It should give the data. It can be as a table with Fieldnames in one column and Fieldvalue in next column.
The script in the question body doesn't include a return statement, but A Google Sheets custom function should have one returning a value / object supported by Google Sheets (number, string, boolean, date, or an Array of Arrays of supported values/objects), example:
/**
* If a and b are numbers these values will be added
* If a or b are strings the these values will be concatenated
*/
function myFunction(a,b){
return a + b;
}
/**
* Returns a Matrix of X having rowNum by colNum
*
*/
function XMATRIX(rowNum,colNum){
var output = [];
for(var i = 0; i < rowNum; i++){
output[i] = [];
for(var j = 0; j < colNum; j++){
output[i][j] = 'X';
}
}
return output
}
Resources
https://developers.google.com/apps-script/guides/sheets/functions
If you have a range or want to build a range of data and you are executing your script in a google sheet, you can do it like:
var sheet = SpreadsheetApp.getActive().getSheetByName("NameOfTheSheet");
sheet.getActiveCell("A1").setValues(valueRange);
If you want to write value by value on its own. Use:
var sheet = SpreadsheetApp.getActive().getSheetByName("NameOfTheSheet");
sheet.setActiveRange("A1").setValues(value);
if you want to iterate in the Cells, then use:
...
sheet.setActiveRange("A" + i).setValues(value);
i++;
...
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: "®EXREPLACE(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;
}
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!
*,
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))
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.