I have some API's for creating dashboard widgets. Those API's return basic name/value data pairs that are passed to Google Charts. Moment.js checks whether the value is an ISO8601 date, and if so passes to Google Charts as a date instance.
However, the ISO_8601 isValid check is currently returning true if the date is a simple integer, e.g. 1234:
var myInt = 1234;
if (moment(myInt, moment.ISO_8601, true).isValid()) {
console.log("Valid!");
}
I couldn't locate the necessary functionality to force a date format in the moment.js code, so this brutal hack works for now:
var myInt = 1234;
if (JSON.stringify(myInt).includes("T") && moment(myInt, moment.ISO_8601, true).isValid()) {
console.log("Valid!");
}
Is there a correct way to use moment.js to configure the isValid() check?
The date format from my API is yyyy-mm-ddThh:mm:ss (without Z on the end).
According to THIS answer, when using strict parsing (last parameter set to true) you should also specify parse format, to avoid situations like you discribed. As many users notice, specyfying string format instead of using moment.ISO_8601 works as expected.
alert(isISODateValid(123)); //false
alert(isISODateValid("2011-10-10T14:48:00")); //true
alert(isISODateValid("2011-10-10T14:48:00Z")); //true
function isISODateValid(date) {
return moment(date.toString().replaceAll("Z",""), "YYYY-MM-DDTHH:mm:ss", true).isValid();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
EDITS: Updated snippet - if date contains "Z" suffix, remove it before parsing validating source date format
Since you stated that: "The date format from my API is yyyy-mm-ddThh:mm:ss (without Z on the end)", the best way to parse it is explictly pass the format you are expecting to moment, using the right moment format tokens instead of using moment.ISO_8601.
So, in your case, simply use moment(myInt, "YYYY-MM-DDTHH:mm:ss", true), as shown in the snipppet:
function checkValid(input) {
if (moment(input, "YYYY-MM-DDTHH:mm:ss", true).isValid()) {
console.log(input + " is valid!");
}
}
checkValid(1234);
checkValid("2021-04-27T20:40:15");
checkValid("2021-04-27T20:40:15Z");
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
Please note that the Z at the end stands for the timezone offset UTC+0, if you have it moment takes it into account, while without it, moment parses the input as local time (see Local vs UTC vs Offset guide)
As a side note, moment.ISO_8601 works as you were expecting in moment versions prior to 2.25.0:
function checkValid(input) {
if (moment(input, moment.ISO_8601, true).isValid()) {
console.log(input + " is valid!");
}
}
checkValid(1234);
checkValid("2021-04-27T20:40:15");
checkValid("2021-04-27T20:40:15Z");
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
You could testify the sting before you pass it to the moment.? I have taken an example from this post
/**
* RegExp to test a string for a full ISO 8601 Date
* Does not do any sort of date validation, only checks if the string is according to the ISO 8601 spec.
* YYYY-MM-DDThh:mm:ss
* YYYY-MM-DDThh:mm:ssTZD
* YYYY-MM-DDThh:mm:ss.sTZD
* #see: https://www.w3.org/TR/NOTE-datetime
* #type {RegExp}
*/
var ISO_8601_FULL = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(([+-]\d\d:\d\d)|Z)?$/i
// Usage:
ISO_8601_FULL.test( "2016-05-24T15:54:14.876Z" ) // true
ISO_8601_FULL.test( "2002-12-31T23:00:00+01:00" ) // true
ISO_8601_FULL.test( "2016-02-01" ) // false
ISO_8601_FULL.test( "2016" ) // false
if (ISO_8601_FULL.test(myDate) && moment(myDate, moment.ISO_8601, true).isValid()) {
console.log("Valid!");
}
I suppose the date should not be an integer.
Interesting is the difference between string and number. If it's a number, it is interpreted as the number of milliseconds since epoc, which is quite helpful in computer languages, but obviously not always what was requested and also not obvious to every developer. This can easily avoided with a type check (typeof input != 'string').
The other variant is more confusing: "12345" is not valid. good. But "1234" is interpreted as a year, and at the same time "34" seems to be interpreted as a time offset in minutes (Sun Jan 01 1234 00:00:00 GMT+0034). To me, this clearly looks like a bug in the library, since it's quite useless to parse the same digits multiple times for different purposes. But also after this is fixed, "1234" stays a valid date (year only) as defined in the standard ISO 8601
https://en.wikipedia.org/wiki/ISO_8601
For reduced precision,[17] any number of values may be dropped from any of the date and time representations, but in the order from the least to the most significant. For example, "2004-05" is a valid ISO 8601 date, which indicates May (the fifth month) 2004. This format will never represent the 5th day of an unspecified month in 2004, nor will it represent a time-span extending from 2004 into 2005.
Btw: "543210" is also valid and means "5432-10", or October of the year 5432
function checkValid(input) {
m = moment(input, true);
console.log(input + " type: " + typeof input + " valid: " + m.isValid() + " result:" + m.toString());
}
checkValid(1234);
checkValid("1234");
checkValid(12345);
checkValid("12345");
checkValid("2021-04-27T20:40:15");
checkValid("2021-04-27T20:40:15Z");
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
Related
Seems there is a problem with definition of ISO date format in Javascript. As far as I undrerstand ISO-formatted date can include TZ offset that includes hours, minutes and seconds, e.g.:
1919-07-01T00:00:00+04:31:19
Trying to parse such datetime string using JavaScript new Date() object leads to an error:
new Date('1919-07-01T00:00:00+04:31:17').toLocaleDateString('ru-RU') => "Invalid Date"
new Date('1919-07-01T00:00:00+04:31').toLocaleDateString('ru-RU') => "01.07.1919"
The date specified in the example comes from the Java backend client/POSTGRESQL database where representation '1919-07-01T00:00:00+04:31:17' treated as a valid ISO date.
The reason the date contains "seconds" in timezone offset is understood if we look as the following data regaring daylight savings changes:
https://www.timeanddate.com/time/change/russia/moscow?year=1919
Is there any suggestion why it is impossible to overcome this limitation or where is the origin of the problem?
Seems there is a problem with definition of ISO date format in Javascript.
Javascript is based on ECMA-262, which is published by ECMA International. ISO 8601 is published by ISO, a different standards organisation. I don't have a full copy of ISO 8601, however I expect that there are extensions to allow seconds in the offset given they were fairly common prior to 1900.
ECMA-262 defines a couple of formats, one is based on a simplification of ISO 8601, so does not support all of the formats ISO 8601 does (the other is the format produced by toString).
Since the format in the OP is not consistent with one of the formats in ECMA-262, parsing is implementation dependent, see Why does Date.parse give incorrect results?
Your best option is to manually parse the string, either with a simple function or a library that supports seconds in the offset.
An example parse function is below (according to timeanddate the seconds part of the offset in 1919 should have been 19, not 17 so I've modified the original string accordingly):
// Parse format 1919-07-01T00:00:00+04:31:19
function parseWithTZSecs(date) {
let [Y, M, D, H, m, s, oH, om, os] = date.match(/\d+/g);
let sign = date.substring(19,20) == '+'? -1 : 1;
return new Date(Date.UTC(Y, M-1, D, +H + sign*oH, +m + sign*om, +s + sign*os));
}
let date = parseWithTZSecs('1919-07-01T00:00:00+04:31:19');
// 1919-07-01, 12:00:00 AM
console.log(date.toLocaleString('en-CA',{timeZone: 'Europe/Moscow'}));
This is just an example, of course it should do validation of input and deal with unexpected formats and values.
when I check the following date it returns true result, Why?
const value = "3";
if (moment(new Date(value), "DD-MM-YYYY HH:mm", true).isValid()) // true
{ }
or
const value = "3";
if (moment(new Date(value)).isValid()) // true
{ }
That is because new Date("3") is valid date and
console.log(new Date("3"))
This is one of those cases that shows you need to sanitize your date strings and should not depend on the native parser, unless you are sure your strings have already been validated and are conformant.
ECMA-262 Date(value) constructor specs
Date.parse
If the String does not conform to that format the function may fall back to any implementation-specific heuristics or implementation-specific date formats.
So it's not conformant to "Date Time String Format", which requires the string to start with "YYYY", so it goes to an implementation specific parsing that is similar to the rules for above, but using the form: "MM-DD-YYYY".
The purpose of using strict mode (setting third argument of moment() to true) is to let moment do the string parsing and determine if it fits the formats that you provide (to for example avoid unexpected parsing behavior like this). If you use Date() to parse, you are no longer using moment's strict mode to validate the string fits your required format.
let value = "3";
function checkDate(value){
console.log(value,
moment(value, "DD-MM-YYYY HH:mm", true).isValid() ? 'valid' : 'invalid')
}
value = "01-01-2011 11:22"
checkDate(value)
value = "01-01-2011 11:22Z"
checkDate(value)
value = "3"
checkDate(value)
value = new Date("3").toString()
checkDate(value)
value = new Date('INVALID DATE')
checkDate(value)
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.25.3/moment-with-locales.min.js" integrity="sha256-8d6kI5cQEwofkZmaPTRbKgyD70GN5mDpTYNP9YWhTlI=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.28/moment-timezone-with-data.js" integrity="sha256-O1PdKrSbpAYWSBteb7yX/CMmHhu3US31mtCbsryGwaY=" crossorigin="anonymous"></script>
If you don't need to validate the date string (for example to prevent unexpected parsing behavior), don't need to worry about non-modern browsers, and really just need to parse conforming Date strings and format to basic string formats, you could just use native Date() with Intl.DateTimeFormat or Date.prototype.toLocaleString.
TL;DR the way you are using it right now implies that you don't actually need moment
Because 3 is a valid Date, the Date also supports only the year or only month format.
If you try new Date("YYYY") --> It's a valid date
If you try new Date("MM") --> It's a valid format
So, for new Date("{1 to 12}") It will accept
For new Date("{13 to 31}") It's invalid
Also, from new Date("{32 to 49}") It considers as year, for two digits it will add the current century that is (2000 + number you specified), for e.g.
new Date("32") is the year 2032 and for new Date("{50 to 99}") it adds in the previous century i.e. (1900 + number you specified), for e.g. new Date("99") is the year "1999"
For three-digit, till "100000" on-wards it's pretty straightforward what input it is will be a year, for e.g. new Date("100") is year 100 and new Date("100000") is the year 100000. The rest are invalid.
The above is valid as of today, instead of taking care of the above information it's always advisable to use Null, NaN, and Undefined values where the date field is not present
I have a datetime from Apple's receipt validation server. I need to compare it to today's date. I'm trying to use moment.js but I am fine with native Date as well.
The date is formatted like this, in what Apple's docs claim, incorrectly, is RFC 3339 format:
2017-11-28 23:37:52 Etc/GMT
Since this isn't a valid RFC 3339 or ISO 8601 datetime, it is failing to parse using moment.js or Date.parse(). How can I parse it?
Note that I don't want to simply strip the Etc/ out because its important for determining the correct timezone according to https://opensource.apple.com/source/system_cmds/system_cmds-230/zic.tproj/datfiles/etcetera:
We use POSIX-style signs in the Zone names and the output abbreviations, even though this is the opposite of what many people expect. POSIX has positive signs west of Greenwich, but many people expect positive signs east of Greenwich. For example, TZ='Etc/GMT+4' uses the abbreviation "GMT+4" and corresponds to 4 hours behind UTC (i.e. west of Greenwich) even though many people would expect it to mean 4 hours ahead of UTC (i.e. east of Greenwich).
You can adopt one of two strategies:
Manually parse the string and use the parts for the Date constructor, then adjust for the timezone.
Transform the timestamp into a string that should be supported by most browsers in use (i.e. ISO 8601 extended format per ECMA-262).
The first way is more robust.
The reference doesn't say how timezones like +0530 are represented in the Etc/ format, so the following doesn't deal with them:
'Etc/GMT' becomes '+00:00'
'Etc/GMT+4' becomes '-04:00'
'Etc/GMT-10' becomes '+10:00'
Also, the following functions don't validate the format, they assume the supplied string is suitable.
// Parse strings in format 2017-11-28 23:37:52 Etc/GMT
function parsePOZIX(s) {
var b = s.split(' ');
// These are the date and time parts
var c = b[0].split(/\D/).concat(b[1].split(/\D/));
// Create a new Date using UTC
var d = new Date(Date.UTC(c[0],c[1]-1,c[2],c[3],c[4],c[5]));
// Now adjust for the timezone
var tz = b[2].split('/')[1];
var sign = /^\+/.test(tz) ? 1 : -1;
var hr = tz.replace(/\D/g, '') || '0';
d.setUTCHours(d.getUTCHours() + sign*hr);
return d;
}
['2017-11-28 23:37:52 Etc/GMT',
'2017-11-28 23:37:52 Etc/+8',
'2017-11-28 23:37:52 Etc/-10'].forEach(function(s){
console.log(s + '\n' + parsePOZIX(s).toISOString());
});
And the second (less reliable way) which parses and reformats the string, then uses the built-in parser:
// Reformat strings like 2017-11-28 23:37:52 Etc/GMT
// to ISO 8601 compliance, then use built-in parser
function parsePOSIX2(s) {
var b = s.split(' ');
var tz = b[2].split('/')[1];
var sign = /^\+/.test(tz)? '-' : '+';
var hr = tz.replace(/\D/g,'') || '0';
hr = ('0' + hr).slice(-2) + ':00'; // Must include colon for Safari
return new Date(b[0] + 'T' + b[1] + sign + hr);
}
['2017-11-28 23:37:52 Etc/GMT',
'2017-11-28 23:37:52 Etc/-4'].forEach(function(s) {
console.log(s + '\n' + parsePOSIX2(s).toISOString());
});
Note that Safari will not parse a timezone like "+0000" (resulting in an invalid date), it must include a colon: "+00:00".
The easiest way to parse an an arbitrary format is to use some regex to split it to pieces, then arrange it in a format you can use.
const date = '2017-11-28 23:37:52 Etc/GMT';
const [,year,month,day,hour,minute,second,timezone] = date.match(/(\d{4})-(\d{2})-(\d{2})\s(\d{2}):(\d{2}):(\d{2})\sEtc\/([A-Z]{3})/);
console.log(year, month, day, hour, minute, second, timezone);
const parsedDate = new Date(`${year}-${month}-${day} ${hour}:${minute}:${second} ${timezone}`);
console.log(parsedDate);
The nice thing about this approach is it works for any random format you may come across (you just need a different regex). With ES6 destructing, it can be quite compact.
Note: I'm not sure if that Etc bit is constant or has special meaning, so you should check out the spec and make sure you don't need to do anything with it.
The format Apple is returning consists of an ISO 8601 datetime followed by an IANA timezone. That's an eccentric format I've never seen elsewhere, and it seems to be a mistake on Apple's part given that their docs claim it's meant to be an RFC 3339 datetime string.
JavaScript has no built-in support for working with IANA timezones. For that matter, nor does the core Moment.js library. However, Moment Timezone does. If you include both the moment and moment-timezone libraries, then you can parse your date like this:
/**
* Parse a datetime string returned from the Apple Receipt Validation Server, in
* a format like:
* 2017-11-28 23:37:52 Etc/GMT
* and return its value as a Moment datetime object.
*/
function parseAppleDatetime(appleDatetimeStr) {
const [dateStr, timeStr, timezoneStr] = appleDatetimeStr.split(' '),
datetimeStr = dateStr + ' ' + timeStr;
return moment.tz(datetimeStr, timezoneStr);
}
Now if you do, for instance:
parseAppleDatetime('2017-11-28 23:37:52 Etc/GMT').format()
... you'll get:
"2017-11-28T23:37:52Z"
datefromJSON = req.body.occasion_date;
occasion_date = new Date(datefromJSON);
console.log(occasion_date);
//while running this i get log like this
//"Invalid Date"
when i get json data from req.body then save in datefromJSON var, and for storing into database i use to convert into Date object that get error
Your date string (whatever it is "31-08-2016") isn't in a format recognized by the Date constructor. So you ended up with a Date whose underlying time value is NaN, which is shown as "Invalid Date" when you ask for the string version. Gratuitous example:
console.log(new Date("foobar").toString());
The only formats the specification requires a JavaScript implementation to support are:
The one added in the ES5 specification in 2009, which was meant to be (and as of all recent specifications actually is; there was an error in ES5 and ES2015/ES2016) a subset of ISO-8601. (This is the format produced by toISOString.) spec
The ones produced by the toString and toUTCString (aka toGMTString) methods. spec
So for instance:
console.log(new Date("2016-08-31T09:25").toString());
Every JavaScript implementation I've run into also unofficially supports parsing the U.S. format with slashes, MM/dd/yyyy, (even in non-U.S. locales), but the timezone varies (most interpret it as local time, others interpret it in GMT).
So you'll need to either:
A) Parse your string (regular expressions, split, etc.) and use the form of the Date constructor that supports supplying the parts individually. Mind the gotcha that the months value starts with 0 = January (not 1 = January). There are several dozen questions with examples of that here on SO. Here's an example of parsing the common dd-MM-yyyy HH:mm:ss or dd-MM-yyyy HH:mm:ss.SSS format as a local date/time:
Regex-based:
function parseIt(str) {
var rex = /^\s*(\d{1,2})[\/-](\d{1,2})[\/-](\d{4})(?: (\d{1,2})(?::(\d{1,2})(?::(\d{1,2})(?:\.(\d{1,3}))?)?)?)?\s*$/;
var parts = rex.exec(str);
var dt = null;
if (parts) {
dt = new Date(+parts[3], // Year
+parts[2] - 1, // Month
+parts[1], // Day
+parts[4] || 0, // Hours
+parts[5] || 0, // Minutes
+parts[6] || 0, // Seconds
+parts[7] || 0 // Milliseconds
);
}
return dt;
}
function test(str) {
var dt = parseIt(str);
console.log(str, "=>");
console.log(" " + String(dt));
console.log(" (" + (dt ? dt.toISOString() : "null") + ")");
}
test("foobar");
test("31-08-2016");
test("31/08/2016");
test("31/08/2016 9");
test("31/08/2016 9:25");
test("31/08/2016 09:25");
test("31/08/2016 09:25:17");
test("31/08/2016 09:25:17.342");
The regex looks complicated, but it's really just a bunch of capture groups nested inside non-capturing, optional groups. Explanation here.
or
B) Use a library (like MomentJS) that lets you parse a string by saying what the format is.
I have a ISO date string as below
var startTimeISOString = "2013-03-10T02:00:00Z";
when I convert it to date object in javascript using below code, it returns
var startTimeDate = new Date(startTimeISOString);
output is
Date {Sun Mar 10 2013 07:30:00 GMT+0530 (India Standard Time)}
It sure converts the ISOString to date but it converts to local time since new Date() is client dependent. How to just convert iso date time string to date and time but not to local date-time..?
Thanks
According to MDN:
Differences in assumed time zone
Given a date string of "March 7, 2014", parse() assumes a local time
zone, but given an ISO format such as "2014-03-07" it will assume a
time zone of UTC. Therefore Date objects produced using those strings
will represent different moments in time unless the system is set with
a local time zone of UTC. This means that two date strings that appear
equivalent may result in two different values depending on the format
of the string that is being converted (this behavior is changed in
ECMAScript ed 6 so that both will be treated as local).
I have done like this and am now getting the exact time which is inside the ISO date string instead of the local time
var startTimeISOString = "2013-03-10T02:00:00Z";
var startTime = new Date(startTimeISOString );
startTime = new Date( startTime.getTime() + ( startTime.getTimezoneOffset() * 60000 ) );
This will give the same date time inside iso date string , the output here is
o/p
Date {Sun Mar 10 2013 02:00:00 GMT+0530 (India Standard Time)}
To sum up the conversation from tracevipin's post:
All Date objects are based on a time value that is milliseconds since 1970-01-01T00:00:00Z so they are UTC at their core. This is different to UNIX, which uses a value that is represents seconds since the same epoch.
The Date.prototype.toString method returns an implementation dependent string that represents the time based on the system settings and timezone offset of the client (aka local time).
If a UTC ISO8601 time string is required, the Date.prototype.toISOString method can be used. It's quite easy to write a "shim" for this methods if required.
Lastly, do not trust Date.parse to parse a string. Support for an ISO8601 format UTC string is specified in ES5, however it's not consistently implemented across browsers in use. It is much better to parse the string manually (it's not hard, there are examples on SO of how to do it) if wide browser support is required (e.g. typical web application).
Simple ISO8601 UTC time stamp parser:
function dateObjectFromUTC(s) {
s = s.split(/\D/);
return new Date(Date.UTC(+s[0], --s[1], +s[2], +s[3], +s[4], +s[5], 0));
}
and here's a shim for toISOString:
if (typeof Date.prototype.toISOString != 'function') {
Date.prototype.toISOString = (function() {
function z(n){return (n<10? '0' : '') + n;}
function p(n){
n = n < 10? z(n) : n;
return n < 100? z(n) : n;
}
return function() {
return this.getUTCFullYear() + '-' +
z(this.getUTCMonth() + 1) + '-' +
z(this.getUTCDate()) + 'T' +
z(this.getUTCHours()) + ':' +
z(this.getUTCMinutes()) + ':' +
z(this.getUTCSeconds()) + '.' +
p(this.getUTCMilliseconds()) + 'Z';
}
}());
}
This happens because date is printed using toString method which by default returns the date and time in local timezone. The method toUTCString will give you the string you need.
Date actually keeps the date as unix time in milliseconds and provides methods to manipulate it.
In vanilla javascript there isn't a way to create a date that assumes the local time of the ISO formatted string you give it. Here's what happens when you pass an ISO 8601 formatted string to javascript. I'm going to use a non UTC time as it illustrates the problem better than using an ISO formatted string:
var startTime = new Date("2013-03-10T02:00:00+06:00"). Note this could also be 2013-03-10T02:00:00Z or any other ISO-formatted string.
read the time, apply the offset and calculate milliseconds since 1970-01-01T00:00:00Z
You now have only milliseconds - you have lost all timezone info. In this case 1362859200000
All functions, apart from the ones that give you a UTC representation of that number, will use the timezone of the computer running the code to interpret that number as a time.
To do what the original poster wants, you need to.
parse the ISO string, interpret the offset ('Z' or '+06:00') as the timezone offset
store the timezone offset
calculate and store the ms since epoch, using the offset timezone offset
hold that offset
whenever attempting to make a calculation or print the date, apply the timezone offset.
This isn't trivial, and requires a complete interpretation of the 8601 spec. Way too much code to put here.
This is exactly what moment.js is designed to do. I strongly recommend using it. Using moment.js:
moment("2013-03-10T02:00:00Z").format()
"2013-03-10T02:00:00Z"
this will result in printing the ISO time of the original string, preserving the offset.
you can try moment js library https://momentjs.com
For my case, I had 2022-10-17T01:00:00 on my database. SO I need to format it to the 01:00:00 AM.
So here was my solution.
var date = "2022-10-17T01:00:00"
var timeFormat = moment(date ).format('HH:mm A');
output: 01:00:00 AM
it will return ISOdate
var getDate = () => {
var dt = new Date();
var off = dt.getTimezoneOffset() * 60000
var newdt = new Date(dt - off).toISOString()
return newdt.slice(0, 19)
}
Output