Google Sheets: Hiding Columns based on date in row 1 - google-apps-script

I have no experience with scripting in Excel or Google Sheets, so I'm trying to branch out a bit and see if there's a solution to my problem. We use Google Sheets for a weekly calendar at our kitchen remodeling business. We organize the weeks from left to right and list the jobs we're currently working on in those columns. I would like to automatically hide all columns that have a date older than 4 weeks, so when the sheet opens, we're not starting with a date from a year ago. I can hide these columns manually each week, but when I do need to go back and look at previous weeks, I'm forced to unhide all thosecolumns and then highlight all of the columns I want to rehide. Having a script seems like the better solution.
Is there a way to have a script run every time the file is open so that we're always only displaying the previous 4 weeks and everything in the future? If so, would you be willing to help me understand how I might write that and get it working? Again, I'm a novice when it comes to anything beyond formulas, but very interested in learning more about the scripting capabilities.
Thank you!

With Apps Script via Tools->Script Editor, you could create a menu with an onOpen() function. The function in the menu (e.g. hidePast), would then need to check a given value in each column (to see what date that column refers to), and then flag it to be hidden or not. The onOpen function, because it is a "simple trigger", cannot do anything that requires "authorization" (such as interacting with non-local Spreadsheet data), hence the intermediate method. By creating a menu, you can make it easy for anyone using the spreadsheet to authorize and activate the function.
Example:
/* #OnlyCurrentDoc */
function onOpen() {
SpreadsheetApp.getActive().addMenu("Date Tools",
[{name:"Hide Past", functionName:"hidePast"},
{name:"Show All", functionName:"showAll"}]);
}
function showAll() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getActiveSheet();
sheet.unhideColumn(sheet.getDataRange());
ss.toast("All columns unhidden.");
}
function hidePast() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getActiveSheet();
// Acquire the 1st row of all used columns as an array of arrays.
var datelist = sheet.getSheetValues(1, 1, 1, sheet.getLastColumn());
// Drop the hours, minutes, seconds, etc. from today.
var now = new Date();
var today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
// Inspect the datelist and compare to today. Start from the rightmost
// column (assuming the dates are chronologically increasing).
var col = datelist[0].length;
while(--col >= 0) {
var then = new Date(datelist[0][col]);
if(then < today) {
break;
}
}
// Bounds check, and convert col into a 1-base index (instead of 0-base).
if(++col < 1) return;
// col now is the first index where the date is before today.
// Increment again, as these are 2-column merged regions (and
// the value is stored in the leftmost range). If not incremented,
// (i.e. hiding only part of a merged range), spreadsheet errors will occur.
sheet.hideColumn(sheet.getRange(1, 1, 1, ++col));
ss.toast("Hid all the columns before today.");
}

Because you don't have a "database like" source it will be very difficult, but you can try to create a very complicated
QUERY()
you should filter the dates in another sheet (and you may face a dead end).
So I will suggest using this kind of structure and it will also allow you to make other kinds of filters (or Pivot Tables) in the future (maintainable and scalable).

Related

Apps Script - How could this code be streamlined?

I've recently started working with Apps Script to improve the scope of what my google sheets can do, and I wanted to ask more experienced people how I might make my script more efficient. I used a mixture of tutorials, documentation, and trial & error to make it. I find that although it usually completes the task it's meant for, sometimes it takes an unreasonably long time or exceeds its runtime and simply stops.
I would like to know which best practices I could implement to make it run more quickly overall, and which things I might be able to include in future scripts to avoid any pitfalls I'd landed in here.
Scope:
The script is meant to take each day's new data and apply it to a new sheet called 'TODAY.' It works as follows.
Rename the tab labeled 'TODAY' to the previous workday's date (if today is 2.3, it renames the sheet to 2.2.)
Hide this renamed tab.
Duplicate the 'TEMPLATE' tab, and rename it to 'TODAY.'
Pull data from the 'RAW DATA' tab, and paste it into the new 'TODAY' tab.
Paste a formula into the new 'TODAY' tab and drag it down to the bottom of the table so that the correct values populate and the conditional formatting occurs.
Any help would be greatly appreciated, I really just need some direction for how to improve my work.
Here is a link to an example sheet with editing permissions enabled: https://docs.google.com/spreadsheets/d/1F7bAd2DjKgk53e-haPgjWfFphMfu5YBn8iRQ3qwC3n0/edit?usp=sharing
In my humble opinion, a good Google Sheet App Script doesn't need to use activate to control the source or destination of data. The sheet and script developer should know what and where they want the data to come from and go. Activate is like using the mouse to click on something.
I've taken your script and rewritten to minimize the use of variables. I have only one sheet variable and reuse it throughout. In fact for the majority of the time it is the copy of the TEMPLATE called TODAY.
Also unless I have to use a sheet last row many times, I avoid using a variable and instead just use sheet.getLastRow(). Same for columns.
I always wrap my code in a try catch block as a matter of habit.
As a last note, unless you change the notation in column C and N you could have used your script to fill in column B.
function myDailyUpdate() {
try {
let spread = SpreadsheetApp.getActiveSpreadsheet();
// Step 1
let sheet = spread.getSheetByName("TODAY");
let oldDate = sheet.getRange("Q4").getValue();
let prevDate = Utilities.formatDate(oldDate,"GMT-5","M.d");
// Renames old 'TODAY' sheet to previous workday's date.
sheet.setName(prevDate);
// Sets the color to red.
sheet.setTabColor("990000");
// Hides the old 'TODAY' sheet
sheet.hideSheet();
// Step 2
sheet = spread.getSheetByName("TEMPLATE");
// Copies the contents of the 'TEMPLATE' sheet to a new sheet called 'TODAY.'
sheet = sheet.copyTo(spread);
sheet.setName("TODAY");
sheet.activate(); // required to move to 1st position
// Move TEMPLATE to first position
spread.moveActiveSheet(1);
// Step 3
// Colors the 'TODAY' tab green to signify it being active.
sheet.setTabColor("6aa85f")
// Identifies the 'RAWDATA' sheet for later use.
let source = spread.getSheetByName("RAWDATA");
// Identifies ranges and values for later use.
let values = source.getDataRange().getValues();
// sheet is still the "TODAY" sheet
// Identifies 'TODAY' sheet as the recipient of 'RAWDATA' data, and identifies the range.
// Sets the values from 'RAWDATA' into 'TODAY.'
sheet.getRange(12,2,values.length,values[0].length).setValues(values);
// Step 4
// sheet is still the "TODAY" sheet
let range = sheet.getRange("C12");
range.setFormula(
'=IFERROR(IFERROR(IFS(VLOOKUP($B12,INDIRECT'
+'('
+'"'
+'\''
+'"'
+'&$Q$4&'
+'"'
+'\''
+'!"&"!"&"A1:O2000")'
+',15,false)="D","D",$N12="Quote","Q",$N12="Important","I",$N12="On Hold","H",$N12="IN TRANSIT","T",$N12="REQUEST","R",$N12="INCOMPLETE","N",$N12="COMMENT","C"),VLOOKUP($N12,$B$3:$C$9,2,FALSE)),"")');
// Pastes the above formula into cell C12.
let fillRange = sheet.getRange(12,3,values.length,1);
range.copyTo(fillRange);
sheet.activate();
}
catch(err) {
console.log(err);
}
}

Snapshot data and append at a specific location in Google Sheets

I have a Google Sheet I use to track data. I have a sheet that pulls data from multiple sheets in a single row. The row has the current date for Column B and then pulls in data for columns C through AC. I am trying to create a mechanism to snapshot that data and put it on the next line below it. I want the ability to continue doing this and keep pushing the data down and dropping the current on the next line. This allows me to select data in column A to use for graphing purposes. This is what I was using:
function recordHistory() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("History");
var source = sheet.getRange("B2:AC2");
var values = source.getValues();
var now = new Date();
values[0][0] = now;
for (var col in values) {
sheet.getRange(sheet.getLastRow(),2,1,28).setValues(values[col]);
}
I used a combination of examples and I think I got my wires crossed with the translation from one to the other. Looking for help to clean this up or point me to a better option. I was originally using appendRow, but that limits me to using the first column. I want the ability to have the snapshot placed in the 2nd column and the corresponding columns after it. Hopefully, that makes sense.
In this sheet, you can see I am pulling data from the first 2 sheets into the last sheet. I am skipping the first column and using Row 2 as the exact values. The script above is supposed to take what is in Row 2, snapshot it as values only, and move the data to Row 3, moving the previous rows down. This provides me a history of the values. I will be using the triggers to run this function every night at midnight, so the data will be a daily capture of the values. Hopefully, this makes it a bit more clear.
EDIT 2: Let me try and simplify the explanation. I have a sheet that has data in cells B2 through AC2. I want to grab that data and copy it to cells B3 through AC3, moving the data down a row. So on the sheet, you should see cells B3:AC3 having yesterdays data. B4:AC4 has the day before. B5:AC5 has the day before that. Basically keeping a log of the data that is captured in B2:AC2 each day.
Is it clearer what I am trying to accomplish or should I explain it further? I really want to get this script corrected so I can schedule it to run over the weekend.
After a few hours of playing with syntax a bit and realizing where my mistake was, I noticed some issues with the way I was capturing the data and trying to apply it to a range. Here is the solution to my problem:
function recordHistory() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("History");
var source = sheet.getRange("B2:AC2");
var values = source.getValues();
sheet.insertRowBefore(3);
sheet.getRange(3, 2, 1, 28).setValues([values[0]]);
};
As you can see in the solution, I realized how the data was being stored in the array and matched it to the setValues part of the script. It is a pretty basic issue I was having, but the use case was difficult to explain. The insertRowBefore was also a vital piece to establish the structure of the sheet.

How to disable editing cells after a particular time in Google Spreadsheet?

Use Case - I have shared a Google Spreadsheet amongst a dozen friends and we are entering our prediction for matches. The catch is to enter it before the game starts.
Using Spreadsheet as everyone can see everyone's prediction.
Problem - Is there an AddOn or any feature which allows to disable editing a few cells after a particular time? Say post midnight A[7]-M[7] cells cannot be edited.
You can set a script to run at a specific time:
In the script editor, create a function:
function protectRangeAtMidnight() {
//My code will go here
}
In the script editor, click on the RESOURCES menu, and choose, CURRENT PROJECTS TRIGGERS.
Add a trigger for a specific date and time.
The problem is, what is the code going to do? If you protect the range, but the people you are sharing the spreadsheet with have edit authority, they can just unprotected the range. If you change their permissions to VIEW only, then you'd have to change it back at some point for the next game. That would work as long as there can be a time period where no one else can edit the sheet.
You can remove a user from the editor list:
Remove Editor
function protectRangeAtMidnight() {
SpreadsheetApp.openById('The SS ID').removeEditor(emailAddress);
}
You can also set file sharing permissions through DriveApp:
setSharing(accessType, permissionType)
I am actually doing exactly the same thing and came across the same problem. The solution I came up with does not lock the cells but uses data validation. Some of the solutions suggested online did not seem to take into account that you need to lock a row of results which have a date associated with it.
This is the layout I am using for my predictions:
Google sheet cropped image example
The cells in blue then have the following data validation (criteria is custom formula, reject input):
=if(isnumber(C1),and(now()<$A1,C1>=0,C1-int(C1)=0))
It checks that what is entered in C1 is a number. If it is, it then checks the following:
If the current date and time is before 'kick off'.
If the number is greater than or equal to zero.
That it is a whole number.
If so, it allows the cell to be changed. If the match has kicked-off, the cell cannot be altered and a red triangle will appear in the cell (because the data validation will be violated as now() will be after the date in question) but it stops the cells from being changed once the game has kicked off.
If you couple the above with locking the entire sheet (apart from the blue cells) it should allow users to make predictions prior to kick-off.
If it is necessary for cells to be altered after the game has begun you can modify the date in column A to then make the update before changing the date back.
Hope this helps!
What works is to:
Lock the cells or columns at a specific time, then remove the protection at another time: using a daily trigger (or even manually)
Modifications needed before running the functions:
Sheets names (In my example it's Sheet1, Sheet2)
Range to protect (in my example it's A:D) (editors won't be able to edit the specified range)
function Lock() {
var tabs = ['Sheet1', 'Sheet2'];
var ss = SpreadsheetApp.getActiveSpreadsheet();
for (var i = 0; i < tabs.length; i++) {
var spreadsheet = ss.getSheetByName(tabs[i]);
var protection = spreadsheet.getRange('A:D').protect();
protection.setDescription('Protected')
protection.removeEditors(protection.getEditors());
}
};
And to remove the protection you'll use the following script: Editors get "back" their permission to edit the specified range:
function Unlock() {
var ss = SpreadsheetApp.getActive();
var protections = ss.getProtections(SpreadsheetApp.ProtectionType.RANGE);
for (var i = 0; i < protections.length; i++) {
if (protections[i].getDescription() == 'Protected') {
protections[i].remove();
}
}
};

Beginner in desperate need: pastevalue() to create historical tab for investments

Thank you very much in advance for your help, I'm new to coding but proficient with standard Excel functions. I would greatly appreciate any input on this project.
I want to create a Google spreadsheet that has 3 sheets. The first is a DATA sheet which lists stock tickers and provides live prices via Google finance {=googlefinance(VTI,price)}.
The second sheet is the MASTER sheet that aggregates all of the positions, including number of shares in stocks, quantity in fixed income instruments, quantity in bullion, etc. The prices used to calculate current market value of positions are drawn from the DATA sheet. All values are added together to create a total value cell, E57, that updates itself automatically from the google finance data throughout the trading day. This all works fine.
The final sheet is the HISTORY sheet. Here's what I want to do. I want cell E57 to copy to this sheet once a day at market close so I have a daily history of the aggregate portfolio. Each time the script copies and pastes the value to the HISTORY sheet, it needs to paste on the next available row in the same column. So far, I've written a script that successfully copies and pastes the value at a defined time interval (using Project Triggers), but it just keeps pasting over the previous value. How can I make it paste to each successive open cell and generate a list?
Also, I need all of this to work without me signing in or opening/activating the sheet. I want it to run completely autonomously, that's why I'm activating the sheet via openById instead of using the ActiveSheet code (I think that reasoning is correct, but not sure).
Here's the script I have:
function PasteValue() {
var ss = SpreadsheetApp.openById("0Ao2pCtssx6TcdGpDWFpSXy1pUXA3MlAtSjZFVHlaZVE");
ss.getRange("MASTER!E57").copyTo(ss.getRange("HIST!C5"),{contentsOnly:true});
}
What do I do to improve?? Thank you!​
Your code suggests the name of the history sheet is HIST.
I think there are a few ways to do this:
Option 1:
Uses the getLastRow() method
function PasteValue() {
var ss = SpreadsheetApp.openById("0Ao2pCtssx6TcdGpDWFpSXy1pUXA3MlAtSjZFVHlaZVE");
var nextRow = ss.getSheetByName('HIST').getLastRow()+1;
ss.getRange("MASTER!E57").copyTo(ss.getRange("HIST!C" + nextRow),{contentsOnly:true});
}
Note that option 1 will not work if you have a column of cells that contain formulas that have been copied down to the bottom of the sheet.
Option 2:
Loops through the cells in column C to find the first blank cell.
function PasteValue() {
var ss = SpreadsheetApp.openById("0Ao2pCtssx6TcdGpDWFpSXy1pUXA3MlAtSjZFVHlaZVE");
var values = ss.getRange("HIST!C:C").getValues();
for (vari=0; i<values.length; ++i) {
if (values[i][0] == "") {
var nextRow = i+1;
break;
}
}
ss.getRange("MASTER!E57").copyTo(ss.getRange("HIST!C" + nextRow),{contentsOnly:true});
}
Option 2 should work as long as you have no blank cells in column C.
Option 3:
Use the appendRow() method
Good luck!

Google script FIlter issue

I am programming a help desk system using google script, forms and spreadsheet.
To filter the queries the submissions are placed into different sheets depending on category, this is done through the FILTER function. however every time a new submission is made the filter function does not update, (it uses the CONTINUE function to cover the other cells)
instead the cell with the FILTER function must be selected and crtl+shift+E must be entered
is there a way around this?
I have tried two methods
the first was looking to have a function to enter the shortcut, but is this possible?
the second is auto entering the continue function everytime a new submission is made, I have this working however google sheets does not recognise the named range, (the continue function has the set up CONTINUE(original cell, rows away, columns away) its the original cell that it does not identify, instead I must manually select the cell and re-write the exact same cell reference.
Thank you for your help, if you need to see my code please ask :)
This is the code for the second option where I try to enter the function manually to the cells.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var numEntry = ss.getSheetByName('Home').getRange("B8").getValue() + 2;
var cat = ss.getSheetByName('Software problem').getRange(numEntry, 4, 1, 9);
cat.getCell(1, 1).setValue('=CONTINUE(D2, '+(numEntry-1)+', 1)');
Your option 1: Have a script enter keystrokes automatically? Not supported in apps-script.
Your Option 2: It shouldn't be necessary to programmatically insert CONTINUE, as the required CONTINUEs for your FILTER should be automatic, when rows in your filter range match the expressed criteria. Something else is wrong, so don't get caught up with this red herring.
You mention "google sheets does not recognise the named range" - I'd like to know what you mean by that, because I suspect this is where your solution will be. You can use named ranges within FILTER statements. You can also use open-ended ranges, like FormInput!A1:X or FormInput!E1:E.
If you're trying to manipulate named ranges using scripts, then you may have run into a known issue, "removeNamedRange() only removes named ranges that were created via Apps Script". (To get around that, manually delete the named range, then create it only from script.)
Here's a function I use to create a named range for all data on a sheet. You could adapt this to your situation. (I use this with QUERY functions instead of FILTER, you might want to consider that as an alternative.)
function setNamedRangeFromSheet(sheetName) {
// Cannot remove a named range that was added via UI - http://code.google.com/p/google-apps-script-issues/issues/detail?id=1041
var ss = SpreadsheetApp.getActiveSpreadsheet();
try { ss.removeNamedRange(sheetName) } catch (error) {};
var sheet = ss.getSheetByName(sheetName);
var range = sheet.getDataRange();
ss.setNamedRange(sheetName,range);
}
Using FILTER, you need to match the length of your sourceArray (which can be a named range) and any criteria arrays you use. To programmatically create a named range for a single-column criteria within your sourceArray, and of the same length, use getNumRows() on the sourceArray range.
Now, within your submission handling function, triggered on form submit, you'd have something like this. (I assume your trouble reports are coming into a single sheet, "FormInput" - adjust as necessary.)
...
var ss = SpreadsheetApp.getActiveSpreadsheet();
try { ss.removeNamedRange("FormInput") } catch (error) {};
var sheet = ss.getSheetByName("FormInput");
var inputRange = sheet.getDataRange();
ss.setNamedRange("FormInput",inputRange);
try { ss.removeNamedRange("Criteria") } catch (error) {};
var criteriaCol = 4; // Another guess, that Column E contains our criteria
var criteriaRange = sheet.getRange(0,criteriaCol,inputRange.getNumRows(),1);
ss.setNamedRange("Criteria",criteriaRange);
...
And with that in place, the content of A1 on your "Software problem" sheet just needs to contain the following. (Assuming that you're looking for "Bug"s.):
=FILTER(FormInput,Criteria="Bug")
I mentioned open-ended ranges earlier. If you aren't doing enough manipulation of data to justify named ranges, you could set up your filter like this, and not have to change it as new input came in:
=FILTER(FormInput!A1:X,FormInput!E1:E="Bug")