Exporting sheet data to JSON file - google-apps-script

This is my simple Sheet divided by freezed rows to Head and Body:
And this is what the final output(json) should look like:
{
"3":{
"AB1":{
"A2":"A3",
"B2":"B3"
},
"C1":{
"C2":"C3"
}
},
"4":{
"AB1":{
"A2":"A4",
"B2":"B4"
},
"C1":{
"C2":"C4"
}
},
...
}
My code look like this:
function doGet() {
var SpreadSheet = SpreadsheetApp.getActiveSpreadsheet();
var Sheet = SpreadSheet.getSheets()[1];
var FirstRow = 1;
var FirstColumn = 1;
var LastRow = Sheet.getLastRow();
var LastColumn = Sheet.getLastColumn();
var FrozenRows = Sheet.getFrozenRows();
var FrozenColumns = Sheet.getFrozenColumns();
var HeadRange = Sheet.getRange(FirstRow, FirstColumn + FrozenColumns, FrozenRows - FirstRow + 1, LastColumn - FrozenColumns); // A1:C2
var HeadData = HeadRange.getValues(); // [[AB1, , C1], [A2, B2, C2]]
var BodyRange = Sheet.getRange(FirstRow + FrozenRows, FirstColumn + FrozenColumns, LastRow - FrozenRows, LastColumn - FrozenColumns); // A3:C6
var BodyData = BodyRange.getValues(); // [[A3, B3, C3], [A4, B4, C4], [A5, B5, C5], [A6, B6, C6]]
and will end with this:
var OutputData = ContentService.createTextOutput(JSON.stringify(InputData)).setMimeType(ContentService.MimeType.JSON);
return OutputData;
and now my problems :), first problem is, how get value from empty merged cell, when don't know his range, only know other side when know range and want value
for (var i = 0; i < HeadData[0].length; i++) {
var Category = HeadData[0][i];
var CellValue = (RangeCell.isPartOfMerge() ? RangeCell.getMergedRanges()[0].getCell(1,1) : RangeCell).getValue();
Second problem is, how put code together when want start json with number of row data, then category, subcategory and last with item data:
var Obj = {};
for (var i = 1; i <= ItemsRange.getNumRows(); i++) {
var ItemIndex = ItemsRange.getCell(i,1).getRowIndex();
for (var j = 0; j < BodyData.length; j++) {
for (var k = 0; k < BodyData[j].length; k++) {
var ItemCell = BodyData[j][k];
}
}
Obj[ItemIndex] = {};
}

In this case, how about the following flow?
Retrieve values from Spreadsheet.
This is from your script.
Recreate header rows.
Create the result object.
Modification points:
In this modification, at first, the header data is created. The merged ranges can be retrieve by getMergedRanges(). I created the header data using this.
When your sample Spreadsheet is used, HeadData becomes [[AB1, AB1, C1], [A2, B2, C2]] from [[AB1, , C1], [A2, B2, C2]].
In your case, it has already been found that the result object is the nested object with 3 levels. I think that this can be used.
Modified script:
function doGet() {
// 1. Retrieve values from Spreadsheet. This is from your script.
var SpreadSheet = SpreadsheetApp.getActiveSpreadsheet();
var Sheet = SpreadSheet.getSheets()[1];
var FirstRow = 1;
var FirstColumn = 1;
var LastRow = Sheet.getLastRow();
var LastColumn = Sheet.getLastColumn();
var FrozenRows = Sheet.getFrozenRows();
var FrozenColumns = Sheet.getFrozenColumns();
var HeadRange = Sheet.getRange(FirstRow, FirstColumn + FrozenColumns, FrozenRows - FirstRow + 1, LastColumn - FrozenColumns); // A1:C2
var HeadData = HeadRange.getValues(); // [[AB1, , C1], [A2, B2, C2]]
var BodyRange = Sheet.getRange(FirstRow + FrozenRows, FirstColumn + FrozenColumns, LastRow - FrozenRows, LastColumn - FrozenColumns); // A3:C6
var BodyData = BodyRange.getValues(); // [[A3, B3, C3], [A4, B4, C4], [A5, B5, C5], [A6, B6, C6]]
// 2. Recreate header rows.
Sheet.getRange(1, 1, 1, Sheet.getLastColumn()).getMergedRanges().forEach(r => {
let temp = "";
for (let i = r.getColumn(); i <= r.getNumColumns(); i++) {
if (HeadData[0][i - 1].toString() != "") temp = HeadData[0][i - 1];
}
for (let i = r.getColumn(); i <= r.getNumColumns(); i++) {
HeadData[0][i - 1] = temp;
}
});
// 3. Create the result object.
const InputData = BodyData.reduce((o1, r, i) => {
o1[i + FirstRow + FrozenRows] = r.reduce((o2, c, j) => {
const t1 = HeadData[0][j];
const t2 = {[HeadData[1][j]]: c};
return Object.assign(o2, {[t1]: o2[t1] ? Object.assign(o2[t1], t2) : t2});
}, {});
return o1;
}, {});
var OutputData = ContentService.createTextOutput(JSON.stringify(InputData)).setMimeType(ContentService.MimeType.JSON);
return OutputData;
}
Result:
When your sample Spreadsheet is used, the value of InputData is as follows.
{
"3": {
"AB1": {
"A2": "A3",
"B2": "B3"
},
"C1": {
"C2": "C3"
}
},
"4": {
"AB1": {
"A2": "A4",
"B2": "B4"
},
"C1": {
"C2": "C4"
}
},
"5": {
"AB1": {
"A2": "A5",
"B2": "B5"
},
"C1": {
"C2": "C5"
}
},
"6": {
"AB1": {
"A2": "A6",
"B2": "B6"
},
"C1": {
"C2": "C6"
}
}
}
Note:
When you modified the script of Web Apps, please redeploy the Web Apps as new version. By this, the latest script is reflected to the Web Apps. Please be careful this.
When your actual situation is largely different from your sample Spreadsheet in your question, this modified script might not work. So please be careful this.
Please use this modified script with V8.
References:
getMergedRanges()
reduce()
Object.assign()

Related

Google App Script: Speed up script - Conditional copy to another tab

There are 2 tabs:
Alerts
MASTER
The script:
In Alerts tab, if A is empty and H >= 45, copies the value from G to A, on the next empty row, in MASTER
Ads specific values to some columns in tab MASTER, on the recently added row. All new rows will have those same values.
The script was retrieving ocasionally some timeouts and I tried to add some
SpreadsheetApp.flush();
With our without any relation to these additions, the script started to work better but I'm sure it will return more timeouts as the list may grow.
Is it possible to speed it up?
function CopyPendingPayment(){
var sheet = SpreadsheetApp.getActive();
var sourceSheet = sheet.getSheetByName("Alerts");
var destination = sheet.getSheetByName("MASTER");
var lastRow = sourceSheet.getDataRange().getLastRow();
var column = destination.getRange('A' + destination.getMaxRows())
SpreadsheetApp.flush();
for(var row=3; row<=lastRow; row++){
if(sourceSheet.getRange(row,1).getValue() == "" & sourceSheet.getRange(row,8).getValue() >= "45"){
var rangeToCopy = "G"+row+":G"+row;
var lastFilledRow = parseInt(column.getNextDataCell(SpreadsheetApp.Direction.UP).getA1Notation().slice(1))
SpreadsheetApp.flush();
sourceSheet.getRange(rangeToCopy).copyTo(destination.getRange(lastFilledRow+1,1)),{contentsOnly:true};
destination.getRange(lastFilledRow+1, 9).setValue("No Payment Received");
destination.getRange(lastFilledRow+1, 10).setValue(new Date());
destination.getRange(lastFilledRow+1, 12).setValue("PENDING");
destination.getRange(lastFilledRow+1, 13).setValue("- Reminder sent");
destination.getRange(lastFilledRow+1, 14).setValue(new Date());
}
}
}
I guess at least the part where it ads the values on the different columns it could be done quicker, right?
Can anyone give me a hand?
Thank you in advance
Sample
I believe your goal is as follows.
You want to reduce the process cost of your script.
In this case, how about the following modification?
Modified script:
function CopyPendingPayment() {
var sheet = SpreadsheetApp.getActive();
var sourceSheet = sheet.getSheetByName("Alerts");
var destination = sheet.getSheetByName("MASTER");
// I modified below script.
var srcValues = sourceSheet.getRange("A3:H" + sourceSheet.getLastRow()).getValues();
var lastRow = destination.getLastRow();
var obj = srcValues.reduce((o, [a,,,,,,g,h], i) => {
if (a == "" && h >= 45) {
o.values.push([g]);
o["No Payment Received"].push(`I${lastRow + i + 1}`);
o["date"].push(`J${lastRow + i + 1}`, `N${lastRow + i + 1}`);
o["PENDING"].push(`L${lastRow + i + 1}`);
o["- Reminder sent"].push(`M${lastRow + i + 1}`);
}
return o;
}, {values: [], "No Payment Received": [], "date": [], "PENDING": [], "- Reminder sent": []});
destination.getRange(lastRow + 1, 1, obj.values.length).setValues(obj.values);
["No Payment Received", "date", "PENDING", "- Reminder sent"].forEach(e => destination.getRangeList(obj[e]).setValue(e == "date" ? new Date() : e));
}
In this modification, the value of column "A" is put with setValues of Class Range. And, the values of columns "I", "J", "L", "M" and "N" are put with setValue of Class RangeList. Because I thought that you might want to keep other columns.
As another method, I think that your goal can be also achieved by overwriting the cells of the destination sheet. In that case, how about the following script?
function CopyPendingPayment() {
var sheet = SpreadsheetApp.getActive();
var sourceSheet = sheet.getSheetByName("Alerts");
var destination = sheet.getSheetByName("MASTER");
var srcValues = sourceSheet.getRange("A3:H" + sourceSheet.getLastRow()).getValues();
var lastRow = destination.getLastRow();
var values = srcValues.reduce((ar, [a,,,,,,g,h], i) => {
if (a == "" && h >= 45) {
var date = new Date();
ar.push([g,,,,,,,,"No Payment Received", date,,"PENDING","- Reminder sent",date]);
}
return ar;
}, []);
destination.getRange(lastRow + 1, 1, values.length, values[0].length).setValues(values);
}
References:
setValues(values) of Class Range
setValue(value) of Class RangeList
Added:
From your following replying,
It seems a lot quicker, indeed. I've tested your versions vs mine. The only problem is that if there is an array in any ot the destination sheet columns, the values are pasted below. The original version takes into consideration that and pastes the values on the 1st blank cell of column A.
How about the following sample script?
Sample script:
function CopyPendingPayment_C() {
// This is from https://stackoverflow.com/a/44563639
Object.prototype.get1stEmptyRowFromTop = function (columnNumber, offsetRow = 1) {
const range = this.getRange(offsetRow, columnNumber, 2);
const values = range.getDisplayValues();
if (values[0][0] && values[1][0]) {
return range.getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow() + 1;
} else if (values[0][0] && !values[1][0]) {
return offsetRow + 1;
}
return offsetRow;
};
var sheet = SpreadsheetApp.getActive();
var sourceSheet = sheet.getSheetByName("Alerts");
var destination = sheet.getSheetByName("MASTER");
// I modified below script.
var srcValues = sourceSheet.getRange("A3:H" + sourceSheet.getLastRow()).getValues();
var lastRow = destination.get1stEmptyRowFromTop(1) - 1;
// I modified below part.
var obj = srcValues.reduce((o, [a,,,,,,g,h]) => {
if (a == "" && h >= 45) {
o.offset++;
o.values.push([g]);
o["No Payment Received"].push(`I${o.offset}`);
o["date"].push(`J${o.offset}`, `N${o.offset}`);
o["PENDING"].push(`L${o.offset}`);
o["- Reminder sent"].push(`M${o.offset}`);
}
return o;
}, {values: [], "No Payment Received": [], "date": [], "PENDING": [], "- Reminder sent": [], offset: lastRow});
destination.getRange(lastRow + 1, 1, obj.values.length).setValues(obj.values);
["No Payment Received", "date", "PENDING", "- Reminder sent"].forEach(e => destination.getRangeList(obj[e]).setValue(e == "date" ? new Date() : e));
}

Script exceeds maximum execution time

I am using a script to first copy a list of all terminated products from "data" tab of the sheet to the "terminated tab"
The data tab looks like below
The code checks if there is an end date written
if it is - the row is copied and pasted in the "terminated" tab
Once all rows (around 2000) are completed
the code the deletes all rows from the "data" tab that have an end date on it
But the code is not very efficient and data is huge - I get a "maximum execution time exceeded" error
function movingTerms() {
var app = SpreadsheetApp ;
var sheet1 = app.getActiveSpreadsheet().getSheetByName("data") ;
var sheet3 = app.getActiveSpreadsheet().getSheetByName("Terminations");
var range1 = sheet1.getRange(2, 1, sheet1.getLastRow() - 1,9);
var range3 = sheet3.getRange(2, 1, sheet3.getLastRow(), 9);
var values1 = range1.getValues();
var values3 = range3.getValues();
var rowcount = sheet1.getLastRow();
var row_deleted = 0;
for (var i = 0; i < (sheet1.getLastRow() - 1); i++)
{
if (values1[i][4] !== "")
{
var rowtodelete = sheet1.getRange(i + 2, 1, 1, 10);
var rowtoadd = sheet3.getRange(sheet3.getLastRow() + 1, 1);
rowtodelete.copyTo(rowtoadd);
}
}
for (var k = 0; k < values1.length; k++)
{
var row = k + 1 - row_deleted;
if (values1[k][4] !== "")
{
var getridof = row +1;
sheet1.deleteRow(getridof);
row_deleted++;
}
}
}
I generally like to see the spreadsheet to do this correctly but this is the way that I would do it.
function movingTerms() {
var ss=SpreadsheetApp.getActive();
var sheet1=ss.getSheetByName("data") ;
var sheet3=ss.getSheetByName("Terminations");
var range1=sheet1.getRange(2, 1, sheet1.getLastRow()-1,9);
var range3=sheet3.getRange(2, 1, sheet3.getLastRow(),9);//You don't really need this
var values1=range1.getValues();
var values3=range3.getValues();//You don't really need this
var rowcount=sheet1.getLastRow();
var rowsdeleted = 0;
for (var i=0;i<values1.length;i++) {
if (values1[i][4]!="") {//column5
var rowtodelete = sheet1.getRange(i-rowsdeleted+2, 1, 1, 10);
var rowtoadd = sheet3.getRange(sheet3.getLastRow()+1,1);//You could also append to sheet 3 if you wish
rowtodelete.copyTo(rowtoadd);
sheet1.deleteRow(i-rowsdeleted+2);
rowsdeleted++;
}
}
}

JSON data not parsed correctly from Google Sheets

I'm trying to convert Google Sheets Data to JSON format using a script.
This is a test google sheet: https://docs.google.com/spreadsheets/d/1zEts1AaAseO4MlLf5OA--Ylp-bH9aXe-1pacjstv4GI/edit#gid=0
But the JSON Format looks like this:
which is not correct at all!
Script code is given below with correct credentials of the test sheet:
function doGet(e) {
var sheetName = "TestSheet";
var sheetId = "1zEts1AaAseO4MlLf5OA--Ylp-bH9aXe-1pacjstv4GI";
var book = SpreadsheetApp.openById(sheetId);
var sheet = book.getSheetByName(sheetName);
var json = convertSheet2JsonText(sheet);
return ContentService
.createTextOutput(JSON.stringify(json))
.setMimeType(ContentService.MimeType.JSON);
}
function convertSheet2JsonText(sheet) {
var sheetName = "TestSheet";
var sheetId = "1zEts1AaAseO4MlLf5OA--Ylp-bH9aXe-1pacjstv4GI";
var book = SpreadsheetApp.openById(sheetId);
var sheet = book.getSheetByName(sheetName);
var colStartIndex = 1;
var rowNum = 1;
var firstRange = sheet.getRange(2, 2, 2, sheet.getLastColumn());
var firstRowValues = firstRange.getValues();
var titleColumns = firstRowValues[0];
// after the second line(data)
var lastRow = sheet.getLastRow();
var rowValues = [];
for(var rowIndex=2; rowIndex<=lastRow; rowIndex++) {
var colStartIndex = 1;
var rowNum = 1;
var range = sheet.getRange(rowIndex, colStartIndex, rowNum,
sheet.getLastColumn());
var values = range.getValues();
rowValues.push(values[0]);
}
// create json
var jsonArray = [];
for(var i=0; i<rowValues.length; i++) {
var line = rowValues[i];
var json = new Object();
for(var j=0; j<titleColumns.length; j++) {
json[titleColumns[j]] = line[j];
}
jsonArray.push(json);
}
return jsonArray;
}
I am guessing the logic inside the convertSheet2JsonText function is wrong which is causing the problem. Your help will be much appreciated. Thank you.
The correct JSON format of this test sheet should look like this:
You want to retrieve the following object from the shared spreadsheet using your script.
[
{
"firstName": "john",
"lastName": "doe",
"age": 12,
"place": "boston"
},
{
"firstName": "nahid",
"lastName": "patwary",
"age": 21,
"place": "sylhet"
},
{
"firstName": "kishor",
"lastName": "pasha",
"age": 15,
"place": "california"
}
]
If my understanding is correct, how about this modification? I think that the range for retrieving titleColumns might be the reason of your issue. So how about modifying as follows?
From:
var firstRange = sheet.getRange(2, 2, 2, sheet.getLastColumn());
To:
var firstRange = sheet.getRange(1, 1, 1, sheet.getLastColumn());
If I misunderstand your question, please tell me. I would like to modify it.

Google Sheets & Twilio Integration

I'm using this code from Greg Baugues (https://www.twilio.com/blog/2016/02/send-sms-from-a-google-spreadsheet.html) to send texts in bulk using Google sheets and Twilio.
I want to tell sendSMS and/or sendAll to skip existing sheet entries where 'sent' appears in the relevant cell, so as to allow the sheet to function as a database as well as a mere messaging machine.
Any ideas?
(The XXX entries are simply to anonymize my Twilio account and phone number).
function onOpen() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var menuEntries = [ {name: "Send text msg to client", functionName:
"sendAll"}, ];
ss.addMenu("Text", menuEntries);
}
function sendSms(to, body) {
var messages_url = "https://api.twilio.com/2010-04-
01/Accounts/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/Messages.json";
var payload = {
"To": to,
"Body" : body,
"From" : "+XXXXXXXXXX"
};
var options = {
"method" : "post",
"payload" : payload
};
options.headers = {
"Authorization" : "Basic " +
Utilities.base64Encode
("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
};
UrlFetchApp.fetch(messages_url, options);
}
function sendAll() {
var sheet = SpreadsheetApp.getActiveSheet();
var startRow = 2;
var numRows = sheet.getLastRow() - 1;
var dataRange = sheet.getRange(startRow, 1, numRows, 4)
var data = dataRange.getValues();
for (i in data) {
var row = data[i];
try {
response_data = sendSms(row[1], row[2]);
status = "sent";
} catch(err) {
Logger.log(err);
status = "error";
}
sheet.getRange(startRow + Number(i), 4).setValue(status);
}
}
function myFunction() {
sendAll();
}
You need to edit your sendAll() function so that it tests the value of the status column to decide if it should skip that row or not.
Use this:
function sendAll() {
var sheet = SpreadsheetApp.getActiveSheet();
var startRow = 2;
var numRows = sheet.getLastRow() - 1;
var dataRange = sheet.getRange(startRow, 1, numRows, 4)
var data = dataRange.getValues();
for (i in data) {
var row = data[i];
try {
if (row[3] != "sent") {
response_data = sendSms(row[1], row[2]);
status = "sent";
}
} catch(err) {
Logger.log(err);
status = "error";
}
sheet.getRange(startRow + Number(i), 4).setValue(status);
}
}
This works (thanks Andy):
function sendAll() {
var sheet = SpreadsheetApp.getActiveSheet();
var startRow = 2;
var numRows = sheet.getLastRow() - 1;
var dataRange = sheet.getRange(startRow, 1, numRows, 5)
var data = dataRange.getValues();
for (i in data) {
var status;
var row = data[i];
if (row[3] !== "sent") {
try {
response_data = sendSms(row[1], row[2]);
status = "sent";
} catch(err) {
Logger.log(err);
status = "error";
}
}
sheet.getRange(startRow + Number(i), 4).setValue(status);
}
}

Looping through selected cells to indent text

I have written the following Google Spreadsheet script that attempts to loop through all selected cells and then update the value so that it adds an indent, by adding =CONCAT(REPT( CHAR( 160 ), 5),"' + value[row] + '") around the value.
However I am hitting an error when processing the loop and I am unsure how the code should be modified to correct/resolve. The error I am getting is: TypeError: Cannot call method "setValue" of undefined.
Does anyone know what I should change?
var ss = SpreadsheetApp.getActiveSpreadsheet();
function indentText() {
var cells = ss.getActiveRange();
var values = cells.getValues();
row = [];
len = [];
Logger.log(values);
for (row = 0, len = values.length; row < len; row++) {
if (values[row] != '') {
cells[row].setValue('=CONCAT(REPT( CHAR( 160 ), 5),"' + value[row] + '")');
}
}
};
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var entries = [{
name : "Indent Text",
functionName : "indentText"
}];
sheet.addMenu("Indent Text", entries);
};
Edit: After reviewing my code I now have a working solution. I have added it here for completeness:
var ss = SpreadsheetApp.getActiveSpreadsheet();
function indentText() {
var values = ss.getActiveRange().getValues();
var newValues = new Array();
for (i = 0; i < values.length; i++) {
if (values[i][0] != '') {
newValues.push(['=CONCAT(REPT( CHAR( 160 ), 5),"' + values[i][0] + '")']);
} else {
newValues.push(['']);
}
}
ss.getActiveRange().setValues(newValues);
};
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var entries = [{
name : "Indent Text",
functionName : "indentText"
}];
sheet.addMenu("Indent Text", entries);
};
Of course would be interested if there is a better way to achieve this.