All I am trying to do is this.
Let's say in Google Sheets, I have a page which has the following columns:
In column I3 I have the date of 11/30/19.
In column J3, I have Today's date of 4/30/20.
I want to be able to calculate the difference between these two dates and tell me how many months has it been since 11/30/19.
Currently, I think the code is working somewhat but the result I get is:
[20-05-02 01:43:18:650 MDT] 5 months, 6 days
[20-05-02 01:43:18:656 MDT] 5 months, 6 days
[20-05-02 01:43:18:660 MDT] 5 months, 6 days
But the date calculations are still wrong. For example from Jan 1st - Jan 25, 2020, it shows 5 month and 6 days.
Also the loop I have inside Filter 1, is just calculating the first available date and then it does this three times instead of going to the next record and calculate.
I have the following code so far:
function myFunction() {
}
var moment = Moment.load();
/**
* #summary gets date difference
* #param {Date} startDate
* #param {Date} endDate
* #returns {string}
*/
function getDuration(startDate, endDate) {
const start = moment(startDate);
const end = moment(endDate);
const units = ['years', 'months', 'days'];
const lastIndex = units.length - 1;
const parts = units
.map((unit,i) => {
const diff = Math.floor(end.diff(start, unit, true));
if (diff > 0 || i === lastIndex) {
end.subtract(unit, diff);
return `${diff} ${unit}`;
}
})
.filter(Boolean);
return parts.join(', ');
}
function Filter2() { // Calculate the Time
const spread = SpreadsheetApp.getActiveSpreadsheet();
const sheets = spread.getSheets();
const [ sheet_1, sheet_2 ] = sheets;
const row = sheet_1.getRange("A:M");
const arr_col = sheet_1.getRange("I3:I50");
const lastSeen_col = sheet_1.getRange("J3:J50");
const startDate = arr_col.getValue();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var headerRowNumber = 2;
var rows = sheet.getDataRange().offset(headerRowNumber, 0, sheet.getLastRow() - headerRowNumber).getValues();
var filter = rows.filter(row => row[9] !== '');
// var digest = filter[0][9];
for(var i=0;i<filter.length; i++){
var res = getDuration(startDate, lastSeen_col);
Logger.log(res);
}
}
Why Dec, 31 1969
31st of December, 1969 in GMT-7 timezone offset is January 1st, 1970 00:00:00 UTC, which is the unix epoch. Therefore it is likely to be caused by an invalid date format passed to the Moment class instance.
Problem
getRange() method call returns an instance of Range from which you have to extract value via getValue() / getValues() before being able to use it. With that in mind, let's track what's going on in your script:
var ArrRow = sheet_1.getRange("I3:I") returns an instance of Range
mydata1 is defined somewhere globally (let's assume it holds an instance of Date)
getDuration is thus called like this: getDuration( <Range>, <Date> )
startDate and endDate are respectively an instance of Range and Date
start and end hold results of invoking the moment factory that creates a wrapper around Date object.
Step 5 is most likely to be the culprit, as per Moment library docs, moment() can accept String, Number, Date instances as well as format options, but start recieves a Range instance instead.
Possible solution
First, you need to ensure you pass in correct types (this why a lot of folks prefer TypeScript - if that's not your cup of tea, at least start using JSDoc - it will save you a ton of debug time).
I don't know how big the I3:I range is, so I assumed from the context that it is a single cell containing start date. Also note that I removed the var sld = new Date(dateString) assignment since you return a human readable string from getDuration() of format Y years, M months, D days which is not a dateString that Date constructor can accept.
Additionally, I would suggest changing forEach() to map() method for cleaner and less side effect prone code. General rule of thumb is that if input and output are of the same type, you likely want to use a mapping function.
function myFunction() {
Filter2();
}
function Filter2() {
const spread = SpreadsheetApp.getActiveSpreadsheet();
const sheets = spread.getSheets();
const [ sheet_1, sheet_2 ] = sheets;
const arr_col = sheet_1.getRange("I3:I");
const startDate = arr_col.getValue();
var dateString = getDuration(startDate, mydata1);
Logger.log(dateString);
}
var moment = Moment.load();
/**
* #summary gets date difference
* #param {Date} startDate
* #param {Date} endDate
* #returns {string}
*/
function getDuration(startDate, endDate) {
const start = moment(startDate);
const end = moment(endDate);
const units = ['years', 'months', 'days'];
const lastIndex = units.length - 1;
const parts = units
.map((unit,i) => {
const diff = Math.floor(end.diff(start, unit, true));
if (diff > 5 || i === lastIndex) {
end.subtract(unit, diff);
return `${diff} ${unit}`;
}
})
.filter(Boolean);
return parts.join(', ');
}
Notes
The answer assumes you use V8 engine (if you don't - switch to it, old one will be deprecated some time in the future, and as a bonus you get to use all the juciest language features [well, most of them]).
References
Date constructor docs on MDN
Range class docs
getValue() method docs
getValues() method docs
Related
I'm trying to create an all-day calendar event to 14 days from today using Google App Script but I keep getting the error
Exception: The parameters (String,String) don't match the method signature for CalendarApp.Calendar.createAllDayEvent.
My script is:
var SheetApp = SpreadsheetApp.getActiveSheet();
var SelectedRow = SheetApp.getActiveRange().getRowIndex();
var value = SheetApp.getRange(SelectedRow, 8).getValue(); //Name of the event
var eventCal = CalendarApp.getCalendarById("sampleemail#gmail.com");
eventCal.createAllDayEvent(value.toString(), new Date() + 14);
When you add a number to a Date object, the Date is automatically converted to its underlying value, i.e., the number of milliseconds since the epoch. The result of the addition will be that number of milliseconds plus 14, which is just a big integer. The createAllDayEvent() method expects a Date object rather than an integer.
To get a date that is 14 days from now, use Date.setDate(). This method has the benefit that it automatically takes care of rolling into the next month or next year as necessary, but be careful. The setDate() method modifies a Date object in place, but does not return the Date object. Instead, it returns the underlying value, i.e., the number of milliseconds since the epoch.
This means that you cannot just assign the result you get from setDate() to a variable in the hope of getting a valid Date. Something like const twoWeeksFromNow = new Date().setDate(new Date().getDate() + 14) will not work if you expect twoWeeksFromNow to contain a Date.
An easy way to get it right is to use a short utility function, like this:
function testCreateAllDayEvent() {
const twoWeeksFromNow = dateOffset_(new Date(), +14);
const eventCal = CalendarApp.getCalendarById("sample_email#gmail.com");
const sheet = SpreadsheetApp.getActiveSheet();
const eventTitle = sheet
.getRange('H' + sheet.getActiveRange().getRow())
.getDisplayValue();
eventCal.createAllDayEvent(eventTitle, twoWeeksFromNow);
}
/**
* Gets a new Date object that is offset by numDays from date.
*
* #param {Date} date The date from which to count.
* #param {Number} numDays The number of days to add or subtract from date.
* #return {Date} A new Date object that is offset by numDays from date.
*/
function dateOffset_(date, numDays) {
const newDate = new Date(date);
newDate.setDate(date.getDate() + numDays);
return newDate;
}
Change second parameter
new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate() + 14)
I am using moment.js library for time.
I want to check if the time I am getting from the backend is in between 8AM to Noon (12PM). I want to store all the objects whose time is in between 8AM to 12PM.
I am getting date in this format - "2022-04-04T21:43:59Z". I want to use timezone"America/Detroit".
Here is what I have tried but this didn't work;
//this code is inside forEach loop
moment.tz.setDefault($scope.userData.account.timeZone);
var format = 'hh:mm:ss'
var time = moment(response.date,format),
beforeTime = moment('08:00:00', format),
afterTime = moment('11:59:59', format);
if (time.isBetween(beforeTime, afterTime)) {
console.log('is between')
} else {
console.log('is not between')
}
In the output I am getting is not between for all the data but in real there is some data which is having date and time falling under 8am - 12pm.
Is there anything wrong because of timezone?
The reason why your compare isn't working it's because it's not only using time but also the date.
You should first extrapolate the time from the input datetime and use that data to make the comparison like this:
let datetime = moment('2022-04-04T10:00:00Z', 'YYYY-MM-DDTHH:mm:ssZ');
moment({
hour:datetime.hour(),
minute:datetime.minute(),
second:datetime.second()
}).isBetween(beforeTime, afterTime);
//returns bool true or false
That's because all those 3 datetimes will lay in the same solar day and only time will be relevant to the comparison.
Plus you incorrectly dealt with formats when parsing both your input datetimes and times used for before and after.
This is a working solution showing the concept:
//those are the formats your input uses for datetimes and times
const datetime_format = 'YYYY-MM-DDTHH:mm:ssZ';
const time_format = 'HH:mm:ss';
//this is your input crafted as objects having the prop date
var response_timeYESInBetween = {date : "2022-04-04T10:00:00Z"};
var response_timeNOTInBetween = {date : "2022-04-04T21:43:59Z"};
//moment.tz.setDefault($scope.userData.account.timeZone);
//this is where you parse those timestamp strings as moment datetime
var datetime_YESInBetween = moment(response_timeYESInBetween.date, datetime_format);
var datetime_NOTInBetween = moment(response_timeNOTInBetween.date, datetime_format);
//this is where those moment datetime get used to create new datetimes holding those same time but laying on today instead of their original dates
var timeonly_YESinBetween = moment({hour:datetime_YESInBetween.hour(), minute:datetime_YESInBetween.minute(), second:datetime_YESInBetween.second()});
var timeonly_NOTinBetween = moment({hour:datetime_NOTInBetween.hour(), minute:datetime_NOTInBetween.minute(), second:datetime_NOTInBetween.second()});
//this is where we create datetimes (ignoring to pass the date, sets them at today)
var beforeTime = moment('08:00:00', time_format);
var afterTime = moment('11:59:59', time_format);
//we make the comparison to know which times are between beforeTime and afterTime
//note: now all those datetimes are all in the same day and only time will affect the comparison result
var firstComparison = timeonly_YESinBetween.isBetween(beforeTime, afterTime);
var secondComparison = timeonly_NOTinBetween.isBetween(beforeTime, afterTime)
console.log( firstComparison );
//outputs: true
console.log( secondComparison );
//outputs: false
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.2/moment.min.js"></script>
And if we wanted to better factor the parts:
console.log( isBetween('2022-04-04T10:00:00Z', '08:00:00', '11:59:59') );
//true
console.log( isBetween('2022-04-04T21:43:59Z', '08:00:00', '11:59:59') );
//false
function isBetween(datetime, before, after){
const datetime_format = 'YYYY-MM-DDTHH:mm:ssZ';
const time_format = 'HH:mm:ss';
let originalDatetime = moment(datetime, datetime_format);
let transformed = moment({hour:originalDatetime.hour(), minute:originalDatetime.minute(), second:originalDatetime.second()});
var beforeTime = moment(before, time_format);
var afterTime = moment(after, time_format);
return transformed.isBetween(beforeTime, afterTime);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.2/moment.min.js"></script>
I'm currently working with a project where I need to find if a time in HH:mm is before another time.
I am using moment time zone and setting the time zone globally to UTC.
The date which I am comparing is: 2020-09-02T00:00:00.0000+00:00
I am running a check where I am doing the following:
const example = '2020-09-02T00:00:00.0000+00:00'
const time = moment(example)
const timeStart = moment('08:00', 'HH:mm')
console.log(time.isBefore(timeStart))
The console log returns a result of false.
The reason I believe is that the timeStart is evaluating to the current day, so it's failing due to the date being in the future. I need to stop moment from comparing the date, is there a way to do this?
What I am trying to achieve is something of the following
'00:00:00'.isBefore('08:00')
const time = moment('2020-09-02T00:00:00.0000+00:00')
const timeStart = moment('08:00', 'HH:mm')
console.log(time)
console.log(timeStart)
console.log(time.isBefore(timeStart))
<script src="https://cdn.jsdelivr.net/momentjs/2.13.0/moment.min.js"></script>
You can do this with moment.js by converting the timestamp to a moment object, then cloning the object and setting its time to the required comparison time. That way you're always comparing times on the same date.
To keep everything as UTC, use utc. E.g.
let ts = '2020-09-02T00:00:00.0000+00:00';
let d = moment.utc(ts); // Invoke UTC mode
let time = '08:30';
let [h, m] = time.split(':');
let e = d.clone().hours(h).minutes(m).seconds(0).milliseconds(0);
console.log(d.format() + ' is before\n' +
e.format() + '? ' + d.isBefore(e));
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/moment.min.js"></script>
You can compare just the time with plain JS fairly easily:
/**
* Compare a time to just the time part of a
* Date, all using UTC.
*
* #param {Date} date - date to compare
* #param {string} time - time in HH:mm format
* #returns {boolean} true if time in date is before time passed to function
*/
function isTimeBefore(date = new Date(), time) {
// Get time value for start of UTC day
// Copy date so don't affect original
let dayStart = new Date(+date).setUTCHours(0,0,0,0);
// Convert the time to milliseconds since midnight
let [h, m] = time.split(':');
let ms = h*3.6e6 + m*6e4;
// Compare to the date's milliseconds elapsed since midnight
return ms > (date - dayStart);
}
// UTC timestamp
let ts = '2020-09-02T00:00:00.0000+00:00';
// Convert to Date
let d = new Date(ts);
// local time
let time = '08:30';
console.log('UTC: ' + d.toISOString() + '\nis before ' + time + ' UTC? : ' + isTimeBefore(d, time));
The built–in parser should generally be avoided, however the above uses it to convert the timestamp to a Date because it's about the only supported format that is reliably parsed by browsers in use.
const time = moment('2020-09-02T00:00:00.0000+00:00')
const timeStart = moment('09:00', 'HH:mm')
time.isBefore(timeStart) //returns true
returns true to me.
I am receiving a JSON object from a the DarkShy weather api, and I want to access the timestamp for each report for a Chart.JS chart where I will display the temperature over day, right now I am stuck in converting the timestamp into a HH:DD:SS format.
This is what I tried
// Displays the wrong time according to https://www.epochconverter.com/
var timeofDay = new Date(daily[i].time)
time.push( timeofDay.toTimeString().split(' ')[0] )
// Gets rid off the time, tho It get the date correctly
var timeofDay = new Date(parseFloat(daily[i].time) * 1000)
time.push( timeofDay )
// Returns the wrong date and time
time.push(new Date(daily[i]))
Here's how I loop through the JSON file
let time = []
let temperatureDaily = []
for(var i=0; i<daily.length; i++){
// Push the values into the arrays
var timeofDay = new Date(parseFloat(daily[i].time) * 1000)
time.push( timeofDay )
temperatureDaily.push( (parseFloat(daily[i].temperatureHigh) + parseFloat(daily[i].temperatureLow)) /2)
}
console.log(time);
If you're only interested in the time, and it seems you want UTC, use UTC methods to format the time. Or you can use toISOString and trim the bits you don't want, e.g.
let timeValue = 1569304800;
let d = new Date(timeValue * 1000);
// Use toISOString
let hms = d.toISOString().substr(11,8);
console.log(hms);
// Manual format
function toHMS(date){
let z = n => ('0'+n).slice(-2);
return `${z(d.getUTCHours())}:${z(d.getUTCMinutes())}:${z(d.getUTCSeconds())}`
}
console.log(toHMS(d));
Try moment.js.
It gives a lot of date utilities and formatting becomes super easy.
I am using a moment library and I would like to use it for getting an array of string months, with the starting date and the end date. So, for example when I have two dates:
const from = moment('2018-05-01', 'YYYY-MM-DD').format('MMMM');
const to = moment('2018-07-01', 'YYYY-MM-DD').format('MMMM');
How can I then get the range of months as a string array, that would then look like this for the given dates:
['May', 'June', 'July']
Is there a way we can achieve this with moment, and what would be the most elegant way to do this?
Kindly, find the below format for moment.
var fromDate = moment('2018-05-01','YYYY-MM-DD');
var toDate = moment('2018-07-01','YYYY-MM-DD');
var monthData = [];
while (toDate > fromDate || fromDate.format('M') === toDate.format('M')){
monthData.push(fromDate.format('MMMM'));
fromDate.add(1,'month');
}
console.log(monthData); // Output ["May", "June", "July"]
Thanks
You can use moment.months and slice it to get only needed months
var months = moment.months();
console.log(JSON.stringify(months))
console.log(JSON.stringify(months.slice(4,7)))
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js"></script>
Just to show a solution without moment.js.
Month names are available in the browser default language using toLocaleString. To get the names for months from the 3rd to 5th months inclusive, slice them from the array, e.g.
/* Return array of month names for month number range in the
* browser default language.
* E.g. 3,5 returns ['March','April','May] with default language English
* Assumes Gregorian calendar, 12 months in year. If input is invalid,
* returns an empty array
*
* #param {number|string} [startMonth=1] - calendar month number
* #param {number|string} [endMonth=12] - calendar month number
* #returns {string[]} Array of month names from startMonth to endMonth inclusive.
*/
var getMonthNames = (function () {
var months = new Array(12).fill(0).map((v,i)=>
new Date(2000,i).toLocaleString(undefined,{month:'long'})
);
return function(startMonth=1, endMonth=12) {
return isNaN(+startMonth) || isNaN(+endMonth)? [] :
months.slice(--startMonth, endMonth);
}
}());
// Example
[['03', '03'], // Single month
['06'], // Month to end of year
[], // Month 1 to 12
['01', '09'], // Month 1 to month 9
['foo'] // Erroneous input
].forEach(args =>
console.log(`${args.toString()}: ${getMonthNames(...args).join(', ')}`)
);
Creating the month name list is a little expensive, but it only runs once and it only needs 12 Dates so trivial.
It needs better error handling, e.g. check start <= end, 0 < start < 13, 0 < end < 13, start and end are integers, etc.
If you are trying to get the month range in this format:
"2022-12-11T00:00:00+01:00 2022-12-17T23:59:59+01:00"
The following code would do the trick.
const start = moment().subtract(1, 'week').startOf('week').format();
const end = moment().subtract(1, 'week').endOf('week').format();
I hope this was helpful.