Skip to content

Commit fa1737a

Browse files
committed
[GR-32375] Temporal Interop JS-to-Java.
PullRequest: js/2474
2 parents 8e81871 + fcacf68 commit fa1737a

File tree

10 files changed

+520
-1
lines changed

10 files changed

+520
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ See [version roadmap](https://www.graalvm.org/release-notes/version-roadmap/) fo
99
* GraalVM JavaScript is now an installable component of GraalVM. It can be installed with `gu install js`.
1010
* Removed experimental option `commonjs-global-properties`. The same functionality can be achieved in user code with a direct call to `require()` after context creation.
1111
* Added an experimental option `--js.zone-rules-based-time-zones` that allows to use timezone-related data from `ZoneRulesProvider` (instead of ICU4J data files).
12+
* Temporal objects can be converted to compatible Java objects when possible, using the `Value` API's methods like `asDate()`.
1213

1314
## Version 22.1.0
1415
* Updated Node.js to version 16.14.2.

graal-js/src/com.oracle.truffle.js.test/src/com/oracle/truffle/js/test/builtins/TemporalBuiltinsTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,31 @@ public void testTimeSpecWithOptionalTimeZoneNotAmbiguous() {
10701070
testTrue(code);
10711071
}
10721072

1073+
@Test
1074+
public void testDuration1DayExactly24Hours() {
1075+
// DST was entered on 2022-03-27 at 1am, jumping to 2am
1076+
String code = "var dur = Temporal.Duration.from('P1D');\n" +
1077+
"var relTo = Temporal.ZonedDateTime.from('2022-03-26T12:00+01:00[Europe/Vienna]'); \n" +
1078+
"var dur2 = dur.round({ 'relativeTo': relTo, 'largestUnit': 'seconds' }); \n" +
1079+
"dur2.seconds === 82800;"; // 23 hours
1080+
testTrue(code);
1081+
1082+
// same DST transition as above, but using 24 HOURS instead of 1 DAY
1083+
code = "var dur = Temporal.Duration.from('PT24H');\n" +
1084+
"var relTo = Temporal.ZonedDateTime.from('2022-03-26T12:00+01:00[Europe/Vienna]'); \n" +
1085+
"var dur2 = dur.round({ 'relativeTo': relTo, 'largestUnit': 'seconds' }); \n" +
1086+
"dur2.seconds === 86400;"; // 24 hours, different than above. 1D != 24H
1087+
testTrue(code);
1088+
1089+
// There was one leap second at 2016-12-31 after 23:59:59 (UTC+0)
1090+
code = "var dur = Temporal.Duration.from('P1D');\n" +
1091+
"var relTo = Temporal.ZonedDateTime.from('2016-12-31T12:00:00+00:00[Europe/London]'); \n" +
1092+
"var dur2 = dur.round({ 'relativeTo': relTo, 'largestUnit': 'seconds' }); \n" +
1093+
"dur2.seconds === 86400"; // exactly 1 day; Temporal does not consider leap
1094+
// seconds
1095+
testTrue(code);
1096+
}
1097+
10731098
private static void testTrue(String code) {
10741099
try (Context ctx = getJSContext()) {
10751100
Value result = ctx.eval(ID, code);
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*
2+
* Copyright (c) 2022, 2022, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* The Universal Permissive License (UPL), Version 1.0
6+
*
7+
* Subject to the condition set forth below, permission is hereby granted to any
8+
* person obtaining a copy of this software, associated documentation and/or
9+
* data (collectively the "Software"), free of charge and under any and all
10+
* copyright rights in the Software, and any and all patent rights owned or
11+
* freely licensable by each licensor hereunder covering either (i) the
12+
* unmodified Software as contributed to or provided by such licensor, or (ii)
13+
* the Larger Works (as defined below), to deal in both
14+
*
15+
* (a) the Software, and
16+
*
17+
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
18+
* one is included with the Software each a "Larger Work" to which the Software
19+
* is contributed by such licensors),
20+
*
21+
* without restriction, including without limitation the rights to copy, create
22+
* derivative works of, display, perform, and distribute the Software and make,
23+
* use, sell, offer for sale, import, export, have made, and have sold the
24+
* Software and the Larger Work(s), and to sublicense the foregoing rights on
25+
* either these or other terms.
26+
*
27+
* This license is subject to the following condition:
28+
*
29+
* The above copyright notice and either this complete permission notice or at a
30+
* minimum a reference to the UPL must be included in all copies or substantial
31+
* portions of the Software.
32+
*
33+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
39+
* SOFTWARE.
40+
*/
41+
42+
package com.oracle.truffle.js.test.interop;
43+
44+
import static com.oracle.truffle.js.lang.JavaScriptLanguage.ID;
45+
46+
import java.time.Duration;
47+
import java.time.Instant;
48+
import java.time.LocalDate;
49+
import java.time.LocalTime;
50+
import java.time.Month;
51+
import java.time.ZoneId;
52+
import java.time.ZoneOffset;
53+
54+
import org.graalvm.polyglot.Context;
55+
import org.graalvm.polyglot.Value;
56+
import org.junit.Assert;
57+
import org.junit.Test;
58+
59+
import com.oracle.truffle.js.test.JSTest;
60+
61+
public class TemporalInteropToJavaTest extends JSTest {
62+
63+
private static Context getJSContext() {
64+
return JSTest.newContextBuilder(ID).option("js.temporal", "true").build();
65+
}
66+
67+
// Calendar, PlainMonthDay, PlainYearMonth cannot be converted to Instant, Date or Time
68+
69+
@Test
70+
public void testInstant() {
71+
try (Context ctx = getJSContext()) {
72+
Value val = ctx.eval(ID, "new Temporal.Instant(0n);");
73+
Instant inst = val.asInstant();
74+
Assert.assertEquals(0, inst.getNano());
75+
Assert.assertEquals(0, inst.getEpochSecond());
76+
}
77+
78+
try (Context ctx = getJSContext()) {
79+
Value val = ctx.eval(ID, "new Temporal.Instant(100_000_000n);");
80+
Instant inst = val.asInstant();
81+
Assert.assertEquals(100_000_000L, inst.getNano());
82+
Assert.assertEquals(0, inst.getEpochSecond());
83+
}
84+
85+
try (Context ctx = getJSContext()) {
86+
Value val = ctx.eval(ID, "new Temporal.Instant(100_123_456_789n);");
87+
Instant inst = val.asInstant();
88+
Assert.assertEquals(123_456_789L, inst.getNano());
89+
Assert.assertEquals(100, inst.getEpochSecond());
90+
91+
LocalDate ld = val.asDate();
92+
Assert.assertEquals(1970, ld.getYear());
93+
Assert.assertEquals(Month.JANUARY, ld.getMonth());
94+
Assert.assertEquals(1, ld.getDayOfYear());
95+
96+
LocalTime lt = val.asTime();
97+
Assert.assertEquals(0, lt.getHour());
98+
Assert.assertEquals(1, lt.getMinute());
99+
Assert.assertEquals(40, lt.getSecond());
100+
Assert.assertEquals(123_456_789L, lt.getNano());
101+
102+
ZoneId zid = val.asTimeZone();
103+
Assert.assertEquals("UTC", zid.getId());
104+
}
105+
}
106+
107+
@Test
108+
public void testPlainDate() {
109+
try (Context ctx = getJSContext()) {
110+
Value val = ctx.eval(ID, "new Temporal.PlainDate(1982, 11, 26);");
111+
LocalDate ld = val.asDate();
112+
Assert.assertEquals(1982, ld.getYear());
113+
Assert.assertEquals(Month.NOVEMBER, ld.getMonth());
114+
Assert.assertEquals(26, ld.getDayOfMonth());
115+
116+
Assert.assertFalse(val.isTime());
117+
Assert.assertFalse(val.isTimeZone());
118+
}
119+
}
120+
121+
@Test
122+
public void testPlainTime() {
123+
try (Context ctx = getJSContext()) {
124+
Value val = ctx.eval(ID, "new Temporal.PlainTime(12, 34, 56, 987, 654, 321);");
125+
LocalTime lt = val.asTime();
126+
Assert.assertEquals(12, lt.getHour());
127+
Assert.assertEquals(34, lt.getMinute());
128+
Assert.assertEquals(56, lt.getSecond());
129+
Assert.assertEquals(987_654_321L, lt.getNano());
130+
131+
Assert.assertFalse(val.isDate());
132+
Assert.assertFalse(val.isTimeZone());
133+
}
134+
}
135+
136+
@Test
137+
public void testPlainDateTime() {
138+
try (Context ctx = getJSContext()) {
139+
Value val = ctx.eval(ID, "new Temporal.PlainDateTime(1982, 11, 26, 12, 34, 56, 987, 654, 321);");
140+
LocalDate ld = val.asDate();
141+
Assert.assertEquals(1982, ld.getYear());
142+
Assert.assertEquals(Month.NOVEMBER, ld.getMonth());
143+
Assert.assertEquals(26, ld.getDayOfMonth());
144+
145+
LocalTime lt = val.asTime();
146+
Assert.assertEquals(12, lt.getHour());
147+
Assert.assertEquals(34, lt.getMinute());
148+
Assert.assertEquals(56, lt.getSecond());
149+
Assert.assertEquals(987_654_321L, lt.getNano());
150+
151+
Assert.assertFalse(val.isTimeZone());
152+
}
153+
}
154+
155+
@Test
156+
public void testTimeZone() {
157+
try (Context ctx = getJSContext()) {
158+
Value val = ctx.eval(ID, "new Temporal.TimeZone('UTC');");
159+
ZoneId zid = val.asTimeZone();
160+
Assert.assertEquals("UTC", zid.getId());
161+
}
162+
163+
try (Context ctx = getJSContext()) {
164+
Value val = ctx.eval(ID, "new Temporal.TimeZone('Europe/Vienna');");
165+
ZoneId zid = val.asTimeZone();
166+
Assert.assertEquals("Europe/Vienna", zid.getId());
167+
}
168+
169+
try (Context ctx = getJSContext()) {
170+
Value val = ctx.eval(ID, "Temporal.TimeZone.from('+0645');");
171+
ZoneId zid = val.asTimeZone();
172+
Assert.assertTrue(zid instanceof ZoneOffset);
173+
ZoneOffset zoff = (ZoneOffset) zid;
174+
Assert.assertEquals(405 * 60, zoff.getTotalSeconds());
175+
}
176+
}
177+
178+
@Test
179+
public void testZonedDateTime() {
180+
try (Context ctx = getJSContext()) {
181+
Value val = ctx.eval(ID, "new Temporal.ZonedDateTime(100_123_456_789n, Temporal.TimeZone.from('Europe/Vienna'));");
182+
Instant inst = val.asInstant();
183+
Assert.assertEquals(123_456_789L, inst.getNano());
184+
Assert.assertEquals(100, inst.getEpochSecond());
185+
186+
LocalDate ld = val.asDate();
187+
Assert.assertEquals(1970, ld.getYear());
188+
Assert.assertEquals(Month.JANUARY, ld.getMonth());
189+
Assert.assertEquals(1, ld.getDayOfYear());
190+
191+
LocalTime lt = val.asTime();
192+
Assert.assertEquals(1, lt.getHour()); // offset of +1 due to timezone
193+
Assert.assertEquals(1, lt.getMinute());
194+
Assert.assertEquals(40, lt.getSecond());
195+
Assert.assertEquals(123_456_789L, lt.getNano());
196+
197+
ZoneId zid = val.asTimeZone();
198+
Assert.assertEquals("Europe/Vienna", zid.getId());
199+
}
200+
}
201+
202+
@Test
203+
public void testDuration() {
204+
try (Context ctx = getJSContext()) {
205+
Value val = ctx.eval(ID, "Temporal.Duration.from('PT2H3M4.987654321S');");
206+
Duration dur = val.asDuration();
207+
208+
long expectedSeconds = 2 * 60 * 60 + 3 * 60 + 4;
209+
long expectedNanos = 987654321;
210+
Assert.assertEquals(0, dur.toDays());
211+
Assert.assertEquals(4, dur.toSecondsPart());
212+
Assert.assertEquals(expectedSeconds, dur.toSeconds());
213+
Assert.assertEquals(expectedNanos, dur.toNanosPart());
214+
Assert.assertEquals(expectedSeconds * 1_000_000_000 + expectedNanos, dur.toNanos());
215+
216+
// invalid duration; java.time.Duration does not accept units larger or equals to DAY
217+
val = ctx.eval(ID, "Temporal.Duration.from('P1Y');");
218+
Assert.assertFalse(val.isDuration());
219+
val = ctx.eval(ID, "Temporal.Duration.from('P2M');");
220+
Assert.assertFalse(val.isDuration());
221+
val = ctx.eval(ID, "Temporal.Duration.from('P3W');");
222+
Assert.assertFalse(val.isDuration());
223+
val = ctx.eval(ID, "Temporal.Duration.from('P4D');");
224+
Assert.assertFalse(val.isDuration());
225+
}
226+
}
227+
}

graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/runtime/builtins/temporal/JSTemporalDurationObject.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,18 @@
4040
*/
4141
package com.oracle.truffle.js.runtime.builtins.temporal;
4242

43+
import java.time.Duration;
44+
45+
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
46+
import com.oracle.truffle.api.interop.InteropLibrary;
47+
import com.oracle.truffle.api.interop.UnsupportedMessageException;
48+
import com.oracle.truffle.api.library.ExportLibrary;
49+
import com.oracle.truffle.api.library.ExportMessage;
4350
import com.oracle.truffle.api.object.Shape;
51+
import com.oracle.truffle.js.runtime.JSRuntime;
4452
import com.oracle.truffle.js.runtime.objects.JSNonProxyObject;
4553

54+
@ExportLibrary(InteropLibrary.class)
4655
public class JSTemporalDurationObject extends JSNonProxyObject {
4756

4857
private final double years;
@@ -110,4 +119,31 @@ public double getMicroseconds() {
110119
public double getNanoseconds() {
111120
return nanoseconds;
112121
}
122+
123+
@ExportMessage
124+
final boolean isDuration() {
125+
// note: java.time.Duration only considers DAYS, while JS Temporal also has W, M, Y.
126+
// due to vague DAYS support in Java, we disallow conversion when any of D, W, M, Y is != 0.
127+
return years == 0 && months == 0 && weeks == 0 && days == 0 && JSRuntime.doubleIsRepresentableAsLong(calcSeconds()) && JSRuntime.doubleIsRepresentableAsLong(calcNanoseconds());
128+
}
129+
130+
@ExportMessage
131+
@TruffleBoundary
132+
final Duration asDuration() throws UnsupportedMessageException {
133+
if (!isDuration()) {
134+
throw UnsupportedMessageException.create();
135+
}
136+
double sec = calcSeconds();
137+
double nanos = calcNanoseconds();
138+
Duration dur = Duration.ofSeconds((long) sec, (long) nanos);
139+
return dur;
140+
}
141+
142+
private double calcNanoseconds() {
143+
return nanoseconds + microseconds * 1_000 + milliseconds * 1_000_000;
144+
}
145+
146+
private double calcSeconds() {
147+
return seconds + minutes * 60 + hours * 60 * 60;
148+
}
113149
}

0 commit comments

Comments
 (0)