Static timestamping in Google Sheets - google-apps-script

I am trying to add STATIC timestamp to my data whenever it is imported or pasted in the sheets.
I am using this formula now
(=ARRAYFORMULA( IFS(I1:I="","",L1:L="",NOW(),TRUE,L1:L)))
but, whenever I open the sheet again the time gets changed automatically to the current time as i am using the now() function. I tried on-Edit in the script, but it's only working when the data is manually entered.
Is there any other way I can use to static timestamp when data is being pasted or imported?

Instead of NOW() on the formula, do it via script using new Date().
The NOW() function updates the timestamp every time the spreadsheet is open or something changes in it, while the new Date() gives you a full (date and time) and static timestamp.
Also, as I've seen on the comments of your question, there really is no way to use onEdit() through automated scripts and macros.

Answer
You can use a custom function to return the actual date with the method new Date() and the Properties Service. Open Apps Script and paste the following function:
Code
function getTimestamp(reset) {
// update the timestamp
if (reset == 1) {
setTime()
}
// try-catch structure in order to set the time in the first execution
try {
var time = ScriptProperties.getProperty('time')
}
catch (err) {
setTime()
var time = ScriptProperties.getProperty('time')
}
return time
}
function setTime() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var time = new Date()
ScriptProperties.setProperty('time', time)
}
How it works
Now, you can use it in any cell like another Sheet function. Call the function with =getTimestamp(0). On the first execution, it tries to get the saved property time, but as the property does not exist it generates a timestamp and saves a new property in the project with the key time and the value of the timestamp.
In the following executions, the value obtained by the function when it is recalculated is the same, since the property is not overwritten unless the function is called with a 1 input: =getTimestamp(1). In this case, the timestamp is updated, but if it is not set back to =getTimestamp(0), every time the function is recalculated (which happens automatically every so often) the timestamp will change.
In conclusion, always use =getTimestamp(0). When you want to update the value, change it to =getTimestamp(1) and go back to the original formula.
update
I have updated the answer to explain how to update the timestamp when new values are added:
Use a cell as input to the function, e.g. =getTimeStamp(A1) 2.
Create an onEdit trigger
Check that the range of the e event belongs to new values.
Update the value of A1 to 1 and then to 0 if you have detected new values.
example:
function onEdit(e){
var range = e.range
var cell = SpreadsheetApp.getActiveSpreadsheet().getRange('A4')
if (range.columnStart > 1 && range.rowStart > 10){
cell.setValue(1)
SpreadsheetApp.flush()
cell.setValue(0)
}
}
If new values are added from column 1 and row 10, A1 is updated to 1 and then to 0, thus updating the value of the timeStamp function and saving it permanently until the trigger is executed again.
References:
Custom Functions in Google Sheets
Working with Dates and Times
Apps Script: Extending Google Sheets
Properties Service

Not sure have your question got a solution. I had the same struggle as yours over the year, especially with pasted data, and I found a solution that works for my case nicely (but not by formula, need to run in Apps Script).
Some background for my case:
I have multiple sheets in the spreadsheet to run and generate the
timestamp
I want to skip my first sheet without running to generate timestamp
in it
I want every edit, even if each value that I paste from Excel to
generate timestamp
I want the timestamp to be individual, each row have their own
timestamp precise to every second
I don't want a total refresh of the entire sheet timestamp when I am
editing any other row
I have a column that is a MUST FILL value to justify whether the
timestamp needs to be generated for that particular row
I want to specify my timestamp on a dedicated column only
function timestamp() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const totalSheet = ss.getSheets();
for (let a=1; a<totalSheet.length; a++) {
let sheet = ss.getSheets()[a];
let range = sheet.getDataRange();
let values = range.getValues();
function autoCount() {
let rowCount;
for (let i = 0; i < values.length; i++) {
rowCount = i
if (values[i][0] === '') {
break;
}
}
return rowCount
}
rowNum = autoCount()
for(let j=1; j<rowNum+1; j++){
if (sheet.getRange(j+1,7).getValue() === '') {
sheet.getRange(j+1,7).setValue(new Date()).setNumberFormat("yyyy-MM-dd hh:mm:ss");
}
}
}
}
Explanation
First, I made a const totalSheet with getSheets() and run it
with a for loop. That is to identify the total number of sheets
inside that spreadsheet. Take note, in here, I made let a=1;
supposed all JavaScript the same, starts with 0, value 1 is to
skip the first sheet and run on the second sheet onwards
then, you will notice a function let sheet = ss.getSheets()[a]
inside the loop. Take note, it is not supposed to use const if
your value inside the variable is constantly changing, so use
let instead will work fine.
then, you will see a function autoCount(). That is to make a for
loop to count the number of rows that have values edited in it. The
if (values[i][0] === '') is to navigate the script to search
through the entire sheet that has value, looking at the row i and
the column 0. Here, the 0 is indicating the first column of the
sheet, and the i is the row of the sheet. Yes, it works like a
json object with panda feeling.
then, you found the number of rows that are edited by running the
autoCount(). Give it a rowNum variable to contain the result.
then, pass that rowNum into a new for loop, and use if (sheeet.getRange(j+1,7).getValue() === '') to determine which row
has not been edited with timestamp. Take note, where the 7 here
indicating the 7th column of the sheet is the place that I want a
timestamp.
inside the for loop, is to setValue with date in a specified
format of ("yyyy-MM-dd hh:mm:ss"). You are free to edit into any
style you like
ohya, do remember to deploy to activate the trigger with event type
as On Change. That is not limiting to edit, but for all kinds of
changes including paste.
Here's a screenshot on how it would look like:
Lastly, please take note on some of my backgrounds before deciding to or not to have the solution to work for your case. Cheers, and happy coding~!

You cannot get a permanent timestamp with a spreadsheet formula, even with a named function or an Apps Script custom function, because formula results refreshed from time to time. When the formula gets recalculated, the original timestamp is lost.
The easiest way to insert the current date in a cell is to press Control + ; or ⌘;. See the keyboard shortcuts help page.
You can also use an onEdit(e) script to create permanent timestamps. Search this forum for [google-apps-script] timestamp to find many examples.

Related

How to tell a script which row to start output

I am using a script to output the date and time that a row was last updated on a Google Sheet. It seems to work just fine, but I want it to only output beginning at the second row, since my first row is a header row full of labels for the columns. I can't seem to figure it out.
The script is from here: https://www.wikihow.com/Google-Sheets-How-to-Insert-Time-in-Cell-Automatically#Script-Editor
This is how it looks in my Apps Script:
/** #OnlyCurrentDoc */
function onEdit(e){
const sh = e.source.getActiveSheet();
sh.getRange ('A' + e.range.rowStart)
.setValue (new Date())
.setNumberFormat ('MM/dd/yyyy HH:MMam/pm');
}
I think I need to define rowStart, but I'm not sure how to do that and search engines haven't pulled up answers I understand.
I tried appending rowStart(2) and rowStart > 0 in place of the original rowStart, which produced errors and made the whole script stop working. I also tried the below with the same response.
rowStart(
value : 2
) : 2;
From here: https://help.grapecity.com/spread/SpreadJSWeb/JavascriptLibrary~GcSpread.Sheets.PrintInfo~rowStart.html
I am new to Google Apps Script (and any scripting), though a longtime Excel and Sheets user. So please explain it to me like I am five. 😅 Branching out!
OK, so this script, as it says on the original wikihow page, will:
insert a timestamp into the specified column any time you enter data into a cell, in the same row as the data you entered. For instance, if you type something into cell A2, a timestamp will appear in cell M2.
So essentially the way that script works, at least per my reading:
It is defining an onEdit handler which handles an edit event every time a user makes any change to any cell in the spreadsheet
the edit event (called e in the script) has a bunch of information about the edit on it, including the row number of the first row that was edited (e.range.rowStart)
the script contains a hard-coded column letter (in your case, A, in the example on wikihow they used M)
every time any edit is made , it goes to the cell identified by the hardcoded column letter and the 1st row that was edited, and it inserts the current date into that cell.
First of all, since this is JavaScript, you can paste your code into a text editor that supports JavaScript, like VSCode for example, and ask it to format it for you. This might bring some clarity to the code because the text editor knows what the syntax means, so it can give you some hints about whats going on or at very least it can make it look nicer:
/** #OnlyCurrentDoc */
function onEdit(e) {
const sh = e.source.getActiveSheet();
sh.getRange('A' + e.range.rowStart)
.setValue(new Date())
.setNumberFormat('MM/dd/yyyy HH:MMam/pm');
}
Second of all, single letter variable names and abbreviations are often frowned upon because they can prevent us from knowing what's going on. And sometimes code can be clarified by giving names to things that might otherwise not be obvious at all. So I will do some naming in this code:
/** #OnlyCurrentDoc */
function onEdit(editEvent) {
const sheet = editEvent.source.getActiveSheet();
const rowThatWasEdited = editEvent.range.rowStart;
const columnThatHoldsTheLastEditedDates = 'A';
const correspondingCell = columnThatHoldsTheLastEditedDates + rowThatWasEdited;
const rightNowDate = new Date();
sheet.getRange(correspondingCell)
.setValue(rightNowDate)
.setNumberFormat('MM/dd/yyyy HH:MMam/pm');
}
Finally, since this is javascript, we have access to all of the javascript features like if statements, loops, functions, etc. Since you want to make it ignore the header row, I would recommend an if statement: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/if...else
if we are on the header row, do nothing. Otherwise, do the normal thing.
/** #OnlyCurrentDoc */
function onEdit(editEvent) {
const sheet = editEvent.source.getActiveSheet();
const rowThatWasEdited = editEvent.range.rowStart;
if(rowThatWasEdited > 1) {
const columnThatHoldsTheLastEditedDates = 'A';
const correspondingCell = columnThatHoldsTheLastEditedDates + rowThatWasEdited;
const rightNowDate = new Date();
sheet.getRange(correspondingCell)
.setValue(rightNowDate)
.setNumberFormat('MM/dd/yyyy HH:MMam/pm');
}
}

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'.

How to set timer for a row in google sheets

I would like to set a timer for an entire row in google sheets where a user can start entering data in the second row only after a certain time after starting row one.
Example: If a user starts filling cells in row 1 then they should be able to fill the data in the second only after the timer ends.
Could anyone suggest me how to get started or suggest me a chrome extension for this use?
You could also suggest me on how to build the chrome extension I can try it along with my colleagues.
This function uses an onEdit trigger to impose a 20 second delay between editing rows. It may not be exactly what you want but perhaps it's a start. It uses PropertiesService to keep state. I think user properties would be a better choice but script properties are easier to develop with since you can modify them directly in the script editor.
function onEdit(e) {
const sh=e.range.getSheet();
const delay=20000;
let ms=Number(new Date().valueOf()).toFixed();
if(sh.getName()=='Sheet10') {
const ps=PropertiesService.getScriptProperties();
let dObj=ps.getProperties();
if(dObj.hasOwnProperty('row') && dObj.hasOwnProperty('delay')) {
if(dObj.row!=e.range.rowStart && Number(ms-dObj.delay)<delay) {
e.range.setValue(e.oldValue);
e.source.toast('Sorry you have ' + (delay-Number(ms-dObj.delay))/1000 + ' seconds left.');
}else{
ps.setProperties({'row':e.range.rowStart,'delay':ms});
}
}else{
ps.setProperties({'row':e.range.rowStart,'delay':ms});
}
}
}
Issue with Protections:
Class Protection is commonly used to protect ranges from being edited. It is not appropriate for your situation, though, because, as specified here, users who are executing the script cannot remove themselves from the list of editors:
Neither the owner of the spreadsheet nor the current user can be removed.
Using oldValue:
Because of this, the best way to go would be to use the parameter oldValue from the onEdit event object.
An onEdit trigger runs every time a user edits a cell. In it, you can use:
PropertiesService to store useful information: (1) whether it is the first time row 1 is edited (isNotFirstTime), and (2) when was last time first row was edited (startTime).
Event object to get information on the edited cell (its row, its old value, etc.).
You can do something along the following lines (check comments):
function onEdit(e) {
var current = new Date(); // Current date
var range = e.range;
var editedRow = range.getRow();
var sheet = range.getSheet();
var props = PropertiesService.getScriptProperties();
var waitingTime = 20 * 1000; // 20 seconds
var isNotFirstTime = props.getProperty("isNotFirstTime"); // Check if first row was previously edited
var startTime = new Date(props.getProperty("startTime")); // Time when first row was first edited
if (editedRow === 1 && !isNotFirstTime) { // Check that (1) edited row is first one, (2) it was not edited before
props.setProperty("startTime", current.toString()); // If it's first time first row was edited, store current time
Utilities.sleep(waitingTime); // Wait for 20 seconds
props.setProperty("isNotFirstTime", true); // Store: first row was previously edited
}
// Check that (1) second row edited, (2) Less than 20 seconds passed since first time first row was edited:
if (editedRow === 2 && (current - startTime) < waitingTime) {
range.setValue(e.oldValue || ""); // Set previous value to edited cell (this avoids editing cells)
}
}
Reference:
onEdit(e)
onEdit Event object
Class PropertiesService

archiving / copying all the values in column A to column C based on date

archiving / copying all the values in column A to column C based on date. Also, if the date is change, a new set of values will be copied and the previous value won't be deleted.I was working on a daily schedule of every employee at the same time it will be recorded as their attendance based on the date. Can someone help me?
Example 1
Example 2 : if I change the date, the previous record won't get deleted and will copy a new set of values based on date
If I press the (save button) I want all the the values from column B2:B4 will be copied to column F2:F4 based on the date on column C1
The 2nd screenshot shows if I change the date and press again the (save button) the previous value won't get deleted and a new set of values will be copied based on the actual date on column C1
In order to solve your issue I suggest you to use Apps Script and write a script and use this function:
function dateFunction() {
var sheet = SpreadsheetApp.getActiveSheet();
var currDate = sheet.getRange("C1").getValue();
var originRange = sheet.getRange("B2:B4");
var headers = sheet.getDataRange().getValues()[0];
var dateFound = false;
for (var i=5; i<headers.length && !dateFound; i++) {
if (currDate.toString() == headers[i].toString()) {
originRange.copyTo(sheet.getRange(2, i + 1, 3));
dateFound = true;
}
}
if (!dateFound)
SpreadsheetApp.getUi().alert("Date not found!");
}
The script works by gathering all the dates from your headers and will try to see if there's a match with the date from the C1 cell. If a match has been found, the values will be copied correspondingly, otherwise, an alert prompt will show up specifying that the date has not been found.
Now, you will need to link this function to a button so it will only be executed when you click it.
You can easily do that by inserting a drawing and shape/draw it so it will resemble a button of your liking. You will have to Save and Close it.
Then, when you right click on it the option of Assign a script will appear and you have to put the name of the function from above in there, as shown below:
So now, every time you press the Save button, the script will be executed.
Furthermore, I suggest you read the following links since they might help you:
Apps Script Spreadsheet Service;
Sheet Class .getDataRange();
Range Class .copyTo().

Google Apps Script - Spreadsheet run script onEdit?

I have a script that I use in my spreadsheet. It is at cell B2 and it takes an argument like so =myfunction(A2:A12). Internally the function gets info from a large range of cells.
Nothing seems to work. I tried adding it to
Scripts > Resources > Current Project Triggers > On edit
Scripts > Resources > Current Project Triggers > On open
How can I have this function update the result with every document edit?
When you are making calls to Google Apps services inside your custom function (like getRange and getValues etc), unfortunately there is no way of updating such custom functions with each edit, other than passing all of the cells that you are "watching" for editing.
And, perhaps even more frustratingly, the workaround of passing say a single cell that references all of your "watched" cells with a formula doesn't trigger an update - it seems that one needs to reference the "watched" cells directly.
You could pass GoogleClock() as an argument which will at least update the function output every minute.
But the advice from many members on this forum (who have much more knowledge about this stuff than me) would simply be: "don't use custom functions".
I am not sure if this exact code will work but you can try something like this...
function onEdit(event){
var activeSheet = event.source.getActiveSheet();
if(activeSheet.getName() == "ActiveSheetName") {
var targetSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("TargetSheetName");
targetSheet.getRange(2,2).setValue("=myfunction(A2:A12)");
}
}
Assuming that B2 cell in on the sheet "TargetSheetName" and assuming that the edited cell is on the sheet "ActiveSheetName", the function onEdit will trigger when you edited any cell in any sheet. Since there is an if statement to check if that edited cell is on the sheet "ActiveSheetName" it will trigger only if the edited cell is on that sheet and it will set the B" cell to the value =myfunction(A2:A12), forcing it to update (i guess).
hope that i am correct and that i was helpful
I had a similar issue, for me I wanted to "watch" one particular cell to trigger my function.
I did the following (pretending A1 is the cell i am watching)
IF(LEN(A1) < 1, " ", customFunction() )
This successfully triggered if I ever edited that cell. However:
"Custom functions return values, but they cannot set values outside
the cells they are in. In most circumstances, a custom function in
cell A1 cannot modify cell A5. However, if a custom function returns a
double array, the results overflow the cell containing the function
and fill the cells below and to the right of the cell containing the
custom function. You can test this with a custom function containing
return [[1,2],[3,4]];."
from: https://developers.google.com/apps-script/execution_custom_functions
which makes it almost useless, but it might work for your case?
If the custom function is assigned to a project trigger it has more power so personally I ended adding it to "Scripts > Resources > Current Project Triggers > On edit"
and basically "watched a column" so it only did things if the current cell was within the "edit range". This is a bit of a bodge and requires some hidden cells, but it works for me at the moment
var rowIndex = thisspreadsheet.getActiveCell().getRowIndex();
var colIndex = thisspreadsheet.getActiveCell().getColumn();
//clamp to custom range
if(rowIndex < 3 && colIndex != 5)
{
return 0;
}
//check against copy
var name = SpreadsheetApp.getActiveSheet().getRange(rowIndex, 5).getValue();
var copy = SpreadsheetApp.getActiveSheet().getRange(rowIndex, 6).getValue();
if(name != copy )
{
///value has been changed, do stuff
SpreadsheetApp.getActiveSheet().getRange(rowIndex, 6).setValue(name);
}