Hello from 2020! Good news: I’ve fully rebuilt the solution of timestamps in Google Sheets. My new script should do more and faster than one described in this post.
The current script works fine, but if you like to try the new one, please visit:
https://sheetswithmaxmakhrov.wordpress.com/2020/09/14/timestamps/
I was wondering how to make a timestamp in Google Sheets and also to make the script that is:
- Easy to reuse
- Easy to set
- Easy to program for different purposes.
Here’s my try.
Sample Sheet
Background
Sample sheet shows 2 timestamps on the sheet called Sales:
Notes:
- Rule #1. When the data changes in columns B:C, or columns starting from F, the new timestamp appears in column A.
- Rule #2. When the data changes in column D, the new timestamp appears in column F.
- Basic rule: do not overwrite. When the timestamp is already set, the trigger won’t overwrite it.
——–> | BTW. I’ve recently found one non-standard usage of timestamps: |
Of course, there’s a possibility that 2 or more users will enter the row in one second. But this possibility is unlikely, so the risk worth it. |
My next thoughts were: how to deal with settings. Here’re 2 options:
- All settings on a sheet. Good for a user, bad for a script. The script needs to store and then read these settings, and reading them back will cost execution time. I wanted the script to execute as fast as possible because it runs on each edit of a user, and I left this option.
- All settings in a script. Good for a script, bad for a user. A user feels uncomfortable with understanding arrays, variables, and other script stuff. I declined this option, too.
I came up with a different solution.
Installation
You may want to make a copy of my file in order to be able to edit your own copy.
I decided to store all settings in a script, but help a user to get all changeable settings. I stored settings that users can change on a separate sheet.
Notes:
- Settings on Sheet. The sheet called onEdit_Ini stores all the settings needed for the first launch
- Columns are labeled by letter (A, B, C). Ranges are named the same as ranges on a sheet, using colon “A:C”. Ranges are joined with delimiter = semicolon ;
- Infinite ranges are allowed. Range “F:” means start from column F and end at the end of a Sheet.
And then I’ve added a custom menu for a user to get the code.
Notes:
- Authorization needed! After the first launch, you need to pass the current authorization process of Google.
- Copy the code you get. The script gives you the code in a message box. Copy it!
- Open the editor. Then open Script Editor. Go to menu Tools > Script Editor…
- Paste the code. And paste it at the very beginning of a script (replace current code)
- Congratulations! You are already using automated timestamps.
The whole code is here:
Notes about the code:
- The new constant, not mentioned in a sheet is: C_TRIGGER_TYPES = [1, 1];
- C_TRIGGER_TYPES is used to edit the logic of the script, all the logic is stored in a function:
getTimeStamp_
- Please read my comments to the code to see more helpful options.
- Have fun =)
FAQ
How can I set the timestamp to be the latest whenever the data is changed?
The timestamp will update only one time by the code. When you update data, the timestamp will not update again. This is the default behavior.
If you want to update the timestamp every time, please do the following modifications:
Delete the condition ( replace only empty value ) in the function getTimeStamp_
. To do this you’ll need
- delete 2 lines of code (55, 56)
- replace the code from these lines to
return getReplaceValue_();
How to insert unique stamps
When user inserts multiple lines of date at once, the timestamp will be the same. You may create unique stamp this way. First, convert values into numbers. Sample function:
function getReplaceValue_()
{
// milliseconds between January 1, 1970 and now
return Number(new Date());
}
Second, create the counter and add +1 for each next value:
var counter_for_unique_values = 0;
function getReplaceValue_()
{
// milliseconds between January 1, 1970 and now
return Number(new Date()) + counter_for_unique_values++;
// Note: ++ adds 1 to the counter
}
Now you’ve got unique values. 1 millisecond shift is a bulletproof for ids when user inserts values up to 100 rows at once.
How to eliminate inserting stamps on empty values with filled values:
In this case you may need not to insert stamps in when the cell of inserted data is blank. Find function called runWithSet_
and replace it with this modified version:
function runWithSet_(index, range, sheet)
{
var row = range.getRow();
if (row < C_FIRST_ROWS[index]) { return -1; } // wrong row
// get Column intersections
var columnIntersactoins = getMatchRangeWithColumns_(sheet, range, C_CHANGE_COLS[index]);
if (columnIntersactoins.length === 0) { return -2; } // wrong column
// split range by rows
var height = range.getHeight();
var letter = C_STAMP_COLS[index];
var rangeStamp = sheet.getRange(letter + row + ':' + letter + (row + height - 1))
var oldValues = rangeStamp.getValues();
var newValues = [];
// modification [ 1 ] for multiple rows
if (height > 1)
{
var original_values = range.getValues().map(function(rowArray) { return rowArray.join(''); });
}
// end of modification [ 1 ]
// make a decision
var boolWriteValues = false;
var oldVal = '';
var newVal = '';
var type = C_TRIGGER_TYPES[index];
for (var i = 0; i < height; i++)
{
oldVal = oldValues[i][0];
// modification [ 2 ] for multiple rows
if (height > 1)
{
// replace old val with dummy text to prevent new stamp
if (original_values[i] === '') { oldVal = '[filled]'; }
}
// end of modification [ 2 ]
newVal = getTimeStamp_(type, oldVal);
// modification [ 3 ] for multiple rows
if (newVal == '[filled]') {
// reset vals
newVal = '';
oldVal = ''; // to prevent running when all cells are empty
}
// end of modification [ 3 ]
if (newVal !== oldVal) { boolWriteValues = true; }
newValues[i] = [];
newValues[i][0] = newVal;
}
if (boolWriteValues) { rangeStamp.setValues(newValues); }
}
In this case the stamp will not show if blank rows are inserted. Note: the code will still allow stamp when the only blank row pasted.
Pingback: How to insert Timestamp in Google Sheets - SheetsInfo
Hi, sorry, I figured out my last question about the date format. What I’m struggling with, however, is when I delete the timestamp in the modified row and or the timestamp row, it puts a timestamp back in there. I would like it to leave the cell blank if I deleted the modified cell or the timestamp cell. Any suggestions appreciated. Thanks
LikeLiked by 1 person
Thank you Jody!
This is how the current version of the script works. I’ve found some other drawbacks in my script too. So I decided to get back here later with a newer version. My first attempt was to create a general solution for easy to reuse. Now I see a lot of people are questioning me for more possibilities and I want to rewrite the code.
I’ll also create another post about the update, so you could subscribe and see the update. Thank you!
LikeLike
Hi, Thanks for writing this code. Can you tell me how I would modify the code to output the date in the format: day of week, date, time
LikeLiked by 1 person
Hi Jody!
Thank you for the comment!
I see you figured this out. In case someone else needs:
The code is from this ref: https://developers.google.com/apps-script/reference/utilities/utilities#formatdatedate,-timezone,-format
There are also handy date functions in Javascript one could use:
getFullYear() Get the year as a four digit number (yyyy)
getMonth() Get the month as a number (0-11)
getDate() Get the day as a number (1-31)
getHours() Get the hour (0-23)
getMinutes() Get the minute (0-59)
getSeconds() Get the second (0-59)
getMilliseconds() Get the millisecond (0-999)
getTime() Get the time (milliseconds since January 1, 1970)
getDay() Get the weekday as a number (0-6)
Date.now() Get the time. ECMAScript 5.
Found them here https://www.w3schools.com/js/js_date_methods.asp and use them constantly.
LikeLike
HI. I NEED HELP!!! I need a timestamp to appear, but not in rows… Here’s the sheet
https://docs.google.com/spreadsheets/d/1iHwdzVkEKiAlGgSlXDbVx4Qq1CKw7AepJ8ii7Zg6a_E/edit?usp=sharing
How do I help this teacher out? THANKS!
LikeLike
Dude, THIS IS HOW YOU DO IT!!!
awesome job! i have a challenge tho, hahaha
i need to timestamp the cell i’m typing on in a note or comment, so i would not neet a second row on other place to put it on, since that would made my sheet SOOOOOOO wide
here’s a screenshot to exemplify: https://i.gyazo.com/2cd61d81528555696efaadea8eb140c6.png
i would like the note in all the red cells, is this doable?
LikeLike
Hey. thank you for the comment!
This could help:
range.setNote('Last modified: ' + new Date());
The line 142 is:
if (boolWriteValues) { rangeStamp.setValues(newValues); }
The modified code will be like this:
Change “Sheet1” to your sheet’s name, letter to your column latter: ‘A’, ‘B’, ‘C’
Please let me know if it doesn’t work.
LikeLike
Hi Maxim, thanks for this – very handy. How can I make the timestamp appear in two places (two separate cells – one in Sheet1, the other in Sheet2 (or in Sheet1, but another column).
Thank you,
Alex
LikeLiked by 1 person
Hi Alex!
Thank you for the comment!
You may insert timestamps in several places at a time.
Modification points:
Edit line 142 of the original code.
Add more ranges according to your needs to insert timestamps.
The line 142 is:
if (boolWriteValues) { rangeStamp.setValues(newValues); }
The modified code will be like this:
Change “Sheet1” to your sheet’s name.
Please let me know if it doesn’t work.
LikeLike
Your code is going to make life much easier at work … Once I can get past line 95. Whenever I run the script, I receive “TypeError: Cannot read property “range” from undefined. (line 95, file “Code”)”
This is my first experience with code so I don’t know where to start. Thank you!
LikeLike
Hi Jessie,
onEdit(e)
is a reserved name for a function. It is run automatically when the user changes the sheet. It passes the parameter into the function with notes about the user’s action.You may also test it from the script. please see this post:
https://stackoverflow.com/questions/16089041/how-can-i-test-a-trigger-function-in-gas
LikeLike
Thanks for the code, really helpful!
I use for my sheet, but it seems no use when I update the data in C_CHANGE_COLS (already have data in it, and I change it).
How can I change the code? Thanks!
LikeLike
Hi chunwei.chang!
Thank you for the question.
If you want to add more rules, you’ll need to modify all constants:
C_SHEETS = [“Sales”, “Sales”];
C_FIRST_ROWS = [3, 3];
C_STAMP_COLS = [“A”, “E”];
C_CHANGE_COLS = [“B:C;F:”, “D”];
C_TRIGGER_TYPES = [1, 1];
Modified version:
C_SHEETS = [“Sales”, “Sales”, “Sales”];
C_FIRST_ROWS = [3, 3, 3];
C_STAMP_COLS = [“A”, “E”, “E”];
C_CHANGE_COLS = [“B:C;F”, “D”, “Z:”];
C_TRIGGER_TYPES = [1, 1, 1];
LikeLiked by 1 person
Thank you for the Answer!
I think I should clarify my question.
I mean : It seems the timestamp will update only one time by the code, isn’t it?
When I update data, the timestamp will not update again.
If I want to update the timestamp every time, how should I modify it ?
Thanks a lot again!
LikeLiked by 1 person
I’ve got the question now. I’m sorry for the delay, the answer is:
You’ll need to delete the condition ( replace only empty value ) in the function getTimeStamp_. To do this you’ll need to replace 2 lines of code (55, 56) with this:
return getReplaceValue_();
LikeLiked by 1 person
Thank you !!
It seems work.
Thank you very much!!
LikeLiked by 1 person
This is amazing, really useful! Question about a funky workaround though. Is there a way to timestamp a second edit? Where I have this placed, inserted rows also copy cells’ data validations and that’s counting as the timestamp but I want to capture when cells are actually edited, not the data validation copy. Is that possible?
Are there other trigger types other than 1?
LikeLike
I think the better question is there a way to ignore data validations and only record when actual content is typed in the cell? Thanks, this is an incredible piece of code that is super, super helpful! (if only I can get it to work for me!)
LikeLike
Iris, nice question!
My thoughts: onEdit triggers both changes from user side: values and formatting. The only thing one can do is to capture these changes with a script and take some action.
Conditional formatting may change by some different rules and those rules may depend on other sheets or calculations. onEdit will run only when the user changes the sheet, not the formula.
LikeLike
How would one go about adding a sheet to this script I was thinking something with row 271
LikeLike
I figured it out thank you
LikeLike
Hi, natlie!
The script is designed to edit settings on the sheet onEdit_ini. Column A is for sheet names.
You could also change sheets directly from the script, line 17.
If I misunderstood the question, I’m sorry.
LikeLike
@maximmakhrov
Hi again!
1) The script for the timestamp is the only script within the spreadsheet.
2) Nothing obvious other than timestamps not changing despite several edits.
Here is a link to a copy of your script (slightly modified). Note: I modified line 55,56 per your reply to replace date with later edits & line 115 to limit the range of rows affected.
https://docs.google.com/spreadsheets/d/1dw6g0ORiiTmSs3hCyf-Qn3MNYvzj4KIdlnR7NOMW87w/edit?usp=sharing
I made sure it worked in the link above before replacing the script in my other spreadsheet. The script is currently not working for me in either spreadsheet.
LikeLike
The correct code is for lines 115-116:
var ends = C_FIRST_ROWS[index].split(‘:’);
if (row > parseInt(ends[1]) || row < parseInt(ends[0])) { rerutn -1; } // wrong row
For line 18:
C_FIRST_ROWS = ['4:26', '4:26'];
LikeLike
Works like a charm. Thank you for your help and your quick response!
On Thu, Feb 28, 2019 at 9:21 AM Sheets with Max Makhrov wrote:
> maximmakhrov commented: “The correct code is for lines 115-116: var ends = > C_FIRST_ROWS[index].split(‘:’); if (row > parseInt(ends[1]) || row parseInt(ends[0])) { rerutn -1; } // wrong row For line 18: C_FIRST_ROWS = > [‘4:26’, ‘4:26’];” >
LikeLike
@maximmakhrov
Excellent write-up! This worked perfectly for me.
One question: Is there any way to limit the script to a Range as far as Rows are concerned? I have mine set to begin at Row 4, and I would like for it to end at Row 26 if possible. I tried editing line 18 as such “C_FIRST_ROWS = [4:26, 4:26];”. As I’m sure you already know it did not work!
Thanks!
LikeLike
Hi Brian,
Thank you for the suggestion!
You could change the line 115 of the code:
if (row < C_FIRST_ROWS[index]) { rerutn -1; } // wrong row
As the new input of C_FIRST_ROWS[index] would be text '4:26' you'll need to split it first:
var ends = C_FIRST_ROWS[index].split(':');
if (row > ends[1] || row < ends[0]) { rerutn -1; } // wrong ro
LikeLike
That did it! Thank you again!
LikeLike
@maximmakhrov
Hi! It appears this script is suddenly no longer working. After working perfectly for over two weeks, it has stopped without any changes being made to the script. That leads me to believe Google has changed something. Any ideas?
Thanks!
LikeLike
Brian, if the script stopped working:
1) The other code in the script could affect it, like duplicate onEdit function.
2) If the code was edited, some error may appear.
Please check the original sample file:
https://docs.google.com/spreadsheets/d/1SmLCkl_CE8w0xUJRmZ2fhoE7Vkgw6MgEbc8EQTGeemw/copy
It works in my environment.
LikeLike
Hi Nathan, thanks for the code.
i have 2 questions,
1. how can i set the timestamp to be the latest whenever the data is changed?
2. how can I set only the first column to register the timestamp whenever other data in the row is changed.
hope you can help ! thanks
Ang
LikeLike
wk ang,
1. You’ll need to delete the condition ( replace only empty value ) in the function getTimeStamp_. To do this you’ll need to replace 2 lines of code (55, 56) with this:
return getReplaceValue_();
2. In the sheet onEdit_ini set Column with a timestamp to A and Columns Change… to B:
LikeLike
@maximmakhrov Looking to add a User stamp I’ve done a little homework and it looks like
Logger.log(Session.getActiveUser().getEmail());
Not sure how to implement.
LikeLike
Hi Nathan,
Nice question!
I’ve met the same problem.
Here’s the question, related to the problem, which might help you:
https://stackoverflow.com/questions/27822903/how-can-i-get-the-editors-email-onedit-or-ask-for-authorization-on-first-edi
Short answer: you need to have G Suite account to know editor’s emails if they are a part of the team. You’ll also need to use an installable trigger.
LikeLike
@Felicia QM
I had the same error but i corrected with:
return Utilities.formatDate(new Date(), “EST”, “MM-dd-yyyy HH:mm:ss”);
PS. I changed around the format of the time-stamp Month-Day-Year
LikeLiked by 1 person
Thank you so much for this! Even though I don’t know how to write code, your script was documented so well that I managed to tweak it a bit to fit my needs even better.
LikeLiked by 1 person
I have a question- I copied your sheet and I’m psyched except that the time stamp is 7 hours ahead. I assume it has something to do with the code and the time zone. I am in EST- what portion of the code should I adjust?
LikeLike
Hi Kristi,
You may change the code from line 72, inside of function getReplaceValue_.
Change it to:
Thanks for the comment! Please ask again if it not works.
LikeLike
First of all, thank you! Creating the spreadsheet with the easy edits was SO HELPFUL! I’m having the same issue as Kristi and when I try to use your code above, it says there is an illegal character in the field – any ideas? It’s likely something very obvious to fix, but I am a total novice! Thanks again for this helpful post!
LikeLike
Hi Felicia,
Thank you for the comment!
I’d love to help you but I can’t remember this type of error. Could you share a sample file with the code and the error.
LikeLike
Thanks for the code, really helpful! I was hoping to get it to work by inserting more sheet titles in C_Sheets or is there another way to do so?
LikeLike
Hi Nick!
Thank you for the comment!
I see you need multiple sheet names. Settings contain a column “Sheet Name”.
Have you tied to insert multiple sheet names into this column and test the script?
LikeLike
Hi Maxim,
Thank you for your quick response. I have tried adding multiple sheet names in many different spots with no results yet. I assume you mean a column under SETS right? I can’t find a column “Sheet Name” there..
LikeLike
The sheet called “onEdit_Ini” stores all the settings needed for the first launch.
Insert multiple sheet names into column A called “Sheet Names”.
The sample file is here:
https://docs.google.com/spreadsheets/d/1SmLCkl_CE8w0xUJRmZ2fhoE7Vkgw6MgEbc8EQTGeemw/copy
LikeLike