Parse date from Apple receipt validation server with JavaScript - javascript

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"

Related

Prevent moment.js accepting an integer as a valid ISO8601 date

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>

Create JavaScript Date object from date string + time string + timezone offset string

I have total 4 different input i.e.:
Date string (2020-05-05)
Time string (15:30)
Timezone offset (-09:00)
I want to combine these strings into one datetime object like (2020-05-05T15:30:00-09:00) no-matter what my local browser timezone is. The issue is when I combine these strings and I try to make date object using new Date() function, my datetime gets converted into UTC timestamp.
I tried this:
const date =
moment(this.actualDateOfSurgeryDate).format(YYYYMMDD) +
'T' +
moment(this.actualDateOfSurgeryTimeDropDown + ' ' + this.actualDateOfSurgeryTimeAM_PMDropDown, ['h:mm A']).format('HH:mm:ss') +
offsetTime;
this.caseDetail.actualDateOfSurgery = new Date(date);
This gives me output something like: 2020-05-05T04:30:00.000Z
How can I get my desired output: 2020-05-05T15:30:00-09:00 ??
I have moment js available in my project
I want to combine these strings into one datetime object like (2020-05-05T15:30:00-09:00)
Date objects are extremely simple, they're just a time value that is an offset in milliseconds since 1970-01-01T00:00:00Z, so are inherently UTC. The built–in parser is unreliable and lacks any functionality such as format tokens.
So if you have separate values like:
Date string (2020-05-05)
Time string (15:30)
Timezone offset (-09:00)
then you can create a string that is compliant with the format defined in ECMA-262 and that should be parsed correctly by the built–in parser, e.g.
new Date('2020-05-05T15:30:00.000-09:00')
However, general advice is to avoid the built–in parser due to differences in implementations. Also, the format must be exact (e.g. including seconds and milliseconds in the timestamp, colon (:) in the offset) or some implementations will reject it as malformed and return an invalid date.
Once you have a Date object, getting a "local" timestamp with offset is an issue of formatting, which has been answered many times before (e.g. How to format a JavaScript date). There aren't any decent built–in formatting functions (toLocaleString with options is OK for some purposes but generally lacking in functionality), so you'll have to either write your own function, or use a library.
The following examples use Luxon, which is suggested as the upgrade path from moment.js.
With Luxon, if you specify a representative location, you'll get the offset for that location at the date's date and time. Alternatively, you can fix the offset to a set value, essentially setting it for a timezone without a representative location, so it doesn't have any reference to daylight saving or historic offset changes:
let DateTime = luxon.DateTime;
// Offset per the specified location
let d0 = DateTime.fromISO('2020-01-01', {zone: 'America/Yakutat'});
let d1 = DateTime.fromISO('2020-06-30', {zone: 'America/Yakutat'});
console.log(d0.toString());
console.log(d1.toString());
// Fixed offset per the supplied string
let d2 = DateTime.fromISO('2020-05-05T15:30:00.000-09:00', { setZone: true});
let d3 = DateTime.fromISO('2020-01-01T15:30:00.000-09:00', { setZone: true});
console.log(d2.toString());
console.log(d3.toString());
<script src="https://cdn.jsdelivr.net/npm/luxon#1.24.1/build/global/luxon.min.js"></script>
I get 16:30 due to DST
A date before March or after October will give 15:30
let dateString = "2020-05-05"+"T"+"15:30"+":00"+"-09:00"
console.log(dateString)
const date = new Date(dateString)
console.log(date)
const Anchorage = date.toLocaleString('en-US', {timeZone: 'America/Anchorage', hour12: false})
console.log(Anchorage)
let options = {}
options.timeZone = 'America/Anchorage';
options.timeZoneName = 'short';
console.log(date.toLocaleDateString('en-US'), date.toLocaleTimeString('en-US', options));

Converting date from string with offset to different timezone

I'm wondering on the correct way to convert a string date in a non-ISO format to a different offset/timezone.
I am currently given 3 values:
the date in format MM/DD/YYYY (23/11/2016)
the time in 24h format (23:13)
timezone offset (-07:00)
I would like to convert said date to the user's timezone.
I am trying to convert the format to the format accepted by moment timezone's moment.tz() function ('2016-11-23T23:13-07:00') but I am not sure how to do that without splitting the date array and converting it to said date.
Moment's timezone has the tools I need to convert the date afterwards to the local timezone. For example:
moment.tz('2016-11-23T23:13-07:00', moment.tz.guess());
Any thoughts on how to convert 23/11/2016 23:13 with offset -07:00 to the local date preferably using momentJS?
Why not just format as an ISO 8601 string with offset and give that to moment.js?
function customToISOString(date, time, offset){
return date.split(/\D/).reverse().join('-') + 'T' + time + offset;
}
document.write(customToISOString('23/11/2016','23:13','-07:00')); // 2016-11-23T23:13-07:00
Most modern browsers will also parse that, but don't do it as there are still plenty of older browsers around where it will fail.
I like Rob's answer, but I'll also give you it in moment.js.
First, you don't need moment-timezone, and you definitely don't need to guess the time zone id just to convert to that zone. In ISO format, it would just be like this:
var m = moment('2016-11-23T23:13-07:00');
This will read in the offset during parsing, apply it, then convert to the local time zone, returning a moment object in "local mode". This is the default mode, so it just works.
With the requirements you described it would be like this:
// your inputs
var d = "23/11/2016";
var t = "23:13";
var o = "-07:00";
var m = moment(d + ' ' + t + o, 'MM/DD/YYYY HH:mmZ');
Note that I add the space between the date and time just for safety, so there's no risk of mixing the year and the hour components.
Again it will automatically apply the offset and convert to the local time zone, since that's the default behavior. If you want some other behavior, there are ways to do that as well.

Using momentjs to convert date to epoch then back to date

I'm trying to convert a date string to epoch, then epoch back to the date string to verify that I'm providing the correct date string.
var epoch = moment("10/15/2014 9:00").unix(); // do I need to do .local()?
var momentDate = moment(epoch); // I've also tried moment.utc(epoch)
var momentDateStr = momentDate.calendar();
alert("Values are: epoch = " + epoch + ", momentDateStr = " + momentDateStr);
Renders
Values are: epoch = 1413378000, momentDateStr = 01/17/1970
Note: I'm using the following version of the moment js script, //cdnjs.cloudflare.com/ajax/libs/moment.js/2.8.3/moment-with-locales.js
There are a few things wrong here:
First, terminology. "Epoch" refers to the starting point of something. The "Unix Epoch" is Midnight, January 1st 1970 UTC. You can't convert an arbitrary "date string to epoch". You probably meant "Unix Time", which is often erroneously called "Epoch Time".
.unix() returns Unix Time in whole seconds, but the default moment constructor accepts a timestamp in milliseconds. You should instead use .valueOf() to return milliseconds. Note that calling .unix()*1000 would also work, but it would result in a loss of precision.
You're parsing a string without providing a format specifier. That isn't a good idea, as values like 1/2/2014 could be interpreted as either February 1st or as January 2nd, depending on the locale of where the code is running. (This is also why you get the deprecation warning in the console.) Instead, provide a format string that matches the expected input, such as:
moment("10/15/2014 9:00", "M/D/YYYY H:mm")
.calendar() has a very specific use. If you are near to the date, it will return a value like "Today 9:00 AM". If that's not what you expected, you should use the .format() function instead. Again, you may want to pass a format specifier.
To answer your questions in comments, No - you don't need to call .local() or .utc().
Putting it all together:
var ts = moment("10/15/2014 9:00", "M/D/YYYY H:mm").valueOf();
var m = moment(ts);
var s = m.format("M/D/YYYY H:mm");
alert("Values are: ts = " + ts + ", s = " + s);
On my machine, in the US Pacific time zone, it results in:
Values are: ts = 1413388800000, s = 10/15/2014 9:00
Since the input value is interpreted in terms of local time, you will get a different value for ts if you are in a different time zone.
Also note that if you really do want to work with whole seconds (possibly losing precision), moment has methods for that as well. You would use .unix() to return the timestamp in whole seconds, and moment.unix(ts) to parse it back to a moment.
var ts = moment("10/15/2014 9:00", "M/D/YYYY H:mm").unix();
var m = moment.unix(ts);
http://momentjs.com/docs/#/displaying/unix-timestamp/
You get the number of unix seconds, not milliseconds!
You you need to multiply it with 1000 or using valueOf() and don't forget to use a formatter, since you are using a non ISO 8601 format. And if you forget to pass the formatter, the date will be parsed in the UTC timezone or as an invalid date.
moment("10/15/2014 9:00", "MM/DD/YYYY HH:mm").valueOf()

How to assume local time zone when parsing ISO 8601 date string?

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

Categories