Importing data from different Spreadsheets - cache issue google app script - google-apps-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))

Related

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;
}

Detect formula errors in Google Sheets using 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!

Return Row Data from Spreadsheet having specified string without looping through all Rows

I have a task at hand wherein I need to return the entire Row Data containing a user-defined string.
One way of achieving it is looping through all the rows, but this works only when you know in which column to search, as shown using the code given below.
var sheetData = SpreadsheetApp.getActiveSpreadsheet().getDataRange().getValues();
var searchString = "testSearch";
for(var i=0;i<sheetData.getLastRow();i++)
{
//assuming we know that the search string is going to be in Column 2
if(sheetData[i][1].search(searchString)!=-1)
{
var rowData = sheetData[i];
return rowData;
}
}
So my question is, is there any way in which we can achieve this, without having to loop through all rows one by one?
To make the problem statement more clear, I wish to achieve something like the 'Find' feature as demonstrated in the image below:
This would make it very easy to skim through huge data spread across multiple sheets/spreadsheets.
Note: I am searching for a solution to this using Google Apps Script.
Here is code that gets the data from the row with the match. It can be a partial match. You do not need to know the number of columns or rows. You do not need to know which column to look in.
To make this code work, replace Put Sheet Tab Name Here with the name of the sheet tab to use. The searchString can be passed into the function. If nothing is passed in, then the code uses a hard coded value for the search.
function findRowOfSearchString(searchString) {
var arrIndexOfAllMatches,dataAsStringWithBrackets,dataRange,i,
isThereA_Match,j,ll,L,L2,matchOfAllInnerBrackets,numberOfColumns,numberOfRows,
rowCutOff,rowsOfMatchedData,sh,sheetData,ss,thisIndex,thisMatchIndex,thisRow,thisRowData;
ll = function(a,b) {
Logger.log(a + ": " + b)
}
if (!searchString) {
searchString = "testSe";
}
ss = SpreadsheetApp.getActiveSpreadsheet();
sh = ss.getSheetByName('Put Sheet Tab Name Here');//
dataRange = sh.getDataRange();
numberOfColumns = dataRange.getNumColumns();
numberOfRows = dataRange.getNumRows(); //changed 'getNumColumns' to 'getNumRows'
sheetData = dataRange.getValues();//Get a 2D array of all sheet data
dataAsStringWithBrackets = JSON.stringify(sheetData);
//ll('dataAsStringWithBrackets: ',dataAsStringWithBrackets)
isThereA_Match = dataAsStringWithBrackets.indexOf(searchString);
//ll('isThereA_Match: ',isThereA_Match)
if (isThereA_Match === -1) {return;}//There is no match - stop
arrIndexOfAllMatches = [];
L = dataAsStringWithBrackets.length;
//ll('L',L)
thisMatchIndex = 0;
for (i=0;i<L;i++) {
//ll('i',i)
thisMatchIndex = dataAsStringWithBrackets.indexOf(searchString,thisMatchIndex + 1);
//ll('thisMatchIndex',thisMatchIndex)
if (thisMatchIndex === -1) {//No more matches were found
//ll('No more matches found',thisMatchIndex)
break;
}
arrIndexOfAllMatches.push(thisMatchIndex);
}
//ll('arrIndexOfAllMatches',arrIndexOfAllMatches)
matchOfAllInnerBrackets = [];
thisMatchIndex = 0;
for (i=0;i<L;i++){
thisMatchIndex = dataAsStringWithBrackets.indexOf("],[",thisMatchIndex + 1);
//ll('thisMatchIndex',thisMatchIndex)
if (thisMatchIndex === -1) {//No more matches were found
//ll('No more matches found',thisMatchIndex)
break;
}
matchOfAllInnerBrackets.push(thisMatchIndex);
}
ll('matchOfAllInnerBrackets',matchOfAllInnerBrackets)
rowsOfMatchedData = [];
L = arrIndexOfAllMatches.length;
L2 = matchOfAllInnerBrackets.length;
for (i=0;i<L;i++){
thisIndex = arrIndexOfAllMatches[i];
ll('thisIndex: ' ,thisIndex)
for (j=0;j<L2;j++){
rowCutOff = matchOfAllInnerBrackets[j];
ll('rowCutOff: ',rowCutOff)
if (rowCutOff > thisIndex) {
ll('greater than: ' ,thisIndex > rowCutOff)
thisRow = j+1;
ll('thisRow: ', (thisRow))
rowsOfMatchedData.push(thisRow)
break;
}
}
}
ll('rowsOfMatchedData: ',rowsOfMatchedData)
L = rowsOfMatchedData.length;
for (i=0;i<L;i++){
thisRowData = sh.getRange(rowsOfMatchedData[i], 1, 1, numberOfColumns).getValues();
ll('thisRowData: ',thisRowData)
}
}

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.

Adding a row above 1st counted instance - Google Spreadsheet

I'm attempting to write a script which will enable me to add a single row above the 1st counted instance of a unique value.
Eg.
A
German
German
German
Italian
Italian
French
French
After running the script it should resemble this:
A
DE
German
German
German
IT
Italian
Italian
FR
French
French
What I have written has only gotten as far as identifying how many values are present:
function insertRowAbove()
{
var report = SpreadsheetApp.getActive().getSheetByName('REPORT');
var lang = report.getRange('A10:A').getValues();
var positions = report.getRange('A10:A').getA1Notation();
var DE = [];
for (var i = 0; i < lang.length; i++)
{
if (lang[i] == 'German')
{
DE++
report.getRange('A1').setValue(DE);//now I know there are 3 German entries
}
}
}
My question:
Is it possible for the script to know the A1notation of the 1st occurrence of a value and add a row above it? I thank you for any sagely advice.
Your specific question was:
My question: Is it possible for the script to know the A1notation of
the 1st occurrence of a value and add a row above it?
By using a combination of a Boolean to indicate whether you've found any of the items you are looking for, and a couple of Range methods, you can get the A1notation of the first occurrence.
var column = 1; // this example is only using column A
var foundGerman = false;
var firstGerman = '';
...
if (lang[i] == 'German') {
if (!foundGerman) {
foundGerman = true;
firstGerman = range.getCell(i, column).getA1Notation();
}
...
}
After that, you will have the A1Notation of the first cell containing "German".
However, the function insertRowsBefore() is a Sheet method, and expects a row number as a parameter, not A1Notation. So figuring out what the address of the first German cell was turns out to be unnecessary.
Script
Here's my entry in the contest! For speed, it's wise to use as few apps script service calls as possible. In this script, all data manipulation is done using arrays, with the final result written once.
In anticipation that you'll have more than three languages that you care about, and for maintainability, the language lookup is handled by an Object, iso639. (Assuming you're using ISO 639-1 Language Codes.) As a result, the actual work takes just 10 lines of code!
function insertViaArray() {
var sheet = SpreadsheetApp.getActive().getSheetByName('REPORT');
var values = sheet.getDataRange().getValues();
var newValues = [];
var iso639 =
{
"German" : "DE",
"Italian" : "IT",
"French" : "FR"
}
var curLang = '';
for (var i in values) {
if (values[i][0] !== curLang) {
curLang = values[i][0];
newValues.push([iso639[curLang]]);
}
newValues.push([values[i][0]]);
}
sheet.getRange(1, 1, newValues.length, 1).setValues(newValues)
};
Edit Script V2
By using Array.splice() to insert the language tags, we can further reduce the code to 8 working lines, and eliminate the need for a parallel newValues array.
function insertViaArrayV2() {
var sheet = SpreadsheetApp.getActive().getSheetByName('REPORT');
var values = sheet.getDataRange().getValues();
var iso639 =
{
"German" : "DE",
"Italian" : "IT",
"French" : "FR"
}
var curLang = '';
for (var i = 0; i < values.length; i++) {
if (values[i][0] !== curLang) {
curLang = values[i][0];
values.splice(i, 0, [iso639[curLang]]);
}
}
sheet.getRange(1, 1, values.length, 1).setValues(values)
};
The following works (see EXAMPLE):
function insertRowAbove() {
var report = SpreadsheetApp.getActive().getSheetByName('Insert Row Above');
var values = report.getRange('B2:B').getValues();
var prevVal = "null";
var index = 1;
while (index < values.length) {
if (values[index].toString() != prevVal.toString()) {
var header = 'unknown';
if (values[index] == 'German')
header = 'DE';
else if (values[index] == 'Italian')
header = 'IT';
else if (values[index] == 'French')
header = 'FR';
report.insertRowBefore(index+1);
report.getRange(index+1, 2).setValue(header);
values = report.getRange('B2:B').getValues();
index++;
}
prevVal = values[index];
index++;
}
}
This is kind of a funny exercise and, as usual, there are probably many ways to get it working.
I'm not pretending my approach is better, it's just different and therefor it it probably worth showing it here ;-)
function testFunction() {
var sh = SpreadsheetApp.getActiveSheet();
var data = sh.getRange('A10:A').getValues();
var rowNum = 11;
var previous = data[0][0];
Logger.log(data)
sh.insertRowBefore(10);
sh.getRange(2,1).setValue(firstLetters(data[0][0]));
for(n=1;n<data.length;++n){
if(data[n][0].replace(/ /g,'')==previous.replace(/ /g,'')){
previous = data[n][0] ;
++rowNum ;
continue ;
}else if(firstLetters(data[n][0])){
sh.insertRowBefore(rowNum+1);
sh.getRange(rowNum+1,1).setValue(firstLetters(data[n][0]));
++rowNum
previous = data[n][0]
++rowNum
Logger.log(rowNum+' '+firstLetters(data[n][0]))
}else{
break
}
}
}
function firstLetters(name){
if (name==''){return false}
var str = name.toString().replace(/ /g,'').toUpperCase().substring(0,2);
if (str=='GE') {str='DE'};// handle german exception
if (str=='PO'){ str='PT'} ;//handle portuguese exception
return str;
}