exceeded maximum execution time, google sheet, how to improve it? - function

Good morning, everyone,
I come to see you because after having fought hard to make my google script work, on the execution page I see that my script works, however on my google sheet I have an error: "Exceeded maximum execution time". I've seen on the internet that for custom google script functions only leave 30 sec of execution, I'm not sure how to do that? add code to tweak this feature? I confess that I didn't understand the difference between a custom function and a google app script but I know that there is 6 minutes of execution time for scripts... here is an extract of my code:
// Standard functions to call the spreadsheet sheet and activesheet
function GetPipedriveDeals2() {
let ss = SpreadsheetApp.getActiveSpreadsheet();
let sheets = ss.getSheets();
let sheet = ss.getActiveSheet();
//the way the url is build next step is to iterate between the end because api only allows a fixed number of calls (100) this way i can slowly fill the sheet.
let url = "https://laptop.pipedrive.com/v1/products:(id)?start=";
let limit = "&limit=500";
//let filter = "&filter_id=64";
let pipeline = 1; // put a pipeline id specific to your PipeDrive setup
let start = 1;
//let end = start+50;
let token = "&api_token=XXXXXXXXXXXXXXXXXXXXXXXX"
let response = UrlFetchApp.fetch(url+start+limit+token); //
let dataAll = JSON.parse(response.getContentText());
let dataSet = dataAll;
//let prices = prices;
//create array where the data should be put
let rows = [], data;
for (let i = 0; i < dataSet.data.length; i++) {
data = dataSet.data[i];
rows.push([data.id,
GetPipedriveDeals4(data.id)
]);
}
Logger.log( 'function2' ,JSON.stringify(rows,null,8) ); // Log transformed data
return rows;
}
// Standard functions to call the spreadsheet sheet and activesheet
function GetPipedriveDeals4(idNew) {
let ss = SpreadsheetApp.getActiveSpreadsheet();
let sheets = ss.getSheets();
let sheet = ss.getActiveSheet();
//the way the url is build next step is to iterate between the end because api only allows a fixed number of calls (100) this way i can slowly fill the sheet.
let url = "https://laptop.pipedrive.com/v1/products/"+idNew+"/deals:(id,d93b458adf4bf84fefb6dbce477fe77cdf9de675)?start=";
let limit = "&limit=500";
//let filter = "&filter_id=64";
let pipeline = 1; // put a pipeline id specific to your PipeDrive setup
let start = 1;
//let end = start+50;
let token = "&api_token=XXXXXXXXXXXXXXXXXXXXXX"
let response = UrlFetchApp.fetch(url+start+limit+token); //
let dataAll = JSON.parse(response.getContentText());
let dataSet = dataAll;
//Logger.log(dataSet)
//let prices = prices;
//create array where the data should be put
let rows = [], data;
if(dataSet.data === null )return
else {
for (let i = 0; i < dataSet.data.length; i++) {
data = dataSet.data[i];
let idNew = data.id;
rows.push([data.id, data['d93b458adf4bf84fefb6dbce477fe77cdf9de675']]);
}
Logger.log( 'function4', JSON.stringify(rows,null,2) ); // Log transformed data
return rows;
}
}
Thank you all in advance.
EDIT : --------------------------FOR EACH LOOP----------------------------
function getPipedriveDeals(start = 0,apiRequestLimit = 39) {
console.log("start="+start);
//Make the initial request to get the ids you need for the details.
var idsListRequest = "https://laptop.pipedrive.com/v1/products:(id)?start=";
var limit = "&limit=" + apiRequestLimit;
var token = "&api_token=XXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
var response = UrlFetchApp.fetch(idsListRequest + start + limit + token);
var data = JSON.parse(response.getContentText()).data;
//For every id in the response, construct a url (the detail url) and push to a list of requests
var requests = [];
console.log("data="+data);
data.forEach(function(product) {
var productDetailUrl = "https://laptop.pipedrive.com/v1/products/" + product.id + "/deals:(id,d93b458adf4bf84fefb6dbce477fe77cdf9de675)?start=";
requests.push(productDetailUrl + start + limit + token)
});
//With the list of detail request urls, make one call to UrlFetchApp.fetchAll(requests)
var responses = UrlFetchApp.fetchAll(requests);
return [responses,JSON.parse(responses[0].getContentText()).additional_data.pagination.more_items_in_collection];
}
function getAllDeals(){
var allResponses = [];
for(var i = 0; i<500; ){
var deals = getPipedriveDeals(start=i);
deals[0].forEach((response)=>{allResponses.push(response)});
if(deals[1]){
// If there are more items sleep for 1000 milliseconds
Utilities.sleep(1000);
i+=39;
}
else{
console.log("No more items in collection.");
break;
}
}
console.log("allResponses="+allResponses);
return allResponses;
}
About the weird behavior in the logs :
-------------------------------EDIT Number2----------------------------------
Until now when I executed =getPipedriveDeals()it return False (like I was saying in my comment) and also when I try to remove additional_data.pagination.more_items_in_collection (Because it's not very usefull for me) I don't know why but the script won't work anymore, and I have some difficulties to select the data I want to return.

You can use UrlFetchApp.fetchAll(requests) to save in quotas and likely a lot in script execution duration.
I have removed a few redundancies in your code to make this following example showing the use of fetchAll for your case.
Example:
function getPipedriveDeals(start = 0,apiRequestLimit = 50) {
console.log("start="+start);
//Make the initial request to get the ids you need for the details.
var idsListRequest = "https://laptop.pipedrive.com/v1/products:(id)?start=";
var limit = "&limit=" + apiRequestLimit;
var token = "&api_token=xxxxxxxxxxx";
var response = UrlFetchApp.fetch(idsListRequest + start + limit + token);
var data = JSON.parse(response.getContentText()).data;
//For every id in the response, construct a url (the detail url) and push to a list of requests
var requests = [];
console.log("data="+data);
data.forEach(function(product) {
var productDetailUrl = "https://laptop.pipedrive.com/v1/products/" + product.id + "/deals:(id,d93b458adf4bf84fefb6dbce477fe77cdf9de675)?start=";
requests.push(productDetailUrl + start + limit + token)
});
//With the list of detail request urls, make one call to UrlFetchApp.fetchAll(requests)
var responses = UrlFetchApp.fetchAll(requests);
return [responses,JSON.parse(responses[0].getContentText()).additional_data.pagination.more_items_in_collection];
}
Now suppose you have a plan that limits the quota to 50 requests per 2 seconds.
You could make the limit of requests in the fetchAll call to 50, and introduce a delay/sleep of (X) milliseconds to initiate a second request, starting with id=50 like:
function getAllDeals(){
var allResponses = [];
for(var i = 0; i<500; ){
var deals = getPipedriveDeals(start=i);
deals[0].forEach((response)=>{allResponses.push(response)});
if(deals[1]){
// If there are more items sleep for 1000 milliseconds
Utilities.sleep(1000);
i+=50;
}
else{
console.log("No more items in collection.");
break;
}
}
console.log("allResponses="+allResponses);
return allResponses;
}

Related

Randomize rows when google doc is opened

I am trying to create a petition with a google form and doc. I've found an app script that will take the responses from the form and input them into a table on the doc. To help reduce the bias early respondents may face, I am trying to develop a script that will randomize the rows of the table every time the document is opened. Trying to scramble the signatures so that the same signatures aren't always at the top (can't be targeted).
Can anyone help me with this?
Below is the code I am using to populate the table from the form
//Since there could be a bunch of people submitting, we lock the script with each execution
//with a 30 second timeout so nothing gets overwritten
const lock = LockService.getScriptLock();
lock.waitLock(30000);
//Here we read the variables from the form submission event
const date = new Date(e.values[0]).toLocaleDateString();
//of you can use toLocaleString method if you want the time in the doc
const name = e.values[1];
const employeeNumber = e.values[2];
var form = FormApp.openById('');
const num = form.getResponses().length;
var num1 = num.toString()
//Next format those values as an array that corresponds to the table row layout
//in your Google Doc
const tableCells = [num1,name,employeeNumber]
//Next we open the letter and get its body
const letter = DocumentApp.openById('')
const body = letter.getBody();
//Next we get the first table in the doc and append an empty table row
const table = body.getTables()[0]
const tableRow = table.appendTableRow()
//Here we loop through our table cells from above and add
// a table cell to the table row for each piece of data
tableCells.forEach(function(cell, index){
let appendedCell = tableRow.appendTableCell(cell)
})
//here we save and close our letter and then release a lock
letter.saveAndClose();
lock.releaseLock();
}
Got this from a helpful vimeo tutorial by Abhishek Narula and Rebekah Modrak.
Randomize row in a google document table
First function Creates a table in a google document from a table in a spreadsheet
function creatTable() {
const style1 = {};
style1[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT]=DocumentApp.HorizontalAlignment.RIGHT;
const ss = SpreadsheetApp.openById("ssid");
const sh = ss.getSheetByName('Sheet0');
const tA = sh.getDataRange().getValues();
const doc = DocumentApp.getActiveDocument();
let body = doc.getBody();
body.clear();
let n = body.getNumChildren();
for(let i =0;i<n-1;i++) {
if(i==0) {
body.getChild(i).asText().setText('');
} else {
body.getChild(i).removeFromParent()
}
}
let table=body.appendTable(tA).setAttributes(style1);
}
Second function reads the table from the document and randomizes the rows it and reloads the table. This function actually searches through all of the children to find a table. If there is more than one table it will randomize all of them so it will need to be modified to integrate with your specific document.
This now removes the first line and then randomizes the array and then adds the first line back
function readandrandomizetable() {
const style1 = {};
style1[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT] = DocumentApp.HorizontalAlignment.RIGHT;
const doc = DocumentApp.getActiveDocument();
let body = doc.getBody();
let vs = [];
let fl = '';
[...Array.from(new Array(body.getNumChildren()).keys())].forEach(idx => {
let ch = body.getChild(idx);
if (ch.getType() == DocumentApp.ElementType.TABLE) {
Logger.log(`This is the index I want: ${idx}`);
let tbl = ch.asTable();
[...Array.from(new Array(tbl.getNumRows()).keys())].forEach(ridx => {
let s = tbl.getRow(ridx).getText().split('\n');
//Logger.log(JSON.stringify(s));
vs.push(s);
});
Logger.log(JSON.stringify(vs));
fl = vs.shift();
vs.sort((a, b) => {
vA = Math.floor(Math.random() * 100);
vB = Math.floor(Math.random() * 100);
return vA - vB;
});
vs.unshift(fl);
}
});
Logger.log(JSON.stringify(vs));
body.clear();
let n = body.getNumChildren();
for (let i = 0; i < n - 1; i++) {
if (i == 0) {
body.getChild(i).asText().setText('');
} else {
body.getChild(i).removeFromParent()
}
}
body.appendTable(vs).setAttributes(style1);
}

How to wait until the importxml is finished loading on Google App Script as code below

Thank you for your help :)
I would like to wait until newSellPriceFunction and newBuyPriceFunction is finished loading before going to the next steps. As now sometimes it doesn't wait until the loading finished and return data with "Loading...".
function GoldPriceNotification() {
// get the Spreadsheet and sheet
var ss = SpreadsheetApp.openById("1oVMpP_MvwX2cUNviJc1NcubBj4ezR0qs2FjHEWfV8ps").getSheetByName("Gold")
// fetch data to get the price gold price
var newSellPriceFunction = ss.getRange("C6").setValue("=importxml(D2,E2)");
var newBuyPriceFunction = ss.getRange("C7").setValue("=importxml(D3,E3)");
// get value from the previous fetching gold price
var lastSellPrice = ss.getRange("C2").getDisplayValue();
var lastBuyPrice = ss.getRange("C3").getDisplayValue();
// get current date and time
var date = ss.getRange("B5").setValue("=now()")
var time = ss.getRange("B5").getDisplayValue();
// get value from the new gold price (newSellPrice is on the second because it creates span of time while its data is fetching)
var newBuyPrice = ss.getRange("C7").getDisplayValue();
var newSellPrice = ss.getRange("C6").getDisplayValue();
// if the data fetched get "#N/A, clear C6:C7
if (newSellPrice == "#N/A" || newBuyPrice == "#N/A") {
ss.getRange("C6:C7").clear()
// if the data fetched get "Loading...", clear C6:C7
} else if (newSellPrice == "Loading..." || newBuyPrice == "Loading...") {
ss.getRange("C6:C7").clear()
// Check if the last and new prices are different, send Line Notify
} else if (lastSellPrice != newSellPrice || lastBuyPrice != newBuyPrice) {
// Copy new Price to the old price area
var reNewOldPrice = ss.getRange("C6:C7").copyValuesToRange(ss, 3, 3, 2, 3)
// Send Line Notify to the group
var message = "\n🏅เอ็งฮงฮวด สวัสดีค่ะ🏅" + "\n😊🙏ขออนุญาตแจ้งราคาทอง🙏😊" + "\n⏳ณ " + time + "⏳" + "\nราคาซื้อ: " + newBuyPrice + "\nราคาขาย: " + newSellPrice + ""
sendLineNotify(message)
// Need to clear as sometime when duplicate function. it doesn't show the latest value
ss.getRange("C6:C7").clear()
// if the new and old price are the same, clear C6:C7
} else {
ss.getRange("C6:C7").clear()
}
}
You can fetch data directly in your code without using IMPORTXML formulas with the UrlFetchApp.fetch method. Try this code
function ImportXMLData() {
const url = 'https://www.goldtraders.or.th/default.aspx',
response = UrlFetchApp.fetch(url);
let content = {
Sell : '',
Buy : ''
}
if (response) {
let html = response.getContentText();
if (html) {
content.Sell = html.match(/<span id="DetailPlace_uc_goldprices1_lblBLSell"><b><font color="Red">(.*)<\/font><\/b><\/span>/i)[1];
content.Buy = html.match(/<span id="DetailPlace_uc_goldprices1_lblBLBuy"><b><font color="Red">(.*)<\/font><\/b><\/span>/i)[1];
}
}
return content;
}

How to pull data from multiple Mailchimp endpoints?

The code below pulls data from the Mailchimp API Reports endpoint and adding it to Sheets.
I would like to add some more data from other endpoints (like fields from the "List/Audience" endpoint: member_count, total_contacts i.e.) but don't have a slick solution to this.
What's the best practice/solution here? Can this task be kept in the same function or is a separate function preferable?
I'm new in this area so bear with me :)
function chimpCampaigns() {
var API_KEY = 'X'; // MailChimp API Key
var REPORT_START_DATE = '2018-01-01 15:54:00'; // Report Start Date (ex. when you sent your first MailChimp Newsletter)
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("CampaignData");
var dc = API_KEY.split('-')[1];
var api = 'https://'+ dc +'.api.mailchimp.com/3.0';
var count = 100; // Max rows to return
var campaignList = '/campaigns?&count='+count+'&since_send_time='+REPORT_START_DATE
var options = {"headers": {"authorization": 'apikey '+API_KEY}};
var apiCall = function(endpoint){
apiResponseCampaigns = UrlFetchApp.fetch(api+endpoint,options);
json = JSON.parse(apiResponseCampaigns);
return json
}
var campaigns = apiCall(campaignList);
var total = campaigns.total_items;
var campaignData = campaigns.campaigns;
if (campaignData) {
sheet.clear(); // Clear MailChimp data in Spreadsheet
// Append Column Headers
sheet.appendRow(["Sent Time", "Campaign ID", "Audience", "Campaign Title", "Subject Line", "Emails Sent", "Abuse Reports", "Unsubscribed", "Unsubscribe Rate", "Hard Bounces", "Soft Bounces", "Bounces Total", "Syntax Errors", "Forwards Count", "Forwards Opens", "Opens Total", "Unique Opens", "Open Rate", "Last Open", "Clicks Total", "Unique Clicks","Unique Subscriber Clicks", "Click Rate", "Last Click"]);
}
for (i=0; i< campaignData.length; i++){
var c = campaignData[i];
var cid = c.id;
var title = c.title;
var subject = c.subject;
var send_time = c.send_time;
if (send_time){
apiResponseReports = UrlFetchApp.fetch('https://'+ dc +'.api.mailchimp.com/3.0/reports/'+cid,options);
reports = JSON.parse(apiResponseReports);
reportsSendTime = reports.send_time;
if(reportsSendTime){
var campaign_title = c.settings.title;
var subject_line = c.settings.subject_line;
var emails_sent = reports.emails_sent;
var list_name = reports.list_name;
var fields = reports.fields;
var abuse_reports = reports.abuse_reports;
var unsubscribed = reports.unsubscribed;
var unsubscribe_rate = unsubscribed/emails_sent;
var hard_bounces = reports.bounces.hard_bounces;
var soft_bounces = reports.bounces.soft_bounces;
var bounces = hard_bounces+soft_bounces;
var syntax_errors = reports.bounces.syntax_errors;
var forwards_count = reports.forwards.forwards_count;
var forwards_opens = reports.forwards.forwards_opens;
var opens_total = reports.opens.opens_total;
var unique_opens = reports.opens.unique_opens;
var open_rate = reports.opens.open_rate;
var last_open = reports.opens.last_open;
var clicks_total = reports.clicks.clicks_total;
var unique_clicks = reports.clicks.unique_clicks;
var unique_subscriber_clicks = reports.clicks.unique_subscriber_clicks;
var click_rate = reports.clicks.click_rate;
var last_click = reports.clicks.last_click;
// the report array is how each row will appear on the spreadsheet
var report = [send_time, fields, cid, list_name, campaign_title, emails_sent, subject_line, abuse_reports, unsubscribed, unsubscribe_rate, hard_bounces, soft_bounces, bounces, syntax_errors, forwards_count, forwards_opens, opens_total, unique_opens, open_rate, last_open, clicks_total, unique_clicks, unique_subscriber_clicks, click_rate, last_click];
sheet.appendRow(report);
}
}
}
}
You can call each endpoint in succession using the error-first pattern. More on this here.
If your previous call returns data and doesn't error out, you pass the next function as a callback, etc.
In the example below, I've omitted the logic that builds URL endpoint, query-string, and the 'options' object as these can simply be borrowed from your code.
Basically, you define a function with a callback parameter for each API endpoint.
Whenever you need to call multiple endpoints, you create a 3rd function that calls them in succession, passing each new function call as a parameter to the previous one.
The inner functions will still have access to the outer scope so you can combine data from multiple endpoints after the last call is executed (provided you assign unique names to the returned data - 'campaigns', 'reports', etc)
//function for the 'campaings' endpoint
function getCampaings(options, callback) {
//API call
var response = UrlFetchApp.fetch(campaignsEndpoint, options);
if (res.getStatusCode() == 200) {
var campaigns = JSON.parse(res.getContentText());
callback(false, campaigns);
} else {
callback("Error: Server responded with the status code of " + res.getStatusCode());
}
}
After creating the function for calling the 'reports' endpoint using the same approach, combine calls in the 3rd function.
function getCampaignsAndReports(){
var combinedData = {};
getCampaigns(options, function(err, campaigns){
if (!err && campaigns) {
//Call is successful - proceed to the next call
getReports(options, function(err, reports){
//Call successful
if (!err && reports) {
//Proceed to the next call or combine data from
//multiple endpoints
combinedData.campaigns = campaigns.campaigns;
combinedData.reports = reports.reports;
//write to sheet
//...
} else {
//Error calling reports endpoint
throw err;
}
});
} else {
//Error calling 'campaigns' endpoint. Throw error or write
//another function to show it to the user
throw err;
}
});
}
This may vary depending on how the MailChimp API data is structured so please change the code accordingly. Also, if you need to call the 'reports' endpoint multiple times for each entry in the 'campaings' endpoint, you can change your function to handle multiple request (options) object using UrlFetchApp.fetchAll(request[]). More on this here. Calling this method will return multiple response objects that you can iterate over.

How can I write in Google Sheets my Firebase data?

I have a database in Firebase, and I want to get the data from there and put them in a Google SpreadSheet.
function getData() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Database");
var data = getFirebaseData('contacts');
var [rows, columns] = [sheet.getLastRow(), sheet.getLastColumn()];
var range = sheet.getRange(1,1,1,1);
Logger.log(data)
range.setValue(data)
}
function getFirebaseData(data){
var firebaseUrl = "https://XXXXX.firebaseio.com/";
var secret = 'XXXXXXXX';
var base = FirebaseApp.getDatabaseByUrl(firebaseUrl, secret);
var result = base.getData('contacts');
for(var i in data) {
Logger.log(data[i].eMail + ' ' + data[i].title);
return result;
}
}
and here the image:
No data is shown, and I cannot understand why
Your problem should be solved by completing several steps:
In your getFirebaseData() function, move the return statement outside of the loop;
Instead of looping over data, loop over result (currently, you iterate over each property of the "contacts" String);
Optionally, add checks for getData() returning null or invalid firebaseUrl (in the last case, getData() will cause an error, use try...catch to account for that);
Change base.getData('contacts') to base.getData(data) (isn't it
the reason you pass data to the function?);

Error in Google Sheets Script when parsing XML

I have this function running in a Google Sheets script that pulls HTML from subreddits and returns them to a spreadsheet. It works for me some/most of the time, but other times I get an error "Could not parse text. (line 13)" which is the line with var doc = Xml.parse(page, true);. Any idea why this is happening or is this just a bug with Google Scripts? Here's the code that works...sometimes.
function getRedditHTML() {
var entries_array = [];
var subreddit_array = ['https://www.reddit.com/r/news/','https://www.reddit.com/r/funny/','https://www.reddit.com/r/science/'];
for (var s = 0; s < subreddit_array.length; s++) {
var page = UrlFetchApp.fetch(subreddit_array[s]);
//this is Line 13 that is breaking
var doc = Xml.parse(page, true);
var bodyHtml = doc.html.body.toXmlString();
doc = XmlService.parse(bodyHtml);
var root = doc.getRootElement();
var entries = getElementsByClassName(root,'thing');
for (var i = 0; i < entries.length; i++) {
var title = getElementsByClassName(entries[i],'title');
title = XmlService.getRawFormat().format(title[1]).replace(/<[^>]*>/g, "");
var link = getElementsByClassName(entries[i],'comments');
link = link[0].getAttribute('href').getValue();
var rank = getElementsByClassName(entries[i],'rank');
rank = rank[0].getValue();
var likes = getElementsByClassName(entries[i],'likes');
likes = likes[0].getValue();
entries_array.push([rank, likes, title, link]);
}
}
return entries_array.sort(function (a, b) {
return b[1] - a[1];
});
}
Here is what I found upon playing with importXML (my usual way of doing this) - for some reason I cannot narrow down - it DOES appear to randomly stall out and return null for a few minutes - so I'm guessing the issue with your thing is not the code but that the site or google temporarily blocks/won't return the data -
however I found the JSON endpoint to the piece you want - and I noticed that when XML went down - the JSON didnt.
You can take that and fix it to push your own array of topics/urls - I just left it for one link for now to show you how the URL breaks down and where it should be modified:
The URL is 'https://www.reddit.com/r/news/hot.json?raw_json=1&subredditName=news&sort=top&t=day&feature=link_preview&sr_detail=true&app=mweb-client
News is mentioned in 2 places so just modify all your URLs to follow that method - you can easily load that javascript in a browser to see all the fields available
Also the portion hot.json is where you can change whether you want the ranked list (called hot), or new,top,promoted, etc. you just change that keyword.
Score is the same as the upvotes/likes
function getSubReddit() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet(); //get Active sheet
var subject = 'news';
var url = 'https://www.reddit.com/r/' + subject + '/hot.json?raw_json=1&subredditName=' + subject + '&sort=top&t=day&feature=link_preview&sr_detail=true&app=mweb-client'; //json endpoint for data
var response = UrlFetchApp.fetch(url); // get api endpoint
var json = response.getContentText(); // get the response content as text
var redditData = JSON.parse(json); //parse text into json
Logger.log(redditData); //log data to logger to check
//create empty array to hold data points
var statsRows = [];
var date = new Date(); //create new date for timestamp
//The following lines push the parsed json into empty stats array
for (var j=0;j<25;j++){
for (var i =0;i<25;i++){
var stats=[];
stats.push(date);//timestamp
stats.push(i+1);
stats.push(redditData.data.children[i].data.score); //score
stats.push(redditData.data.children[i].data.title); //title
stats.push(redditData.data.children[i].data.url); //article url
// stats.push('http://www.reddit.com' + redditData.data.children[i].data.permalink); //reddit permalink
statsRows.push(stats)
}
//append the stats array to the active sheet
sheet.appendRow(statsRows[j])
}
}