Going from a google spreadsheet to a JS array - javascript

I have a google spreadsheet which i've made public and all and want to go from that to a JS array. I've been playing with it and trying to convert it into a JSON then into an array but i'm having no luck. Is there a way to go from my spreadsheet to a 2d JS array which I can then run functions on.
So if the spreadsheet looks like this:
NAME COLOR SIZE
MARK BLUE 6
DAVE RED 8
The array will be a 2d array such that value [0][0] will be MARK, [1][0] will be BLUE and [0][1] will be DAVE etc.
Any help would be much appreciated

Edit: since google API4 is our, and the module was updated accordingly, and since API3 will be discontinued on September, I updated the code accordingly.
I know this is old now, but in case any one else searching for an answer:
I played around with manipulating google spreadsheet in JS, and wanted to go from a worksheet with several sheets, to an array I can easily use in other functions.
So I wrote a program to access a Worksheet, get all of the sheets (tab) from within, reduce the all-empty rows and columns, and return the a new worksheet array.
Each element in the worksheet array represents the sheet from the Excel worksheet.
Also, each element has a 2d array of its cells, as such:
cellsArr:[[row],[col]]
To run the program, simply replace YOUR_CLIENT_SECRET.json to your client secret json file, and ENTER_GOOGLE_SPREADSHEET_ID_FROM_URL to the ID of the spreadsheet from the URL. For example:
docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit#gid=01
and then run:
getWorksheet(ENTER_GOOGLE_SPREADSHEET_ID_FROM_URL);
This returns a promise, which is resolved into the revised worksheet (as described above).
Please note, reduceRowsCols() deletes empty rows and columns from each sheet. It deletes each row where the first column cell is empty, and every column where the first row cell is empty, so every row where [i][0]==null and every column where [0][i]==null.
Simply remove reduceRowsCols() from the code to negate that
const fs = require('fs');
const {GoogleSpreadsheet} = require('google-spreadsheet'),
creds = require('./client_secret.json'),
/**
* #Description Authenticate with the Google Spreadsheets API using credentials json.
* #param {string} spreadsheet_id - spreadsheet id to get. Get this from the URL of the spreadsheet
* Returns the entire raw worksheet.
* */
async function accessSpreadsheet(spreadsheet_id) {
const doc = new GoogleSpreadsheet(spreadsheet_id);
await doc.useServiceAccountAuth(creds);
await doc.loadInfo();
console.log(`${getCurrentTime()}: Loaded raw doc ${doc.title}`);
return doc;
}
/**
* #Description This is an async function to retrieve the raw worksheet
* #param {string} spreadsheet_id - spreadsheet id to get. Get this from the URL of the spreadsheet
* */
async function getWorksheet(spreadsheet_id) {
try {
let res = await accessSpreadsheet(spreadsheet_id);
console.log(`${getCurrentTime()}: Organizing sheets for extraction...`);
res = await getCondensedWorksheets(res.sheetsByIndex);
createSheetsCopy(res);
return res;
} catch (e) {
throw (e);
}
}
/**
* #param worksheetsArr
*/
async function getCondensedWorksheets(worksheetsArr) {
if (!Array.isArray(worksheetsArr)) {
throw `getCondensedWorksheets: worksheets variable passed is not an array. Object passed:\n${worksheetsArr}`;
}
let revisedArr = [];
for (let i = 0; i < worksheetsArr.length; i++) {
// for (let i = 0; i < 1; i++) {
revisedArr.push(await worksheetsArr[i].getRows().then((res)=>{
let thisRevised = {
id: worksheetsArr[i]._rawProperties.sheetId,
title: worksheetsArr[i].title,
rowCount: worksheetsArr[i].rowCount,
colCount: worksheetsArr[i].columnCount,
getRows: worksheetsArr[i].getRows,
getHeaders: worksheetsArr[i].headerValues,
getCells : res.map( row => {return(row._rawData);}),
resize: worksheetsArr[i].resize,
}
return getCells2dArray(thisRevised);
}))
}
return Promise.all(revisedArr).then(()=>{
return revisedArr;
})
}
/**
* #param {array} thisSheet - a single sheet (tab)
*/
function getCells2dArray(thisSheet) {
let sheetCellsArr = [];
sheetCellsArr.push(thisSheet.getHeaders);
thisSheet.getCells.map(row => {
sheetCellsArr.push(row);
})
thisSheet.cellsArr = sheetCellsArr;
delete thisSheet.getCells;
reduceRowsCols(thisSheet);
return thisSheet;
}
function reduceRowsCols(thisSheet) {
for (let i = 0; i < thisSheet.cellsArr.length; i++) {
if (thisSheet.cellsArr[i][0] == null) {
thisSheet.cellsArr.slice(0, i);
break;
}
}
for (let i = 0; i < thisSheet.cellsArr[0].length; i++) {
if (thisSheet.cellsArr[0][i] == null) {
thisSheet.cellsArr[0].slice(0, i);
break;
}
}
}
const getCurrentTime = function (){
var date = new Date();
return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}:${date.getMilliseconds()}`
}
module.exports = {getWorksheet};

There are two methods in GAS that allow for reading values in cells. They are .getValue() and .getValues(). As it is implied, .getValue() retrieves only the value of the top left cell in a range. .getValues() retrieves a 2D array of all values in your range.
The code below defines the variables necessary to read the range that follows. The range is defined (based off your example) as starting at the second row (ignores your header row), continuing for 3 columns and down to the last row with content.
You'll notice that we find .getLastRow() but then define the number of rows to read as lastRow - 1. This is because .getLastRow() gives us the position of the last row with data; or more simply, an integer of how many rows have data. If we have 20 rows with data, that includes the header row which we would like to ignore. Reading 20 rows after shifting our range down a row will include the first empty row. Reading 19 rows instead stops after reading the last data-filled row.
Note that .getRange() requires the input of .getRange(startRow, startColumn, numRows, numColumns). This can be found with more detail and explanation in the GAS Reference sheet here
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSpreadsheet();
var lastRow = sheet.getLastRow();
var range = sheet.getRange(2, 1, (lastRow - 1), 3);
var dataArray = range.getValues();
}
Values in this array can then be called, defined, read, etc. as you would expect for Javascript. You will find in the GAS Reference that many methods have a singular and plural version such as .getBackground() and .getBackgrounds(). These will act in the same comparative way as .getValue() vs. .getValues().
It may also be helpful to note that even if your range is a singular row but many columns, .getValues() gives you a 2D array where all values will start with [0] as the only possible value for row is 0.

Related

Optimization of code while working with range object in excel

I have recently moved for office add-in from vb.net to JavaScript and Office.js. In General, I have observed that JavaScript add-in is way too fast than to vb.net.
In one of the operations, I am not able to get benefit of speed in JavaScript add-in. Maybe it is due to inadequate code that I am using!
I have one master sheet which contains a large table, first column of table is name of every other sheets of that workbook and first row of table have address of cells. Rest of data in table are values to be transferred at the sheet name defined in the first column and cell adress defined in the first row.
In coding, I am creating an array of range object for each value indicated in table and then I run context. Sync () function to restore values in every sheets.
In my real-life application data in table can be 10K to 50K, I can see time taken for this operation is about approx. one minute (for 10K). In contrast of this, I can create the table (master sheet) within few seconds (5 -6 sec) only.
Is there any other workaround or suggestion to reduce time?
/* global Excel, console*/
export default async function restoreData() {
var allRollback = false;
await Excel.run(async (context) => {
var sheets = context.workbook.worksheets.load("items/name");
var wsdb = context.workbook.worksheets.getItem("db");
const arryRange = wsdb.getUsedRange();
var addRow = 0;
var sheetName = [];
var rangeObj = [];
//Get last row/column from used range
arryRange.load(["rowCount", "columnCount", "values", "address"]);
await context.sync();
sheets.items.forEach((sheet) => sheetName.push(sheet.name));
for (let iRow = 0; iRow < arryRange.rowCount; iRow++) {
if (arryRange.values[iRow][0] == "SheetName/CellAddress") {
addRow = iRow;
}
if (sheetName.indexOf(arryRange.values[iRow][0]) != -1) {
for (let iCol = 1; iCol < arryRange.columnCount; iCol++) {
if (arryRange.values[addRow][iCol]) {
const sheet = context.workbook.worksheets.getItem(arryRange.values[iRow][0]);
const range = sheet.getRange(arryRange.values[addRow][iCol]);
range.values = arryRange.values[iRow][iCol];
rangeObj.push(range);
}
}
} else {
// code for higlight Row in db
console.log("Y");
}
}
console.log("Range object created");
await context.sync();
// console.log(arryRange.rowCount);
// console.log(arryRange.columnCount);
console.log("done");
// Copy a range starting at a single cell destination.
});
allRollback = true;
return allRollback;
}
First, base on your statement, I assume you have a table in master sheet. This table's heading is like ["sheetName","A13","D23",...] ("A13" and "D23" are examples of cell address). In each row of this table, contain sheet's name and some values. The sheet's name may not related to a real sheet's name(not exist), and values may contain some blank. And you want to set values on other sheets based on the information given by master sheet's table.
Then I have some suggestions based on my assumptions and your code.
Move unchanged value out of loops.
For example, you called const sheet = context.workbook.worksheets.getItem(arryRange.values[iRow][0]);. We can move context.workbook.worksheets out of loops by define var sheets = context.workbook.worksheets and const sheet = sheets.getItem(arryRange.values[iRow][0]). Which could increase the performance.
Also some reused values like arryRange.values[iRow][0], arryRange.values[0][iCol] can be moved out of loop.
Seems you use arryRange.values[addRow][iCol] only for get the address in table's heading. You can replace it by arryRange.values[0][iCol].
Below is the code I rewrite, just for reference, it may not fully satisfy what you need.
export default async function restoreData() {
var allRollback = false;
await Excel.run(async (context) => {
var sheets = context.workbook.worksheets.load("items/name");
var wsdb = context.workbook.worksheets.getItem("db");
const arryRange = wsdb.getUsedRange();
//var addRow = 0;
var sheetName = [];
var rangeObj = [];
//Get last row/column from used range
arryRange.load(["rowCount", "columnCount", "values", "address"]);
await context.sync();
sheets.items.forEach((sheet) => sheetName.push(sheet.name));
var cellAddress, curSheetName;
const mySheets = context.workbook.worksheets;
for (let iRow = 0; iRow < arryRange.rowCount; iRow++) {
curSheetName = arryRange.values[iRow][0]
if (sheetName.indexOf(curSheetName) != -1) {
for (let iCol = 1; iCol < arryRange.columnCount; iCol++) {
cellAddress = arryRange.values[0][iCol];
if (cellAddress) {
const sheet = mySheets.getItem(curSheetName);
const range = sheet.getRange(cellAddress);
range.values = arryRange.values[iRow][iCol];
rangeObj.push(range);
}
}
} else {
// code for higlight Row in db
console.log("Y");
}
}
console.log("Range object created");
await context.sync();
// console.log(arryRange.rowCount);
// console.log(arryRange.columnCount);
console.log("done");
// Copy a range starting at a single cell destination.
});
allRollback = true;
return allRollback;
}
More references:
https://learn.microsoft.com/en-us/office/dev/add-ins/excel/performance?view=excel-js-preview
https://learn.microsoft.com/en-us/office/dev/add-ins/concepts/correlated-objects-pattern
with refer of you assumption, please note that master sheet was crated based on actual workbook with user selected area + deletion of empty value columns in master sheet (I,e empty cell at the adress at each sheet). sheet's name will be as same as actual sheet name unless user don’t change value in master sheet by accident.
With reference to yours,
Suggestions 1) I believe that Move unchanged value out of loops will be the key to my problem. I will refabricate one range and get data comparison for changes. I believe speed will be approved drastically in best case scenario. (I will also have some worst-case scenario (less than 5% case), where I will be required to write every values of master sheet).
Suggestions 2) I am planning to have a new functionality, which may have more rows as address in, that is the reason I have I am keep looking for address rows.
Thanks for your reply.

How to update a custom formula inside google spredsheets automatically without loosing cells around?

Hello guys I am trying to receive crypto prices by using Googles Spreadsheet Formula, putting a Formula into google spreadsheets works just fine and I am getting the prices without any problems.
=IMPORTXML("https://www.marketscreener.com/quote/cryptocurrency/BITCOIN-EURO-45553925/";"//td[#class='fvPrice colorBlack']")
The Problem: Custom Formulas like IMPORTXML do not update automatically in the spreadsheet, so I grabbed every method from google which does this task, none of them which worked. Until I found one that works partially, it does the job but unfortunately deletes the cells around my formula.
This is my spreadsheet before running the method Before
This is my spreadsheet after running the method After
And this is the method that partially works:
/**
* Get all sheet names from active spreadsheet
*/
function sheetNames() {
var sheetNames = [];
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i = 0; i < sheets.length; i++) sheetNames.push( sheets[i].getName() )
return sheetNames;
}
/**
* Update all formula data in cell range
*/
function updateData() {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet(),
activeSheet = spreadsheet.getActiveSheet()
activeSheetName = activeSheet.getSheetName(),
activeRange = activeSheet.getActiveRange(),
sheets = sheetNames() // OR define an array of sheet names: ['Sheet1', 'Sheet2'],
// cellRange = (activeRange.getValues().length > 1) ? activeRange.getA1Notation() : Browser.inputBox('Enter your range'),
range = '';
// loop through all sheets
sheets.forEach(function(sheet) {
var originalFormulas = [],
range = activeRange,
defaultRanges = {
'europeList': 'A1:F9',
'europeList': 'A1:F9'
},
// set cell range if entered, ortherwise fall back to default
cellRange = defaultRanges[sheet];
// set range to active range, otherwise fall back to defined cell range
range = spreadsheet.getSheetByName(sheet).getRange(cellRange);
originalFormulas = range.getFormulas();
// modify formulas
const modified = originalFormulas.map(function (row) {
return row.map(function (col) {
return col.replace('=', '?');
});
});
// update formulas
range.setFormulas(modified);
SpreadsheetApp.flush();
// return formulas to original
range.setFormulas(originalFormulas);
});
}
Anyone got a idea, how to fix this?

Delete Row if cell starts with certain text - Google Sheets / Apps Script

I have rows of data in column A containing cells starting with AR. I would like any cell that contains AR to be deleted. I have script already but this only deletes exact matches
So example is AR12345 in Column A & A12345. So it should ONLY delete the cell row with AR and not just A
function DeleteAny() {
var sheet = SpreadsheetApp.getActive();
sheet.setActiveSheet(sheet.getSheetByName('MULTI KIT DATA'), true);
var rows = sheet.getDataRange();
var numRows = rows.getNumRows();
var values = rows.getValues();
var rowsDeleted = 0;
for (var i = 0; i <= numRows - 1; i++) {
var row = values[i];
// I cant put AR here because it wont delete anything. the AR numbers keep changing also
if (row[14] == '') {
sheet.deleteRow((parseInt(i)+1) - rowsDeleted);
rowsDeleted++;
}
}
};
I have searched but cannot find anything.
How about using javascript Array#filter ? This is probably simplest for simple data.
// filter values
let values = sheet.getDataRange().getValues()
.filter(row => !row.find(v => v.match(/^AR/)))
// clear range
sheet.getDataRange().clear()
// write back filtered values
sheet.getRange(1, 1, values.length, values[0].length).setValues(values)
How about the following modification?
Pattern 1:
In this pattern, as a simple modification, your script is modified.
From:
if (row[14] == '') {
To:
if (row[0].length > 1 && row[0].substr(0, 2) == "AR") {
In this modification, the top 2 characters are retrieved with row[0].substr(0, 2).
If AR is included in the inner value, if (row[0].length > 1 && row[0].includes("AR")) { might be suitable.
Note:
In your script, row[14] is used. But in your question, the values are in the column "A". So I used row[0]. If you want to check other column, please modify it.
Pattern 2:
In this pattern, as other sample script, TextFinder and Sheets API are used. When you use this, please enable Sheets API at Advanced Google services.
function myFunction() {
const sheetName = "MULTI KIT DATA"; // Please set the sheet name.
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(sheetName);
const sheetId = sheet.getSheetId();
const requests = sheet
.getRange(`A1:A${sheet.getLastRow()}`)
.createTextFinder("^AR")
.matchCase(true)
.useRegularExpression(true)
.findAll()
.map(r => r.getRow())
.reverse()
.map(r => ({deleteDimension:{range:{sheetId:sheetId,startIndex:r - 1,endIndex:r,dimension:"ROWS"}}}));
Sheets.Spreadsheets.batchUpdate({requests: requests}, ss.getId());
}
In this sample script, the ranges of values which have AR at the top 2 characters are retrieved with TextFinder. And the request body is created using the retrieved ranges, and then, it requests to Sheets API with the request body. By this, the rows you want to delete are deleted.
When there are a lot of rows you want to delete, the process cost of this sample script might be low.
References:
substr()
includes()
TextFinder
Method: spreadsheets.batchUpdate
DeleteDimensionRequest

Parsing Data in Google Sheets From an Object

I have thousands of rows of data in a Google Sheets File in a column that looks something like
[{"amountMax":49.99,"amountMin":49.99,"availability":"true","color":"Brown","currency":"USD","dateSeen":["2019-04-11T08:00:00Z"],"isSale":"false","offer":"Online only","sourceURLs":["https://www.walmart.com/ip/SadoTech-Model-CXR-Wireless-Doorbell-1-Remote-Button-2-Plugin-Receivers-Operating-500-feet-Range-50-Chimes-Batteries-Required-Receivers-Beige-Fixed-C/463989633"]}]
I would like to be able to return the max value, the currency, the color attributes. How can I do that in Google Sheets. Ideally would like to do something like being able to retrieve the data attributes how I would normally in javascript like in this link here https://repl.it/#alexhoy/WetSlateblueDribbleware
However this does not seem to work for me when creating a function in script.google.com
For example, here is a slugify function which takes an input (cell) and turns it into a slug/handle without the need for looping. In Google Sheets I can then call =slugify(b2) and turn that value into slug form
/**
* Converts value to slug
* #customfunction
*/
function slugify(value) {
/*
* Convert the the vs in a range of cells into slugs.
* #customfunction
*/
let slug = '';
slug = value.substring(0, 100).toLowerCase();
slug = slug.replace(/[^\w\s-]/g, '');
slug = slug.replace(/\s+/g, '-');
Logger.log(slug);
return slug;
}
I want to do the same thing without looping to parse the object data above or declaring a range of values and what not.
Any suggestions on how I can do this in a simple way like shown above without the need for declaring active spreadsheet, range values and looping.
The following script will give you an idea about how to approach this task.
It assumes that:
the json data described in your question is in Cell A2.
the max value will be inserted into cell D2
the currency will be inserted into cell E2
the color will be inserted into cell F2
The script uses temporary arrays to capture the values and then assign it to a 2d array.
If you have many rows of data, then you will need to create a loop. I suggest that you build the arraydata progressively, and only update the target range at the end of the loop. This will give you the most efficient outcome.
function so6031098604() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet()
var content = JSON.parse(sheet.getRange("A2").getValue());
// temp arrar to capture the data
var temparray = [];
temparray.push(content[0]["amountMax"]);
temparray.push(content[0]["currency"]);
temparray.push(content[0]["color"]);
// second array to accept the row data
var arraydata =[];
arraydata.push(temparray)
// define the target range
var targetrange = sheet.getRange(2, 4, 1, 3);
// update with the arraydata
targetrange.setValues(arraydata);
}
You want a custom function that will return certain fields from a JSON array.
In the following example, the target cell can be a single cell or an array.
This example does not use an arrayformula. The mechanics of using an arrayformula with a custom function may be something that you can research here Custom SHEETNAME function not working in Arrayformula.
Note: A 30 second quota applies to the execution of a Custom function
/**
* gets the MaxAmount, Current and Color from the data
*
* #param {cell reference or range} range The range to analyse.
* #return amountMax,currency and color
* #customfunction
*/
function getJsonData(range) {
//so6031098606
// Test whether range is an array.
if (range.map) {
// if yes, then loop through the rows and build the row values
var jsonLine = [];
for (var i = 0; i < range.length; i++) {
var jsonValues=[];
var v = JSON.parse(range[i][0]);
jsonValues.push(v.amountMax);
jsonValues.push(v.currency);
jsonValues.push(v.color);
// aggregate the row values
jsonLine.push(jsonValues);
} // end i
return jsonLine;
} else {
// if no, then just return a single set of values
var v = JSON.parse(range);
var jsonValues = [];
jsonValues.push(v.amountMax);
jsonValues.push(v.currency);
jsonValues.push(v.color);
return [jsonValues];
}
}

Paste values from one sheet to another and remove duplicates

I have two worksheets in my google spreadsheet:
Input data is coming into the Get Data worksheet via the importxml function.
However, I would like to copy all values of the Get Data sheet to the Final Data sheet and if there are duplicates(in terms of rows) append the unique row.
Here is what I tried:
function onEdit() {
//get the data from old Spreadsheet
var ss = SpreadsheetApp.openById("1bm2ia--F2b0495iTJotp4Kv1QAW-wGUGDUROwM9B-D0");
var dataRange = ss.getSheetByName("Get Data").getRange(1, 1, ss.getLastRow(), ss.getLastColumn());
var dataRangeFinalData = ss.getSheetByName("Final Data").getRange(1, 1, ss.getLastRow(), ss.getLastColumn());
var myData = dataRange.getValues();
//Open new Spreadsheet & paste the data
newSS = SpreadsheetApp.openById("1bm2ia--F2b0495iTJotp4Kv1QAW-wGUGDUROwM9B-D0");
Logger.log(newSS.getLastRow());
newSS.getSheetByName("Final Data").getRange(newSS.getLastRow()+1, 1, ss.getLastRow(), ss.getLastColumn()).setValues(myData);
//remove duplicates in the new sheet
removeDups(dataRangeFinalData)
}
function getId() {
Browser.msgBox('Spreadsheet key: ' + SpreadsheetApp.getActiveSpreadsheet().getId());
}
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;
}
Below you can find the link to a sample spreadsheet:
Sample Sheet
My problem is that the data does not get pasted.
I appreciate your replies!
tl;dr: See script at bottom.
An onEdit() function is inappropriate for your use case, as cell contents modified by spreadsheet functions are not considered "edit" events. You can read more about that in this answer. If you want this to be automated, then a timed trigger function would be appropriate. Alternatively, you could manually invoke the function by a menu item, say. I'll leave that to you to decide, as the real meat of your problem is how to ensure row-level uniqueness in your final data set.
Merging unique rows
Although your original code is incomplete, it appears you were intending to first remove duplicates from the source data, utilizing case-insensitive string comparisons. I'll suggest instead that some other JavaScript magic would help here.
We're interested in uniqueness in our destination data, so we need to have a way to compare new rows to what we already have. If we had arrays of strings or numbers, then we could just use the techniques in How to merge two arrays in Javascript and de-duplicate items. However, there's a complication here, because we have an array of arrays, and arrays cannot be directly compared.
Hash
Fine - we could still compare rows element-by-element, which would require a simple loop over all columns in the rows we were comparing. Simple, but slow, what we would call an O(n2) solution (Order n-squared). As the number of rows to compare increased, the number of unique comparison operations would increase exponentially. So, let's not do that.
Instead, we'll create a separate data structure that mirrors our destination data but is very efficient for comparisons, a hash.
In JavaScript we can quickly access the properties of an object by their name, or key. Further, that key can be any string. We can create a simple hash table then, with an object whose properties are named using strings generated from the rows of our destination data. For example, this would create a hash object, then add the array row to it:
var destHash = {};
destHash[row.join('')] = true; // could be anything
To create our key, we're joining all the values in the row array with no separator. Now, to test for uniqueness of a row, we just check for existence of an object property with an identically-formed key. Like this:
var alreadyExists = destHash.hasOwnProperty(row.join(''));
One additional consideration: since the source data can conceivably contain duplicate rows that aren't yet in the destination data, we need to continuously expand the hash table as unique rows are identified.
Filter & Concatenate
JavaScript provides two built-in array methods that we'll use to filter out known rows, and concatenate only unique rows to our destination data.
In its simple form, that would look like this:
// Concatentate source rows to dest rows if they satisfy a uniqueness filter
var mergedData = destData.concat(sourceData.filter(function (row) {
// Return true if given row is unique
}));
You can read that as "create an array named mergedData that consists of the current contents of the array named destData, with filtered rows of the sourceData array concatenated to it."
You'll find in the final function that it's a little more complex due to the other considerations already mentioned.
Update spreadsheet
Once we have our mergedData array, it just needs to be written into the destination Sheet.
Padding rows: The source data contains rows of inconsistent width, which will be a problem when calling setValues(), which expects all rows to be squared off. This will require that we examine and pad rows to avoid this sort of error:
Incorrect range width, was 6 but should be 5 (line ?, file "Code")
Padding rows is done by pushing blank "cells" at the end of the row array until it reaches the intended length.
for (var col=mergedData[row].length; col<mergedWidth; col++)
mergedData[row].push('');
With that taken care of for each row, we're finally ready to write out the result.
Final script
function appendUniqueRows() {
var ss = SpreadsheetApp.getActive();
var sourceSheet = ss.getSheetByName('Get Data');
var destSheet = ss.getSheetByName('Final Data');
var sourceData = sourceSheet.getDataRange().getValues();
var destData = destSheet.getDataRange().getValues();
// Check whether destination sheet is empty
if (destData.length === 1 && "" === destData[0].join('')) {
// Empty, so ignore the phantom row
destData = [];
}
// Generate hash for comparisons
var destHash = {};
destData.forEach(function(row) {
destHash[row.join('')] = true; // could be anything
});
// Concatentate source rows to dest rows if they satisfy a uniqueness filter
var mergedData = destData.concat(sourceData.filter(function (row) {
var hashedRow = row.join('');
if (!destHash.hasOwnProperty(hashedRow)) {
// This row is unique
destHash[hashedRow] = true; // Add to hash for future comparisons
return true; // filter -> true
}
return false; // not unique, filter -> false
}));
// Check whether two data sets were the same width
var sourceWidth = (sourceData.length > 0) ? sourceData[0].length : 0;
var destWidth = (destData.length > 0) ? destData[0].length : 0;
if (sourceWidth !== destWidth) {
// Pad out all columns for the new row
var mergedWidth = Math.max(sourceWidth,destWidth);
for (var row=0; row<mergedData.length; row++) {
for (var col=mergedData[row].length; col<mergedWidth; col++)
mergedData[row].push('');
}
}
// Write merged data to destination sheet
destSheet.getRange(1, 1, mergedData.length, mergedData[0].length)
.setValues(mergedData);
}

Categories

Resources