Repository: jsonata-js/jsonata (js)

Issue:
$toMillis() fails when a timezone is added to picture string
Both of these expressions fail to execute on any version of JSONata:

```
$toMillis('2020-09-09 00:00:00 +02:00', '[Y0001]-[M01]-[D01] [H01]:[m01]:[s01] [Z]')
$toMillis($now('[Y0001]-[M01]-[D01] [H01]:[m01]:[s01] [Z]', '+0200'), '[Y0001]-[M01]-[D01] [H01]:[m01]:[s01] [Z]')
```

If you remove the timezone from the picture string and timestamp string everything work as normal.

Links to try these out:
* https://try.jsonata.org/jYoHi-8k0
* https://try.jsonata.org/H3rxBmGEn

Pull request description:
Implement timezone support for $toMillis
resolves #477 

Signed-off-by: andrew-coleman <andrew_coleman@uk.ibm.com>

Patch:
diff --git a/src/datetime.js b/src/datetime.js
index b8fa07f..15500bd 100644
--- a/src/datetime.js
+++ b/src/datetime.js
@@ -947,6 +947,41 @@ const dateTime = (function () {
                 var res = {};
                 if (part.type === 'literal') {
                     res.regex = part.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+                } else if (part.component === 'Z' || part.component === 'z') {
+                    // timezone
+                    let separator;
+                    if (!Array.isArray(part.integerFormat.groupingSeparators)) {
+                        separator = part.integerFormat.groupingSeparators;
+                    }
+                    res.regex = '';
+                    if (part.component === 'z') {
+                        res.regex = 'GMT';
+                    }
+                    res.regex += '[-+][0-9]+';
+                    if (separator) {
+                        res.regex += separator.character + '[0-9]+';
+                    }
+                    res.parse = function(value) {
+                        if (part.component === 'z') {
+                            value = value.substring(3); // remove the leading GMT
+                        }
+                        let offsetHours = 0, offsetMinutes = 0;
+                        if (separator) {
+                            offsetHours = Number.parseInt(value.substring(0, value.indexOf(separator.character)));
+                            offsetMinutes = Number.parseInt(value.substring(value.indexOf(separator.character) + 1));
+                        } else {
+                            // depends on number of digits
+                            const numdigits = value.length - 1;
+                            if (numdigits <= 2) {
+                                // just hour offset
+                                offsetHours = Number.parseInt(value);
+                            } else {
+                                offsetHours = Number.parseInt(value.substring(0, 3));
+                                offsetMinutes = Number.parseInt(value.substring(3));
+                            }
+                        }
+                        return offsetHours * 60 + offsetMinutes;
+                    };
                 } else if (part.integerFormat) {
                     res = generateRegex(part.integerFormat);
                 } else {
@@ -1240,6 +1275,10 @@ const dateTime = (function () {
             }
 
             var millis = Date.UTC(components.Y, components.M, components.D, components.H, components.m, components.s, components.f);
+            if(components.Z || components.z) {
+                // adjust for timezone
+                millis -= (components.Z || components.z) * 60 * 1000;
+            }
             return millis;
         }
     }


Test patch:
diff --git a/test/test-suite/groups/function-tomillis/parseDateTime.json b/test/test-suite/groups/function-tomillis/parseDateTime.json
index fae5862..b01e16f 100644
--- a/test/test-suite/groups/function-tomillis/parseDateTime.json
+++ b/test/test-suite/groups/function-tomillis/parseDateTime.json
@@ -326,5 +326,45 @@
         "expr": "$toMillis('2018-32-5', '[X]-[W]-[F1]')",
         "data": {},
         "code": "D3136"
+    },
+    {
+        "function": "#toMillis",
+        "category": "timezones",
+        "description": "should parse a date containing timezones",
+        "expr": "$toMillis('2020-09-09 08:00:00 +02:00', '[Y0001]-[M01]-[D01] [H01]:[m01]:[s01] [Z]') ~> $fromMillis() ",
+        "data": {},
+        "result": "2020-09-09T06:00:00.000Z"
+    },
+    {
+        "function": "#toMillis",
+        "category": "timezones",
+        "description": "should parse a date containing timezones",
+        "expr": "$toMillis('2020-09-09 08:00:00 GMT-05:00', '[Y0001]-[M01]-[D01] [H01]:[m01]:[s01] [z]') ~> $fromMillis() ",
+        "data": {},
+        "result": "2020-09-09T13:00:00.000Z"
+    },
+    {
+        "function": "#toMillis",
+        "category": "timezones",
+        "description": "should parse a date containing timezones",
+        "expr": "$toMillis('2020-09-09 12:00:00 +05:30', '[Y0001]-[M01]-[D01] [H01]:[m01]:[s01] [Z]') ~> $fromMillis() ",
+        "data": {},
+        "result": "2020-09-09T06:30:00.000Z"
+    },
+    {
+        "function": "#toMillis",
+        "category": "timezones",
+        "description": "should parse a date containing timezones",
+        "expr": "$toMillis('2020-09-09 12:00:00 GMT-5', '[Y0001]-[M01]-[D01] [H01]:[m01]:[s01] [z01]') ~> $fromMillis() ",
+        "data": {},
+        "result": "2020-09-09T17:00:00.000Z"
+    },
+    {
+        "function": "#toMillis",
+        "category": "timezones",
+        "description": "should parse a date containing timezones",
+        "expr": "$toMillis('2020-09-09 12:00:00 +0530', '[Y0001]-[M01]-[D01] [H01]:[m01]:[s01] [Z0001]') ~> $fromMillis() ",
+        "data": {},
+        "result": "2020-09-09T06:30:00.000Z"
     }
 ]