Goal: I'm trying to create a behavior tracker for four classes in Google Spreadsheets. The tracker has nine sheets: Class7A, Class7B, Class8A, Class8B, and Mon-Fri summary sheets. The goal was for each ClassXX sheet to have behavior tracking information for an entire week, but for the default view to show only the current day's information.
Attempts: During initial workup (with only the Class7A sheet created), I got this to work using a modification of the script found here (Thank you Jacob Jan Tuinstra!): Optimize Google Script for Hiding Columns
I modified it to check the value in the third row of each column (which held a 1 for Monday, 2 for Tuesday, etc), and if it did not match the numerical equivalent for the day of the week (var d = new Date(); var n = d.getDay();), then it would hide that column. This process was somewhat slow - I'm assuming because of the iterating through each column - but it worked.
Quite excited, I went ahead and added the rest of the sheets, and tried again - but the code as written, seems to affect only the current sheet. I tried modifying it by replacing var sheet = ss.getSheets()[0]; with for script that iterated through the columns, until i>4 (I've since lost that piece of code), with no luck.
Deciding to go back and try adapting the original version of the script to instead explicitly run multiple times for each named sheet, I found the that script no longer seems to work at all. I get various version of "cannot find XX function in sheet" or "cannot find XX function in Range."
Source: A shared version (with student info scrubbed) can be found here: https://docs.google.com/spreadsheets/d/1OMq4a4_Gh_xyNk_IRy-mwJn5Hq36RXmdAzTzx7dGii0/edit?usp=sharing (editing is on).
Stretch Goal: Ultimately, I need to get this to reliably show only the current day's columns (either through preset ranges (same for each sheet), or the 1-5 values), and I need it to do so for all four ClassXX sheets, but not the summary pages (and preferably more quickly than the iterations). If necessary, I can remove the summary pages and set them up externally, but that's not my first preference. I would deeply appreciate any help with this; so far my attempts have seemed to only take me backwards.
Thanks!
Current code:
function onOpen() {
// get active spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
// create menu
var menu = [
{name: "Show Today Only", functionName: "hideColumn"},
{name: "Show All Days", functionName: "showColumn"},
{name: "Clear Week - WARNING will delete all data", functionName: "clearWeek"}
];
// add to menu
ss.addMenu("Show Days", menu);
}
var d = new Date();
var n = d.getDay();
function hideColumn() {
// get active spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
// get first sheet
var sheet = ss.getSheets()[0];
// get data
var data = sheet.getDataRange();
// get number of columns
var lastCol = data.getLastColumn()+1;
Logger.log(lastCol);
// itterate through columns
for(var i=1; i<lastCol; i++) {
if(data.getCell(2, i).getValue() != n) {
sheet.hideColumns(i);
}
}
}
function showColumn() {
// get active spreadsheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
// get first sheet
var sheet = ss.getSheets()[0];
// get data
var data = sheet.getDataRange();
// get number of columns
var lastCol = data.getLastColumn();
// show all columns
sheet.showColumns(1, lastCol);
}
I cannot recreate the problem of the script not working at all, it's working fine for Class7A so that part is working fine.
So let's look at the two other problems:
Applying this to all Sheets
Speeding up the script
First let's create some globals we use in both functions
var d = new Date();
var n = d.getDay();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetNames = ss.getSheets().map(function(sheet) {return sheet.getName();});
var classSheets = sheetNames.filter(function(sheetName) {return sheetName.match("Class")});
Now we can iterate over classSheets and get the sheet by name and hide columns in each.
However hiding each individual column is very slow.
The sheet is built very structured, every week has 12 columns (except for friday which doesn't have the grey bar), so we can just calculate the ranges we want to hide.
function hideColumn() {
classSheets.map(function(sheetName){
var sheet = ss.getSheetByName(sheetName);
if (n == 1) {
// Hide everything after the first three columns + Monday
sheet.hideColumns(3 + 11, 12 * 4);
} else if (n == 5) {
// Hide everything to the left except the leftmost three columns
sheet.hideColumns(3, 4 * 12);
} else {
// Hide everything left of the current day
sheet.hideColumns(3, (n - 1) * 12);
// Hide everything after the current day
sheet.hideColumns(3 + n * 12, (5 - n) * 12 - 1);
}
});
}
Lastly we can shorten showColumn
function showColumn() {
classSheets.map(function(sheetName){
var sheet = ss.getSheetByName(sheetName);
var lastCol = sheet.getLastColumn();
sheet.showColumns(1, lastCol);
});
}
Related
I've seen other approaches, but haven't been able to get anything working. I've been using a script similar to this to transfer a list from one TAB to another.
I thought I could just add in a different ID, set the variables, and then transfer it to a completely different WORKSHEET:
function archivesingle() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ssd = SpreadsheetApp.openById('sheetid');
var idstoadd = ss.getActiveSheet();
var fulllist = ssd.getSheetByName("History");
Logger.log("Option#1 - Start")
// step 1 - get the last row on the target: Full List
var last_row = fulllist.getRange(1, 1, fulllist.getLastRow(), 1).getValues().filter(String).length;
Logger.log("Step#1 - the last row on fulllist = "+last_row)
// step 2 - create a target range (or at least the top left corner of the target)
var targetrange = fulllist.getRange(last_row+1,1);
Logger.log("Step#2 - the target range will be "+targetrange.getA1Notation())
// step 3 - create a source range
// how many header rows
var numheaderrows = 0
// so the range will be...
var sourcerange = idstoadd.getRange(1+numheaderrows, 14, idstoadd.getLastRow()-numheaderrows, 2);/// to pull in more columns, adjust last number
Logger.log("Stewp#3 - the sourcerange = "+sourcerange.getA1Notation());
// step4 - copy from source to target
// method = range.copyto
// so copyto from the source range to the target range
sourcerange.copyTo(targetrange,{contentsOnly:true});
Logger.log("Step#4 - copied the source range to the target range and flushed");
// step 6 - apply all the pending changes.
SpreadsheetApp.flush();
Logger.log("Step#6 - Flushed the spreadsheet")
Logger.log("Option#1 - Completed")
return false;
}
I was hit with the "target range and source range must be on the same spreadsheet" error.
I've seen other approaches, but I'm new to this and don't really understand how they work.
Is there a way to tweak this in order to accomplish my task (append a range of data to a list of data hosted in a separate worksheet)?
Any insight is much appreciated.
Try this:
function archivesingle() {
const ss = SpreadsheetApp.getActive();
const ssd = SpreadsheetApp.openById('sheetid');
const ish = ss.getActiveSheet();
const hsh = ssd.getSheetByName("History");
let ivs = ish.getRange(1, 14, ish.getLastRow(), 2).getValues()
hsh.getRange(hsh.getLastRow() + 1,1,ivs.length,ivs[0].length).setValues(ivs);
}
I would like the below code to run on how ever many tabs get created - after Sheet 7 (First 7 tabs always remain unchanged). Currently I use an array and must number them which works if you know exactly how many tabs get created - which I dont always know. So I currently create script for [7] then [8] etc etc. this does return an error when I say have [20] but Tab 20 doesnt exist.
function Company_ONE() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[7]; //SHEET 8
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheets()[7];
var lr = sheet.getLastRow();
var cell = SpreadsheetApp.getActive().getRange('AK3');
var criteria = SpreadsheetApp.DataValidationCriteria.CHECKBOX;
var rule = SpreadsheetApp.newDataValidation()
.requireCheckbox()
.build();
cell.setDataValidation(rule);
sheet.getRange('AK3').activate();
var destinationRange = sheet.getActiveRange().offset(0, 0, lr-3);
sheet.getRange('AK3').copyTo(destinationRange);
}
Explanation:
Use getSheet().slice(7) to get from 8th sheet onwards. See here how slice works.
Then you can use forEach() to iterate through every sheet (after sheet 7th).
I also removed some unnecessary lines of codes. For example, you use SpreadsheetApp.getActive() multiple times in the sheet or you define the same variables twice like ss or sheet.
Since you are interacting with the sheets iteratively you might need to use SpreadsheetApp.flush() to make sure all the pending changes are completed.
Solution:
function Company_ONE() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets().slice(7); // get 8th sheet onwards
sheets.forEach(sheet=>{
var lr = sheet.getLastRow();
var cell = ss.getRange('AK3');
var criteria = SpreadsheetApp.DataValidationCriteria.CHECKBOX;
var rule = SpreadsheetApp.newDataValidation()
.requireCheckbox()
.build();
cell.setDataValidation(rule);
sheet.getRange('AK3').activate();
var destinationRange = sheet.getActiveRange().offset(0, 0, lr-3);
sheet.getRange('AK3').copyTo(destinationRange);
SpreadsheetApp.flush();
});
}
I've just written my first google apps scripts, ported from VBA, which formats a column of customer order information (thanks to you all of your direction).
Description:
The code identifies state codes by their - prefix, then combines the following first name with a last name (if it exists). It then writes "Order complete" where the last name would have been. Finally, it inserts a necessary blank cell if there is no gap between the orders (see image below).
Problem:
The issue is processing time. It cannot handle longer columns of data. I am warned that
Method Range.getValue is heavily used by the script.
Existing Optimizations:
Per the responses to this question, I've tried to keep as many variables outside the loop as possible, and also improved my if statements. #MuhammadGelbana suggests calling the Range.getValue method just once and moving around with its value...but I don't understand how this would/could work.
Code:
function format() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var lastRow = s.getRange("A:A").getLastRow();
var row, range1, cellValue, dash, offset1, offset2, offset3;
//loop through all cells in column A
for (row = 0; row < lastRow; row++) {
range1 = s.getRange(row + 1, 1);
//if cell substring is number, skip it
//because substring cannot process numbers
cellValue = range1.getValue();
if (typeof cellValue === 'number') {continue;};
dash = cellValue.substring(0, 1);
offset1 = range1.offset(1, 0).getValue();
offset2 = range1.offset(2, 0).getValue();
offset3 = range1.offset(3, 0).getValue();
//if -, then merge offset cells 1 and 2
//and enter "Order complete" in offset cell 2.
if (dash === "-") {
range1.offset(1, 0).setValue(offset1 + " " + offset2);
//Translate
range1.offset(2, 0).setValue("Order complete");
};
//The real slow part...
//if - and offset 3 is not blank, then INSERT CELL
if (dash === "-" && offset3) {
//select from three rows down to last
//move selection one more row down (down 4 rows total)
s.getRange(row + 1, 1, lastRow).offset(3, 0).moveTo(range1.offset(4, 0));
};
};
}
Formatting Update:
For guidance on formatting the output with font or background colors, check this follow-up question here. Hopefully you can benefit from the advice these pros gave me :)
Issue:
Usage of .getValue() and .setValue() in a loop resulting in increased processing time.
Documentation excerpts:
Minimize calls to services:
Anything you can accomplish within Google Apps Script itself will be much faster than making calls that need to fetch data from Google's servers or an external server, such as requests to Spreadsheets, Docs, Sites, Translate, UrlFetch, and so on.
Look ahead caching:
Google Apps Script already has some built-in optimization, such as using look-ahead caching to retrieve what a script is likely to get and write caching to save what is likely to be set.
Minimize "number" of read/writes:
You can write scripts to take maximum advantage of the built-in caching, by minimizing the number of reads and writes.
Avoid alternating read/write:
Alternating read and write commands is slow
Use arrays:
To speed up a script, read all data into an array with one command, perform any operations on the data in the array, and write the data out with one command.
Slow script example:
/**
* Really Slow script example
* Get values from A1:D2
* Set values to A3:D4
*/
function slowScriptLikeVBA(){
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
//get A1:D2 and set it 2 rows down
for(var row = 1; row <= 2; row++){
for(var col = 1; col <= 4; col++){
var sourceCellRange = sh.getRange(row, col, 1, 1);
var targetCellRange = sh.getRange(row + 2, col, 1, 1);
var sourceCellValue = sourceCellRange.getValue();//1 read call per loop
targetCellRange.setValue(sourceCellValue);//1 write call per loop
}
}
}
Notice that two calls are made per loop(Spreadsheet ss, Sheet sh and range calls are excluded. Only including the expensive get/set value calls). There are two loops; 8 read calls and 8 write calls are made in this example for a simple copy paste of 2x4 array.
In addition, Notice that read and write calls alternated making "look-ahead" caching ineffective.
Total calls to services: 16
Time taken: ~5+ seconds
Fast script example:
/**
* Fast script example
* Get values from A1:D2
* Set values to A3:D4
*/
function fastScript(){
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
//get A1:D2 and set it 2 rows down
var sourceRange = sh.getRange("A1:D2");
var targetRange = sh.getRange("A3:D4");
var sourceValues = sourceRange.getValues();//1 read call in total
//modify `sourceValues` if needed
//sourceValues looks like this two dimensional array:
//[//outer array containing rows array
// ["A1","B1","C1",D1], //row1(inner) array containing column element values
// ["A2","B2","C2",D2],
//]
//#see https://stackoverflow.com/questions/63720612
targetRange.setValues(sourceValues);//1 write call in total
}
Total calls to services: 2
Time taken: ~0.2 seconds
References:
Best practices
What does the range method getValues() return and setValues() accept?
Using methods like .getValue() and .moveTo() can be very expensive on execution time. An alternative approach is to use a batch operation where you get all the column values and iterate across the data reshaping as required before writing to the sheet in one call. When you run your script you may have noticed the following warning:
The script uses a method which is considered expensive. Each
invocation generates a time consuming call to a remote server. That
may have critical impact on the execution time of the script,
especially on large data. If performance is an issue for the script,
you should consider using another method, e.g. Range.getValues().
Using .getValues() and .setValues() your script can be rewritten as:
function format() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var lastRow = s.getLastRow(); // more efficient way to get last row
var row;
var data = s.getRange("A:A").getValues(); // gets a [][] of all values in the column
var output = []; // we are going to build a [][] to output result
//loop through all cells in column A
for (row = 0; row < lastRow; row++) {
var cellValue = data[row][0];
var dash = false;
if (typeof cellValue === 'string') {
dash = cellValue.substring(0, 1);
} else { // if a number copy to our output array
output.push([cellValue]);
}
// if a dash
if (dash === "-") {
var name = (data[(row+1)][0]+" "+data[(row+2)][0]).trim(); // build name
output.push([cellValue]); // add row -state
output.push([name]); // add row name
output.push(["Order complete"]); // row order complete
output.push([""]); // add blank row
row++; // jump an extra row to speed things up
}
}
s.clear(); // clear all existing data on sheet
// if you need other data in sheet then could
// s.deleteColumn(1);
// s.insertColumns(1);
// set the values we've made in our output [][] array
s.getRange(1, 1, output.length).setValues(output);
}
Testing your script with 20 rows of data revealed it took 4.415 seconds to execute, the above code completes in 0.019 seconds
new to using javascript in Google Sheets. I am trying to create a sheet that automatically calculates what business partners will receive based upon their dynamic split percentage and the incoming money. The percentage comes from one cell, but each payment made to the partners will be entered in a column. Here's what I have so far:
/**
* Creates a Date Stamp if a column is edited.
* Also performs dynamic split calculations
*/
//CORE VARIABLES
// The column you want to check if something is entered.
var COLUMNTOCHECK = 13;
// Where you want the date time stamp offset from the input location. [row, column]
var DATETIMELOCATION = [0,-1];
// Where the calculations will be performed for the Partners.
var PARTNERONELOCATION = [0, 1];
var PARTNERTWOLOCATION = [0, 2];
var PARTNERTHREELOCATION = [0, 3];
// Sheet you are working on
var SHEETNAME1 = 'Sheet1'
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var partnerOnePercent = sheet.getRange("G2").getValue();
var partnerOnePaySource = sheet.getRange("M2").getValue();
var partnerTwoPercent = sheet.getRange("G3").getValue();
var partnerTwoPaySource = sheet.getRange("M2").getValue();
var partnerThreePercent = sheet.getRange("G4").getValue();
var partnerThreePaySource = sheet.getRange("M2").getValue();
//checks that we're on the correct sheet.
if(sheet.getSheetName() == SHEETNAME1 ) {
var selectedCell = ss.getActiveCell();
//checks the column to ensure it is on the one we want to cause the date and calculations to appear.
if( selectedCell.getColumn() == COLUMNTOCHECK) {
var dateTimeCell = selectedCell.offset(DATETIMELOCATION[0],DATETIMELOCATION[1]);
dateTimeCell.setValue(new Date());
var partnerOneCell = selectedCell.offset(PARTNERONELOCATION[0],PARTNERONELOCATION[1]);
partnerOneCell.setValue((partnerOnePercent/100)*partnerOnePaySource);
var partnerTwoCell = selectedCell.offset(PARTNERTWOLOCATION[0],PARTNERTWOLOCATION[1]);
partnerTwoCell.setValue((partnerTwoPercent/100)*partnerTwoPaySource);
var partnerThreeCell = selectedCell.offset(PARTNERTHREELOCATION[0],PARTNERTHREELOCATION[1]);
partnerThreeCell.setValue((partnerThreePercent/100)*partnerThreePaySource);
}
}
}
The thing I would like it to do is when I enter a payment into the column (column 13), Is for the payment to be split into the partner location cells (var partnerOneCell, var partnerTwoCell, var partnerThreeCell) that are offset from the cell that the payment amount is typed in, and I only want it to be calculated by the percentages as they are at the moment the payment amount is entered. Any help would be appreciated.
Right now it is only calculating based upon cell M2 (row 2, column 13). I want it to be able to calculate based upon the amount I enter in any cell within that column, and only the cell I am currently updating. I think that this has something to do with getActiveRange or something, but I'm not familiar enough with these methods.
Edit: Upon testing, it also seems that row 3 column 14 is reflecting a calculation of partnerOnePaySource*partnerOnePercent with PaySource coming from row 3 column 13 (M3), though I'm not sure why.
Alright everyone, after reading over my code, I realized how I could use the getActiveCell method within the PaySource variables.
var partnerOnePercent = sheet.getRange("G2").getValue();
var partnerOnePaySource = ss.getActiveCell().getValue();
var partnerTwoPercent = sheet.getRange("G3").getValue();
var partnerTwoPaySource = ss.getActiveCell().getValue();
var partnerThreePercent = sheet.getRange("G4").getValue();
var partnerThreePaySource = ss.getActiveCell().getValue();
With this I was able to fix my problem and it now works.
I have a sheet that employees will update daily with information about tasks done that day. Each column has a date in the header row (row 3 in this case), and after the end of the following day I want that column to lock so it cannot be edited further except by myself and one other. This is to prevent people from covering up mistakes or accidentally changing or deleting data.
I am looking for a script or something that will accomplish this. This sheet has about 45 tabs and I need the same thing applied to all of them.
My idea is possibly a script that triggers at a certain time based off the date in the header row, so if the date is May 5th 2017, the respective column would lock itself at midnight on the 6th.
A link to a copy of my sheet, minus data is here.
Alternatively, if there is a way to simply lock any cell 24 hours after the most recent data is entered into it, and prevent further editing by everyone except select people, that could work too if the ideal method isn't doable.
Yes, there is a way to do this.
I will briefly describe the solution:
Let's say that the first row has 1:1 contains consecutive dates.
Create function lockColumns which would create new protected range.
Add function lockColumns to time trigger, which triggers every day between 0:01 and 1:00 am.
And now some code:
function lockColumns() {
var ss = SpreadsheetApp.getActive().getSheetByName('Sheet 1')
var range = ss.getRange('1:1').getValues()[0];
var today = new Date();
var todayCol = null;
for (var i=0; i<range.length; i++) {
if (today.isSameDateAs(range[i])) {
todayCol = i;
break;
}
}
var rangeToProtect = ss.getRange(1, todayCol +1, ss.getMaxRows(), 1)
var protection = rangeToProtect.protect().setDescription('Protected range');
// Ensure the current user is an editor before removing others. Otherwise, if the user's edit
// permission comes from a group, the script will throw an exception upon removing the group.
var me = Session.getEffectiveUser();
protection.addEditor(me);
protection.removeEditors(protection.getEditors());
if (protection.canDomainEdit()) {
protection.setDomainEdit(false);
}
protection.addEditor('email#gmail.com'); // second person with edit permissions
}
/*
http://stackoverflow.com/a/4428396/2351523
*/
Date.prototype.isSameDateAs = function(pDate) {
return (
this.getFullYear() === pDate.getFullYear() &&
this.getMonth() === pDate.getMonth() &&
this.getDate() === pDate.getDate()
);
}