I am trying to build a simple "game" that will teach multiplication to my son. The script is below as a link to the screenshot.
Issue:
The multiplication does not seem to work. x1*x2 returns #NUM!. When I checked the spreadsheet, the numbers are not a text string.
What am I getting wrong here?
function multiplicationgame() {
var GameSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Multiplication");
var x1 = GameSheet.getRange("A2");
var x2 = GameSheet.getRange("B2");
// for generating a random number Math.floor((Math.random() * 10) + 1);
x1.setValue(Math.floor((Math.random() * 10) + 1));
x2.setValue(Math.floor((Math.random() * 10) + 1));
var ui = SpreadsheetApp.getUi();
var response = ui.prompt('Enter Answer Here:');
// creates a temp halt in speardsheet execution till the answer is entered
SpreadsheetApp.flush();
var Answer = GameSheet.getRange("A7").getValue();
var x3 = GameSheet.getRange("B7");
if (Answer == x1*x2) (x3.setValue('Thats correct! Well done'));
else (x3.setValue('Thats wrong! Better luck next time'))
}
Try this:
Multiplication process restored.
You were using the range as a value.
function multiplicationgame() {
var GameSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet71");
var x1 = GameSheet.getRange("A2");
var x2 = GameSheet.getRange("B2");
var x3 = GameSheet.getRange("A7");
var x4=GameSheet.getRange("B7");
// for generating a random number Math.floor((Math.random() * 10) + 1);
var m1=Math.floor((Math.random() * 10) + 1);
var m2=Math.floor((Math.random() * 10) + 1);
x1.setValue(m1);
x2.setValue(m2);
SpreadsheetApp.flush();
var ui = SpreadsheetApp.getUi();
var response = ui.prompt('Multiplication', Utilities.formatString('%s x %s = ?',m1,m2 ), ui.ButtonSet.OK);
// creates a temp halt in speardsheet execution till the answer is entered
x3.setValue(response.getResponseText());
SpreadsheetApp.flush();
var Answer = GameSheet.getRange("A7").getValue();
if (Answer == m1*m2) {
x4.setValue('Thats correct! Well done');
}else{
x4.setValue('Thats wrong! Better luck next time');
}
}
Related
I wrote a code as follows. When I run the deLete function here, I get this TypeError: tab.getRange is not a function error. At first I did not have a problem writing the code. But then when adding various functions to this, did he have a problem? Can you help me solve this?
Originally the variable was like this
var [tabb,x4, start,x1, end,x2, side,x3, item] = sheet.getRange("C5:C13").getValues().flat();
var [tesNo,y1, rfiNo] = sheet.getRange("G15:G17").getValues().flat();
After the error came, I tried to change it as follows
const tab =spreadsheet.getSheetByName(spreadsheet.getActiveSheet().getRange("C5").getValue());
This is the function that receives the error
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Data Entry Form');
var regiWs = ss.getSheetByName('Data Registry');
const spreadsheet = SpreadsheetApp.getActive();
const tab =spreadsheet.getSheetByName(spreadsheet.getActiveSheet().getRange("C5").getValue());
function get_rows(tab, side, item) {
try{
Logger.log("test "+tab);
// var tab = ss.getSheetByName(c5);//copy
var items = tab.getRange("C:C").getValues().flat();
if (side == 'LHS') return [items.indexOf(item) + 1];
if (side == 'RHS') return [items.lastIndexOf(item) + 1];
if (side == "BOTH SIDE") return [items.indexOf(item) + 1, items.lastIndexOf(item) + 1];
//return []; // just in case, to avoud any errors due trash input data
}catch(err){
Logger.log("ERROR-get_rows "+ err)
}
}
//find location & clor the bar
function colorize_rows(tab, rows, start, end) {
//var tab = ss.getSheetByName(c5); //copy
var chainage_row = tab.getRange('c:c').getValues().flat().indexOf('Chainage') + 1;
var chainage = tab.getRange(chainage_row, 4, 1, tab.getLastColumn()).getValues().flat();
var start_index = chainage.findIndex(x => x > start);
var end_index = chainage.findIndex(x => x >= end) - start_index;
var tesNo= sheet.getRange("G15").getValue();
var rfiNo= sheet.getRange("G17").getValue();
var comment =tesNo+" => "+rfiNo;
for (row of rows) {
var color = tab.getRange('c' + row).getBackground();
if(tab.getRange(row, start_index+3, 1, end_index+1).isPartOfMerge() == true){
SpreadsheetApp.getUi().alert("Can't mark the vlaues in bar chart. Part length or full length of it has overlapped. Please check it.");
Logger.log("Can't mark the bar chart, it's part length or full length overlapped")
//to be run delete last record
}else{
tab.getRange(row, start_index+3, 1, end_index+1)
.setBackground(color)
.mergeAcross()
.setValue(comment)
.setHorizontalAlignment("center")
.setVerticalAlignment("center")
.setBorder(null, true, null, true, false, false);
}
}
}
//use for edit & delete
function unMerge(tab, rows, start, end) {
Logger.log("trst "+tab);
var chainage_row = tab.getRange("C:C").getValues().flat().indexOf('Chainage') + 1;
var chainage = tab.getRange(chainage_row, 4, 1, tab.getLastColumn()).getValues().flat();
var start_index = chainage.findIndex(x => x > start);
var end_index = chainage.findIndex(x => x >= end) - start_index;
for (row of rows) {
var color = tab.getRange('c' + row).getBackground();
tab.getRange(row, start_index+3, 1, end_index+1).clear().setBackground(null).breakApart();
}
}
//Delete function unmerge barchart
function deLete() {
var [tab,x4,start,x1, end,x2, side,x3, item] = sheet.getRange("C5:C13").getValues().flat();
var [tesNo,y1, rfiNo] = sheet.getRange("G15:G17").getValues().flat();
var rows = get_rows(tab, side, item);
unMerge(tab, rows, start, end);
}
//set delete button
function deleteRocrd(){
//before check save or edit and after save new record
if(searchCell == ""){
return
}
if(accept == true){
//set edit
const cellfound = regiWs.getRange("A2:A")
.createTextFinder(idCell)
.matchCase(true)
.matchEntireCell(true)
.findNext();
if(!cellfound) return
const rowNo = cellfound.getRowIndex();
regiWs.deleteRow(rowNo);
deLete();
newRecord();
// ss.toast("Recrod Deleted..!","Delete Id:- "+idCell);
}
else{
SpreadsheetApp.getUi().alert("Please tick the I Confirmed checkbox");
return
}
}
How can this problem be solved? I printed in the log and searched for myself? But I don't know if it's worth it.
If you run deLete() first then tab is not a sheet it is assigned a value
function deLete() {
var [tab, x4, start, x1, end, x2, side, x3, item] = sheet.getRange("C5:C13").getValues().flat();//tab is assigned a value
var [tesNo, y1, rfiNo] = sheet.getRange("G15:G17").getValues().flat();
var rows = get_rows(tab, side, item);
unMerge(tab, rows, start, end);//tab is not a sheet here
}
function unMerge(tab, rows, start, end) {
Logger.log("trst " + tab);
var chainage_row = tab.getRange("C:C").getValues().flat().indexOf('Chainage') + 1;
var chainage = tab.getRange(chainage_row, 4, 1, tab.getLastColumn()).getValues().flat();
var start_index = chainage.findIndex(x => x > start);
var end_index = chainage.findIndex(x => x >= end) - start_index;
for (row of rows) {
var color = tab.getRange('c' + row).getBackground();
tab.getRange(row, start_index + 3, 1, end_index + 1).clear().setBackground(null).breakApart();
}
}
Change one of the variables and rewrite the function
Globally tab is defined as:
const tab =spreadsheet.getSheetByName(spreadsheet.getActiveSheet().getRange("C5").getValue());
globally it's a sheet
but in the delete function tab is defined:
var [tab, x4, start, x1, end, x2, side, x3, item] = sheet.getRange("C5:C13").getValues().flat();
inside delete it's the name of the sheet
I have a sheet with ~18k rows in. I have written a script to divide the number of rows by the number of users who want to get involved in calculating values in the rows in the sheet and then allocating those rows to those users. A collaborative effort to speed things up as each row takes approximately 1s and it takes ~ 6-9 hours to go through it on my own with network issues etc.
When a user opens the sheet, they are allocated a user number and a chunk of rows to work on with 5 users 18k rows breaks down to 3600 rows each.
The issue is that when more than 1 or 2 users are working on the sheet, the performance becomes erratic. Sometimes a row takes 15-20s to be processed. I am saving nothing by having friends share the task.
I read somewhere that only 100 users can work on a sheet at the same time but here I am talking about 5 (up to a maximum of maybe 10 or 11) people working on the sheet, each running maybe 5-8 functions each on a trigger. Right now I have 5 users connected to the sheet, each running 8 functions and the work has slowed almost to a stop.
Does anyone have any experience with this and know of any limits google place on accounts working on a script? Any way to work with apps script to make this work properly?
Thanks for your insights!
Chad
/*NEW CODE WITH TRIGGER - WE START IT UP WITH A CALL TO runTriggersTwoTimes()*/
function runTriggersTwoTimes() {
createSplitWorkTrigger();
ScriptApp.newTrigger('createSplitWorkTrigger')
.timeBased()
.after(80 * 60 * 1000)
.create();
}
function createSplitWorkTrigger() {
ScriptApp.newTrigger('splitWork2')
.timeBased()
.everyMinutes(5)
.create();
ScriptApp.newTrigger('deleteAllTriggers')
.timeBased()
.after(60 * 60 * 1000)
.create();
}
/*This function splits the work of getting the "From" prices between multiple accounts*/
function splitWork2() {
var accountsArray = [user1, user2, user3, user4, user5#gmail.com];
var numberAccounts = accountsArray.length;
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spreadsheet.getSheetByName("CK Formatted");
var RANGE = sheet.getDataRange();
var rangeVals = RANGE.getValues();
var numberRows = rangeVals.length;
var totalChunkSize = Math.floor(numberRows / numberAccounts);
var userName = Session.getActiveUser().getEmail();
var userNumber;
for (i = 0; i < numberAccounts; i++) {
if (userName == accountsArray[i]) {
userNumber = i;
}
}
var usersStartRow = userNumber * totalChunkSize + 1
var usersLastRow = usersStartRow + totalChunkSize - 1;
if (userName == accountsArray[numberAccounts - 1]) {
usersLastRow = numberRows;
}
// This one does the main work
findFromPricesByChunks2(userNumber, usersStartRow, usersLastRow, 110, totalChunkSize);
}
/*This function adds the "From" prices in chunks*/
function findFromPricesByChunks2(userNumber, startRow, lastRow, chunkSize, totalChunkSize, checkingRound = false) {
if (startRow >= lastRow) {
var allTriggers = ScriptApp.getProjectTriggers();
for (var i = 0; i < allTriggers.length; i++) {
ScriptApp.deleteTrigger(allTriggers[i]);
}
return;
}
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spreadsheet.getSheetByName("CK Formatted");
var RANGE = sheet.getDataRange();
var rangeVals = RANGE.getValues();
Logger.log(rangeVals.length);
var addedPrices = 0;
for (var i = startRow; i <= lastRow; i++) {
if (addedPrices == chunkSize) {
return;
}
var cellValue = sheet.getRange(`A${i + 1}`).getValue();
Logger.log('Cell Value:' + cellValue);
if (cellValue == '') {
Utilities.sleep(1000);
var html = UrlFetchApp.fetch(URL).getContentText();
var $ = Cheerio.load(html);
var s = $('#table .price-container .font-weight-bold').first().text();
// if we fail to get a value because of network issues or whatever this next piece is going ahead and putting a full stop in what should be an empty box. WIll think about this
if (s) {
s = s.replace(".", ",");
s = s.substring(0, s.lastIndexOf(',')) + '.' + s.substring(s.lastIndexOf(',') + 1);
}
fromArrayCell = sheet.getRange(`A${i + 1}`);
fromArrayCell.setValue(s);
addedPrices++;
}
}
var sheetCk = spreadsheet.getSheetByName("CK");
// This adds the amount of processed items to the processed rows counter (because now there are multiple workers working
// at the same time)
if (!checkingRound) {
sheetCk.getRange(userNumber + 2, 9).setValue(sheetCk.getRange(userNumber + 2, 9).getValue() + addedPrices);
} else {
sheetCk.getRange(userNumber + 2, 10).setValue(sheetCk.getRange(userNumber + 2, 10).getValue() + addedPrices);
}
}
function deleteAllTriggers() {
var allTriggers = ScriptApp.getProjectTriggers();
for (var i = 0; i < allTriggers.length; i++) {
ScriptApp.deleteTrigger(allTriggers[i]);
}
}
The single most important thing to come out of this post was the limit of 30 executions which I was told before I posted any code :p I didn't know about that and it was the crux of the problem I was having. Bear in mind that the original code (a few versions back) was trying to cram 13 users in with 8 processes each :)
After drifting for a few days I had a good sleep and came up with this new improved version of the above:
function splitWork() {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var sheetCk = spreadsheet.getSheetByName("CK");
var sheet = spreadsheet.getSheetByName("CK Formatted");
var RANGE = sheet.getDataRange();
var rangeVals = RANGE.getValues();
var userName = Session.getActiveUser().getEmail();
Logger.log(userName)
var accountsArray = [gmail1, gmail2, gmail3, gmail4, gmail5];
var numberRows = rangeVals.length;
var numberAccounts = accountsArray.length;
var totalChunkSize = Math.floor(numberRows / numberAccounts);
var userNumber;
for (i = 0; i < numberAccounts; i++) {
if (userName == accountsArray[i]) {
userNumber = i;
}
}
var usersStartRow = (userNumber * totalChunkSize) + 1; // = 1
var usersLastRow = usersStartRow + totalChunkSize - 1; // = 211
if(userName == accountsArray[numberAccounts - 1]) { usersLastRow = numberRows; }
var completeRowsOriginal = sheetCk.getRange(userNumber + 2, 9).getValue() // = 109
usersStartRow = usersStartRow + completeRowsOriginal // = 110
for (var i = usersStartRow; i <= usersLastRow; i++) { // 110 - 211
var cellValue = sheet.getRange(`A${i + 1}`).getValue();
var completeRows = 0
if (cellValue == '') {
Utilities.sleep(1000);
var html = UrlFetchApp.fetch(URL).getContentText();
var $ = Cheerio.load(html);
var fromArrayCell = sheet.getRange(`A${i + 1}`);
if(sheetCk.getRange(userNumber + 2, 10).getValue() != 1){
var s = $('#table .price-container .font-weight-bold').first().text();
}
else if(sheetCk.getRange(userNumber + 2, 10).getValue() == 1) {
var s = $('#table .price-container .font-weight-bold').first().text();
if(!s) {
var s = $('#tabContent-info .info-list-container dd:nth-child(12)').text();
if(s) { fromArrayCell.setBackground('#ff8680'); }
}
}
if (s) {
s = s.replace(".", ",");
s = s.substring(0, s.lastIndexOf(',')) + '.' + s.substring(s.lastIndexOf(',') + 1);
}
fromArrayCell.setValue(s);
completeRows++
sheetCk.getRange(userNumber + 2, 9).setValue(sheetCk.getRange(userNumber + 2, 9).getValue() + 1)
}
if(completeRows >= 100 || usersStartRow + completeRows >= usersLastRow) {break}
}
if(sheetCk.getRange(userNumber + 2, 9).getValue() + usersStartRow >= usersLastRow) {
if(sheetCk.getRange(userNumber + 2, 10).getValue() != 1) {
sheetCk.getRange(userNumber + 2, 9).setValue(0)
sheetCk.getRange(userNumber + 2, 10).setValue(1)
}
}
else { deleteAllTriggers() }
}
Which works like a charm.
A user opens the sheet, is allocated a chunk in the background, presses a button to start a trigger to work on their chunk every 5 minutes and if they are feeling frisky they can run the function above while waiting for the trigger to start (also from a button). They'll scrape values for 100 rows then wait for another trigger. After they have gone through their whole chunk their counter resets and their master counter is set to 1. They then make a second pass over their chunk filling in any gaps due to network errors. If they can't get a value from the regular place on the second pass they can get a value from another place but it is marked with a red background as it is not reliable.
Don't think there is anything else left to say about this one. Try it yourselves if you have a large sheet of data to churn through and want to do it in less time with help from your friends with google accounts.
PS: CK is the sheet which is used to hold counters only
PPS!! If you literally do it as above, you will manage to process about 1 row per second. The real power coms from getting all the helper account to make their own copy of the main sheet and work on that. Here we'd get 13 rows per second processed, for instance. Then it's a simple matter to recombine everything into single sheet when they're done.
Let me just preface this with I know this script could probably be better but I'm very new to scripting in any language and I'm just playing around with an idea for work. This is essentially just a frankensteined combination of google results for what I want to accomplish but I've hit a deadend.
I'm using an aspect from another script to return contentservice in JSON format for a webapp except it's not working in this case
As far as I can tell it should work perfectly fine and if I replace the return contentservice with the browser.msgbox below I get the values returned that I want but when using contentservice and going to my scripts Web App URL with the action pointing to range I get the error The script completed but did not return anything.
var mysheet = SpreadsheetApp.openByUrl("https://docs.google.com/spreadsheets/d/1sPuqdg0Va9LLQudl2ta23b-CGEF_-FFSTeggRw3J4L4/edit").getSheetByName('Sheet3');
var sheet = SpreadsheetApp.openByUrl("https://docs.google.com/spreadsheets/d/1sPuqdg0Va9LLQudl2ta23b-CGEF_-FFSTeggRw3J4L4/edit").getSheetByName('Sheet1');
function doGet(e){
var action = e.parameter.action;
if(action == 'Range'){
return Range(e);
}
}
function Range(e) {
let term = 'Customer 6';
var sdata = mysheet.getRange("A:A").getValues();
sdata.forEach((val, index) => {
if(val == term){
var msgr = "B" + (index+1)
var msgc = "D" + (index+1)
var rrow = mysheet.getRange(msgr).getValue();
var ccol = mysheet.getRange(msgc).getValue();
var data = sheet.getRange("E" + rrow + ":I"+ccol);
var records={};
var rows = sheet.getRange("E" + rrow + ":I"+ccol).getValues();
data = [];
for (var r = 0, l = rows.length; r < l; r++) {
var row = rows[r],
record = {};
record['Product'] = row[0];
record['Case']=row[2];
record['Order QTY']=row[3];
record['Packed']=row[4];
data.push(record);
}
records.items = data;
var result=JSON.stringify(records);
return ContentService.createTextOutput(result).setMimeType(ContentService.MimeType.JSON);
// Browser.msgBox(result);
}})}
I can't figure out why I'm getting the correct returned values for the msgBox, but no results for the ContentService.
Any help would be greatly appreciated. Thanks in advance.
Edit: I have been publishing new webapp versions every revision
This breaks because you're returning from within a forEach() loop. Check out the MDN web docs:
There is no way to stop or break a forEach() loop other than by throwing an exception. If you need such behavior, the forEach() method is the wrong tool.
The simplest fix is to use a for loop instead.
function Range(e) {
let term = 'Customer 6';
var sdata = mysheet.getRange("A:A").getValues();
for (var index = 0; index < sdata.length; index++) {
var val = sdata[index];
if(val == term){
var msgr = "B" + (index+1)
var msgc = "D" + (index+1)
var rrow = mysheet.getRange(msgr).getValue();
var ccol = mysheet.getRange(msgc).getValue();
var data = sheet.getRange("E" + rrow + ":I"+ccol);
var records={};
var rows = sheet.getRange("E" + rrow + ":I"+ccol).getValues();
data = [];
for (var r = 0, l = rows.length; r < l; r++) {
var row = rows[r],
record = {};
record['Product'] = row[0];
record['Case']=row[2];
record['Order QTY']=row[3];
record['Packed']=row[4];
data.push(record);
}
records.items = data;
var result=JSON.stringify(records);
return ContentService.createTextOutput(result).setMimeType(ContentService.MimeType.JSON);
}
}
}
I have an existing code which looks like,
var copyPageNumbers = [5, 7, 9];
I have the same number 5, 7, 9 in a column on my google sheet and have set up a variable to get value,
var credentials = ss.getRange(i, 11).getValue(); // The value of this is 5, 7, 9
Based on the variables above. I want to use a code which uses the credentials variable in the copyPageNumbers variable to look something like this,
var copyPageNumbers = [credentials];
For example, the below chunk of code works,
var credentials = ss.getRange(i, 11).getValue();
var copyPageNumbers = [2, 4];
var offset = 1;
var slides = otherPresentation.getSlides();
var page = 0;
slides.forEach(function(slide, i) {
if (copyPageNumbers.indexOf(i + 1) != -1) {
currentPresentation.insertSlide(offset + page, slide);
page++;
}
});
However when I try both the methods stated below - it doesnt work,
var credentials = ss.getRange(i, 11).getValue().split(", ");
var copyPageNumbers = credentials
var offset = 1;
var slides = otherPresentation.getSlides();
var page = 0;
slides.forEach(function(slide, i) {
if (copyPageNumbers.indexOf(i + 1) != -1) {
currentPresentation.insertSlide(offset + page, slide);
page++;
}
});
var credentials = ss.getRange(i, 11).getValue();
var offset = 1;
var copyPageNumbers = []
copyPageNumbers.push(credentials);
var slides = otherPresentation.getSlides();
var page = 0;
slides.forEach(function(slide, i) {
if (copyPageNumbers.indexOf(i + 1) != -1) {
currentPresentation.insertSlide(offset + page, slide);
page++;
}
});
var credentials = ss.getRange(i, 11).getValue(); return the string "5, 7, 9", but you need that as an array.
Assuming that the format is consistent (i.e., [number][comma][space]), you can use split() to turn it into an array.
var credentials = ss.getRange(i, 11).getValue().split(", "); // [5, 7, 9]
var copyPageNumbers = credentials;
var copyPageNumbers = [];
var credentials = ss.getRange(i, 11).getValue();
copyPageNumbers.push(credentials);
You want an array ([5,7,9]), 'credentials' is a string (5,7,9). With the (javascript) push() method that string is added to the array 'copyPageNumbers'. If I understand it well.
Thanks to the above answers and thanks to Pattern 2 here, I have managed to solve this.
See updated script below. The reason it was not working before was because of the For each loop which was looping the slide and the number and the array was not working.
var credentials = ss.getRange(i, 11).getValue().split(", ");
var offset = 0;
var slides_stack = otherPresentation.getSlides();
var page = 0;
credentials.forEach(function(p, i) {
currentPresentation.insertSlide(offset + i, slides_stack[p - 1]);
});
I am looking for help with a script that posts that a specific sheet has been edited rather than when any sheet in the document is modified.
Here is what I currently have:
function onEdit() {
var d = new Date();
var h = d.getHours();
var min = d.getMinutes();
var sec = d.getSeconds();
var h_str = h;
var min_str = min;
var sec_str = sec;
// format time nicely
if (h < 10)
h_str = '0' + h;
if (min < 10)
min_str = '0' + min;
if (sec < 10)
sec_str = '0' + sec;
// create the formatted time string
var time_str = h_str + ':' + min_str + ':' + sec_str;
// create the message
var s = 'Roster last modified on: ' + d.toDateString() + ' # ' + time_str;
// change the range
SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Infernal Dawn').getRange("A1").setValue(s);}
This works perfectly fine to only update the sheet "Infernal Dawn" with the modification date, yet it also updates when ANY sheet is modified.
I found that there were 2 different ways of doing this (one using the gid). The only problem is that my skills in coding are extremely limited and I couldn't figure out how to integrate it into the above code. Any help would be appreciated!
The function onEdit has an optional argument that remembers which cell\range\sheet was edited. The structure of this argument is described here.
function onEdit(e)
{ var d=new Date();
var s=d.toLocaleString();
if(e.source.getActiveSheet().getName()=="TheSheetThatWeWishToTrace")
{ SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Infernal Dawn').getRange("A1").setValue(s);}
}