Dynamic Page Breaks in Google Sheets - google-apps-script

I have mild general programming knowledge but know basically nothing about google apps scripts specifically.
I am trying to create dynamic page breaks in google sheets, or find another way to keep certain rows grouped together when printing.
On my data sheet I have 100s of rows of information and within each row the data can vary significantly in length (from a single number to many paragraphs of text). I have created a second sheet that both filters the information that I want and displays it in a visually-appealing way, taking the original data from each row and parsing it into 8 total rows (7 with information, and one blank to visually separate one block of info from the next) per one original. The problem is that the varying length of the data means I have to manually move the page breaks every time I change the filter.
Here is a blank section of the second sheet for reference.
I want to be able to print with as many 8-row groupings on a page as I can, but not split up a group onto the next page.
I'm honestly not sure how to get started, though I presume that I can use the blank row to trigger the page breaks somehow. Any help would be greatly appreciated!
Updated
I have been able to write some rudimentary code to (mostly) accomplish what I wanted. However the best that I can tell is that getRowHeight() is not working with my wrapped text, as it properly formats when I have any empty data set, but not otherwise.
Can someone confirm, and tell me what I'm missing?
function dynamicPageBreaks() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var curRow = 2; //start on Row 2 (1 contains the filter selection)
var curTotPix = ss.getRowHeight(curRow);
var pgPix = 1030;
//loop until end of sheet
var endRow = ss.getLastRow();
do {
//find row that goes past page break
do {
curRow++;
curTotPix = curTotPix + ss.getRowHeight(curRow);
} while (curTotPix <= pgPix);
//get value of cell in column B of that row
var curCell = sheet.getRange(curRow,2).getValue();
//back up until we find an empty row
if (curCell == "") {
break;
} else do {
curTotPix = curTotPix - ss.getRowHeight(curRow);
curRow = curRow - 1;
curCell = sheet.getRange(curRow,2).getValue();
} while (curCell != "");
//expand empty row to match necessary pixels
var addHeight = pgPix - curTotPix;
ss.setRowHeight(curRow - 1, ss.getRowHeight(curRow) + addHeight);
//reset for next iteration
curTotPix = ss.getRowHeight(curRow);
} while (curRow < endRow);
}

I would recommend you get started by following an Apps script quickstart like the one about extending sheets here[1]
Then you can define your logic using methods from the Spreadsheet service[2].
You have the method getRowHeight()[3] which could help you in determining the length of the range you are looking to print. And that also depends on the paper size you choose.
[1]https://developers.google.com/apps-script/guides/sheets#get_started
[2]https://developers.google.com/apps-script/reference/spreadsheet
[3]https://developers.google.com/apps-script/reference/spreadsheet/sheet#getrowheightrowposition

Related

Using for and if loops in Google Apps Script

Dear programming Community,
at first I need to state, that I am not quite experienced in VBA and programming in general.
What is my problem? I have created a topic list in google sheets in order to collect topics for our monthly meeting among members in a little dance club. That list has a few columns (A: date of creation of topic; B: topic; C: Name of creator; ...). Since it is hard to force all the people to use the same format for the date (column A; some use the year, others not, ...), I decided to lock the entire column A (read-only) and put a formular there in all cells that looks in the adjacent cell in column B and sets the current date, if someone types in a new topic (=if(B2="";"";Now()). Here the problem is, that google sheets (and excel) does then always update the date, when you open the file a few days later again. I tried to overcome this problem by using a circular reference, but that doesn't work either. So now I am thinking of creating a little function (macro) that gets triggered when the file is closed.
Every cell in Column B (Topic) in the range from row 2 to 1000 (row 1 is headline) shall be checked if someone created a new topic (whether or not its empty). If it is not empty, the Date in the adjacent cell (Column A) shall be copied and reinserted just as the value (to get rid of the formular in that cell). Since it also can happen, that someone has created a topic, but a few days later decides to delete it again, in that case the formular for the date shall be inserted again. I thought to solve this with an If-Then-Else loop (If B is not empty, then copy/paste A, else insert formula in A) in a For loop (checking rows 1 - 1000). This is what I have so far, but unfortunately does not work. Could someone help me out here?
Thanks in advance and best regards,
Harry
function NeuerTest () {
var ss=SpreadsheetApp.getActive();
var s=ss.getSheetByName('Themenspeicher');
var thema = s.getCell(i,2);
var datum = s.getCell(i,1);
for (i=2;i<=100;i++) {
if(thema.isBlank){
}
else {
datum.copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
}}
}
The suggested approach is to limit the calls to the Spreadsheet API, therefore instead of getting every cell, get all the data at once.
// this gets all the data in the Sheet
const allRows = s.getDataRange().getValues()
// here we will store what is written back into the sheet
const output = []
// now go through each row
allRows.forEach( (row, ind) => {
const currentRowNumber = ind+1
// check if column b is empty
if( !row[1] || row[1]= "" ){
// it is, therefore add a row with a formula
output.push( ["=YOUR_FORMULA_HERE"] )
} else {
// keep the existing value
output.push( [row[0]] )
}
})
Basically it could be something like this:
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Themenspeicher');
var range = sheet.getRange('A2:B1000');
var data = range.getValues(); // <---- or: range.getDisplayValues();
for (let row in data) {
var formula = '=if(B' + (+row+2) + '="";"";Now())';
if (data[row][1] == '') data[row][0] = formula;
}
range.setValues(data);
}
But actual answer depends on what exactly you have, how your formula looks like, etc. It would be better if you show a sample of your sheet (a couple of screenshots would be enough) 'before the script' and 'after the script'.

Google Apps Script adds extra blank questions to Form Responses sheet when importing from Google Sheets

I am using Google Apps Script to add questions from a Google Sheets into a Google Form. The lists are read from two separate arrays and added as individual Grid type questions. I also set the Sheet as the destination for the Form results.
When I run the script, the Form gets updated perfectly. If there are 10 items to be added, 10 questions are added to the Form. However, in the Form Responses sheet that is linked, there are often times additional columns titled " [Row 1]" that are added. The number of additional columns and their position change what seems like every other time I run the script. I haven't been able to pick up on any patterns.
I do know that "Row 1" appears as the default first item in a Grid type question when creating the question in the Form's UI. I'm not sure if that has anything to do with it. FYI - grid items allow multiple rows of questions to be added but I am only adding one question. I know there is a multiple choice grid type but I do not like the formatting of it.
As a workaround, I've created a script to delete all of these additional columns but I would really like to figure out what is actually happening.
Any ideas on what is happening?
Here is part of my code:
function editForm()
{
var setupSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Setup'); //Create variable for the Setup sheet
var metricSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Final Metric Statements'); //Create variable for the Metric Statements sheet
var form = FormApp.openById(setupSheet.getRange("D1").getValue()); //Open form using the form ID from the setup sheet
var pageFour = form.addPageBreakItem().setTitle('Questions');
var emotLen = 0; //Counter for sub-emotion items
var featLen = 0; //Counter for sub-feature items
//Count number of items for first list
while (((metricSheet.getRange("C"+ (emotLen+2)).getValue()) != "") && ((metricSheet.getRange("C"+ (emotLen+2)).getValue()) != "#N/A")) //Go through sub-emotion column until a blank cell
{
emotLen++; //Increase the counter
}
//Count number of items for second list
while (((metricSheet.getRange("D"+ (featLen+2)).getValue()) != "") && ((metricSheet.getRange("D"+ (featLen+2)).getValue()) != "#N/A")) //Go through sub-feature column until a blank cell
{
featLen++; //Increase the counter
}
if (emotLen > 0)
{
var emotRng = metricSheet.getRange(2,3,emotLen); //Create a range for sub-emotions based on number of items and column
var emotArray = emotRng.getValues(); //Copy the items into an array
for (i=0; i <= (emotLen-1); i++)
{
var emotItem = form.addGridItem();
emotItem.setRows(emotArray[i]);
emotItem.setColumns(['Strongly Disagree', 'Disagree','Slightly Disagree', 'Neither Disagree or Agree', 'Slightly Agree', 'Agree', 'Strongly Agree', 'N/A']);//Add column header
emotItem.setRequired(true);
}
}
if (featLen > 0 )
{
var featRng = metricSheet.getRange(2,4,featLen); //Create a range for features based on number of items
var featArray = featRng.getValues(); //Copy the items into an array
for (i = 0; i <=(featLen-1); i++) //Go through all items in the array
{
var featItem = form.addGridItem();//Add item to survey
featItem.setRows(featArray[i]); //Add row item
featItem.setColumns(['Strongly Disagree', 'Disagree','Slightly Disagree', 'Neither Disagree or Agree', 'Slightly Agree', 'Agree', 'Strongly Agree', 'N/A']);//Add column header
featItem.setRequired(true);
}
}
//deleteRowColumns();
//Logger.log(emotLen, emotArray, featLen, featArray); //Logger used for debugging
}
And here is a screenshot of the Form Responses sheet with the additional column:
This appears to be a bug!
I have taken the liberty of reporting this on Google's Issue Tracker for you, detailing the behaviour:
Adding GridItems to a form, that has a Sheet attached to it, from an Apps Script function, intermittently adds question columns that do not exist.
You can hit the ☆ next to the issue number in the top left on the page which lets Google know more people are encountering this and so it is more likely to be seen to faster.

Copying Notes from one sheet to another. Importrange?

OK, still rookie coder but getting better. Able to modify codes but not write most of them myself yet. Here is my current problem.
I run a small business and we use google sheets as our CRM (its faster and easier for us to do it this way) I have a master sheet that I bring in with =importrange everyone else jobs. It works perfect, makes my life real easy but there is one thing I can not get to come over. That is the notes stored in a cell. I have to actually open their sheet to view the notes. So I am trying to get a script that would update the notes down the importedrange. Then each day when I go over their info I push a button and it would write the notes from the other persons sheet onto my sheet, can just write over the last note and replace it.
I made an example with 3 sheets (all made editable for everyone) since I can't post our actual business sheets to work with. I should be able to modify it and transfer to my actual sheets after some help.
(Master Sheet) https://docs.google.com/spreadsheets/d/1TMNyohd5Vtn3p9cpLebmZASt2TzbWL80fIesCu89-ig/edit?usp=sharing
(Employee 1)
https://docs.google.com/spreadsheets/d/1n4iFXGuC7yG1XC-UIbuT9VrQ7rJWngPkDCv0vsvDed4/edit?usp=sharing
(Employee 2)
https://docs.google.com/spreadsheets/d/1EJVa5TgF6UkLhiLtfQ6o7BzpdzXDGcVhkibLCYwlfAU/edit?usp=sharing
Links are provided to each other sheet on the top of the master sheet. This is above my head so I won't try to butcher the code below lol. Here is a function I found but don't know how to implement it to use with import range.
function getNotes(rangeAddress) {
// returns notes inserted with Insert > Note from one cell or a range of cells
// usage:
// =getNotes("A1"; GoogleClock())
// =getNotes("A1:B5"; GoogleClock())
// see /docs/forum/AAAABuH1jm0xgeLRpFPzqc/discussion
var ss = SpreadsheetApp.getActiveSpreadsheet();
var range = ss.getRangeByName(rangeAddress);
if (!range) return '#N/A: invalid range. Usage: =getNotes("A1:B5"; GoogleClock())';
var notesArray = new Array();
for (var i = 0; i < range.getHeight(); i++) {
notesArray[i] = new Array();
for (var j = 0; j < range.getWidth(); j++) {
notesArray[i][j] = range.getCell(i + 1, j + 1).getComment();
}
}
return notesArray;
}
So The code I would want it to read the note from "Employee 1" sheet and write it onto the "Master" sheet in the correct cell. Since it is an =importrange the orientation of the cells will always be the same on both sheets, just needs to pick the starting cell and go down the list. I want to make it work with the button I put on the top of the master sheet on each tab.
This script uses outdated methods. For example, GoogleClock() is gone.
Please see my other response to your similar question for a possible solution.

How slow can the google sheets api be? [duplicate]

This question already has answers here:
How much faster are arrays than accessing Google Sheets cells within Google Scripts?
(2 answers)
Closed 3 years ago.
In my script I iterate through cells each of which represents a day on The calendar grid. So it goes from an earlier date (from a cell changed by the user) to a later one. Top down and left to right.
The script starts when a user changed value of a cell. And it must fill every subsequent days-cells by the same value up to certain cell. Lets say it must stop on the cell with red font. Thus every iteration the script must get the cell font color.
...Or, The iterations must stop when the script gets a cell representing a certain date. Thus every iteration the script must verify which date the cell represets. To understand what date a cell represents I get The values from the helper cells (headers) and use getValue(). Whatever.
Everything is bearable: looping through cells, changing the values in each cell, getting helper cells(ranges). But! As soon as I add getValue() to the given headers it starts to work unbelievably slowly. Or even I just get font color... Any function starting from "get" included in iteration makes the job unbelievably slowly!
A script with looping as many as you like getRange(), setValue() works out in tolerable time, but with just one getFontColor() or getValue this job runs in the same time for just one cell.
Either I do somethin illegal or google ?
Is there an opportunity to accelerate this job significantly?
Or job like this should be done quite differently?
function onEdit(evt) {
var aSheet = evt.source.getActiveSheet();
// veryfy which sheet
switch( aSheet.getName().toLowerCase() ) {
case "wage":
// get range - calendar grid
var wageGrid = aSheet.getParent().getRangeByName("wageGrid");
var editedCell = evt.range;
// loop exit flag
var weBreak = false;
editedCell.setFontColor("red");
// loop through rows
for(var rowIndex = editedCell.getRow(); rowIndex <= wageGrid.getLastRow(); rowIndex++) { if(weBreak) break;
// loop through columns
for(var collIndex = (rowIndex == editedCell.getRow())?editedCell.getColumn():wageGrid.getColumn(); collIndex <= wageGrid.getLastColumn(); collIndex++) {
// as many as you like
var currentLoopCell = aSheet.getRange(rowIndex, collIndex);
var dayHeaderCell = aSheet.getRange(rowIndex, 1);
var monthHeaderCell = aSheet.getRange(1, collIndex);
cell.setValue(evt.value);
// but getValue() or getSomeAttribute() will slow down the process
//var cellFontColor = cell.getFontColor();
//if(cellFontColor=="red") weBreak = true; break;
}
}
break;
case "nonexistentyet":
break;
default:
Logger.log("What was it?")
}
}
What you have encountered is normal behaviour. Each call to the sheet such as getValue() and getFontColor() takes a fair amount of time, often 1 to 2 seconds each. You should avoid calling these functions in a loop.
When you want to loop over a large set of cells and work with their values, define the entire range with getRange() and use getValues() and getFontColors() instead. These functions will return the data from an entire range in a two-dimensional array. You can even bring in all the data in the sheet with getDataRange().
Similarly, it is best to write in blocks as well, using setValues() and setFontColors() rather than setting values/colors on cells one by one

Google app script: find first empty cell in a row

Im learning Google app script while building a dashboard. I'm collecting data from several sheets. My goal is to see by how many rows each sheet grows every week. This gives me insight in how my business is doing.
I can get the length of all the sheets I want to check, however I cant find any code which helps me to find the first empty cell in a specific row. I want to place the length of each sheet there (in my dashboard datacollection sheet) to create a graphs later on.
What I have is:
var range = ss.getRange(2, 1, 1, 1000);
var waarden = range.getValues();
Logger.log(waarden);
var counter = 0
for (var j = 0; j < ss.getLastColumn(); j++) {
Logger.log(waarden[0][j]);
if (waarden[0][j] == ""){
break
} else {
counter++;
}
Logger.log(counter);
}
This works but I can't image this being the best solution (or quickest solution). Any tips in case my length goes beyond 1000 without me noticing it (although it would take a couple of years to do so in this case ;) )?! Why does getLastColumn() behave so much different than getLastRow()?
Thanks for helping me learn :)
*** edited I figured out I have to use if (waarden[0][j] === ""){ with three = otherwise if my sheet in the row that I use as a check has a length of 0 than this is also counted as empty with two =operators.
Try indexOf()
function firstEmptyCell () {
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
var range = ss.getRange(2, 1, 1, ss.getMaxColumns());
var waarden = range.getValues();
// Get the index of the first empty cell from the waarden array of values
var empty_cell = waarden[0].indexOf("");
Logger.log("The index of the first empty cell is: %s", empty_cell);
}
This will give you the column position of the empty cell starting from a 0 index. So if the returned index is 4, the column is "E".
edit: As for the getLastColumn() question; you could use getMaxColumns() instead. Updated code to get all columns in the sheet.